Cómo programar en C # [2a ed.]
 9789702610564, 9702610567

Table of contents :
Como programar C#
Contenido
Capítulo 1: Introducción a las computadoras, Internet y Visual C#
Capítulo 2: Introducción al IDE de Visual C# 2005 Express ed.
Capítulo 3: Introducción a las aplicaciones de C#
Capítulo 4: Introducción a las clases y los objetos
Capítulo 5: Instrucciones de control: Parte I
Capítulo 6: Instrucciones de control: Parte 2
Capítulo 7: Métodos: Un análisis más detallado
Capítulo 8: Arreglos
Capítulo 9: Clases y objetos: Un análisis más detallado
Capítulo 10: Programación orientada a objetos: Herencia
Capítulo 11: Polimorfismo, interfaces y sobrecarga de operadores
Capítulo 12: Manejo de excepciones
Capítulo 13: Conceptos de interfaz gráfica de usuario: Parte I
Capítulo 14: Conceptos de interfaz gráfica de usuario: Parte 2
Capítulo 15: Subprocesamiento múltiple
Capítulo 16: Cadenas, caracteres y expresiones regulares
Capítulo 17: Gráficos y multimedia
Capítulo 18: Archivos y flujos
Capítulo 19: Lenguaje de marcado extensible (XML)
Capítulo 20: Bases de datos, SQL y ADO.NET
Capítulo 21: ASP.NET 2.0, formularios web forms y controles web
Capítulo 22: Servicios Web
Capítulo 23: Redes: Sockets basados en flujos y datagramas
Capítulo 24: Estructuras de datos
Capítulo 25: Genéricos
Capítulo 26: Colecciones
A: Tabla de precedencia de los operadores
B: Sistemas numéricos
C: Uso del depurador de Visual Studio 2005
D: Conjunto de caracteres ASCII
E: Unicode
F: Introducción a XHTML. Parte I
G: Introducción a XHTML: Parte 2
H: Caracteres especiales de HTML/XHTML
I: Colores en HTML/XHTML
J: Código del caso de estudio del ATM
K: UML 2: Tipos de diagramas adicionales
L: Los tipos simples
Índice

Citation preview

C

Segunda edición

CÓMO # PROGRAMAR

Esta nueva edición presenta, en C#, el poderoso .NET Framework de Microsoft

• C# 2.0, .NET 2.0, FCL • ASP.NET 2.0, formularios Web Forms y controles Web • Bases de datos, SQL y ADO.NET 2.0 • Redes y .NET Remoting • XML, servicios Web • Genéricos, colecciones • GUI/Windows® Forms

• POO: clases, herencia y polimorfismo • Caso de estudio de un ATM con DOO/UML™ • Gráficos y multimedia • Subprocesamiento múltiple • Manejo de excepciones • Y mucho más…

El CD-ROM que acompaña a este libro incluye: • Todo el código utilizado en los ejercicios del libro • Material complementario ¡completamente en español!

Segunda edición

• Descargue los ejemplos de código • Reciba actualizaciones sobre este libro al suscribirse al boletín de noticias electrónico y gratuito DEITEL® BUZZ ONLINE • Lea ediciones archivadas del boletín electrónico DEITEL® BUZZ ONLINE • Obtenga información sobre capacitación empresarial

AMPLIA COBERTURA PRÁCTICA CON EJEMPLOS SOBRE:

CÓMO

Visite el sitio Web de este libro: www.pearsoneducacion.net/deitel

DEITEL™ DEITEL

# PROGRAMAR

Comienza con una concisa introducción a los fundamentos de C#, con un enfoque anticipado a las clases y a los objetos, para después pasar a temas más avanzados, como el subprocesamiento múltiple, XML, ADO.NET 2.0, ASP.NET 2.0, servicios Web, programación de redes y .NET Remoting. Conforme avance en el texto, disfrutará las clásicas discusiones de los Deitel en relación con la programación orientada a objetos, junto con el nuevo caso de estudio de un ATM con DOO/UML™, del cual se incluye una implementación completa en C#. Para cuando termine, tendrá todo lo que necesita para crear aplicaciones Windows, aplicaciones Web y servicios Web de próxima generación.

C

CÓMO # PROGRAMAR

C

Escrito para programadores con conocimientos en C++, Java o cualquier otro lenguaje de alto nivel, este libro aplica el método de código activo (live-code) de Deitel para enseñar programación; además explora detalladamente el lenguaje C# de Microsoft y el nuevo .NET 2.0. El libro, actualizado para Visual Studio® 2005 y C# 2.0, presenta los conceptos de C# en el contexto de programas completos y probados, descripciones detalladas del código, línea por línea, y resultados de los programas. Incluye más de 200 aplicaciones en C#, con más de 16,000 líneas de código probado, así como más de 300 tips de programación que le ayudarán a crear aplicaciones robustas.

Segunda edición

HARVEY M. DEITEL y PAUL J. DEITEL

Port.C# Programadores.indd 1

5/25/07 4:01:45 PM

CÓMO PROGRAMAR EN C# SEGUNDA EDICIÓN

cuarta edición

Cómo programar en

C#

Harvey M. Deitel Deitel & Associates, Inc.

Paul J. Deitel

Deitel & Associates, Inc. TRADUCCIÓN

Alfonso Vidal Romero Elizondo Ingeniero en Electrónica y Comunicación Instituto Tecnológico y de Estudios Superiores de Monterrey Campus Monterrey

REVISIÓN TÉCNICA

César Coutiño Gómez Director del Departamento de Computación Instituto Tecnológico y de Estudios Superiores de Monterrey Campus Ciudad de México

José Martín Molina Espinosa Profesor investigador Instituto Tecnológico y de Estudios Superiores de Monterrey Campus Ciudad de México

Rafael Lozano Espinosa Profesor del Departamento de Computación Instituto Tecnológico y de Estudios Superiores de Monterrey Campus Ciudad de México

Sergio Fuenlabrada Velázquez Profesor investigador Unidad Profesional Interdisciplinaria de Ingeniería, Ciencias Sociales y Administrativas Instituto Politécnico Nacional

Mario Alberto Sesma Martínez Profesor investigador Unidad Profesional Interdisciplinaria de Ingeniería, Ciencias Sociales y Administrativas Instituto Politécnico Nacional

Edna Martha Miranda Chávez Profesora investigadora Unidad Profesional Interdisciplinaria de Ingeniería, Ciencias Sociales y Administrativas Instituto Politécnico Nacional

DEITEL, HARVEY M. Y PAUL J. DEITEL Cómo programar en C#. Segunda edición PEARSON EDUCACIÓN, México 2007 ISBN: 978-970-26-1056-4 Área: Computación

Formato: 20 × 25.5 cm

Páginas: 1080

Authorized translation from the English language edition, entitled C# for programmers, second edition, by Harvey M. Deitel and Paul J. Deitel, published by Pearson Education, Inc., publishing as Prentice Hall, Inc., Copyright ©2006. All rights reserved. ISBN 0-13-134591-5 Traducción autorizada de la edición en idioma inglés, titulada C# for programmers, second edition, por Harvey M. Deitel y Paul J. Deitel, publicada por Pearson Education, Inc., publicada como Prentice-Hall Inc., Copyright ©2006. Todos los derechos reservados. Esta edición en español es la única autorizada. Edición en español Editor:

Luis Miguel Cruz Castillo e-mail: luis.cruzpearsoned.com Editor de desarrollo: Bernardino Gutiérrez Hernández Supervisor de producción: Enrique Trejo Hernández SEGUNDA EDICIÓN, 2007 D.R. © 2007 por Pearson Educación de México, S.A. de C.V. Atlacomulco 500-5o. piso Col. Industrial Atoto 53519, Naucalpan de Juárez, Edo. de México Cámara Nacional de la Industria Editorial Mexicana. Reg. Núm. 1031. Prentice Hall es una marca registrada de Pearson Educación de México, S.A. de C.V. Reservados todos los derechos. Ni la totalidad ni parte de esta publicación pueden reproducirse, registrarse o transmitirse, por un sistema de recuperación de información, en ninguna forma ni por ningún medio, sea electrónico, mecánico, fotoquímico, magnético o electroóptico, por fotocopia, grabación o cualquier otro, sin permiso previo por escrito del editor. El préstamo, alquiler o cualquier otra forma de cesión de uso de este ejemplar requerirá también la autorización del editor o de sus representantes. ISBN 10: 970-26-1056-7 ISBN 13: 978-970-26-1056-4 Impreso en México. Printed in Mexico. 1 2 3 4 5 6 7 8 9 0 - 10 09 08 07

®

A Janie Schwark de Microsoft Con sincera gratitud por el privilegio y el placer de trabajar de cerca contigo, durante tantos años. Harvey M. Deitel y Paul J. Deitel

Marcas registradas Microsoft® Visual Studio® 2005 son marcas registradas o marcas registradas por Microsoft Corporation en Estados Unidos y/u otros países. OMG, Object Management Group, UML, Lenguaje de Modelado Unificado, son marcas registradas de Object Management Group, Inc.

Contenido Prefacio

1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11

2 2.1 2.2 2.3 2.4

Introducción a las computadoras, Internet y Visual C# Introducción Sistema operativo Windows® de Microsoft C# Internet y World Wide Web Lenguaje de Marcado Extensible (XML) Microsoft .NET .NET Framework y Common Language Runtime Prueba de una aplicación en C# (Única sección obligatoria del caso de estudio) Caso de estudio de ingeniería de software: introducción a la tecnología de objetos y el UML Conclusión Recursos Web

Introducción al IDE de Visual C# 2005 Express Edition

xix

1 2 2 3 3 4 5 5 7 9 14 14

17 18 18 22 25 25 29 30 31

2.7 2.8

Introducción Generalidades del IDE de Visual Studio 2005 Barra de menús y barra de herramientas Navegación por el IDE de Visual Studio 2005 Explorador de soluciones 2.4.1 2.4.2 Cuadro de herramientas 2.4.3 Ventana Propiedades Uso de la Ayuda Uso de la programación visual para crear un programa simple que muestra texto e imagen Conclusión Recursos Web

3

Introducción a las aplicaciones de C#

47

3.1 3.2 3.3 3.4 3.5 3.6 3.7

Introducción Una aplicación simple en C#: mostrar una línea de texto Cómo crear una aplicación simple en Visual C# Express Modificación de su aplicación simple en C# Formato del texto con Console.Write y Console.WriteLine Otra aplicación en C#: suma de enteros Conceptos sobre memoria

2.5 2.6

34 45 46

48 48 53 60 61 63 66

viii

Contenido

3.8 3.9 3.10

Aritmética Toma de decisiones: operadores de igualdad y relacionales (Opcional) Caso de estudio de ingeniería de software: análisis del documento de requerimientos del ATM Conclusión

3.11

67 69 74 82

4

Introducción a las clases y los objetos

83

4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11

Introducción Clases, objetos, métodos, propiedades y variables de instancia Declaración de una clase con un método e instanciamiento del objeto de una clase Declaración de un método con un parámetro Variables de instancia y propiedades Diagrama de clases de UML con una propiedad Ingeniería de software con propiedades y los descriptores de acceso set y get Comparación entre tipos por valor y tipos por referencia Inicialización de objetos con constructores Los números de punto flotante y el tipo decimal (Opcional) Caso de estudio de ingeniería de software: identificación de las clases en el documento de requerimientos del ATM Conclusión

84 84 86 89 92 96 97 98 99 102

4.12

5

Instrucciones de control: parte I

5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 5.10 5.11 5.12 5.13

Introducción Estructuras de control Instrucción de selección simple if Instrucción de selección doble if…else Instrucción de repetición while Cómo formular algoritmos: repetición controlada por un contador Cómo formular algoritmos: repetición controlada por un centinela Cómo formular algoritmos: instrucciones de control anidadas Operadores de asignación compuestos Operadores de incremento y decremento Tipos simples (Opcional) Caso de estudio de ingeniería de software: identificación de los atributos de las clases en el sistema ATM Conclusión

6

Instrucciones de control: parte 2

6.1 6.2 6.3 6.4 6.5 6.6 6.7 6.8 6.9

Introducción Fundamentos de la repetición controlada por un contador Instrucción de repetición for Ejemplos acerca del uso de la instrucción for Instrucción de repetición do…while Instrucción de selección múltiple switch Instrucciones break y continue Operadores lógicos (Opcional) Caso de estudio de ingeniería de software: identificación de los estados y actividades de los objetos en el sistema ATM Conclusión

6.10

107 114

115 116 116 118 119 122 123 127 130 134 134 137 137 141

143 144 144 145 149 153 155 162 164 168 173

Contenido

7

Métodos: un análisis más detallado

7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9

7.16

Introducción Empaquetamiento de código en C# Métodos static, variables static y la clase Math Declaración de métodos con múltiples parámetros Notas acerca de cómo declarar y utilizar los métodos La pila de llamadas a los métodos y los registros de activación Promoción y conversión de argumentos La Biblioteca de clases del .NET Framework Caso de estudio: generación de números aleatorios 7.9.1 Escalar y desplazar números aleatorios 7.9.2 Repetitividad de números aleatorios para prueba y depuración Caso de estudio: juego de probabilidad (introducción a las enumeraciones) Alcance de las declaraciones Sobrecarga de métodos Recursividad Paso de argumentos: diferencia entre paso por valor y paso por referencia (Opcional) Caso de estudio de ingeniería de software: identificación de las operaciones de las clases en el sistema ATM Conclusión

8

Arreglos

8.1 8.2 8.3 8.4 8.5 8.6 8.7 8.8 8.9 8.10 8.11 8.12 8.13 8.14 8.15

Introducción Arreglos Declaración y creación de arreglos Ejemplos acerca del uso de los arreglos Caso de estudio: simulación para barajar y repartir cartas Instrucción foreach Paso de arreglos y elementos de arreglos a los métodos Paso de arreglos por valor y por referencia Caso de estudio: la clase LibroCalificaciones que usa un arreglo para ordenar calificaciones Arreglos multidimensionales Caso de estudio: la clase LibroCalificaciones que usa un arreglo rectangular Listas de argumentos de longitud variable Uso de argumentos de línea de comandos (Opcional) Caso de estudio de ingeniería de software: colaboración entre los objetos en el sistema ATM Conclusión

9

Clases y objetos: un análisis más detallado

9.1 9.2 9.3 9.4 9.5 9.6 9.7 9.8 9.9 9.10

Introducción Caso de estudio de la clase Tiempo Control del acceso a los miembros Referencias a los miembros del objeto actual mediante this Indexadores Caso de estudio de la clase Tiempo: constructores sobrecargados Constructores predeterminados y sin parámetros Composición Recolección de basura y destructores Miembros de clase static

7.10 7.11 7.12 7.13 7.14 7.15

ix

175 176 176 177 179 183 184 184 186 187 191 191 192 196 198 200 204 207 213

215 216 216 217 218 226 230 231 233 237 242 246 250 253 254 260

263 264 264 268 269 271 274 279 279 282 283

x

Contenido

9.11 9.12 9.13 9.14 9.15 9.16 9.17 9.18

Variables de instancia readonly Reutilización de software Abstracción de datos y encapsulamiento Caso de estudio de la clase Tiempo: creación de bibliotecas de clases Acceso internal Vista de clases y Examinador de objetos (Opcional) Caso de estudio de ingeniería de software: inicio de la programación de las clases del sistema ATM Conclusión

10

Programación orientada a objetos: herencia

10.1 10.2 10.3 10.4

Introducción Clases base y clases derivadas Miembros protected Relación entre las clases base y las clases derivadas 10.4.1 Creación y uso de una clase EmpleadoPorComision 10.4.2 Creación de una clase EmpleadoBaseMasComision sin usar la herencia 10.4.3 Creación de una jerarquía de herencia EmpleadoPorComisionEmpleadoBaseMasComision

10.5 10.6 10.7 10.8

La jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision mediante el uso de variables de instancia protected 10.4.5 La jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision mediante el uso de variables de instancia private Los constructores en las clases derivadas Ingeniería de software mediante la herencia La clase object Conclusión

11

Polimorfismo, interfaces y sobrecarga de operadores

11.1 11.2 11.3 11.4 11.5

Introducción Ejemplos de polimorfismo Demostración del comportamiento polimórfico Clases y métodos abstractos Caso de estudio: sistema de nómina utilizando polimorfismo 11.5.1 Creación de la clase base abstracta Empleado 11.5.2 Creación de la clase derivada concreta EmpleadoAsalariado 11.5.3 Creación de la clase derivada concreta EmpleadoPorHoras 11.5.4 Creación de la clase derivada concreta EmpleadoPorComision 11.5.5 Creación de la clase derivada concreta indirecta EmpleadoBaseMasComision 11.5.6 El procesamiento polimórfico, el operador is y la conversión descendente 11.5.7 Resumen de las asignaciones permitidas entre variables de la clase base y de la clase derivada Métodos y clases sealed Caso de estudio: creación y uso de interfaces 11.7.1 Desarrollo de una jerarquía IPorPagar 11.7.2 Declaración de la interfaz IPorPagar 11.7.3 Creación de la clase Factura 11.7.4 Modificación de la clase Empleado para implementar la interfaz IPorPagar 11.7.5 Modificación de la clase EmpleadoAsalariado para usarla en la jerarquía IPorPagar 11.7.6 Uso de la interfaz IPorPagar para procesar objetos Factura y Empleado mediante el polimorfismo

288 290 291 292 295 297 298 304

305 306 307 309 309 310 314 319

10.4.4

11.6 11.7

321 326 331 336 337 339

341 342 343 344 347 349 350 352 353 355 356 357 362 362 363 364 365 365 367 369 370

Contenido 11.7.7 Interfaces comunes de la Biblioteca de clases del .NET Framework Sobrecarga de operadores (Opcional) Caso de estudio de ingeniería de software: incorporación de la herencia y el polimorfismo en el sistema ATM 11.10 Conclusión 11.8 11.9

12

Manejo de excepciones

12.1 12.2 12.3 12.4

12.6 12.7 12.8 12.9

Introducción Generalidades acerca del manejo de excepciones Ejemplo: división entre cero sin manejo de excepciones Ejemplo: manejo de las excepciones DivideByZeroException y FormatException 12.4.1 Encerrar código en un bloque try 12.4.2 Atrapar excepciones 12.4.3 Excepciones no atrapadas 12.4.4 Modelo de terminación del manejo de excepciones 12.4.5 Flujo de control cuando ocurren las excepciones Jerarquía Exception de .NET 12.5.1 Las clases ApplicationException y SystemException 12.5.2 Determinar cuáles excepciones debe lanzar un método El bloque finally Propiedades de Exception Clases de excepciones definidas por el usuario Conclusión

13

Conceptos de interfaz gráfica de usuario: parte I

13.1 13.2 13.3

13.4 13.5 13.6 13.7 13.8 13.9 13.10 13.11 13.12 13.13

Introducción Formularios Windows Forms Manejo de eventos 13.3.1 Una GUI simple controlada por eventos 13.3.2 Otro vistazo al código generado por Visual Studio 13.3.3 Delegados y el mecanismo de manejo de eventos 13.3.4 Otras formas de crear manejadores de eventos 13.3.5 Localización de la información de los eventos Propiedades y distribución de los controles Controles Label, TextBox y Button Controles GroupBox y Panel Controles CheckBox y RadioButton Controles PictureBox Controles ToolTip Control NumericUpDown Manejo de los eventos del ratón Manejo de los eventos del teclado Conclusión

14

Conceptos de interfaz gráfica de usuario: parte 2

14.1 14.2 14.3 14.4

Introducción Menús Control MonthCalendar Control DateTimePicker

12.5

xi 372 372 376 383

385 386 387 387 390 392 392 392 392 394 394 394 395 395 402 406 409

411 412 413 415 416 417 419 419 420 420 425 428 430 438 440 442 444 446 449

451 452 452 461 461

xii

Contenido

14.5 14.6 14.7 14.8 14.9 14.10 14.11 14.12 14.13 14.14 14.15

Control LinkLabel Control ListBox Control CheckedListBox Control ComboBox Control TreeView Control ListView Control TabControl Ventanas de la interfaz de múltiples documentos (MDI) Herencia visual Controles definidos por el usuario Conclusión

15

Subprocesamiento múltiple

15.1 15.2 15.3 15.4 15.5 15.6 15.7 15.8 15.9 15.10

Introducción Estados de los subprocesos: ciclo de vida de un subproceso Prioridades y programación de subprocesos Creación y ejecución de subprocesos Sincronización de subprocesos y la clase Monitor Relación productor/consumidor sin sincronización de subprocesos Relación productor/consumidor con sincronización de subprocesos Relación productor/consumidor: búfer circular Subprocesamiento múltiple con GUIs Conclusión

16

Cadenas, caracteres y expresiones regulares

16.1 16.2 16.3 16.4 16.5 16.6 16.7 16.8 16.9 16.10 16.11

16.17

Introducción Fundamentos de los caracteres y las cadenas Constructores de string Indexador de string, propiedad Length y método CopyTo Comparación de objetos string Localización de caracteres y subcadenas en objetos string Extracción de subcadenas de objetos string Concatenación de objetos string Métodos varios de la clase string La clase StringBuilder Las propiedades Length y Capacity, el método EnsureCapacity y el indexador de la clase StringBuilder Los métodos Append y AppendFormat de la clase StringBuilder Los métodos Insert, Remove y Replace de la clase StringBuilder Métodos de char Simulación para barajar y repartir cartas Expresiones regulares y la clase Regex 16.16.1 Ejemplo de expresión regular 16.16.2 Validación de la entrada del usuario con expresiones regulares 16.16.3 Los métodos Replace y Split de Regex Conclusión

17

Gráficos y multimedia

17.1 17.2

Introducción Las clases de dibujo y el sistema de coordenadas

16.12 16.13 16.14 16.15 16.16

465 469 473 474 479 484 489 494 500 503 507

509 510 511 513 514 517 519 525 532 539 543

545 546 547 547 549 550 553 555 556 557 558 558 560 563 565 567 570 571 573 577 579

581 582 582

Contenido 17.3 17.4 17.5 17.6 17.7 17.8 17.9 17.10 17.11 17.12 17.13 17.14 17.15

Contextos de gráficos y objetos Graphics Control de los colores Control de las fuentes Dibujo de líneas, rectángulos y óvalos Dibujo de arcos Dibujo de polígonos y polilíneas Herramientas de gráficos avanzadas Introducción a multimedia Carga, visualización y escalado de imágenes Animación de una serie de imágenes Reproductor de Windows Media Microsoft Agent Conclusión

18

Archivos y flujos

18.1 18.2 18.3 18.4 18.5 18.6 18.7 18.8

Introducción Jerarquía de datos Archivos y flujos Las clases File y Directory Creación de un archivo de texto de acceso secuencial Lectura de datos desde un archivo de texto de acceso secuencial Serialización Creación de un archivo de acceso secuencial mediante el uso de la serialización de objetos 18.9 Lectura y deserialización de datos de un archivo de texto de acceso secuencial 18.10 Conclusión

19

Lenguaje de marcado extensible (XML)

19.1 19.2 19.3 19.4 19.5 19.6 19.7 19.8 19.9 19.10 19.11 19.12

Introducción Fundamentos de XML Estructuración de datos Espacios de nombres de XML Definiciones de tipo de documento (DTDs) Documentos de esquemas XML del W3C (Opcional) Lenguaje de hojas de estilos extensible y transformaciones XSL (Opcional) Modelo de objetos de documento (DOM) (Opcional) Validación de esquemas con la clase XmlReader (Opcional) XSLT con la clase XslCompiledTransform Conclusión Recursos Web

20

Bases de datos, SQL y ADO.NET

20.1 20.2 20.3 20.4

Introducción Bases de datos relacionales Generalidades acerca de las bases de datos relacionales: la base de datos Libros SQL 20.4.1 Consulta SELECT básica 20.4.2 Cláusula WHERE 20.4.3 Cláusula ORDER BY

xiii 584 585 591 595 597 600 603 608 608 610 619 621 632

633 634 634 636 637 645 654 663 663 669 673

675 676 676 679 685 688 691 697 705 717 720 722 723

725 726 727 728 731 731 732 733

xiv

Contenido

20.4.4 Mezcla de datos de varias tablas: INNER JOIN 20.4.5 Instrucción INSERT 20.4.6 Instrucción UPDATE 20.4.7 Instrucción DELETE 20.5 Modelo de objetos ADO.NET 20.6 Programación con ADO.NET: extraer información de una base de datos 20.6.1 Mostrar una tabla de base de datos en un control DataGridView 20.6.2 Cómo funciona el enlace de datos 20.7 Consulta de la base de datos Libros 20.8 Programación con ADO.NET: caso de estudio de libreta de direcciones 20.9 Uso de un objeto DataSet para leer y escribir XML 20.10 Conclusión 20.11 Recursos Web

21

ASP.NET 2.0, formularios Web Forms y controles Web

21.1 21.2 21.3 21.4

Introducción Transacciones HTTP simples Arquitectura de aplicaciones multinivel Creación y ejecución de un ejemplo de formularioWeb Forms simple 21.4.1 Análisis de un archivo ASPX 21.4.2 Análisis de un archivo de código subyacente (code-behind) 21.4.3 Relación entre un archivo ASPX y un archivo de código subyacente 21.4.4 Cómo se ejecuta el código en una página Web ASP.NET 21.4.5 Análisis del XHTML generado por una aplicación ASP.NET 21.4.6 Creación de una aplicación Web ASP.NET 21.5 Controles Web 21.5.1 Controles de texto y gráficos 21.5.2 Control AdRotator 21.5.3 Controles de validación 21.6 Rastreo de sesiones 21.6.1 Cookies 21.6.2 Rastreo de sesiones con HttpSessionState 21.7 Caso de estudio: conexión a una base de datos en ASP.NET 21.7.1 Creación de un formulario Web Forms que muestra datos de una base de datos 21.7.2 Modificación del archivo de código subyacente para la aplicación Libro de visitantes 21.8 Caso de estudio: aplicación segura de la base de datos Libros 21.8.1 Análisis de la aplicación segura de la base de datos Libros completa 21.8.2 Creación de la aplicación segura de la base de datos Libros 21.9 Conclusión 21.10 Recursos Web

22

Servicios Web

22.1 22.2

Introducción Fundamentos de los servicios Web .NET 22.2.1 Creación de un servicio Web en Visual Web Developer 22.2.2 Descubrimiento de servicios Web 22.2.3 Determinación de la funcionalidad de un servicio Web 22.2.4 Prueba de los métodos de un servicio Web 22.2.5 Creación de un cliente para utilizar un servicio Web

734 737 737 738 739 740 740 747 751 759 765 768 769

771 772 773 775 776 777 778 779 780 780 781 789 789 793 798 808 809 816 824 825 833 834 834 838 861 861

863 864 865 866 866 867 868 870

Contenido 22.3 22.4

22.7 22.8 22.9

Protocolo simple de acceso a objetos (SOAP) Publicación y consumo de los servicios Web 22.4.1 Definición del servicio Web EnteroEnorme 22.4.2 Creación de un servicio Web en Visual Web Developer 22.4.3 Despliegue del servicio Web EnteroEnorme 22.4.4 Creación de un cliente para consumir el servicio Web EnteroEnorme 22.4.5 Consumo del servicio Web EnteroEnorme Rastreo de sesiones en los servicios Web 22.5.1 Creación de un servicio Web de Blackjack 22.5.2 Consumo del servicio Web de Blackjack Uso de formularios Web Forms y servicios Web 22.6.1 Agregar componentes de datos a un servicio Web 22.6.2 Creación de un formulario Web Forms para interactuar con el servicio Web de reservaciones de una aerolínea Tipos definidos por el usuario en los servicios Web Conclusión Recursos Web

23

Redes: sockets basados en flujos y datagramas

22.5 22.6

23.1 23.2

Introducción Comparación entre la comunicación orientada a la conexión y la comunicación sin conexión 23.3 Protocolos para transportar datos 23.4 Establecimiento de un servidor TCP simple (mediante el uso de sockets de flujo) 23.5 Establecimiento de un cliente TCP simple (mediante el uso de sockets de flujo) 23.6 Interacción entre cliente/servidor mediante conexiones de sockets de flujo 23.7 Interacción entre cliente/servidor sin conexión mediante datagramas 23.8 Juego de Tres en raya cliente/servidor mediante el uso de un servidor con subprocesamiento múltiple 23.9 Control WebBrowser 23.10 .NET Remoting 23.11 Conclusión

24

Estructuras de datos

24.1 24.2 24.3 24.4 24.5 24.6 24.7 24.8

Introducción Tipos struct simples, boxing y unboxing Clases autorreferenciadas Listas enlazadas Pilas Colas Árboles 24.7.1 Árbol binario de búsqueda de valores enteros 24.7.2 Árbol binario de búsqueda de objetos IComparable Conclusión

25

Genéricos

25.1 25.2 25.3

Introducción Motivación para los métodos genéricos Implementación de un método genérico

xv 871 872 872 877 879 880 883 887 887 890 899 901 903 906 915 915

917 918 918 919 919 921 922 931 936 948 951 961

963 964 964 965 967 977 980 984 985 991 997

999 1000 1001 1002

xvi

Contenido

25.4 25.5 25.6 25.7 25.8

Restricciones de los tipos Sobrecarga de métodos genéricos Clases genéricas Observaciones acerca de los genéricos y la herencia Conclusión

26

Colecciones

26.1 26.2 26.3 26.4

Introducción Generalidades acerca de las colecciones La clase Array y los enumeradores Colecciones no genéricas 26.4.1 La clase ArrayList 26.4.2 La clase Stack 26.4.3 La clase Hashtable Colecciones genéricas 26.5.1 La clase genérica SortedDictionary 26.5.2 La clase genérica LinkedList Colecciones sincronizadas Conclusión

26.5 26.6 26.7

1005 1007 1007 1016 1016

1017 1018 1018 1020 1024 1024 1027 1030 1034 1034 1036 1040 1040

Apéndices (Incluidos en el CD-ROM que acompaña al libro)

A B

Tabla de precedencia de los operadores

1041

Sistemas numéricos

1043

B.1 B.2 B.3 B.4 B.5 B.6

Introducción Abreviatura de los números binarios como números octales y hexadecimales Conversión de números octales y hexadecimales a binarios Conversión de un número binario, octal o hexadecimal a decimal Conversión de un número decimal a binario, octalo hexadecimal Números binarios negativos: notación de complemento a dos

C

Uso del depurador de Visual Studio® 2005

C.1 C.2 C.3 C.4

C.6

Introducción Los puntos de interrupción y el comando Continuar Las ventanas Variables locales e Inspección Control de la ejecución mediante los comandos Paso a paso por instrucciones, Paso a paso por procedimientos, Paso a paso para salir y Continuar Otras características C.5.1 Editar y Continuar C.5.2 Ayudante de excepciones C.5.3 Depuración Sólo mi código ( Just My Code™) C.5.4 Otras nuevas características del depurador Conclusión

D

Conjunto de caracteres ASCII

C.5

1044 1046 1047 1048 1048 1050

1051 1052 1052 1057 1059 1062 1062 1065 1065 1065 1066

1067

Contenido

xvii

E

Unicode®

E.1 E.2 E.3 E.4 E.5 E.6

Introducción Formatos de transformación de Unicode Caracteres y glifos Ventajas y desventajas de Unicode Uso de Unicode Rangos de caracteres

F

Introducción a XHTML: parte 1

F.1 F.2 F.3 F.4 F.5 F.6 F.7 F.8 F.9 F.10 F.11

Introducción Edición de XHTML El primer ejemplo en XHTML Servicio de validación de XHTML del W3C Encabezados Vínculos Imágenes Caracteres especiales y más saltos de línea Listas desordenadas Listas anidadas y ordenadas Recursos Web

G

Introducción a XHTML: parte 2

G.1 G.2 G.3 G.4 G.5 G.6 G.7 G.8 G.9 G.10 G.11

Introducción Tablas básicas en XHTML Tablas intermedias en XHTML y formatos Formularios básicos en XHTML Formularios más complejos en XHTML Vínculos internos Creación y uso de mapas de imágenes Elementos meta Elemento frameset Elementos frameset anidados Recursos Web

H

Caracteres especiales de HTML/XHTML

1123

I

Colores en HTML/XHTML

1125

J

Código del caso de estudio del ATM

1129

J.1 J.2 J.3 J.4 J.5 J.6

Implementación del caso de estudio del ATM La clase ATM La clase Pantalla La clase Teclado La clase DispensadorEfectivo La clase RanuraDeposito

1069 1070 1070 1071 1072 1072 1074

1077 1078 1078 1079 1081 1083 1084 1086 1090 1091 1092 1094

1095 1096 1096 1098 1101 1103 1110 1112 1115 1116 1120 1122

1129 1130 1135 1135 1136 1137

xviii Contenido J.7 J.8 J.9 J.10 J.11 J.12 J.13 J.14

La clase Cuenta La clase BaseDatosBanco La clase Transaccion La clase SolicitudSaldo La clase Retiro La clase Deposito La clase CasoEstudioATM Conclusión

K

UML 2: tipos de diagramas adicionales

K.1 K.2

Introducción Tipos de diagramas adicionales

L

Los tipos simples

Índice

1138 1140 1142 1144 1144 1148 1150 1151

1153 1153 1153

1155

1157

Prefacio “Ya no vivas en fragmentos, conéctate.” —Edgar Morgan Foster

Bienvenido a C# y al mundo de Windows, Internet y la programación Web con Visual Studio 2005 y la plataforma .NET 2.0. En este libro se presentan las tecnologías de computación más recientes e innovadoras para los desarrolladores de software y profesionales de TI. En Deitel & Associates escribimos libros de texto de ciencias computacionales para estudiantes universitarios, y libros profesionales para desarrolladores de software. Además, utilizamos este material para impartir seminarios industriales a nivel mundial. Fue muy divertido crear este libro. Para empezar, analizamos cuidadosamente la edición anterior: • Auditamos nuestra presentación de C# en comparación con las especificaciones más recientes del lenguaje C# de Microsoft y Ecma, las cuales se encuentran en los sitios msdn.microsoft.com/vcsharp/programming/language y www.ecma-international.org/publications/standards/Ecma-334.html, respectivamente. • Hemos actualizado y mejorado en forma considerable todos los capítulos. • Cambiamos de pedagogía y ahora se analizarán antes las clases y los objetos. Es decir, podrá crear clases reutilizables a partir del capítulo 4. • Actualizamos nuestra presentación orientada a objetos para acoger la versión más reciente del UML (Lenguaje Unificado de Modelado): UML™ 2.0, el lenguaje gráfico estándar en la industria para modelar sistemas orientados a objetos. • En los capítulos 1, 3 a 9 y 11 agregamos un caso de estudio sobre un cajero automático (ATM) de DOO/UML. Este caso de estudio incluye una implementación completa del ATM en código de C#. • Agregamos diversos casos de estudio de programación orientada a objetos con varias secciones. • Incorporamos nuevas características clave de la versión más reciente de C# de Microsoft: Visual C# 2005; además, agregamos análisis sobre los elementos genéricos, el trabajo remoto y la depuración en .NET. • Mejoramos de manera considerable el manejo sobre XML, ADO.NET, ASP.NET y los servicios Web. Todo esto ha sido revisado con detalle por un equipo de distinguidos desarrolladores de .NET en la industria, profesionales académicos y miembros del equipo de desarrollo de Microsoft C#.

Quién debe leer este libro Tenemos varias publicaciones acerca de C#, para distintas audiencias. Cómo programar en C#, 2ª edición forma parte de la Serie Deitel ® para desarrolladores, diseñada para desarrolladores profesionales de software que desean analizar a profundidad la nueva tecnología, sin dedicar demasiado tiempo al material introductorio. Este libro se concentra en lograr una claridad en los programas a través de las técnicas demostradas: programación estructurada, programación orientada a objetos (POO) y programación controlada por eventos. Continúa con temas de nivel superior como XML, ASP.NET 2.0, ADO.NET 2.0 y los servicios Web.

xx

Prefacio

Cómo programar en C#, 2ª edición presenta muchos programas completos y funcionales en C#, y describe sus entradas y salidas mediante capturas de pantalla reales de los programas en ejecución. Éste es nuestro método distintivo al que llamamos código activo (LIVE-CODE™); presentamos los conceptos dentro del contexto de programas funcionales completos. El código fuente del libro (en inglés) puede descargarlo sin costo de los siguientes sitios Web: www.deitel.com/books/csharpforprogrammers2/ www.pearsoneducacion.com/deitel

En nuestras instrucciones de prueba del capítulo 1 asumimos que usted extraerá estos ejemplos en la carpeta de su computadora. Si surgen dudas o preguntas, a medida que lea este libro, puede enviar un correo electrónico a [email protected]; le responderemos a la mayor brevedad. Para actualizaciones sobre este libro y el estado del software de C#, asimismo, obtendrá las noticias más recientes acerca de todas las publicaciones y servicios Deitel, visite, con regularidad, el sitio www.deitel.com e inscríbase en el boletín de correo electrónico gratuito Deitel ® Buzz Online en www.deitel. com/newsletter/suscribe.html. C:\

Cómo descargar el software Microsoft Visual C# 2005 Express Edition Microsoft pone a disposición del público una versión gratuita de su herramienta de desarrollo en C# llamada Visual C# 2005 Express Edition. Puede utilizarla para compilar los programas de ejemplo que vienen en el libro. Para descargar el software Visual C# 2005 Express Edition y Visual Web Developer 2005 Express Edition visite el sitio: www.microsoft.com/spanish/msdn/vstudio/express/VCS/default.mspx

Microsoft cuenta con un foro dedicado, en idioma inglés, para obtener ayuda acerca del uso del software Express Edition: forums.microsoft.com/msdn/showForum.aspx?ForumID=24

En nuestro sitio Web www.deitel.com y en nuestro boletín de correo electrónico sin costo www.deitel. com/newsletter/subscribe.html proporcionamos actualizaciones sobre el estado de este software.

Características en Cómo programar en C#, 2ª edición Esta edición contiene muchas características nuevas y mejoradas.

Actualización de Visual Studio 2005, C# 2.0 y .NET 2.0 Actualizamos todo el libro para reflejar la versión más reciente de Microsoft Visual C# 2005. Las nuevas características son: • Se actualizaron las capturas de pantalla para el IDE Visual Studio 2005. • Accesores de propiedades con distintos modificadores. • Uso del Asistente de excepciones (Exception Assistant) para ver los datos de las excepciones (una nueva característica del depurador de Visual Studio 2005). • Uso de las técnicas de “arrastrar y soltar” para crear formularios de ventanas enlazados a datos en ADO.NET 2.0. • Uso de la ventana Orígenes de datos (Data Sources) para crear conexiones de datos a nivel de aplicación. • Uso de la clase subyacente.

BindingSource

para simplificar el proceso de enlazar controles a un origen de datos

• Uso de la clase BindingNavigator para habilitar las operaciones simples de navegación, inserción, eliminación y edición de los datos de una base de datos en un formulario de Windows. • Uso de la página Diseñador de páginas principales para crear una apariencia visual común para las páginas Web de ASP.NET.

Prefacio

xxi

• Uso de los menús de etiqueta inteligente (smart tag) de Visual Studio 2005 para realizar muchas de las tareas de programación más comunes cuando se arrastran nuevos controles en un formulario de Windows o en una página Web de ASP.NET. • Uso del servidor Web integrado de Visual Web Developer para probar las aplicaciones y servicios Web de ASP.NET 2.0. • Uso de la clase XmlDataSource para enlazar orígenes de datos de XML a un control. • Uso de la clase SqlDataSource para enlazar una base de datos de SQL Server a un control o a un conjunto de controles. • Uso de la clase ObjectDataSource para enlazar un control a un objeto que sirve como origen de datos. • Uso del control de inicio de sesión (login) de ASP.NET 2.0 y creación de nuevos controles de usuarios para personalizar el acceso a las aplicaciones Web. • Uso de genéricos y colecciones genéricas para crear modelos generales de métodos y clases que pueden declararse una vez y utilizarse con muchos tipos de datos. • Uso de colecciones genéricas del espacio de nombres Systems.Collections.Generic.

Nuevo diseño interior Para rediseñar el estilo interior de nuestros libros trabajamos con el equipo de creativos en Prentice Hall. En respuesta a las peticiones de los lectores, ahora colocamos los términos clave en negritas cursivas, para facilitar su referencia. Enfatizamos los componentes en pantalla mediante el tipo de letra Helvetica en negrita (por ejemplo, el menú Archivo) y el texto de los programas de C# mediante otro tipo de letra Lucida (por ejemplo, int x = 5).

Sombreado de la sintaxis Sombreamos la sintaxis de todo el código de C#, de manera similar al código de color de sintaxis que utilizan casi todos los entornos de desarrollo integrados y editores de código de C#. Esto mejora en forma considerable la legibilidad del código; una meta en especial importante, dado que este libro contiene más de 16,800 líneas de código. Nuestras convenciones de sombreado de sintaxis son las siguientes: los comentarios aparecen en cursiva las palabras clave aparecen en negrita, cursiva los errores y los delimitadores de scriptlets de JSP aparecen en color negro y negrita las constantes y los valores literales aparecen en color gris y negrita todos los demás elementos del código aparecen en letra normal y color negro

Resaltado del código El uso extensivo de resaltado del código facilita a los lectores detectar los segmentos de código que se presentan en cada programa; colocamos rectángulos color gris claro alrededor del código clave.

Método para presentar clases y objetos de manera anticipada En el capítulo 1 seguimos presentando los conceptos básicos de la tecnología orientada a objetos y la terminología relacionada. En el capítulo 9 de la edición anterior desarrollamos clases personalizadas, pero en esta edición empezamos a hacerlo en el capítulo 4. Hemos reescrito con mucho cuidado los capítulos 5 a 8, con base en nuestro “método de presentación de clases y objetos de manera anticipada”.

El cuidadoso proceso para poner a punto la programación orientada a objetos en los capítulos del 9 al 11 Hemos realizado una actualización muy precisa de Cómo programar en C#, 2ª edición. Esta edición es sin duda más clara y accesible; en especial si es la primera vez que trabaja con la programación orientada a objetos (POO). Reescribimos por completo los capítulos sobre la POO, en los cuales integramos un caso de estudio sobre la jerarquía de clases de una nómina de empleados y motivamos las interfaces con una jerarquía de cuentas por pagar.

xxii

Prefacio

Casos de estudio Incluimos muchos casos de estudio, algunos de los cuales abarcan varias secciones y capítulos: • La clase RegistroCalificaciones en los capítulos 4, 5, 6 y 8. • El sistema ATM opcional de DOO/UML en las secciones de ingeniería de software de los capítulos 1, 3 a 9 y 11. • La clase Tiempo en varias secciones del capítulo 9. • La aplicación de nómina de Empleado en los capítulos 10 y 11. • La aplicación LibroVisitantes de ASP.NET en el capítulo 21. • La aplicación segura de base de datos de libros de ASP.NET en el capítulo 21. • El servicio Web de reservación en una aerolínea en el capítulo 22.

Caso de estudio integrado: RegistroCalificaciones Para reforzar nuestra presentación anticipada de las clases, en los capítulos 4 a 6 y 8 presentamos un caso de estudio integrado en el que se utilizan clases y objetos. En este caso de estudio crearemos una clase llamada RegistroCalificaciones, a la que iremos agregando elementos. Esta clase representa el registro de calificaciones de un instructor y realiza varios cálculos con base en un conjunto de calificaciones de los estudiantes: encontrar el promedio, la calificación máxima y la mínima e imprimir un gráfico de barras. Nuestro objetivo es que usted se familiarice con los conceptos importantes de los objetos y las clases, a través de un ejemplo real de una clase sustancial. Desarrollamos esta clase desde cero, construyendo métodos a partir de instrucciones de control y de algoritmos desarrollados con mucho cuidado; se agregaron variables de instancia y arreglos según fue necesario para mejorar la funcionalidad de la clase.

Lenguaje Unificado de Modelado (UML): uso del UML 2.0 para desarrollar un diseño orientado a objetos de un ATM El Lenguaje Unificado de Modelado (UML™) se ha convertido en el lenguaje de modelado gráfico preferido para diseñar sistemas orientados a objetos. Todos los diagramas de UML en el libro cumplen con la especificación UML 2.0. Utilizamos los diagramas de clases de UML para representar visualmente las clases y sus relaciones de herencia, además, usamos los diagramas de actividades para demostrar el flujo de control en cada una de las diversas instrucciones de control de C#. Esta segunda edición incluye un nuevo caso de estudio opcional (pero que recomendamos ampliamente) sobre el diseño orientado a objetos mediante el uso de UML, y su revisión estuvo a cargo de un distinguido equipo de profesionales académicos y de la industria relacionados con el DOO/UML, incluyendo líderes en el campo por parte de Rational (los creadores del UML, que ahora son una división de IBM) y el Grupo de Administración de Objetos (responsable del mantenimiento y la evolución del UML). En este caso de estudio diseñamos e implementamos en su totalidad el software para un cajero automático (ATM) simple. Las secciones tituladas Caso de estudio de ingeniería de software al final de los capítulos 1, 3 a 9 y 11 presentan una introducción que lo lleva paso a paso en el diseño orientado a objetos mediante el uso de UML. Primero presentamos un subconjunto conciso y simplificado del UML 2.0, después lo guiamos a través de su primera experiencia de diseño, que está enfocada al diseñador/programador orientado a objetos novato. El caso de estudio no es un ejercicio, sino una experiencia de aprendizaje de extremo a extremo, la cual concluye con un paseo detallado por todo el código de C#. Las secciones tituladas Caso de estudio de ingeniería de software lo ayudan a desarrollar un diseño orientado a objetos para complementar los conceptos de programación orientada a objetos que comenzará a aprender en el capítulo 1 y que implementará en el capítulo 4. En la primera de estas secciones al final del capítulo 1, presentamos los conceptos básicos del DOO y su terminología relacionada. En las secciones opcionales de Caso de estudio de ingeniería de software al final de los capítulos 3 a 6 consideramos cuestiones más sustanciales al resolver un problema avanzado con las técnicas del DOO. Analizamos un documento de requerimientos común, en el cual se especifica la construcción de un sistema, se determinan las clases necesarias para implementar ese sistema, se determinan los atributos que deben tener esas clases, los comportamientos que deben exhibir y se especifica cómo deben interactuar las clases entre sí para cumplir con los requerimientos del sistema. En el apéndice J incluimos una implementación completa en C# del

Prefacio

xxiii

sistema orientado a objetos que diseñamos en los capítulos anteriores. Empleamos un proceso de diseño orientado a objetos incremental, desarrollado con mucho cuidado, para producir un modelo en UML para nuestro sistema ATM. Con base en este diseño producimos una robusta implementación funcional en C# mediante el uso de las nociones clave de programación, incluyendo clases, objetos, encapsulamiento, visibilidad, composición, herencia y polimorfismo.

Formularios Web, controles Web y ASP.NET 2.0 La plataforma .NET permite a los desarrolladores crear aplicaciones basadas en Web robustas y escalables. La tecnología del lado del servidor .NET de Microsoft, conocida como Páginas Activas de Servidor (ASP) .NET, permite a los programadores crear documentos Web que respondan a las peticiones de los clientes. Para habilitar las páginas Web interactivas, los programas del lado del servidor procesan información que los usuarios introducen en formularios en HTML. ASP.NET provee las capacidades de programación visual mejoradas, similares a las que se utilizan en la creación de formularios de Windows para programas de escritorio. Los programadores pueden crear páginas Web en forma visual, arrastrando y soltando controles en formularios Web. En el capítulo 21, ASP. NET, Formularios Web y Controles Web, introducimos estas poderosas tecnologías.

Servicios Web y ASP.NET 2.0 La estrategia .NET de Microsoft integra a Internet y la Web en el desarrollo y el despliegue de software. La tecnología de servicios Web permite compartir información, el comercio electrónico y demás interacciones mediante el uso de protocolos y tecnologías estándar de Internet, como el Protocolo de Transferencia de Hipertexto (HTTP), el Lenguaje de Marcado Extensible (XML) y el Protocolo Simple de Acceso a Objetos (SOAP). Los servicios Web permiten a los programadores encapsular la funcionalidad de una aplicación de forma que convierta a la Web en una biblioteca de componentes de software reutilizables. En el capítulo 22 presentamos un servicio Web que permite a los usuarios manipular enteros enormes (huge); enteros demasiado grandes como para representarlos con los tipos de datos integrados en C#. En este ejemplo, un usuario introduce dos enteros enormes y oprime botones para invocar servicios Web que suman, restan y comparan ambos enteros. También presentamos un servicio Web de Blackjack y un sistema de reservación de una aerolínea, controlado por una base de datos.

Programación orientada a objetos La programación orientada a objetos es la técnica más utilizada a nivel mundial para desarrollar software robusto y reutilizable. Este libro ofrece un extenso tratamiento de las características de programación orientada a objetos de C#. En el capítulo 4 se proporciona una introducción a la creación de clases y objetos. Estos conceptos se extienden en el capítulo 9. El capítulo 10 muestra cómo crear nuevas y poderosas clases con rapidez, mediante el uso de la herencia para “absorber” las capacidades de las clases existentes. El capítulo 11 lo familiariza con los conceptos cruciales del polimorfismo, las clases abstractas, las clases concretas y las interfaces, para facilitar manipulaciones poderosas entre objetos que pertenecen a una jerarquía de herencia.

XML El uso del Lenguaje de Marcado Extensible (XML) es cada vez más amplio en la industria del desarrollo de software y en la comunidad del comercio electrónico (e-business), además de ser predominante en la plataforma .NET. Como XML es una tecnología independiente de la plataforma para describir datos y crear lenguajes de marcado, su portabilidad de datos se integra a la perfección con las aplicaciones y servicios portables basados en C#. En el capítulo 19 se introduce el XML, el marcado del XML y las tecnologías como DTDs y Schema, las cuales se utilizan para validar el contenido de los documentos de XML. También explicamos cómo manipular documentos de XML mediante programación, utilizando el Modelo de Objetos de Documento (DOM™) y cómo transformar documentos de XML en otros tipos de documentos a través de la tecnología de Transformación del Lenguaje de Hojas de estilo Extensible (XSLT).

ADO.NET 2.0 Las bases de datos almacenan enormes cantidades de información a las que deben tener acceso los individuos y las organizaciones para realizar sus actividades comerciales. Como evolución de la tecnología de Objetos de Datos ActiveX de Microsoft (ADO), ADO.NET representa un nuevo enfoque para la creación de aplicaciones que interactúan con bases de datos. ADO.NET utiliza XML y un modelo de objetos mejorado para proporcionar

xxiv

Prefacio

a los desarrolladores las herramientas que necesitan para acceder a las bases de datos y manipularlas a través de aplicaciones multinivel, de misión específica, extensibles y de gran escala. En el capítulo 20 se introducen las capacidades de ADO.NET y el Lenguaje de Consulta Estructurado (SQL) para manipular bases de datos.

Depurador de Visual Studio 2005 En el apéndice C explicamos cómo utilizar las características clave del depurador, como establecer “puntos de interrupción” (breakpoints) e “inspecciones” (watches), entrar y salir de los métodos y examinar la pila de llamadas a los métodos.

Enfoque de enseñanza Cómo programar en C#, 2ª edición contiene una vasta colección de ejemplos que se han probado en Windows 2000 y Windows XP. El libro se concentra en los principios de la buena ingeniería de software, haciendo hincapié en la claridad de los programas. Evitamos la terminología arcana y las especificaciones sintácticas para favorecer la enseñanza mediante ejemplos. Somos educadores que enseñamos temas de vanguardia, sobre la materia, en salones de clases alrededor del mundo. El doctor Harvey M. Deitel tiene 20 años de experiencia de enseñanza universitaria y 15 en la enseñanza en la industria. Paul Deitel tiene 12 años de experiencia de enseñanza en la industria, además de ser un experimentado instructor corporativo que ha impartido cursos de todos los niveles a clientes del gobierno, la industria, la milicia y académicos de Deitel & Associates.

Aprenda C# a través del método de código activo (Live-Code ) Cómo programar en C#, 2ª edición está lleno de ejemplos de código activo; cada nuevo concepto se presenta en el contexto de una aplicación completa y funcional en C#, seguida inmediatamente por una o más ejecuciones que muestran las entradas y salidas del programa. Este estilo ejemplifica la manera en que enseñamos y escribimos acerca de la programación. A este método de enseñar y escribir le llamamos método del código activo (o método LIVE-CODE™). Utilizamos lenguajes de programación para enseñar lenguajes de programación.

Acceso a World Wide Web Todos los ejemplos de código fuente para esta segunda edición (y demás publicaciones de los autores) están disponibles (en inglés) en Internet, de manera que puedan descargarse de los siguientes sitios Web: www.deitel.com/books/csharpforprogrammers2 www.pearsoneducacion.net/deitel

Puede registrarse en forma rápida y sencilla, las descargas son gratuitas. Baje todos los ejemplos y después ejecute cada programa a medida que lea las correspondientes discusiones en el libro. Hacer cambios a los ejemplos e inmediatamente ver los efectos de esos cambios es una excelente manera de mejorar su experiencia de aprendizaje de C#.

Objetivos Cada capítulo comienza con una declaración de los objetivos.

Frases Los objetivos de aprendizaje van seguidos de una o más frases. Algunas son graciosas, otras filosóficas y algunas más ofrecen ideas interesantes.

Plan general El plan general de cada capítulo le permitirá abordar el material de manera ordenada jerárquicamente, para que se pueda anticipar a lo que está por venir y así establecer un ritmo de aprendizaje cómodo y efectivo.

16,785 líneas de código en 213 programas de ejemplo (con la salida de los programas) Nuestros programas de código activo varían en tamaño, desde unas cuantas líneas de código hasta ejemplos importantes con cientos de líneas de código (por ejemplo, nuestra implementación del sistema de ATM contiene 655 líneas de código). Cada programa va seguido por una ventana que contiene los resultados que se producen al ejecutar dicho programa; de esta manera usted podrá confirmar que los programas se ejecuten como se esperaba.

Prefacio

xxv

Nuestros programas demuestran las diversas características de C#. La sintaxis del código va sombreada; las palabras clave de C#, los comentarios y demás texto del programa se enfatizan con variaciones de texto en negrita, cursiva y de color gris. Esto facilita la lectura del código, en especial cuando se leen los programas más grandes.

700 Ilustraciones/Figuras Incluimos una gran cantidad de gráficas, tablas, dibujos lineales, programas y salidas de programa. Modelamos el flujo de control en las instrucciones de control mediante diagramas de actividad en UML. Los diagramas de clases en UML modelan los campos, constructores y métodos de las clases. Utilizamos tipos adicionales de diagramas en UML a lo largo de nuestro caso de estudio opcional del ATM de DOO/UML.

316 Tips de programación Hemos incluido tips de programación para enfatizar los aspectos importantes del desarrollo de programas. Resaltamos estos tips como Buenas prácticas de programación, Errores comunes de programación, Tips para prevenir errores, Observaciones de apariencia visual, Tips de rendimiento, Tips de portabilidad y Observaciones de ingeniería de software. Estos tips y prácticas representan lo mejor que hemos recabado a lo largo de seis décadas (combinadas) de experiencia en la programación y la enseñanza. Este método es similar al de resaltar axiomas, teoremas y corolarios en los libros de matemáticas; proporciona una base sólida sobre la cual se puede construir buen software.

Buena práctica de programación Las buenas prácticas de programación llaman la atención hacia todas las técnicas que ayudan a los desarrolladores a producir programas más legibles, más fáciles de entender y mantener.

Error común de programación Los desarrolladores que están aprendiendo un lenguaje a menudo cometen ciertos tipos de errores. Al poner atención en estos Errores comunes de programación se reduce la probabilidad de cometerlos.

Tip de prevención de errores Cuando diseñamos por primera vez este tipo de tips, pensamos que contendrían sugerencias estrictamente para exponer errores y eliminarlos de los programas. De hecho, muchos de los tips describen aspectos de C# que evitan de antemano la aparición de errores en los programas, con lo cual se simplifican los procesos de prueba y depuración. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual Ofrecemos las Observaciones de apariencia visual para resaltar las convenciones de la interfaz gráfica de usuario. Estas observaciones le ayudarán a diseñar interfaces gráficas de usuario atractivas y amigables para el usuario en conformidad con las normas de la industria.

Tip de rendimiento A los desarrolladores les gusta “turbo cargar” sus programas. Incluimos Tips de rendimiento que resaltan las oportunidades para mejorar el rendimiento de los programas: hacer que se ejecuten más rápido o minimizar la cantidad de memoria que ocupan.

Tip de portabilidad Incluimos Tips de portabilidad para ayudarle a escribir código portable y para explicar cómo C# logra su alto grado de portabilidad.

Observación de ingeniería de software El paradigma de la programación orientada a objetos requiere de una completa reconsideración de la manera en que construimos los sistemas de software. C# es un lenguaje efectivo para lograr una buena ingeniería de software. Las Observaciones de ingeniería de software resaltan los asuntos de arquitectura y diseño, lo cual afecta la construcción de sistemas de software, en especial los de gran escala.

xxvi

Prefacio

Sección de conclusión Cada capítulo termina con una breve sección de conclusión, en la cual se recapitula el contenido del capítulo y las transiciones hacia el siguiente capítulo.

Aproximadamente 5500 entradas en el índice Hemos incluido un índice extenso, el cual es en especial útil para los desarrolladores que utilizan el libro como una referencia.

“Doble indexado” de ejemplos de código activo en C# Cómo programar en C#, 2ª edición cuenta con 213 ejemplos de código activo, los cuales hemos indexado dos veces. Para cada programa de código fuente en el libro, indexamos la leyenda de la figura en forma alfabética y también como un elemento de subíndice bajo “Ejemplos”. Esto facilita la búsqueda de ejemplos en los que se utilizan características específicas.

Un paseo por el caso de estudio opcional sobre diseño orientado a objetos con UML En esta sección daremos un paseo por el caso de estudio opcional del libro, el cual trata sobre el diseño orientado a objetos con UML. En este paseo echaremos un vistazo al contenido de las nueve secciones de Caso de estudio de ingeniería de software (en los capítulos 1, 3 a 9 y 11). Después de completar estas secciones, usted estará muy familiarizado con el diseño y la implementación orientados a objetos para una aplicación importante en C#. El diseño presentado en el caso de estudio del ATM se desarrolló en Deitel & Associates, Inc.; varios profesionales de la industria lo analizaron con detalle. Nuestro principal objetivo a lo largo del proceso de diseño fue crear un diseño simple que fuera más claro para los desarrolladores que incursionan por primera vez en el DOO y el UML, y que al mismo tiempo se siguieran demostrando los conceptos clave del DOO y las técnicas relacionadas de modelado en UML. Sección 1.9 (la única sección requerida para el caso de estudio), Caso de estudio de ingeniería de software: introducción a la tecnología de objetos y al UML. En esta sección presentamos el caso de estudio de diseño orientado a objetos con UML. También presentamos los conceptos básicos y la terminología relacionada con la tecnología de los objetos, incluyendo: clases, objetos, encapsulamiento, herencia y polimorfismo. Hablamos sobre la historia de UML. Ésta es la única sección requerida del caso de estudio. Sección 3.10 (opcional), Caso de estudio de ingeniería de software: análisis del documento de requerimientos del ATM. En esta sección hablamos sobre un documento de requerimientos, en el cual se especifican los requerimientos para un sistema que diseñaremos e implementaremos: el software para un cajero automático simple (ATM). Investigamos la estructura y el comportamiento de los sistemas orientados a objetos en general. Hablamos sobre cómo el UML facilitará el proceso de diseño en las subsiguientes secciones del Caso de estudio de ingeniería de software, al proporcionar diversos diagramas para modelar nuestro sistema. Incluimos una lista de URLs y referencias a libros sobre diseño orientado a objetos con UML. Hablamos sobre la interacción entre el sistema ATM especificado por el documento de requerimientos y su usuario. En especial, investigamos los escenarios que pueden ocurrir entre el usuario y el sistema en sí; a éstos se les llama casos de uso. Modelamos estas interacciones mediante el uso de los diagramas de caso de uso en UML. Sección 4.11 (opcional), Caso de estudio de ingeniería de software: identificación de las clases en el documento de requerimientos del ATM. En esta sección comenzamos a diseñar el sistema ATM. Identificamos sus clases mediante la extracción de los sustantivos y las frases nominales del documento de requerimientos. Ordenamos estas clases en un diagrama de clases de UML, el cual describe la estructura de las clases de nuestra simulación. Este diagrama de clases también describe las relaciones, conocidas como asociaciones, entre las clases. Sección 5.12 (opcional), Caso de estudio de ingeniería de software: identificación de los atributos de las clases en el sistema ATM. En esta sección nos enfocamos en los atributos de las clases descritas en la sección 3.10. Una clase contiene tanto atributos (datos) como operaciones (comportamientos). Como veremos en secciones posteriores, los cambios en los atributos de un objeto a menudo afectan el comportamiento de ese objeto. Para determinar los atributos para las clases en nuestro caso de estudio, extraemos los adjetivos que describen los sustantivos y las frases nominales (que definieron nuestras clases) del documento de requerimientos, y después colocamos los atributos en el diagrama de clases que creamos en la sección 3.10.

Prefacio

xxvii

Sección 6.9 (opcional), Caso de estudio de ingeniería de software: identificación de los estados y las actividades de los objetos en el sistema ATM. En esta sección veremos cómo un objeto, en un tiempo dado, ocupa una condición específica conocida como estado. Una transición de estado ocurre cuando ese objeto recibe un mensaje para cambiar de estado. UML cuenta con el diagrama de máquina de estado, el cual identifica el conjunto de posibles estados que puede ocupar un objeto y modela las transiciones de estado de ese objeto. Un objeto también tiene una actividad: el trabajo que desempeña durante su tiempo de vida. UML cuenta con el diagrama de actividades: un diagrama de flujo que modela la actividad de un objeto. En esta sección utilizamos ambos tipos de diagramas para comenzar a modelar los aspectos específicos del comportamiento de nuestro sistema ATM, como la forma en que el ATM lleva a cabo una transacción de retiro de dinero y cómo responde cuando el usuario es autenticado. Sección 7.15 (opcional), Caso de estudio de ingeniería de software: identificación de las operaciones de las clases en el sistema ATM. En esta sección identificamos las operaciones, o servicios, de nuestras clases. Del documento de requerimientos extraemos los verbos y las frases verbales que especifican las operaciones para cada clase. Después modificamos el diagrama de clases de la sección 3.10 para incluir cada operación con su clase asociada. En este punto del caso de estudio ya hemos recopilado toda la información posible del documento de requerimientos. No obstante y a medida que los futuros capítulos introduzcan temas como la herencia, modificaremos nuestras clases y diagramas. Sección 8.14 (opcional), Caso de estudio de ingeniería de software: colaboración entre los objetos en el sistema ATM. En esta sección hemos creado un “borrador” del modelo para nuestro sistema ATM. Aquí vemos cómo funciona. Investigamos el comportamiento de la simulación al hablar sobre las colaboraciones: mensajes que los objetos se envían entre sí para comunicarse. Las operaciones de las clases que hemos descubierto en la sección 6.9 resultan ser las colaboraciones entre los objetos de nuestro sistema. Determinamos las colaboraciones y después las reunimos en un diagrama de comunicación: el diagrama de UML para modelar colaboraciones. Este diagrama revela qué objetos colaboran y cuándo lo hacen. Presentamos un diagrama de comunicación de las colaboraciones entre los objetos para realizar una solicitud de saldo en el ATM. Después presentamos el diagrama de secuencia de UML para modelar las interacciones en un sistema. Este diagrama enfatiza el ordenamiento cronológico de los mensajes. Un diagrama de secuencia modela la manera en que interactúan los objetos en el sistema para llevar a cabo transacciones de retiro y depósito. Sección 9.17 (opcional), Caso de estudio de ingeniería de software: cómo empezar a programar las clases del sistema ATM. En esta sección tomamos un descanso, dejando a un lado el diseño del comportamiento de nuestro sistema. Comenzamos el proceso de implementación para enfatizar el material descrito en el capítulo 8. Utilizando el diagrama de clases de UML de la sección 3.10 y los atributos y operaciones descritas en las secciones 4.11 y 6.9, mostramos cómo implementar una clase en C# a partir de un diseño. No implementamos todas las clases, ya que no hemos completado el proceso de diseño. Basándonos en nuestros diagramas de UML, creamos código para la clase Retiro. Sección 11.9 (opcional), Caso de estudio de ingeniería de software: cómo incorporar la herencia y el polimorfismo al sistema ATM. En esta sección continuamos con nuestra discusión acerca de la programación orientada a objetos. Consideramos la herencia: las clases que comparten características comunes pueden heredar atributos y operaciones de una clase “base”. En esta sección investigamos cómo nuestro sistema ATM puede beneficiarse del uso de la herencia. Documentamos nuestros descubrimientos en un diagrama de clases que modela las relaciones de herencia; UML se refiere a estas relaciones como generalizaciones. Después modificamos el diagrama de clases de la sección 3.10 mediante el uso de la herencia para agrupar clases con características similares. En esta sección concluimos el diseño de la porción correspondiente al modelo de nuestra simulación. En el apéndice J implementamos el modelo en forma de código en C#. Apéndice J: Código del caso de estudio del ATM. La mayor parte del caso de estudio hace hincapié en el diseño del modelo (es decir, los datos y la lógica) del sistema ATM. En este apéndice implementamos ese modelo en C#. Utilizando todos los diagramas de UML que creamos, presentamos las clases de C# necesarias para implementar el modelo. Aplicamos los conceptos del diseño orientado a objetos con el UML y la programación orientada a objetos en C# que usted aprendió a lo largo del libro. Al final de este apéndice usted habrá completado el diseño y la implementación de un sistema real, por lo que deberá sentir la confianza de lidiar con sistemas más grandes, como los que construyen los ingenieros profesionales de software. Apéndice K, UML 2: tipos de diagramas adicionales. En este apéndice presentamos las generalidades sobre los tipos de diagramas de UML que no se incluyen en el caso de estudio de DOO/UML.

xxviii

Prefacio

Boletín de correo electrónico gratuito DEITEL® Buzz Online Nuestro boletín de correo electrónico gratuito Deitel ® Buzz Online incluye, sin costo alguno, comentarios sobre las tendencias y desarrollos en la industria, vínculos hacia artículos y recursos de nuestros libros publicados y de las próximas publicaciones, itinerarios de liberación de productos, erratas, retos, anécdotas, información sobre nuestros cursos de capacitación corporativa impartidos por instructores, y mucho más. También es una buena forma para mantenerse actualizado acerca de las cuestiones relacionadas con este libro. Para suscribirse, visite el sitio Web: www.deitel.com/newsletter/subscribe.html

Nota: sitio administrado por los autores del libro.

Agradecimientos Es un gran placer para nosotros agradecer los esfuerzos de muchas personas cuyos nombres tal vez no aparezcan en la portada, pero cuyo arduo trabajo, cooperación, amistad y comprensión fueron cruciales para la producción del libro. Muchas personas en Deitel & Associates, Inc., dedicaron largas horas a este proyecto. • Andrew B. Goldberg, graduado de Ciencias Computacionales por la Amherst College, contribuyó en la actualización de los capítulos 19 a 22; también colaboró en el diseño y fue coautor en el nuevo caso de estudio opcional del ATM de DOO/UML. Además fue coautor del apéndice K. • Su Zhang posee títulos de B.Sc. y una Maestría en Ciencias Computacionales por la Universidad McGill. Su Zhang contribuyó en los capítulos 25 y 26, así como también en el apéndice J. • Cheryl Yeager, graduado de la Universidad de Boston como licenciado en Ciencias Computacionales, ayudó a actualizar los capítulos 13-14. • Barbara Deitel, Directora en Jefe de Finanzas en Deitel & Associates, Inc., aplicó las correcciones al manuscrito del libro. • Abbey Deitel, Presidente de Deitel & Associates, Inc., licenciado en Administración Industrial por la Universidad Carnegie Mellon, fue coautor del capítulo 1. • Christi Kelsey, graduada de la Universidad Purdue con título en comercio y especialización secundaria en sistemas de información, fue coautora del capítulo 2, el prefacio y el apéndice C. Ella editó el índice y paginó todo el manuscrito. También trabajó en estrecha colaboración con el equipo de producción en Prentice Hall, coordinando prácticamente todos los aspectos relacionados con la producción de este libro. También queremos agradecer a los tres participantes de nuestro Programa de pasantía para estudiantes sobresalientes (Honors Internship) y los programas de educación cooperativa que contribuyeron a esta publicación: Nick Santos, con especialidad en Ciencias Computacionales en el Dartmouth Collage; Jeffrey Peng, estudiante de Ciencias Computacionales en la Universidad Cornell y William Chen, estudiante de Ciencias Computacionales en la Universidad Cornell. Nos sentimos afortunados de haber trabajado en este proyecto con el talentoso y dedicado equipo de editores profesionales en Pearson Education/PTG. En especial, apreciamos los extraordinarios esfuerzos de Mark Taub, Editor asociado de Prentice Hall/PTR, y Marcia Horton, Directora editorial de la División de Ciencias computacionales e ingeniería de Prentice Hall. Noreen Regina y Jennifer Cappello hicieron un extraordinario trabajo al reclutar los equipos de revisión para el libro y administrar el proceso de revisión. Sandra Schroeder hizo un maravilloso trabajo al rediseñar por completo la portada del libro. Vince O’Brien, Bob Engelhardt, Donna Crilly y Marta Samsel hicieron un estupendo trabajo al administrar la producción del libro. Nos gustaría agradecer de manera especial a Dan Fernandez, Gerente de producto de C#, y a Janie Schwark, Gerente Comercial en Jefe de la División de Mercadotecnia para el Desarrollador, ambos de Microsoft, por su esfuerzo especial al trabajar con nosotros en este proyecto. También agradecemos a los muchos otros miembros del equipo de Microsoft que se tomaron el tiempo para responder a nuestras preguntas a lo largo de este proceso: Anders Hejlsburg, Ayudante técnico (C#) Brad Abrams, Gerente de Programa en Jefe (.NET Framework) Jim Miller, Arquitecto de Software (.NET Framework) Joe Duffy, Gerente de Programa (.NET Framework) Joe Stegman, Gerente de Programa en Jefe (Windows Forms) Kit George, Gerente de Programa (.NET Framework) Luca Bolognese, Gerente de Programa en Jefe (C#)

Prefacio

xxix

Luke Hoban, Gerente de Programa (C#) Mads Torgersen, Gerente de Programa (C#) Peter Hallam, Ingeniero de Diseño de Software (C#) Scout Nonnenberg, Gerente de Programa (C#) Shamez Rajan, Gerente de Programa (Visual Basic) Deseamos agradecer también los esfuerzos de nuestros revisores. Al adherirse a un apretado itinerario de trabajo, escudriñaron el texto y los programas, proporcionando innumerables sugerencias para mejorar la precisión y finalización de la presentación. Revisores de Microsoft George Bullock, Gerente de Programa en Microsoft, Equipo Comunitario Microsoft.com Darmesh Chauhan, Microsoft Shon Katzenberger, Microsoft Matteo Taveggia, Microsoft Matt Tavis, Microsoft Revisores de la Industria Alex Bondarev, Investor’s Bank and Trust Peter Bromberg, Arquitecto en Jefe de Merrill Lynch y MVP de C# Vijay Cinnakonda, TrueCommerce, Inc. Jay Cook, Alcon Laboratorios Jeff Cowan, Magenic, Inc. Ken Cox, Consultor independiente, escritor y desarrollador, y MVP de ASP.NET Stochio Goutsev, Consultor independiente, escritor y desarrollador, y MVP de C# James Huddleston, Consultor independiente Rex Jaeschke, Consultor independiente Saurabh Ñandú, AksTech Solutions Pvt. Ltd. Simon North, Quintiq BV Mike O’Brien, Departamento de Desarrollo de Empleos del Estado de California José Antonio González Seco, Parlamento de Andalucía Devan Shepard, Xmalpha Technologies Pavel Tsekov, Caesar BSC John Varghese, UBS Stacey Yasenka, Desarrollador de software en Hyland Software y MVP de C# Revisores académicos Rekha Bhowmik, Universidad Luterana de California Ayad Boudiab, Georgia Perimiter Collage Harlan Brewer, Universidad de Cincinnati Sam Gill, Universidad Estatal de San Francisco Gavin Osborne, Saskatchewan Institute of Applied Science and Technology Catherine Wyman, DeVry-Phoenix Bueno, eso es todo. C# es un poderoso lenguaje de programación que le ayudará a escribir programas con rapidez y efectividad. C# se adapta perfectamente al ámbito del desarrollo de sistemas a nivel empresarial, para ayudar a las organizaciones a construir sus sistemas críticos de información para los negocios y de misión crítica. Le agradeceremos sinceramente que, a medida que lea el libro, nos haga saber sus comentarios, críticas, correcciones y sugerencias para mejorar el texto. Por favor dirija toda su correspondencia a: [email protected]

Le responderemos oportunamente y publicaremos las correcciones y aclaraciones en nuestro sitio Web: www.deitel.com

¡Esperamos que disfrute leyendo Cómo programar en C#, 2 a edición tanto como nosotros disfrutamos el escribirlo! Dr. Harvey M. Deitel Paul J. Deitel

xxx

Prefacio

Acerca de los autores

El Dr. Harvey M. Deitel, Presidente y Jefe de Estrategias de Deitel & Associates, Inc., tiene 44 años de experiencia en el campo de la computación, esto incluye un amplio trabajo académico y en la industria. El Dr. Deitel tiene una licenciatura y una maestría por el Massachussets Institute of Technology y un doctorado de la Boston University. Trabajó en los proyectos pioneros de sistemas operativos de memoria virtual en IBM y el MIT, con los cuales se desarrollaron técnicas que ahora se implementan ampliamente en sistemas como UNIX, Linux y Windows XP. Tiene 20 años de experiencia como profesor universitario, incluyendo un puesto vitalicio y el haber sido presidente del Departamento de Ciencias Computacionales en el Boston Collage antes de fundar, con su hijo Paul J. Deitel, Deitel & Associates, Inc. Él y Paul son coautores de varias docenas de libros y paquetes multimedia, y piensan escribir muchos más. Los textos de los Deitel se han ganado el reconocimiento internacional y han sido traducidos al japonés, alemán, ruso, español, chino tradicional, chino simplificado, coreano, francés, polaco, italiano, portugués, griego, urdú y turco. El Dr. Deitel ha impartido cientos de seminarios profesionales para grandes empresas, instituciones académicas, organizaciones gubernamentales y diversos sectores del ejército. Paul J. Deitel, CEO y Jefe Técnico de Deitel & Associates, Inc., es egresado del Sloan School of Management del Massachussets Institute of Technology, en donde estudió Tecnología de la Información. A través de Deitel & Associates, Inc., ha impartido cursos de Java, C, C++, Internet y World Wide Web a clientes de la industria, incluyendo IBM, Sun Microsystems, Dell, Lucent Technologies, Fidelity, la NASA en el Centro Espacial Kennedy, el Nacional Severe Storm Laboratory, White Sands Missile Range, Rogue Wave Software, Boeing, Stratus, Cambridge Technology Partners, Open Environment Corporation, One Wave, Hyperion Software, Adra Sistems, Entergy, CableData Systems y muchos más. Paul es uno de los instructores de Java más experimentados, ya que ha impartido cerca de 100 cursos de Java profesionales. También ha ofrecido conferencias sobre C++ y Java para la Boston Chapter de la Association for Computing Machinery. Él y su padre, el Dr. Harvey M. Deitel, son autores de los libros de texto de lenguajes de programación con más ventas en el mundo.

Acerca de Deitel & Associates, Inc. Deitel & Associates, Inc. es una empresa reconocida a nivel mundial, dedicada al entrenamiento corporativo y la creación de contenido, con especialización en lenguajes de programación de computadoras, tecnología de software para Internet/World Wide Web, educación en tecnología de objetos y desarrollo de comercios en Internet. La empresa proporciona cursos impartidos por instructores sobre los principales lenguajes y plataformas de programación, como Java, Java Avanzado, C, C++, los lenguajes de programación .NET, XML, Perl, Piton; tecnología de objetos y programación en Internet y World Wide Web. Los fundadores de Deitel & Associates, Inc. son el Dr. Harvey M. Deitel y Paul J. Deitel. Sus clientes incluyen muchas de las empresas de cómputo más grandes del mundo, agencias gubernamentales, sectores del ejército y empresas comerciales. A lo largo de su sociedad editorial de 29 años con Prentice Hall, Deitel & Associates, Inc. ha publicado libros de texto de vanguardia sobre programación, libros profesionales, multimedia interactiva en CD como los Cyber Classrooms, cursos de capacitación basados en Web y contenido electrónico para sistemas de administración de cursos populares como WebCT, BlackBoard y CourseCompass de Pearson. Deitel & Associates, Inc. y los autores pueden ser contactados mediante correo electrónico en: [email protected]

Para conocer más acerca de Deitel & Associates, Inc., sus publicaciones y su currículo de Capacitación corporativa mundial de la serie DIVE INTO™, consulte las últimas páginas de este libro o visite el sitio Web: www.deitel.com

y suscríbase al boletín de correo electrónico gratuito Deitel ® Buzz Online en: www.deitel.com/newsletter/subscribe.html

Las personas que deseen comprar libros de Deitel, Cyber Classrooms, Cursos de capacitación completos y cursos de capacitación basados en Web pueden hacerlo a través de la siguiente página Web: www.deitel.com/books/index.html

Las empresas e instituciones académicas que deseen hacer pedidos en grandes cantidades pueden hacerlo directamente con Pearson Educación.

1

Introducción a las computadoras, Internet y Visual C# El principal mérito del lenguaje es la claridad. —Galen

OBJETIVOS En este capítulo aprenderá lo siguiente: Q

La historia del lenguaje de programación Visual C#.

Q

Algunos fundamentos sobre la tecnología de los objetos.

Q

La historia del UML: el lenguaje de modelado de sistemas orientado a objetos estándar en la industria.

Q

La historia de Internet y World Wide Web.

Q

La motivación detrás de la iniciativa .NET de Microsoft y sus generalidades, en la cual se involucra a Internet en el desarrollo y uso de sistemas de software.

Q

Cómo probar una aplicación de Visual C# 2005 que le permitirá dibujar en la pantalla.

Los pensamientos elevados deben tener un lenguaje elevado. —Aristófanes

Nuestra vida se malgasta con los detalles… Simplifica, simplifica. —Henry David Thoreau

Mi objetivo sublime lo lograré con el tiempo. —W. S. Gilbert

El hombre es aún la computadora más extraordinaria de todas. —John F. Kennedy

Pla n g e ne r a l

2

Capítulo 1 Introducción a las computadoras, Internet y Visual C#

1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9

Introducción Sistema operativo Windows® de Microsoft C# Internet y World Wide Web Lenguaje de Marcado Extensible (XML) Microsoft .NET .NET Framework y Common Language Runtime Prueba de una aplicación en C# (Única sección obligatoria del caso de estudio) Caso de estudio de ingeniería de software: Introducción a la tecnología de objetos y el UML 1.10 Conclusión 1.11 Recursos Web

1.1 Introducción ¡Bienvenido a Visual C# 2005! Nos hemos esforzado mucho para ofrecerle información precisa y completa relacionada con este poderoso lenguaje de programación, que de ahora en adelante llamaremos simplemente C#. Este lenguaje es apropiado para crear sistemas de información complejos. Esperamos que su trabajo con este libro sea una experiencia informativa, retadora y divertida. El principal objetivo de este libro es lograr la legibilidad de los programas a través de las técnicas comprobadas de la programación orientada a objetos (POO) y la programación controlada por eventos. Tal vez lo más importante de este libro es que presenta cientos de programas completos y funcionales, además de la descripción de sus entradas y salidas. A esto le llamamos método de código activo. Puede descargar todos los ejemplos del libro del sitio web www.deitel.com/books/csharpforprogrammers2/index.html y de www.prenhall.com/ deitel. Esperamos que disfrute la experiencia de aprendizaje con este libro. Está a punto de emprender un camino lleno de retos y recompensas. Si tiene dudas a medida que avance, no dude en enviarnos un correo electrónico a [email protected]

Para mantenerse actualizado con los avances sobre C# en Deitel & Associates, y recibir actualizaciones en relación con este libro, suscríbase a nuestro boletín electrónico gratuito Deitel ® Buzz Online en: www.deitel.com/newsletter/subscribe.html

1.2 Sistema operativo Windows® de Microsoft Durante las décadas de 1980 y 1990, Microsoft Corporation se convirtió en la compañía dominante de software. En 1981 sacó al mercado la primera versión del sistema operativo DOS para la computadora personal IBM. A mediados de la década de 1980, desarrolló el sistema operativo Windows, una interfaz gráfica de usuario creada para ejecutarse sobre el DOS. En 1990, Microsoft liberó el sistema Windows 3.0, esta nueva versión contaba con una interfaz amigable para el usuario y una funcionalidad robusta. El sistema operativo Windows se hizo increíblemente popular después de la liberación, en 1992, de Windows 3.1, cuyos sucesores (Windows 95 y Windows 98) prácticamente atajaron el mercado de los sistemas operativos de escritorio a finales de la década de 1990. Estos sistemas operativos, que tomaron muchos conceptos (como los iconos, los menús y las ventanas) popularizados por los primeros sistemas operativos Apple Macintosh, permitían a los usuarios navegar por varias aplicaciones al mismo tiempo. En 1993, Microsoft entró al mercado de los sistemas operativos corporativos con la presentación de Windows NT ®. Windows XP está basado en el sistema operativo Windows NT y se presentó en el 2001. Este sistema operativo combina las líneas de sistemas operativos corporativos y para el consumidor de Microsoft. Hasta ahora, Windows es el sistema operativo más utilizado en el mundo. El mayor competidor del sistema operativo Windows es Linux. Este nombre se deriva de Linus (por Linus Torvalds, quien desarrolló Linux) y UNIX (el sistema operativo en el cual está basado Linux); este último se

1.4 Internet y World Wide Web

3

desarrolló en los Laboratorios Bell y está escrito en el lenguaje de programación C. Linux es un sistema operativo gratuito, de código fuente abierto. A diferencia de Windows, que es propietario (Microsoft posee los derechos de propiedad y el control); el código fuente de Linux está disponible sin costo para los usuarios, y pueden modificarlo para ajustarlo a sus necesidades.

1.3 C# El avance de las herramientas de programación y los dispositivos electrónicos para el consumidor (por ejemplo, los teléfonos celulares y los PDAs) ha creado problemas y nuevos requerimientos. La integración de componentes de software de diversos lenguajes fue difícil y los problemas de instalación eran comunes, ya que las nuevas versiones de los componentes compartidos eran incompatibles con el software anterior. Los desarrolladores también descubrieron que necesitaban aplicaciones basadas en Web, que pudieran accesarse y utilizarse a través de Internet. Como resultado de la popularidad de los dispositivos electrónicos móviles, los desarrolladores de software se dieron cuenta que sus clientes ya no estaban restringidos sólo a las computadoras de escritorio. Reconocieron que hacía falta software accesible para todos y que estuviera disponible a través de casi cualquier tipo de dispositivo. Para satisfacer estas necesidades, en el año 2000, Microsoft anunció el lenguaje de programación C#. Este lenguaje, desarrollado en Microsoft por un equipo dirigido por Anders Helsjberg y Scott Wiltamuth, se diseñó en específico para la plataforma .NET (de la cual hablaremos en la sección 1.6) como un lenguaje que permitiera a los programadores migrar con facilidad hacia .NET. Tiene sus raíces en C, C++ y Java; adapta las mejores características de cada uno de estos lenguajes y agrega nuevas características propias. C# está orientado a objetos y contiene una poderosa biblioteca de clases, que consta de componentes preconstruidos que permiten a los programadores desarrollar aplicaciones con rapidez; C# y Visual Basic comparten la Biblioteca de Clases Framework (FCL), de la cual hablaremos en la sección 1.6. C# es apropiado para las tareas de desarrollo de aplicaciones demandantes, en especial para crear las aplicaciones populares basadas en la Web actual. La plataforma .NET es la infraestructura sobre la cual pueden distribuirse aplicaciones basadas en Web a una gran variedad de dispositivos (incluso los teléfonos celulares) y computadoras de escritorio. La plataforma ofrece un nuevo modelo de desarrollo de software, que permite crear aplicaciones en distintos lenguajes de programación, de manera que se comuniquen entre sí. C# es un lenguaje de programación visual, controlado por eventos, en el cual se crean programas mediante el uso de un Entorno de Desarrollo Integrado (IDE ). Con un IDE, un programador puede crear, ejecutar, probar y depurar programas en C# de manera conveniente, con lo cual se reduce el tiempo requerido para producir un programa funcional en una fracción del tiempo que se llevaría sin utilizar el IDE. La plataforma .NET permite la interoperabilidad de los lenguajes: los componentes de software de distintos lenguajes pueden interactuar como nunca antes se había hecho. Los desarrolladores pueden empaquetar incluso hasta el software antiguo para que trabaje con nuevos programas en C#. Además, las aplicaciones en C# pueden interactuar a través de Internet mediante el uso de estándares industriales como XML, del cual hablaremos en el capítulo 19, y el Protocolo Simple de Acceso a Objetos (SOAP) basado en XML, del cual hablaremos en el capítulo 22, Servicios Web. El lenguaje de programación C# original se estandarizó a través de Ecma International (www.ecma-international.org) en diciembre del 2002, como Estándar ECMA-334: Especificación del lenguaje C# (ubicado en www. ecma-international.org/publications/standards/Ecma-334.htm). Desde entonces, Microsoft propuso varias extensiones del lenguaje que se han adoptado como parte del estándar Ecma C# revisado. Microsoft hace referencia al lenguaje C# completo (incluyendo las extensiones adoptadas) como C# 2.0. [Nota: A lo largo de este libro proporcionamos referencias a secciones determinadas de la Especificación del lenguaje C#. Utilizamos los números de sección señalados en la versión de Microsoft de esa especificación, la cual está compuesta de dos documentos: la Especificación del lenguaje C# 1.2 y la Especificación del lenguaje C# 2.0 (una extensión del documento 1.2 que contiene las mejoras del lenguaje C# 2.0). Ambos documentos se encuentran en msdn.microsoft.com/vcsharp/programming/language/.]

1.4 Internet y World Wide Web

Internet (una red global de computadoras) se inició hace casi cuatro décadas, con fondos suministrados por el Departamento de Defensa de los Estados Unidos. Aunque originalmente se diseñó para conectarse con los principales sistemas computacionales de aproximadamente una docena de universidades y organizaciones de investigación,

4

Capítulo 1 Introducción a las computadoras, Internet y Visual C#

su principal beneficio ha sido, desde ese entonces, la capacidad de comunicarse en forma fácil y rápida a través de lo que llegó a conocerse como correo electrónico (e-mail ). Esto es cierto incluso en la actualidad, en donde el correo electrónico, la mensajería instantánea y la transferencia de archivos facilitan las comunicaciones entre cientos de millones de personas en todo el mundo. Internet se ha convertido en uno de los mecanismos de comunicación más importantes a nivel mundial, y continúa creciendo con rapidez. La World Wide Web permite a los usuarios de computadora localizar y ver, a través de Internet, documentos basados en multimedia, sobre cualquier tema. A pesar de que Internet se desarrolló hace varias décadas, la introducción de la Web es un evento relativamente reciente. En 1989, Tim Berners-Lee de CERN (la Organización Europea de Investigación Nuclear) comenzó a desarrollar una tecnología para compartir información a través de documentos de texto con hipervínculos. Berners-Lee llamó a su invención el Lenguaje de Marcado de Hipertexto (HTML). También escribió protocolos de comunicación para formar las bases de su nuevo sistema de información, al cual denominó World Wide Web. En el pasado, la mayoría de las aplicaciones se ejecutaba en computadoras que no estaban conectadas entre sí. Las aplicaciones actuales pueden escribirse de manera que se comuniquen con computadoras de todo el mundo. Internet mezcla las tecnologías de la computación y la comunicación, facilitando nuestro trabajo. Hace que la información esté accesible de manera instantánea y conveniente en todo el mundo, y permite a las personas y pequeñas empresas tener una presencia que no tendrían de otra manera. Está cambiando la forma en que se hacen los negocios. La gente puede buscar los mejores precios de casi cualquier producto o servicio, mientras que los miembros de las comunidades de interés especial pueden permanecer en contacto unos con otros, y los investigadores pueden estar actualizados de inmediato sobre los descubrimientos más recientes. Sin duda, Internet y World Wide Web se encuentran entre las creaciones más importantes de la humanidad. En los capítulos del 19 al 22 aprenderá a crear aplicaciones basadas en Internet y Web. En 1994, Tim Berners-Lee fundó una organización llamada Consorcio World Wide Web (o W3C ), dedicada a desarrollar tecnologías no propietarias e interoperables para World Wide Web. Una de las principales metas de la W3C es hacer que la Web sea accesible de manera universal, sin importar las discapacidades, el lenguaje o la cultura. La W3C (www.w3.org) es también una organización de estandarización. Las tecnologías Web estandarizadas por la W3C se conocen como Recomendaciones; y las más actuales incluyen el Lenguaje de Marcado Extensible (XML). En la sección 1.5 presentaremos el XML y en el capítulo 19, Lenguaje de Marcado Extensible (XML), lo veremos con detalle. Este lenguaje es la tecnología clave detrás de la próxima versión de World Wide Web, que algunas veces se le denomina “Web semántica”. Es también una de las tecnologías clave detrás de los servicios Web, de los cuales hablaremos en el capítulo 22.

1.5 Lenguaje de Marcado Extensible (XML) A medida que fue creciendo la popularidad de la Web, sus limitaciones del HTML se hicieron aparentes. La falta de extensibilidad (la habilidad de cambiar o agregar características) frustró a los desarrolladores, y su definición ambigua permitía la proliferación de HTML con errores. Era inminente la necesidad de un lenguaje estandarizado, estricto en estructura y completamente extensible. Como resultado, la W3C desarrolló XML. La independencia de datos, la separación del contenido de su presentación, es la característica esencial del XML. Debido a que los documentos de XML describen los datos en forma independiente de la máquina, es concebible que cualquier aplicación pueda procesarlos. Los desarrolladores de software están integrando XML en sus aplicaciones para mejorar la funcionabilidad e interoperabilidad de la Web. XML no se limita a las aplicaciones Web. Por ejemplo, cada vez se está utilizando más en bases de datos; la estructura de un documento XML le permite integrarse fácilmente con las aplicaciones de bases de datos. A medida que las aplicaciones incluyan más funcionalidad para la Web, es probable que XML se convierta en la tecnología universal para la representación de datos. Todas las aplicaciones que empleen XML podrán comunicarse entre sí, siempre y cuando puedan comprender sus esquemas de marcado de XML respectivos, a los cuales se les conoce como vocabularios. El Protocolo Simple de Acceso a Objetos (SOAP ) es una tecnología para la transmisión de objetos (marcados como XML) a través de Internet. Las tecnologías .NET de Microsoft (que veremos en las siguientes dos secciones) utilizan XML y SOAP para marcar y transferir datos a través de Internet. XML y SOAP se encuentran en el núcleo de .NET; permiten la interoperación de los componentes de software (es decir, que se comuniquen fácilmente unos con otros). Como las bases de SOAP son en XML y HTTP (Protocolo de Transferencia de

1.7 .NET Framework y Common Language Runtime

5

Hipertexto; el protocolo de comunicación clave de la Web), la mayoría de los tipos de sistemas computacionales lo soporta. En el capítulo 19, Lenguaje de marcado extensible (XML), hablaremos sobre el XML y en el capítulo 22, Servicios Web, hablaremos sobre SOAP.

1.6 Microsoft .NET

En el año 2000, Microsoft anunció su iniciativa .NET (www.microsoft.com/net en inglés, www.microsoft. en español), una nueva visión para incluir Internet y Web en el desarrollo y uso de software. Un aspecto clave de .NET es su independencia de un lenguaje o plataforma específicos. En vez de estar forzados a utilizar un solo lenguaje de programación, los desarrolladores pueden crear una aplicación .NET en cualquier lenguaje compatible con .NET. Los programadores pueden contribuir al mismo proyecto de software, escribiendo código en los lenguajes .NET (como Microsoft Visual C#, Visual C++, Visual Basic y muchos otros) en los que sean más competentes. Parte de la iniciativa incluye la tecnología ASP.NET de Microsoft, la cual permite a los programadores crear aplicaciones para Web. En el capítulo 21, ASP.NET 2.0, Formularios y Controles Web, hablaremos sobre ASP.NET. En el capítulo 22 utilizaremos la tecnología ASP.NET para crear aplicaciones que utilicen servicios Web. La arquitectura .NET puede existir en varias plataformas, no sólo en los sistemas basados en Microsoft Windows, con lo cual se extiende la portabilidad de los programas .NET. Un ejemplo es Mono (www.mono-project. com/Main_Page), un proyecto de software libre de Novell. Otro ejemplo es .NET Portable de DotGNU (www. dotgnu.org). Un componente clave de la arquitectura .NET son los servicios Web, que son componentes de software reutilizables que pueden usarse a través de Internet. Los clientes y otras aplicaciones pueden usar los servicios Web como bloques de construcción reutilizables. Un ejemplo de un servicio Web es el sistema de reservación de Dollar Rent a Car (http://www.microsoft.com/casestudies/casestudy.aspx?casestudyid=50318). Un socio de una aerolínea deseaba permitir que los clientes hicieran reservaciones de autos de renta desde el sitio Web de la aerolínea. Para ello, la aerolínea necesitaba acceder al sistema de reservaciones de Dollar. En respuesta, Dollar creó un servicio Web que permitía a la aerolínea el acceso a su base de datos para hacer reservaciones. Los servicios Web permitían que dos computadoras de las dos compañías se comunicaran a través de Web, aun y cuando la aerolínea utiliza sistemas UNIX y Dollar utiliza Microsoft Windows. Dollar pudo haber creado una solución específica para esa aerolínea en particular, pero nunca hubiera podido reutilizar dicho sistema personalizado. El servicio Web de Dollar permite que muchas aerolíneas, hoteles y agencias de viaje utilicen su sistema de reservaciones sin necesidad de crear un programa personalizado para cada relación. La estrategia .NET extiende el concepto de reutilización de software hasta Internet, con lo cual permite que los programadores y las compañías se concentren en sus especialidades sin tener que implementar cada componente de cada aplicación. En vez de ello, las compañías pueden comprar los servicios Web y dedicar sus recursos a desarrollar sus propios productos. Por ejemplo, una aplicación individual que utilice los servicios Web de varias compañías podría administrar los pagos de facturas, devoluciones de impuestos, préstamos e inversiones. Un comerciante de aerolínea podría comprar servicios Web para los pagos de tarjeta de crédito en línea, la autenticación de los usuarios, la seguridad de la red y bases de datos de inventario para crear un sitio Web de comercio electrónico.

com/latam/net

1.7 .NET Framework y Common Language Runtime

El .NET Framework de Microsoft es el corazón de la estrategia .NET. Este marco de trabajo administra y ejecuta aplicaciones y servicios Web, contiene una biblioteca de clases (llamada Biblioteca de clases del .NET Framework, o FCL), impone la seguridad y proporciona muchas otras herramientas de programación. Los detalles del .NET Framework se encuentran en la Infraestructura de Lenguaje Común (CLI ), la cual contiene información acerca del almacenamiento de los tipos de datos (es decir, datos que tienen características predefinidas como fecha, porcentaje o una cantidad monetaria), los objetos y demás. Ecma International (conocida en un principio como la Asociación Europea de Fabricantes de Computadoras) ha estandarizado la CLI, con lo cual se facilita la creación del .NET Framework para otras plataformas. Esto es como publicar los planos del marco de trabajo; cualquiera puede construirlo con sólo seguir las especificaciones. El entorno en tiempo de ejecución Common Language Runtime (CLR ) es otra parte central del .NET Framework: ejecuta los programas de .NET. Los programas se compilan en instrucciones específicas para la máquina

6

Capítulo 1 Introducción a las computadoras, Internet y Visual C#

en dos pasos. Primero, el programa se compila en Lenguaje Intermedio de Microsoft (MSIL), el cual define las instrucciones para el CLR. El CLR puede unir el código convertido en MSIL proveniente de otros lenguajes y fuentes. El MSIL para los componentes de una aplicación se coloca en el archivo ejecutable de la aplicación (lo que se conoce como ensamblaje). Cuando la aplicación se ejecuta, otro compilador (conocido como compilador justo a tiempo o compilador JIT ) en el CLR traduce el MSIL del archivo ejecutable en código de lenguaje máquina (para una plataforma específica) y después el código de lenguaje máquina se ejecuta en esa plataforma. [Nota: MSIL es el nombre de Microsoft para lo que la especificación del lenguaje C# denomina Lenguaje Intermedio Común (CIL).] Si existe el .NET Framework (y está instalado) para una plataforma, esa plataforma puede ejecutar cualquier programa .NET. La habilidad de un programa para ejecutarse (sin modificaciones) en varias plataformas se conoce como independencia de la plataforma. De esta forma, el código escrito puede usarse en otro tipo de computadora sin modificación, con lo cual se ahorra tiempo y dinero. Además, el software puede abarcar una audiencia mayor; antes las compañías tenían que decidir si valía la pena el costo de convertir sus programas para usarlos en distintas plataformas (lo que algunas veces se conoce como portar). Con .NET, el proceso de portar los programas ya no representa un problema (una vez que .NET esté disponible en esas plataformas). El .NET Framework también proporciona un alto nivel de interoperabilidad de lenguajes. Los programas escritos en distintos lenguajes se compilan en MSIL; las diferentes partes pueden combinarse para crear un solo programa unificado. MSIL permite que el .NET Framework sea independiente del lenguaje, ya que los programas .NET no están atados a un lenguaje de programación en especial. Cualquier lenguaje que pueda compilarse en MSIL se denomina lenguaje compatible con .NET. La figura 1.1 lista muchos de los lenguajes de programación disponibles para la plataforma .NET (msdn.microsoft.com/netframework/technologyinfo/overview/ default.aspx). La interoperabilidad de lenguajes ofrece muchos beneficios a las compañías de software. Por ejemplo, los desarrolladores en C#, Visual Basic y Visual C++ pueden trabajar lado a lado en el mismo proyecto, sin tener que aprender otro lenguaje de programación; todo su código se compila en MSIL y se enlaza para formar un solo programa. Cualquier lenguaje .NET puede utilizar la Biblioteca de Clases del .NET Framework (FCL). Esta biblioteca contiene una variedad de componentes reutilizables, con lo cual los programadores se ahorran el problema de crear nuevos componentes. En este libro le explicaremos cómo desarrollar software .NET con C#.

Lenguajes de programación .NET APL

Mondrian

C#

Oberon

COBOL

Oz

Component Pascal

Pascal

Curriculum

Perl

Eiffel

Python

Forth

RPG

Fortran

Scheme

Haskell

Smalltalk

Java

Standard ML

JScript

Visual Basic

Mercury

Visual C++

Figura 1.1 | Los lenguajes de programación .NET.

1.8 Prueba de una aplicación en C#

7

1.8 Prueba de una aplicación en C# En esta sección usted “probará” una aplicación, en C#, que le permite dibujar en la pantalla mediante el uso del ratón. Ejecutará una aplicación funcional e interactuará con ella. En el capítulo 13, Conceptos de interfaz gráfica de usuario: parte 1 creará una aplicación similar. La aplicación Drawing le permite dibujar con distintos tamaños de brocha y diversos colores. Los elementos y la funcionalidad que se pueden ver en esta aplicación son muestras de lo que usted aprenderá a programar en este texto. Utilizamos tipos de letras para diferenciar las características del IDE (como los nombres y los elementos de los menús) y otros elementos que aparecen en el IDE. Nuestra convención es enfatizar las características del IDE (como el menú Archivo) en un tipo de letra sans-serif Helvética seminegrita y los demás elementos, como los nombres de archivo (por ejemplo, Form1.cs) en un tipo de letra sans-serif Lucida. Los siguientes pasos le mostrarán cómo probar la aplicación. 1. Compruebe su instalación. Confirme que ha instalado Visual C# 2005 Express o Visual Studio 2005, como vimos en el Prefacio. 2. Localice el directorio de aplicaciones. Abra el Explorador de Windows y navegue hasta el directorio C:\ examples\Ch01\Drawing. 3. Ejecute la aplicación Drawing. Ahora que se encuentra en el directorio correcto, haga doble clic sobre el nombre de archivo Drawing.exe para ejecutar la aplicación. En la figura 1.2 se etiquetan varios elementos gráficos (llamados controles). En esta aplicación se incluyen dos controles GroupBox (en este caso, Color y Size), siete controles RadioButton y un control Panel (más adelante hablaremos sobre estos controles de forma detallada). La aplicación Drawing le permite dibujar con una brocha color rojo, azul, verde o negro de un tamaño pequeño, mediano o grande. En esta prueba explorará todas estas opciones. Puede utilizar controles existentes (que son objetos) para crear poderosas aplicaciones que se ejecuten en C# con mucha más rapidez que si tuviera que escribir todo el código por su cuenta. En este libro aprenderá a utilizar muchos de los controles preexistentes, así como también a escribir su propio código de programa para personalizar sus aplicaciones. Las propiedades de la brocha, que se seleccionan mediante los controles RadioButton (los pequeños círculos en donde para seleccionar una opción se hace clic sobre ella con el ratón) etiquetados como Black y Small son opciones predeterminadas que constituyen la configuración inicial que se ve cuando se ejecuta la aplicación por primera vez. Los programadores incluyen opciones predeterminadas para ofrecer selecciones razonables que utilizan la aplicación en caso de que el usuario no desee modificar la configuración. Ahora usted elegirá sus propias opciones.

Controles RadioButton Panel

Controles GroupBox

Figura 1.2 | Aplicación Drawing en Visual C#.

8

Capítulo 1 Introducción a las computadoras, Internet y Visual C# 4. Cambie el color de la brocha. Haga clic en el control RadioButton con la etiqueta Red para cambiar el color de la brocha. Mantenga oprimido el botón del ratón y el puntero del mismo posicionado en cualquier parte del control Panel, después arrastre el ratón para dibujar con la brocha. Dibuje los pétalos de una flor, como se muestra en la figura 1.3. Después haga clic en el control RadioButton con la etiqueta Green para cambiar el color de la brocha de nuevo. 5. Cambie el tamaño de la brocha. Haga clic en el control RadioButton con la etiqueta Large para cambiar el tamaño de la brocha. Dibuje pasto y el tallo de una flor, como se muestra en la figura 1.4. 6. Termine el dibujo. Haga clic en el control RadioButton con la etiqueta Blue. Después haga clic en el control RadioButton con la etiqueta Medium. Dibuje gotas de lluvia, como se muestra en la figura 1.5 para completar el dibujo. 7. Cierre la aplicación. Haga clic en el cuadro cerrar,

, para cerrar la aplicación en ejecución.

Aplicaciones adicionales incluidas en C# para Programadores, 2a. edición La figura 1.6 lista unas cuantas aplicaciones de ejemplo que introducen algunas de las poderosas y divertidas herramientas de C#. Le recomendamos que practique ejecutando algunas de ellas. La carpeta de ejemplos del

Figura 1.3 | Dibuje con un nuevo color de brocha.

Figura 1.4 | Dibuje con un nuevo tamaño de brocha.

1.9 Introducción a la tecnología de objetos y el UML

9

Figura 1.5 | Termine el dibujo. capítulo 1 contiene todos los archivos requeridos para ejecutar cada una de las aplicaciones que se listan en la figura 1.6. Sólo haga doble clic en el nombre de archivo de cualquier aplicación que desee ejecutar. [Nota: la aplicación Garage.exe asume que el usuario introduce un valor entre 0 y 24.]

1.9 (Única sección obligatoria del caso de estudio) Caso de estudio de ingeniería de software: introducción a la tecnología de objetos y el UML Ahora comenzaremos nuestra introducción anticipada a la orientación a los objetos, una manera natural de pensar acerca del mundo y de escribir programas de computadora. En un momento en el que hay cada vez más demanda por software nuevo y más poderoso, la habilidad de crear software con rapidez, economía y sin errores sigue siendo una meta difícil. Este problema puede tratarse en parte a través del uso de los objetos, los cuales son componentes de software reutilizables que modelan elementos en el mundo real. Un enfoque modular orientado a objetos para el diseño y la implementación puede hacer que los grupos de desarrollo de software sean más productivos de lo que es posible mediante el uso de las primeras técnicas de programación. Lo que es más, los programas orientados a objetos son más fáciles de comprender, corregir y modificar. Los capítulos 1, 3-9 y 11 terminan con una sección breve titulada “Caso de estudio de ingeniería de software”, en la cual presentamos, a un ritmo cómodo, una introducción a la orientación a objetos. Nuestro objetivo aquí es ayudarlo a desarrollar una manera de pensar orientada a objetos y de presentarle el Lenguaje Unificado de

Nombre de la aplicación

Archivo a ejecutar

Cuotas de estacionamiento

Garage.exe

Tic Tac Toe

TicTacToe.exe

Dibujar estrellas

DrawStars.exe

Dibujar figuras

DrawShapes.exe

Dibujar polígonos

DrawPolygons.exe

Figura 1.6 | Ejemplos de programas en C# incluidos en este libro.

10

Capítulo 1 Introducción a las computadoras, Internet y Visual C#

Modelado™ (UML™): un lenguaje gráfico que permite a las personas que diseñan sistemas de software orientado a objetos utilizar una notación estándar en la industria para representarlos. En esta sección, que es la única obligatoria del caso de estudio, presentaremos los conceptos básicos de la orientación a objetos y su terminología relacionada. Las secciones opcionales en los capítulos 3-9 y 11 presentan un diseño orientado a objetos y la implementación del software para un sistema de cajero automático (ATM) simple. Las secciones tituladas “Caso de estudio de ingeniería de software” al final de los capítulos 3-9:

• • • • •

analizan un documento de requerimientos típico, el cual describe cómo se creará un sistema de software (el ATM). determinan los objetos requeridos para implementar el sistema. determinan los atributos que tendrán los objetos. determinan los comportamientos que exhibirán los objetos. especifican cómo interactuarán los objetos entre sí para cumplir con los requerimientos del sistema.

Las secciones tituladas “Caso de estudio de ingeniería de software” al final de los capítulos 9 y 11 modifican y mejoran el diseño presentado en los capítulos 3-8. El apéndice J contiene una implementación completa y fun− cional en C# del sistema ATM orientado a objetos. Aunque nuestro caso de estudio es una versión simplificada de un problema de nivel industrial, de todas formas cubrimos muchas prácticas comunes en la industria. Usted experimentará una sólida introducción al diseño orientado a objetos con el UML. Además, agudizará sus habilidades de lectura de código al dar un vistazo a una implementación completa, directa y bien documentada en C# del ATM.

Conceptos básicos de la tecnología de los objetos Comenzaremos nuestra introducción a la orientación a objetos con cierta terminología clave. Cada vez que voltee a su alrededor encontrará objetos: personas, animales, plantas, autos, aviones, edificios, computadoras, etcétera. Los humanos piensan en términos de los objetos. Los teléfonos, las casas, los semáforos, los hornos de microondas y los enfriadores de agua son sólo algunos objetos más que vemos a nuestro alrededor todos los días. En ocasiones dividimos a los objetos en dos categorías: animados e inanimados. Los objetos animados están “vivos” en cierto sentido; se mueven a su alrededor y hacen cosas. Los objetos inanimados no se mueven por cuenta propia. No obstante, ambos tipos de objetos tienen cosas en común. Todos tienen atributos (por ejemplo: tamaño, forma, color y peso) y todos exhiben comportamientos (es decir, una pelota rueda, rebota, se infla y se desinfla; un bebé llora, duerme, gatea, camina y parpadea; un auto acelera, frena y da vuelta; una toalla absorbe agua). Estudiaremos los tipos de atributos y comportamientos que tienen los objetos de software. Los humanos aprenden acerca de los objetos al estudiar sus atributos y observar sus comportamientos. Distintos objetos pueden tener atributos similares y exhibir comportamientos similares. Por ejemplo, pueden hacerse comparaciones entre los bebés y los adultos, y entre los humanos y los chimpancés. El diseño orientado a objetos (DOO) modela el software en términos similares a los que utilizan las personas para modelar los objetos del mundo real. Aprovecha las relaciones de las clases, en donde los objetos de cierta clase, como una clase de vehículos, tienen las mismas características: los autos, los camiones, las pequeñas vagonetas rojas y los patines tienen mucho en común. El DOO aprovecha las relaciones de herencia, en donde se derivan nuevas clases de objetos al absorber las características de las clases existentes y agregar características únicas para cada objeto. Sin duda un objeto de la clase “convertible” tiene las características de la clase “automóvil” que es más general, pero el techo se quita y se pone de manera específica. El diseño orientado a objetos ofrece una manera natural e intuitiva de ver el proceso de diseño del software; a saber, se modelan los objetos por sus atributos, comportamientos e interrelaciones, de igual forma que describimos los objetos del mundo real. El DOO también modela la comunicación entre los objetos. Así como las personas se envían mensajes unas con otras (por ejemplo, un sargento ordena a un soldado que se ponga en posición firme, o un adolescente envía mensajes de texto a un amigo para verse en el cine), los objetos también se comunican a través de mensajes. Un objeto tipo cuenta de banco puede recibir un mensaje para reducir su saldo por cierta cantidad, debido a que el cliente ha retirado esa cantidad de dinero.

1.9 Introducción a la tecnología de objetos y el UML

11

El DOO encapsula (es decir, envuelve) los atributos y las operaciones (comportamientos) en los objetos; los atributos y las operaciones de un objeto están enlazadas íntimamente. Los objetos tienen la propiedad conocida como ocultamiento de información. Esto significa que pueden saber cómo comunicarse unos con otros a través de interfaces bien definidas, pero por lo general no se les permite saber cómo se implementan otros objetos; los detalles de implementación se ocultan dentro de los mismos objetos. Por ejemplo, usted puede conducir un auto a la perfección sin necesidad de conocer los detalles de cómo funcionan los motores, las transmisiones, los frenos y los sistemas de escape de manera interna; lo importante es que sepa cómo utilizar el pedal del acelerador, el del freno, el volante y lo demás. Como veremos más adelante, el ocultamiento de la información es imprescindible para la buena ingeniería de software. Los lenguajes como C# están orientados a objetos. La programación en un lenguaje de este tipo se llama programación orientada a objetos (POO), y permite a los programadores de computadoras implementar en forma conveniente un diseño orientado a objetos como un sistema de software funcional. Por otro lado, los lenguajes como C son por procedimientos, de manera que la programación tiende a estar orientada a las acciones. En C, la unidad de programación es la función. En C#, la unidad de programación es la clase, a partir de la cual los objetos se instancian (un término del DOO para indicar la “creación”). Las clases en C# contienen métodos (el equivalente de C# para las funciones de C) que implementan las operaciones y datos que implementan los atributos.

Clases, miembros de datos y métodos

Los programadores de C# se concentran en crear sus propios tipos definidos por el usuario, a los cuales se les llama clases. Cada clase contiene datos y también el conjunto de métodos que manipulan esos datos y proporcionan servicios a los clientes (es decir, otras clases que utilizan esa clase). Los componentes de datos de una clase se llaman atributos, o campos. Por ejemplo, una clase tipo cuenta bancaria podría incluir un número de cuenta y un balance. Los componentes de operación de la clase se llaman métodos. Por ejemplo, una clase tipo cuenta bancaria podría incluir métodos para hacer un depósito (aumentar el saldo), hacer un retiro (reducir el saldo) y solicitar el saldo actual. El programador utiliza tipos integrados (y otros tipos definidos por el usuario) como “bloques de construcción” para crear nuevos tipos definidos por el usuario (clases). Los sustantivos en la especificación de un sistema ayudan al programador de C# a determinar el conjunto de clases a partir de las cuales se crean los objetos que trabajan en conjunto para implementar el sistema. Las clases son para los objetos como los planos para las casas; una clase es un “plano” para crear objetos de esa clase. Así como podemos construir muchas casas a partir de un plano, también podemos instanciar (crear) muchos objetos de una clase. Usted no puede preparar alimentos en la cocina de un plano, pero sí puede prepararlos en la cocina de una casa. No puede dormir en la recámara de un plano, pero sí puede hacerlo en la recámara de una casa. Las clases pueden tener relaciones con otras clases. Por ejemplo, en un diseño orientado a objetos de un banco, la clase “cajero” necesita relacionarse con otras clases, como la clase “cliente”, la clase “caja registradora”, la clase “caja fuerte”, y así en lo sucesivo. A estas relaciones se les conoce como asociaciones. El empaquetamiento de software en forma de clases hace posible que los futuros sistemas de software reutilicen esas clases. A menudo los grupos de clases relacionadas se empaquetan como componentes reutilizables. Así como los agentes de bienes raíces dicen con frecuencia que los tres factores más importantes que afectan al precio de los bienes raíces son “la ubicación, la ubicación y la ubicación”, también algunas personas en la comunidad de desarrollo de software dicen a menudo que los tres factores más importantes que afectan al futuro del desarrollo del software son “la reutilización, la reutilización y la reutilización”.

Observación de ingeniería de software 1.1 La reutilización de las clases existentes al crear nuevas clases y programas ahorra tiempo, dinero y esfuerzo. La reutilización también ayuda a los programadores a crear sistemas más confiables y efectivos, ya que por lo general las clases y los componentes existentes han pasado por un proceso exhaustivo de prueba, depuración y puesta a punto para el rendimiento.

Sin duda, con la tecnología de objetos usted podrá crear la mayor parte del nuevo software que necesite mediante la combinación de clases existentes, de igual manera que los fabricantes de automóviles combinan las piezas intercambiables. Cada nueva clase que usted cree tendrá el potencial de convertirse en un valioso patrimonio de software que usted y otros programadores podrán reutilizar para agilizar y mejorar la calidad de los esfuerzos de desarrollo del software a futuro.

12

Capítulo 1 Introducción a las computadoras, Internet y Visual C#

Introducción al análisis y diseño orientados a objetos (A/DOO) Pronto usted estará escribiendo programas en C#. ¿Cómo creará el código para sus programas? Tal vez, como muchos programadores novatos, sólo encenderá su computadora y empezará a escribir. Este método puede funcionar para programas pequeños (como los que presentaremos en los primeros capítulos del libro), pero ¿qué tal si se le pidiera que creara un sistema de software para controlar miles de cajeros automáticos para un banco importante? O ¿qué pasaría si se le pidiera que trabajara como parte de un equipo de 1,000 desarrolladores de software para crear la siguiente generación del sistema de control de tráfico aéreo de Estados Unidos? Para proyectos tan grandes y complejos no puede tan sólo sentarse y empezar a escribir programas. Para crear las mejores soluciones es conveniente que siga un proceso detallado para analizar los requerimientos de su proyecto (es decir, determinar qué es lo que hará el sistema) y desarrollar un diseño que lo satisfaga (es decir, debe decidir cómo lo hará su sistema). En teoría, usted pasaría por este proceso y revisaría con cuidado el diseño (y haría que otros profesionales de software revisaran también su diseño) antes de escribir cualquier código. Si este proceso implica analizar y diseñar su sistema desde un punto de vista orientado a los objetos, se llama análisis y diseño orientados a objetos (A/DOO ). Los programadores experimentados saben que un proceso apropiado de análisis y diseño puede ahorrarles muchas horas, ya que ayuda a evitar un método de diseño del sistema mal planeado que tendría que abandonarse a mitad de su implementación, con la posibilidad de desperdiciar una cantidad considerable de tiempo, dinero y esfuerzo. A/DOO es el término genérico para el proceso de analizar un problema y desarrollar un método para resolverlo. Los pequeños problemas como los que veremos en los primeros capítulos de este libro no requieren de un proceso exhaustivo de A/DOO. Podría ser suficiente, antes de empezar a escribir código en C#, con escribir seudocódigo: un medio informal basado en texto para expresar la lógica de un programa. En realidad no es un lenguaje de programación, pero puede utilizarlo como un tipo de plan general para guiarlo a la hora que escriba su código. En el capítulo 5 hablaremos sobre el seudocódigo. A medida que los problemas y los grupos de personas que los tratan de resolver aumentan en tamaño, el A/DOO se vuelve con rapidez más apropiado que el seudocódigo. En teoría, un grupo debería acordar un proceso definido de manera estricta para resolver su problema y una manera uniforme de comunicarse unos a otros los resultados de ese proceso. Aunque existen muchos procesos de A/DOO distintos, hay un solo lenguaje gráfico para comunicar los resultados de cualquier proceso de A/DOO que se ha empezado a utilizar mucho. Este lenguaje, conocido como Lenguaje Unificado de Modelado (UML), se desarrolló a mediados de la década de 1990 bajo la dirección inicial de tres metodologistas de software: Grady Booch, James Rumbaugh e Ivar Jacobson.

Historia del UML En la década de 1980, aumentó el número de organizaciones que comenzaron a utilizar la POO para crear sus aplicaciones, y se desarrolló la necesidad de un proceso estándar de A/DOO. Muchos metodologistas (incluyendo a Grady Booch, James Rumbaugh e Ivar Jacobson) produjeron y promovieron por su cuenta procesos separados para satisfacer esta necesidad. Cada proceso tenía su propia notación, o “lenguaje” (en forma de diagramas gráficos), para conllevar los resultados del análisis (es decir, determinar qué es lo que se supone que debe hacer un sistema propuesto) y el diseño (es decir, determinar cómo debe implementarse un sistema propuesto para que haga lo que tiene que hacer). A principios de 1990, varias organizaciones utilizaban sus propios procesos y notaciones únicas. Al mismo tiempo, estas organizaciones también querían utilizar herramientas de software que tuvieran soporte para sus procesos específicos. Los distribuidores de software se dieron cuenta que era difícil proveer herramientas para tantos procesos. Se necesitaban tanto una notación como un proceso estándar. En 1994, James Rumbaugh se unió con Grady Booch en Rational Software Corporation (ahora una división de IBM) y ambos comenzaron a trabajar para unificar sus populares procesos. Poco después se les unió Ivar Jacobson. En 1996, el grupo publicó varias versiones del UML para la comunidad de ingeniería de software y solicitó retroalimentación. Casi al mismo tiempo, una organización conocida como el Grupo de Administración de Objetos™ (OMG™) solicitó presentaciones para un lenguaje de modelado común. La OMG (www.omg.org) es una organización sin fines de lucro que promueve la estandarización de las tecnologías orientadas a objetos mediante la publicación de lineamientos y especificaciones, como el UML. Varias empresas (entre ellas HP, IBM, Microsoft, Oracle y Rational Software) ya habían reconocido la necesidad de un lenguaje de modelado común.

1.9 Introducción a la tecnología de objetos y el UML

13

En respuesta a la petición de proposiciones por parte de la OMG, estas compañías formaron el consorcio UML Partners; este consorcio desarrolló la versión 1.1 del UML y la envió a la OMG, quien aceptó la proposición y, en 1997, asumió la responsabilidad del mantenimiento y la revisión continuos del UML. A lo largo de este libro presentamos la terminología y la notación del UML 2, que se adoptaron recientemente.

¿Qué es el UML?

El Lenguaje Unificado de Modelado (UML) es el esquema de representación gráfica más utilizado para modelar sistemas orientados a objetos. Sin duda ha unificado los diversos esquemas de notación populares. Las personas que diseñan sistemas utilizan el lenguaje (en forma de diagramas, muchos de los cuales veremos a lo largo de nuestro caso de estudio del ATM) para modelar sus sistemas. En este libro utilizaremos diversos tipos populares de diagramas de UML. Una característica atractiva del UML es su flexibilidad. El UML es extensible (es decir, capaz de mejorar mediante nuevas características) e independiente de cualquier proceso de A/DOO particular. Los modeladores de UML tienen la libertad de utilizar varios procesos al diseñar sistemas, pero todos los desarrolladores pueden ahora expresar sus diseños mediante un conjunto estándar de notaciones gráficas. UML es un lenguaje gráfico lleno de características. En las subsiguientes (y opcionales) secciones de “Caso de estudio de ingeniería de software” que tratan sobre el desarrollo del software para un cajero automático (ATM), presentaremos un subconjunto conciso de estas características. Después utilizaremos este subconjunto para guiarlo a través de su primera experiencia de diseño con el UML. Utilizaremos algunas notaciones de C# en nuestros diagramas de UML para evitar la confusión y mejorar la legibilidad. En la práctica industrial, en especial con herramientas de UML que generan código de manera automática (una conveniente característica de muchas herramientas de UML), es probable que uno se adhiera más a las palabras clave y las convenciones de nomenclatura de UML para los atributos y las operaciones. Este caso de estudio se desarrolló con mucho cuidado bajo la guía de varios revisores académicos y profesionales distinguidos. Es nuestro más sincero deseo que usted se divierta al trabajar con él. Si tiene dudas o preguntas, por favor envíe un correo electrónico a [email protected] y le responderemos a la brevedad.

Recursos del UML en Internet y Web Para más información acerca de UML, consulte los siguientes sitios Web. Para sitios de UML adicionales, consulte los recursos de Internet y Web que se listan al final de la sección 3.10. www.uml.org

Este sitio de recursos de UML del Grupo de Administración de Objetos (OMG) contiene los documentos de las especificaciones para el UML y otras tecnologías orientadas a objetos. www.ibm.com/software/rational/uml

Ésta es la página de recursos de UML para IBM Rational, el sucesor de Rational Software Corporation (la compañía que creó UML).

Lecturas recomendadas Se han publicado muchos libros acerca de UML. Los siguientes libros que recomendamos proporcionan información acerca del diseño orientado a objetos con UML.



Arlow, J. e I. Neustadt. UML and the Unified Process: Practical Object-Oriented Analysis and Design, Second Edition. Londres: Addison-Wesley, 2005.



Fowler, M. UML Distilled, Third Edition: Applying the Standard Object Modeling Language. Boston: Addison-Wesley, 2004.



Rumbaugh, J., I. Jacobson y G. Booch. The Unified Modeling Language User Guide, Second Edition. Upper Saddle River, NJ: Addison-Wesley, 2005.

Para libros adicionales acerca de UML, consulte las lecturas recomendadas que se listan al final de la sección 3.10 o visite www.amazon.com, www.bn.com y www.informIT.com. IBM Rational, antes Rational Software Corporation, también proporciona una lista de lecturas recomendadas para libros de UML en http://www-306.ibm. com/software/rational/sw-library/#Books.

14

Capítulo 1 Introducción a las computadoras, Internet y Visual C#

Sección 1.9 Ejercicios de autoevaluación 1.1 Liste tres ejemplos de objetos del mundo real que no mencionamos. Para cada objeto, liste varios atributos y comportamientos. 1.2

El seudocódigo es __________. a) otro término para A/DOO b) un lenguaje de programación que se utiliza para mostrar diagramas de UML c) un medio informal para expresar la lógica del programa d) un esquema de representación gráfica para modelar sistemas orientados a objetos

1.3

El UML se utiliza principalmente para __________. a) evaluar sistemas orientados a objetos b) diseñar sistemas orientados a objetos c) implementar sistemas orientados a objetos d) las opciones a y b

Respuestas a los ejercicios de autoevaluación de la sección 1.9 1.1 [Nota: las respuestas pueden variar.] a) Los atributos de una televisión son: el tamaño de la pantalla, el número de colores que puede visualizar y el canal y volumen actuales. Una televisión se enciende y se apaga, cambia de canal, muestra video y reproduce sonidos. b) Los atributos de una cafetera son: el volumen máximo de agua que puede contener, el tiempo requerido para elaborar una jarra de café y la temperatura del plato calentador debajo de la jarra de café. Una cafetera se enciende y se apaga, elabora café y lo calienta. c) Los atributos de una tortuga son: su edad, el tamaño de su concha y su peso. Una tortuga se arrastra, se mete en su concha, emerge de la misma y come vegetación. 1.2

c.

1.3

b.

1.10 Conclusión En este capítulo se presentaron los conceptos básicos de la tecnología de objetos, incluyendo: clases, objetos, atributos y comportamientos. Presentamos una breve historia de los sistemas operativos, como Microsoft Windows. Hablamos sobre la historia de Internet y Web. Presentamos la historia de la programación en C# y la iniciativa .NET de Microsoft, la cual le permite programar aplicaciones basadas en Internet y Web mediante el uso de C# (y otros lenguajes). Conoció los pasos para ejecutar una aplicación en C#. Probó una aplicación de C# de ejemplo, similar a los tipos de aplicaciones que aprenderá a programar en este libro. Aprendió acerca de la historia y el propósito de UML (el lenguaje gráfico estándar en la industria para modelar sistemas de software). Iniciamos nuestra presentación anticipada de los objetos y las clases con la primera de nuestras secciones “Caso de estudio de ingeniería de software” (y la única que es obligatoria). El resto de las secciones (todas opcionales) del caso de estudio utilizan el diseño orientado a objetos y el UML para diseñar el software para nuestro sistema de cajero automático simplificado. En el apéndice J presentamos la implementación completa del código en C# del sistema ATM. En el siguiente capítulo utilizará el IDE (Entorno de Desarrollo Integrado) de Visual Studio para crear su primera aplicación en C# mediante el uso de las técnicas de la programación visual. También aprenderá acerca de las características de ayuda de Visual Studio.

1.11 Recursos Web Sitio Web de Deitel & Associates www.deitel.com/books/csharpforprogrammers2/index.html

El sitio Web de Deitel & Associates para este libro incluye vínculos a los ejemplos y otros recursos del mismo. www.deitel.com

Visite este sitio para actualizaciones, correcciones y recursos adicionales para todas las publicaciones de Deitel. www.deitel.com/newsletter/subscribe.html

Visite este sitio para suscribirse al boletín de correo electrónico gratuito Deitel ® Buzz Online; aquí podrá seguir el programa de publicación de Deitel & Associates y recibirá actualizaciones sobre C# y este libro.

1.11 Recursos Web

15

www.prenhall.com/deitel

El sitio Web de Prentice Hall para las publicaciones de Deitel. Incluye información detallada sobre los productos, capítulos de muestra y Sitios Web complementarios (Companion Web Sites) que contienen recursos específicos para cada libro y cada capítulo, tanto para los estudiantes como para los instructores.

Sitios Web de Microsoft msdn.microsoft.com/vcsharp/default.aspx

El sitio del Centro de desarrollo de Microsoft Visual C# incluye información sobre el producto, descargas, tutoriales, grupos de Chat y mucho más. Incluye casos de estudio de las compañías que utilizan C# en sus negocios. msdn2.microsoft.com/es-es/library/kx37x362.aspx

Sitio Web que muestra una descripción de Microsoft Visual C# en español. Incluye una introducción a Visual C#, cómo utilizar el IDE y escribir aplicaciones, y otras cosas más. msdn.microsoft.com/vstudio/default.aspx

Visite este sitio para que aprenda más acerca de los productos y recursos de Microsoft Visual Studio. msdn2.microsoft.com/es-es/library/ms310242.aspx

Sitio que habla sobre Visual Studio 2005 en español. www.gotdotnet.com

Sitio Web para la Comunidad del .NET Framework de Microsoft. Incluye tableros de mensajes, un centro de recursos, programas de ejemplo y demás. www.thespoke.net

Los estudiantes pueden chatear, publicar su código, calificar el código de otros estudiantes, crear centros y publicar preguntas en este sitio Web.

Recursos www.ecma-international.org/publications/standards/Ecma-334.html

La página de Ecma International para la Especificación del lenguaje C#. www.w3.org

El Consorcio World Wide Web (W3C) desarrolla tecnologías para Internet y Web. Este sitio incluye vínculos a las tecnologías del W3C, noticias y preguntas frecuentes (FAQs). www.error-bank.com

El Banco de errores es una colección de errores, excepciones y soluciones de .NET. www.csharp-station.com

Este sitio Web ofrece noticias, vínculos, tutoriales, ayuda y otros recursos de C#. www.mentores.net

Este sitio Web es una comunidad en línea sobre .NET, C#, ASP.NET, Visual Basic .NET, Programación y noticias de tecnología en español, que incita el compartir conocimientos entre “mentores” o colaboradores y usuarios de todo el mundo. www.csharphelp.com

Este sitio incluye un foro de ayuda de C#, tutoriales y artículos. www.codeproject.com/index.asp?cat=3

Esta página de recursos incluye código, artículos, noticias y tutoriales sobre C#. www.dotnetpowered.com/languages.aspx

Este sitio Web proporciona una lista de los lenguajes implementados en .NET.

Recursos de UML www.uml.org

Esta página de recursos de UML del Grupo de administración de objetos (OMG) proporciona los documentos de las especificaciones para el UML y otras tecnologías de orientación a objetos. www.ibm.com/software/rational/uml

Ésta es la página de recursos de UML para IBM Rational: el sucesor de Rational Software Corporation (la compañía que creó UML).

16

Capítulo 1 Introducción a las computadoras, Internet y Visual C#

Juegos en C# www.c-sharpcorner.com/Games.asp

En este sitio Web encontrará una variedad de juegos desarrollados con C#. También puede enviar sus propios juegos para publicarlos en el sitio. www.gamespp.com/cgi-bin/index.cgi?csharpsourcecode

Este sitio Web de recursos incluye juegos en C#, código fuente y tutoriales.

2

Introducción al IDE de Visual C# 2005 Express Edition Hay que ver para creer. —Proverbio

OBJETIVOS

La forma siempre sigue a la función.

En este capítulo aprenderá lo siguiente:

—Louis Henri Sullivan

Q

Los fundamentos del Entorno Integrado de Desarrollo (IDE) de Visual Studio, el cual le ayudará a escribir, ejecutar y depurar sus programas en Visual C#.

Q

Las características de ayuda de Visual Studio.

Q

Los comandos clave contenidos en los menús y las barras de herramientas del IDE.

Q

El propósito de los diversos tipos de ventanas en el IDE de Visual Studio 2005.

Q

Qué es la programación visual y cómo simplifica y agiliza el desarrollo de programas.

Q

Cómo crear, compilar y ejecutar un programa simple en Visual C# para visualizar texto y una imagen mediante el uso del IDE de Visual Studio y la técnica de programación visual.

La inteligencia …es la facultad de crear objetos artificiales, en especial herramientas para crear herramientas. —Henri-Louis Bergson

Pla n g e ne r a l

18

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

2.1 2.2 2.3 2.4

2.5 2.6 2.7 2.8

Introducción Generalidades del IDE de Visual Studio 2005 Barra de menús y barra de herramientas Navegación por el IDE de Visual Studio 2005 2.4.1 Explorador de soluciones 2.4.2 Cuadro de herramientas 2.4.3 Ventana Propiedades Uso de la Ayuda Uso de la programación visual para crear un programa simple que muestra texto e imagen Conclusión Recursos Web

2.1 Introducción

Visual Studio® 2005 es el Entorno de Desarrollo Integrado (IDE) de Microsoft para crear, ejecutar y depurar programas (también conocidos como aplicaciones) escritos en una variedad de lenguajes de programación .NET. En este capítulo veremos las generalidades del IDE Visual Studio 2005 y demostraremos cómo crear un programa simple en Visual C# mediante el procedimiento de arrastrar y colocar bloques de construcción predefinidos en el lugar adecuado; a esta técnica se le llama programación visual. Este capítulo se dedica en específico a Visual C#, la implementación de Microsoft del estándar C# de Ecma.

2.2 Generalidades del IDE de Visual Studio 2005

Existen diversas versiones disponibles de Visual Studio; pero para este libro utilizamos Microsoft Visual C# 2005 Express Edition, la cual sólo tiene soporte para el lenguaje de programación Visual C#. Microsoft también ofrece una versión completa de Visual Studio 2005, que incluye soporte para otros lenguajes además de Visual C#, como Visual Basic y Visual C++. Nuestras capturas de pantalla y discusiones se enfocan en el IDE de Visual C# 2005 Express Edition. Supondremos que usted tiene cierta familiaridad con Windows. Utilizaremos varios tipos de letra para diferenciar las características del IDE (como nombres y elementos de menús) y otros elementos que aparecen en el IDE. Enfatizaremos las características del IDE en un tipo de letra Helvética sans-serif negrita (por ejemplo, menú Archivo) y resaltaremos los demás elementos, como los nombres de archivo (por ejemplo, Form1.cs) y los nombres de las propiedades (que veremos en la sección 2.4) en un tipo de letra Lucida sans-serif.

Introducción a Microsoft Visual C# 2005 Express Edition Para iniciar Microsoft Visual C# 2005 Express Edition en Windows XP, seleccione Inicio > Todos los programas > Microsoft Visual C# 2005 Express Edition. Si utiliza Windows 2000, seleccione Inicio > Programas > Microsoft Visual C# 2005 Express Edition. Una vez que comience la ejecución de esta aplicación, aparecerá la Página de inicio (figura 2.1). Dependiendo de su versión de Visual Studio, su Página de inicio podría ser distinta. Para los nuevos programadores que no estén familiarizados con Visual C#, la Página de inicio contiene una lista de vínculos a recursos en el IDE de Visual Studio 2005 y en Internet. A partir de aquí nos referiremos al IDE de Visual Studio 2005 como “Visual Studio” o “el IDE”. Para los desarrolladores experimentados, esta página proporciona vínculos a los desarrollos más recientes en Visual C# (como actualizaciones y correcciones de errores) y a información sobre temas avanzados de programación. Una vez que empiece a explorar el IDE, para regresar a la Página de inicio sólo tiene que seleccionar la página del menú desplegable de la barra de dirección (figura 2.2), o también puede seleccionar Ver > Otras ventanas > Página de inicio o hacer clic en el icono Página de inicio de la Barra de herramientas del IDE (figura 2.9). En la sección 2.3 hablaremos sobre la Barra de herramientas y sus diversos iconos. Utilizamos el carácter > para indicar la selección de un comando de un menú. Por ejemplo, utilizamos la notación Archivo > Abrir archivo para indicar que debe seleccionar el comando Abrir archivo del menú Archivo.

2.2 Generalidades del IDE de Visual Studio 2005

Botón

Ficha

Nuevo proyecto

Página de inicio

Ventana oculta

Vínculos de la Página de inicio

19

Explorador de soluciones

vacío (no hay proyectos abiertos)

Figura 2.1 | La Página de inicio en Visual C# 2005 Express Edition.

Vínculos en la Página de inicio Los vínculos de la Página de inicio se organizan en secciones: Proyectos recientes, Introducción, Encabezados de Visual C# Express y MSDN: Visual C# 2005 Express Edition; estas secciones contienen vínculos hacia recursos de programación útiles. Al hacer clic sobre cualquier vínculo de la Página de inicio se muestra la información asociada

con ese vínculo específico. Para referirnos a la acción de hacer un solo clic con el botón izquierdo del ratón utilizaremos los términos seleccionar o hacer clic; para referirnos a la acción de hacer doble clic con el botón izquierdo del ratón usaremos el término hacer doble clic. La sección Proyectos recientes contiene información sobre los proyectos que usted ha creado o modificado recientemente. También puede abrir los proyectos existentes o crear nuevos proyectos al hacer clic en los vínculos de esa sección. La sección Introducción se concentra en cómo utilizar el IDE para crear programas, aprender Visual C#, conectarse a la comunidad de desarrollo de Visual C# (es decir, otros desarrolladores de software con los cuales usted se puede comunicar a través de grupos de noticias y sitios Web) y proporcionar varias herramientas de desarrollo como los Starter Kits. Por ejemplo, al hacer clic en el vínculo Utilizar un Starter Kit obtendrá recursos y vínculos para crear una aplicación de protector de pantalla simple o una aplicación de colección de películas. La aplicación de protector de pantalla crea un protector de pantalla que visualiza artículos de noticias recientes. El Starter Kit de la colección de películas crea una aplicación que le permite mantener un catálogo de sus películas en DVD y VHS, o puede modificar la aplicación para que lleve el registro de cualquier otra cosa que usted desee coleccionar (por ejemplo, CDs, videojuegos). Las secciones Encabezados de Visual C# Express y MSDN: Visual C# 2005 Express Edition proporcionan vínculos hacia información acerca de la programación en Visual C#, incluyendo un paseo por el lenguaje, nuevas

20

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Página Web solicitada (URL en el menú desplegable de la barra de dirección)

Ficha seleccionada para la página Web solicitada

Figura 2.2 | Visualización de una página Web en Visual Studio. características de Visual C# 2005 y cursos en línea. Para acceder a más información sobre Visual Studio, puede navegar por la biblioteca en línea MSDN (Microsoft Developer Network) en msdn.microsoft.com (inglés) y www.microsoft.com/spanish/msdn/latam (español). El sitio de MSDN contiene artículos, descargas y tutoriales sobre las tecnologías de interés para los desarrolladores de Visual Studio. También puede navegar a través de la Web desde el IDE, mediante el uso de Internet Explorer. Para obtener información detallada sobre C#, visite el Centro de desarrollo de C# en msdn.microsoft.com/vcsharp (inglés) y http://www.microsoft.com/ spanish/msdn/centro_recursos/csharp/default.mspx (español). Para solicitar una página Web, escriba la URL en la barra de dirección (figura 2.2) y oprima Intro; desde luego que su computadora debe tener conexión a Internet. (Si la barra de dirección no se encuentra presente de antemano en el IDE, seleccione Ver > Otras ventanas > Explorador Web.) La página Web que desea ver aparecerá como otra ficha, la cual puede seleccionar dentro del IDE de Visual Studio (figura 2.2).

Personalización del IDE y creación de un nuevo proyecto Para comenzar a programar en Visual C#, debe crear un nuevo proyecto o abrir uno existente; existen dos formas para hacerlo. Puede seleccionar Archivo > Nuevo proyecto… o Archivo > Abrir proyecto… en el menú Archivo, con lo cual se crea un nuevo proyecto o se abre uno existente, en forma respectiva. Desde la Página de inicio, bajo la sección Proyectos recientes puede también hacer clic en los vínculos Crear: Proyecto o Abrir: Proyecto/Solución. Un proyecto es un grupo de archivos relacionados, como el código de Visual C# y cualquier imagen que pueda conformar un programa. Visual Studio 2005 organiza los programas en proyectos y soluciones, que pueden contener uno o más proyectos. Las soluciones con múltiples proyectos se utilizan para crear programas de mayor escala. Cada uno de los programas que crearemos en este libro consiste de un solo proyecto.

2.2 Generalidades del IDE de Visual Studio 2005

21

Seleccione Archivo > Nuevo proyecto… o el vínculo Crear: Proyecto… en la Página de inicio para que aparezca el cuadro de diálogo Nuevo proyecto (figura 2.3). Los cuadros de diálogo son ventanas que facilitan la comunicación entre el usuario y la computadora. En unos momentos hablaremos sobre el proceso detallado para crear nuevos proyectos. Visual Studio cuenta con plantillas para diversos tipos de proyectos (figura 2.3). Las plantillas son los tipos de proyectos que los usuarios pueden crear en Visual C#: aplicaciones para Windows, aplicaciones de consola y otras (en este libro de texto utilizaremos principalmente aplicaciones Windows y aplicaciones de consola). Los usuarios también pueden utilizar o crear plantillas de aplicación personalizadas. En este capítulo nos concentraremos en las aplicaciones para Windows. En el capítulo 3, Introducción a las aplicaciones de C#, hablaremos sobre la Aplicación de consola. Una aplicación para Windows es un programa que se ejecuta dentro de un sistema operativo Windows (por ejemplo, Windows 2000 o Windows XP) y por lo general tiene una interfaz gráfica de usuario (GUI ): la parte visual del programa con la que interactúa el usuario. Las aplicaciones para Windows incluyen productos de software de Microsoft como Microsoft Word, Internet Explorer y Visual Studio; los productos de software creados por otros distribuidores y el software personalizado que usted y otros programadores crean. En este libro creará muchas aplicaciones para Windows. [Nota: Novell patrocina un proyecto de código fuente abierto llamado Mono, el cual permite a los desarrolladores crear aplicaciones .NET para Linux, Windows y Mac OS X. Mono está basado en los estándares de Ecma para C# y la Infraestructura de lenguaje común (CLI). Para obtener más información sobre Mono, visite el sitio Web www.mono-project.com]. Visual Studio asigna de manera predeterminada el nombre WindowsApplication1 al nuevo proyecto y solución (figura 2.3). Usted cambiará el nombre del proyecto y la ubicación en la que se almacena. Haga clic en Aceptar para mostrar el IDE en vista de diseño (figura 2.4), la cual contiene todas las características necesarias para que usted comience a crear programas. La porción de la vista de diseño del IDE se conoce también como Diseñador de formularios de Windows. El rectángulo de color gris titulado Form1 (al cual se le conoce como Formulario) representa la ventana principal de la aplicación Windows que usted creará. Las aplicaciones de C# pueden tener varios Formularios (ventanas); no obstante, la mayoría de las aplicaciones que usted creará en este texto sólo utilizan uno. Más adelante en este capítulo aprenderá a personalizar el Formulario mediante el proceso de agregar controles (es decir, componentes reutilizables) a un programa; en este caso son Label (etiqueta) y PictureBox (cuadro de imagen) (como se puede ver en la figura 2.27). Por lo general, un control Label contiene texto descriptivo (por ejemplo, "Bienvenido a Visual C#!") y un control PictureBox muestra imágenes como el insecto mascota de Deitel. Visual Studio cuenta con más de 65 controles preexistentes que usted puede utilizar para crear y personalizar sus programas. Muchos de

Aplicación para Windows

de Visual C# (seleccionada)

Nombre predeterminado para el proyecto (proporcionado por Visual Studio)

Figura 2.3 | Cuadro de diálogo Nuevo proyecto.

Descripción de la plantilla del proyecto seleccionado (proporcionada por Visual Studio)

22

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Ventana Ficha activa

Fichas

Menú

Barra de menús

Formulario (aplicación para Windows)

Explorador de soluciones

Ventana Propiedades

Figura 2.4 | Vista de diseño del IDE. estos controles se definen y utilizan a lo largo de este libro. Además de los controles incluidos con Visual C# 2005 Express o Visual Studio 2005, hay muchos otros controles de terceros disponibles; puede descargarlos de la página Web msdn.microsoft.com/vcsharp/downloads/components/default.aspx. Cuando comience a programar, trabajará con controles que no necesariamente formarán parte de su programa, sino que son parte de la FCL. A medida que coloque controles en el formulario, podrá modificar sus propiedades (que veremos con detalle en la sección 2.4) al introducir texto alternativo en un cuadro de texto (figura 2.5) o seleccionar opciones y después oprimir un botón, como Aceptar o Cancelar, como se muestra en la figura 2.6. En conjunto, el formulario y los controles constituyen la GUI del programa. Los usuarios introducen datos (entrada) en el programa a través de una variedad de formas, como escribir en el teclado, hacer clic en los botones del ratón y escribir en los controles de la GUI como los cuadros de texto (TextBox). Los programas utilizan la GUI para mostrar instrucciones y demás información (salida) para que la lean los usuarios. Por ejemplo, el cuadro de diálogo Nuevo proyecto de la figura 2.3 presenta una GUI en la que el usuario hace clic en el botón del ratón para seleccionar una plantilla de proyecto y después escribe un nombre para el proyecto por medio del teclado (observe que la figura sigue mostrando el nombre predeterminado del proyecto WindowsApplication1, asignado por Visual Studio). El nombre de cada documento abierto se lista en una ficha; en la figura 2.4, los documentos abiertos son Form1.cs [Diseño] y la Página de inicio. Para ver un documento, haga clic en su ficha. Las fichas facilitan el acceso cuando hay varios documentos abiertos. La ficha activa (la ficha del documento que se muestra en el IDE en un momento dado) se muestra con el texto en negrita (por ejemplo, Form1.cs [Diseño] en la figura 2.4) y se posiciona en frente de todas las demás fichas.

2.3 Barra de menús y barra de herramientas Los comandos para administrar el IDE y para desarrollar, mantener y ejecutar programas están contenidos en menús, los cuales se encuentran en la barra de menús del IDE (figura 2.7). Observe que el conjunto de menús que se muestra en la figura 2.7 cambia en base al contexto.

2.3 Barra de menús y barra de herramientas

23

El cuadro de texto (que muestra el texto Form1) puede modificarse

Figura 2.5 | Ejemplo de un control de cuadro de texto en el IDE de Visual Studio.

Botón Aceptar Botón Cancelar

Figura 2.6 | Ejemplos de botones en el IDE de Visual Studio. Los menús contienen grupos de comandos relacionados (también conocidos como elementos de menú) que, cuando se seleccionan, hacen que el IDE realice acciones específicas (es decir, abrir una ventana, almacenar un archivo, imprimir un proyecto y ejecutar un programa). Por ejemplo, para crear nuevos proyectos puede seleccionar Archivo > Nuevo proyecto…. Los menús que se muestran en la figura 2.7 se sintetizan en la figura 2.8. En el capítulo 14, Conceptos de interfaz gráfica de usuario: parte 2, hablaremos sobre cómo crear y agregar menús y elementos de menú a sus programas.

Figura 2.7 | Barra de menús de Visual Studio.

24

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Menú

Descripción

Archivo

Contiene comandos para abrir, cerrar y guardar proyectos, así como para imprimir los datos de un proyecto y salir de Visual Studio. Contiene comandos para editar programas, como cortar, copiar, pegar, deshacer, rehacer, eliminar, buscar y seleccionar. Contiene comandos para mostrar ventanas (por ejemplo, Explorador de Windows, Cuadro de herramientas, ventana Propiedades) y para agregar barras de herramientas al IDE. Contiene comandos para administrar proyectos y sus archivos. Contiene comandos para compilar un programa. Contiene comandos para depurar (es decir, identificar y corregir los problemas en un programa) y ejecutar un programa. En el apéndice C hablaremos detalladamente sobre el proceso de depuración. Contiene comandos para interactuar con bases de datos (es decir, colecciones organizadas de datos que se almacenan en las computadoras), que veremos en el capítulo 20, Bases de datos, SQL y ADO.NET. Contiene comandos para organizar y modificar los controles de un formulario. El menú Formato sólo aparece cuando se selecciona un componente de la GUI en vista Diseño. Contiene comandos para acceder a las herramientas adicionales del IDE (por ejemplo, el Cuadro de herramientas) y opciones que le permiten personalizar el IDE. Contiene comandos para organizar y visualizar ventanas. Contiene comandos para enviar preguntas en forma directa a Microsoft, verificar el estado de las preguntas, enviar retroalimentación acerca de Visual C# y buscar en el centro de desarrollo CodeZone y en el sitio comunitario de desarrollo de Microsoft. Contiene comandos para acceder a las características de ayuda del IDE.

Edición Ver Proyecto Generar Depurar

Datos

Formato Herramientas Ventana Comunidad

Ayuda

Figura 2.8 | Resumen de los menús del IDE de Visual Studio 2005. En vez de navegar por los menús de la barra de menús, puede acceder a muchos de los comandos más comunes desde la barra de herramientas (figura 2.9), la cual contiene gráficos llamados iconos, que representan comandos en forma gráfica. [Nota: la figura 2.9 divide la barra de herramientas en dos partes, para que podamos ilustrar los gráficos con mayor claridad; la barra de herramientas aparece en una línea dentro del IDE.] La barra de herramientas estándar se muestra de manera predeterminada cuando ejecutamos Visual Studio por primera vez; esta barra contiene iconos para los comandos que se utilizan con más frecuencia, como abrir un archivo, agregar un elemento a un proyecto, guardar y ejecutar (figura 2.9). Algunos comandos están deshabilitados al principio (es decir, no se pueden utilizar). Estos comandos, que en un principio aparecen en color gris, Visual Studio los habilita sólo cuando son necesarios. Por ejemplo, habilita el comando para guardar un archivo una vez que usted comienza a editarlo. Puede personalizar el IDE agregando más barras de herramientas. Para ello, seleccione Ver > Barras de herramientas (figura 2.10). Cada barra de herramienta que seleccione se mostrará con las demás en la parte superior de la ventana de Visual Studio (figura 2.10). Otra forma en la que puede agregar barras de herramientas a su IDE (que no mostraremos en este capítulo) es a través de Herramientas > Personalizar. Después, en la ficha Barras de herramientas seleccione las que desea que aparezcan en el IDE. Para ejecutar un comando por medio de la barra de herramientas, haga clic sobre su icono. Algunos iconos contienen una flecha hacia abajo que, cuando se hace clic sobre ella, muestra un comando o comandos relacionados, como se ilustra en la figura 2.11. Es difícil recordar qué representa cada uno de los iconos en la barra de tareas. Puede posicionar el puntero del ratón sobre un icono para resaltarlo y que, después de unos instantes, aparezca una descripción, a lo cual se le conoce como cuadro de información sobre herramientas (tool tip) (figura 2.12). Estos cuadros ayudan a los programadores novatos a familiarizarse con las características del IDE, y sirven como recordatorios útiles de la funcionalidad de cada icono de la barra de herramientas.

2.4 Navegación por el IDE de Visual Studio 2005

25

a) Nuevo proyecto

Abrir archivo

Agregar nuevo elemento

Guardar todo

Guardar

Copiar

Cortar

Pegar

Deshacer

Desplazarse hacia atrás

Rehacer

Iniciar depuración

Desplazarse hacia delante

Configuraciones de soluciones

b) Otras ventanas

Ventana del Ventana

Plataformas de solución

Buscar

Buscar en archivos

Propiedades

Explorador de soluciones

Cuadro de herramientas

Examinador de objetos

Página de inicio

Figura 2.9 | La barra de herramientas estándar en Visual Studio.

2.4 Navegación por el IDE de Visual Studio 2005 El IDE ofrece ventanas para acceder a los archivos de proyecto y personalizar controles. En esta sección presentaremos algunas de las que usted utilizará con frecuencia al desarrollar programas en Visual C#. Puede acceder a estas ventanas a través de los iconos de la barra de herramientas (figura 2.13) o seleccionando el nombre de la ventana deseada en el menú Ver. Visual Studio cuenta con una característica de ahorro de espacio llamada ocultar automáticamente. Cuando se habilita esta característica, aparece una ficha a lo largo del borde izquierdo o derecho de la ventana del IDE (figura 2.14). Esta ficha contiene uno o más iconos, cada uno de los cuales identifica a una ventana oculta. Si coloca el puntero del ratón encima de uno de estos iconos, se muestra esa ventana (figura 2.15). La ventana se oculta nuevamente cuando se mueve el ratón fuera de su área. Para “marcar” una ventana (es decir, para deshabilitar la característica de ocultarse automáticamente y mantenerla abierta), haga clic sobre el icono del marcador. Observe que cuando se habilita el ocultamiento automático, el icono del marcador está en posición horizontal (figura 2.15), mientras que cuando una ventana se “marca”, el icono del marcador está en posición vertical (figura 2.16). En las siguientes tres secciones veremos las generalidades sobre tres de las principales ventanas que se utilizan en Visual Studio: el Explorador de soluciones, el Cuadro de herramientas y la ventana Propiedades. Estas ventanas muestran información acerca del proyecto e incluyen herramientas que le ayudarán a generar sus programas. 2.4.1 Explorador de soluciones La ventana del Explorador de soluciones (figura 2.17) proporciona acceso a todos los archivos en una solución. Si esta ventana no aparece en el IDE, seleccione Ver > Explorador de soluciones o haga clic en el icono Explorador de soluciones (figura 2.13). La primera vez que abre Visual Studio, el Explorador de soluciones está vacío; no hay archivos qué mostrar. Una vez que abre una solución, el Explorador de soluciones muestra el contenido de la solución y sus proyectos o cuando usted crea un nuevo proyecto, se muestra su contenido.

26

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Barra de herramientas Estándar

La barra de herramientas Generar se agregó a la barra de herramientas Estándar

Figura 2.10 | Agregue la barra de herramientas Generar al IDE.

Barra de herramientas

La flecha hacia abajo indica que hay comandos adicionales

Figura 2.11 | Icono de la barra de herramientas del IDE que muestra comandos adicionales.

2.4 Navegación por el IDE de Visual Studio 2005

27

El cuadro de información sobre herramientas aparece cuando el puntero del ratón se coloca sobre el icono Nuevo proyecto

Figura 2.12 | Demostración del cuadro de información sobre herramientas (tool tip).

Ventana Propiedades

Explorador de soluciones

Cuadro de herramientas

Figura 2.13 | Iconos de la barra de herramientas para tres ventanas de Visual Studio.

Ficha para la ventana oculta (característica de ocultarse automáticamente activada)

Figura 2.14 | Demostración de la característica de ocultarse automáticamente. El proyecto de inicio de la solución es el proyecto que se ejecuta cuando corre el programa. Si usted tiene varios proyectos en una solución dada, puede especificar cuál de ellos será el de inicio, esto se logra al hacer clic con el botón derecho del ratón sobre el nombre del proyecto de la ventana del Explorador de soluciones; después seleccione la opción Establecer como proyecto de inicio. Para una solución con un solo proyecto, el proyecto de inicio es el único (en este caso, WindowsApplication1) y su nombre aparece en negrita en la ventana del Explorador de soluciones. Todos los programas que veremos en este libro son soluciones con un solo proyecto.

28

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Barra de título del Cuadro de herramientas

Orientación horizontal para el icono del marcador

Figura 2.15 | Mostrar una ventana oculta cuando se habilita la característica de ocultarse automáticamente. Cuadro de herramientas

“desmarcado”

Orientación vertical para el icono del marcador

Figura 2.16 | Deshabilitar la característica de ocultarse automáticamente (“desmarcar” una ventana). Para los programadores que utilizan Visual Studio por primera vez, la ventana del Explorador de soluciones sólo lista los archivos Properties, References, Form1.cs y Program.cs (figura 2.17). La ventana Explorador de soluciones incluye una barra de herramientas que contiene varios iconos. El archivo de Visual C# que corresponde al formulario que se muestra en la figura 2.4 se llama Form1.cs (y es el que está seleccionado en la figura 2.17). (Los archivos de Visual C# utilizan la extensión de nombre de archivo .cs, abreviatura de “C Sharp”).

2.4 Navegación por el IDE de Visual Studio 2005

29

Icono Mostrar todos los archivos

Barra de herramientas Proyecto de inicio

Figura 2.17 | Explorador de soluciones con un proyecto abierto. El IDE muestra de manera predeterminada sólo los archivos que probablemente necesiten editarse; los demás archivos generados por el IDE están ocultos. Al hacer clic en el icono Mostrar todos los archivos (figura 2.17) se muestran todos los archivos en la solución, incluyendo los generados por el IDE. Puede hacer clic en los cuadros de los signos más y menos que aparecen (figura 2.18) para expandir y colapsar el árbol de proyecto, en forma respectiva. Haga clic en el cuadro del signo más a la izquierda de Properties para mostrar los elementos agrupados bajo el titular a la derecha del cuadro de signo más (figura 2.19); haga clic en los cuadros de signo menos a la izquierda de Properties y References para colapsar el árbol de su estado expandido (figura 2.20). Otras ventanas de Visual Studio también utilizan esta convención de cuadros de signo más/signo menos. 2.4.2 Cuadro de herramientas El Cuadro de herramientas contiene iconos que representan los controles utilizados para personalizar formularios (figura 2.21). Mediante el uso de la programación visual, podemos “arrastrar y soltar” controles en el formulario, lo cual es más rápido y simple que crearlos mediante la escritura de código de GUI (en el capítulo 5, Instrucciones de control: parte 1, presentaremos la escritura de este tipo de código). Así como no necesita saber cómo construir un motor para conducir un auto, tampoco necesita saber cómo crear controles para utilizarlos. La reutilización de controles preexistentes ahorra tiempo y dinero cuando se desarrollan programas. La amplia variedad de controles contenidos en el Cuadro de herramientas es una poderosa característica de la FCL de .NET. Más adelante en este capítulo utilizará el Cuadro de herramientas al crear su primer programa. El Cuadro de herramientas contiene grupos de controles relacionados. Ejemplos de estos grupos son: Todos los formularios Windows Forms, Controles comunes, Contenedores, Menús y barras de herramientas, Datos, Componentes, Impresión, Cuadros de diálogo y General, los cuales se listan en la figura 2.21. Observe nuevamente el uso de los cuadros de signos más y menos para expandir o colapsar un grupo de controles. El Cuadro de herramientas contiene aproximadamente 65 controles preconstruidos para utilizarse en Visual Studio, por lo que tal vez tenga

Cuadro de signo más Cuadro de signo menos

Figura 2.18 | El Explorador de soluciones muestra cuadros de signos más y menos para expandir y colapsar el árbol, de manera que se muestren o se oculten los archivos del proyecto.

30

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

El cuadro de signo menos indica que el archivo o carpeta está expandido (después de hacer clic en el cuadro de signo más)

Figura 2.19 | El Explorador de soluciones expande el archivo Properties después de hacer clic en su cuadro de signo más.

Los cuadros de signo más indican que el archivo o carpeta está colapsado (después de hacer clic en el cuadro de signo menos)

Figura 2.20 | El Explorador de soluciones colapsa todos los archivos después de hacer clic en cualquier cuadro de signo menos. que desplazarse por el Cuadro de herramientas para ver controles adicionales a los que se muestran en la figura 2.21. A lo largo de este libro hablaremos sobre muchos de los controles del Cuadro de herramientas y su funcionalidad. 2.4.3 Ventana Propiedades Para mostrar la ventana Propiedades en caso de que no esté visible, puede seleccionar Ver > Ventana Propiedades, hacer clic en el icono ventana Propiedades que se muestra en la figura 2.13 o puede oprimir la tecla F4. La ventana Propiedades muestra las propiedades para el formulario (Form) (figura 2.22), control o archivo que esté seleccionado en ese momento en vista de diseño. Las propiedades especifican información acerca del formulario o control, como su tamaño, color y posición. Cada formulario o control tiene su propio conjunto de propiedades; la descripción de una propiedad se muestra en la parte inferior de la ventana Propiedades cada vez que se selecciona esa propiedad. La figura 2.22 muestra la ventana Propiedades del formulario. La columna izquierda lista las propiedades del Formulario; la columna derecha muestra el valor actual de cada propiedad. Los iconos en la barra de herramientas ordenan las propiedades, ya sea alfabéticamente al hacer clic en el icono Alfabético o por categorías, al hacer clic en el icono Por categorías. Puede ordenar de manera alfabética las propiedades en orden ascendente o descendente; al hacer clic en el icono Alfabético varias veces se alterna el orden de las propiedades entre A-Z y Z-A. Al utilizar el orden por categorías se agrupan las propiedades de acuerdo con su uso (por ejemplo, Apariencia, Comportamiento, Diseño). Dependiendo del tamaño de la ventana Propiedades, algunas de las propiedades pueden ocultarse de la vista en la pantalla, en cuyo caso los usuarios pueden desplazarse a través de la lista de propiedades. Más adelante en este capítulo le mostraremos cómo establecer propiedades individuales.

2.5 Uso de la Ayuda

31

Controles

x

Nombres de grupo

Figura 2.21 | Ventana del Cuadro de herramientas, en la cual se muestran los controles para el grupo Controles comunes. La ventana Propiedades es imprescindible para la programación visual, ya que nos permite modificar las propiedades de un control en forma visual, sin necesidad de escribir código. Podemos ver qué propiedades están disponibles para modificarse y, cuando sea apropiado, podemos conocer el rango de valores aceptables para una propiedad dada. La ventana Propiedades muestra una breve descripción de la propiedad seleccionada, con lo cual nos ayuda a comprender su propósito. Podemos establecer una propiedad rápidamente a través de esta ventana; por lo general sólo se requiere un clic y no hay que escribir código. En la parte superior de la ventana Propiedades está el cuadro de lista desplegable de selección de componente, la cual nos permite seleccionar el formulario o control cuyas propiedades deseamos mostrar en la ventana Propiedades (figura 2.22). Una manera alternativa de mostrar las propiedades de un control sin seleccionar el formulario o control en la GUI es mediante el cuadro de lista desplegable de selección de componente.

2.5 Uso de la Ayuda

Visual Studio cuenta con muchas características de ayuda. En la figura 2.23 se sintetizan los comandos del menú Ayuda. La ayuda dinámica (figura 2.24) es una excelente y rápida forma para obtener información acerca del IDE y sus características. Proporciona una lista de artículos que corresponden al “contenido actual” (es decir, los elementos seleccionados). Para abrir la ventana Ayuda dinámica, seleccione Ayuda > Ayuda dinámica. Después, cuando haga clic en una palabra o componente (como en un formulario o control) aparecerán vínculos hacia artículos correspondientes en la ventana Ayuda dinámica. La ventana lista los temas de ayuda, ejemplos de código y demás información relevante. También hay una barra de herramientas que proporciona acceso a las características de ayuda Cómo, Buscar, Índice y Contenido.

32

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Cuadro de lista desplegable de selección de componente Barra de herramientas

Icono Por categorías Icono Alfabético

Descripción de la propiedad Text Propiedades

Valor de la propiedad

Figura 2.22 | La ventana Propiedades muestra la propiedad Text del formulario. Visual Studio también cuenta con ayuda sensible al contexto, la cual es similar a la ayuda dinámica, sólo que muestra al instante un artículo de ayuda relevante, en vez de presentar una lista de artículos. Para utilizarla haga clic sobre un elemento, como el formulario, y oprima F1. La figura 2.25 muestra los artículos relacionados con un formulario. Las opciones de Ayuda pueden establecerse en el cuadro de diálogo Opciones (para acceder a este cuadro de diálogo, seleccione Herramientas > Opciones…). Para mostrar todas las configuraciones que puede modificar (incluyendo la configuración de las opciones de Ayuda), asegúrese de que esté seleccionada la casilla de verificación Comando

Descripción

Cómo

Contiene vínculos a temas relevantes, incluyendo cómo actualizar programas y aprender más acerca de los servicios Web, la arquitectura y el diseño, los archivos y la E/S, datos, depuración y muchos más. Busca artículos de ayuda con base en palabras clave de búsqueda. Muestra una lista en orden alfabético de los temas que usted puede explorar. Muestra una tabla por categorías del contenido en el que los artículos de ayuda se organizan por tema.

Buscar Índice Contenido

Figura 2.23 | Comandos del menú Ayuda.

2.5 Uso de la Ayuda

Elemento seleccionado

Cómo

Ventana Ayuda dinámica

Buscar

Índice

33

Contenido

Artículos de ayuda relevantes sobre el elemento seleccionado (por ejemplo, el formulario)

Figura 2.24 | La ventana Ayuda dinámica. Mostrar todas las configuraciones en la esquina inferior izquierda del cuadro de diálogo (figura 2.26). Para cambiar la opción de mostrar la Ayuda en el interior o en el exterior, seleccione Ayuda a la izquierda y después ubique el cuadro de lista desplegable Mostrar la Ayuda usando: que se encuentra a la derecha. Dependiendo de su preferencia, si selecciona Visor de ayuda externa se mostrará un artículo de ayuda relevante en una ventana separada fuera del IDE (algunos programadores prefieren ver las páginas Web por separado del proyecto en el que se encuentran trabajando). Si selecciona Visor de ayuda integrada se mostrará un artículo de ayuda en forma de ventana tipo ficha dentro del IDE.

Figura 2.25 | Uso de la ayuda sensible al contexto.

34

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition Opción seleccionada de

Cuadro de lista

Ayuda

Visor de ayuda externa

Casilla de verificación Mostrar todas las configuraciones

Figura 2.26 | El cuadro de diálogo Opciones muestra las configuraciones de Ayuda.

2.6 Uso de la programación visual para crear un programa simple que muestra texto e imagen En esta sección crearemos un programa que muestra el texto “¡Bienvenido a Visual C#!” y una imagen del insecto mascota de Deitel & Associates. El programa consiste en un solo formulario que utiliza un control Label y un control PictureBox. La figura 2.27 muestra el resultado de la ejecución del programa. Este programa y la imagen del insecto están disponibles junto con los ejemplos de este capítulo. Puede descargar los ejemplos de la página Web www.deitel.com/books/csharpforprogrammers2/index.html. Para crear el programa cuya salida se muestra en la figura 2.27, no escribirá ni una sola línea de código. En vez de ello utilizará las técnicas de la programación visual. Visual Studio procesa sus acciones (como hacer clic con el ratón, arrastrar y soltar) para generar código de programa. En el capítulo 3 comenzaremos nuestra discusión acerca de cómo escribir código de programa. A lo largo del libro producirán programas cada vez más robustos que a menudo incluirán una combinación de código escrito por usted y código generado por Visual Studio. El código generado puede ser difícil de entender para los programadores principiantes; por fortuna, muy pocas veces tendrá que ver este código. La programación visual es útil para crear programas con uso intensivo de la GUI, que por ende requieren mucha interacción con el usuario. La programación visual no puede utilizarse para crear programas que no tengan GUIs; debe escribir código de manera directa. Para crear, ejecutar y terminar éste, su primer programa, realice los siguientes 13 pasos: 1. Cree el nuevo proyecto. Si ya hay un proyecto abierto, ciérrelo seleccionando Archivo > Cerrar solución. Tal vez aparezca un cuadro de diálogo que le pregunte si desea guardar el proyecto actual. Haga clic en Guardar para guardar cualquier cambio. Para crear una nueva aplicación Windows para nuestro programa, seleccione Archivo > Nuevo proyecto… para desplegar el cuadro de diálogo Nuevo proyecto (figura 2.28). De las opciones de plantillas, seleccione Aplicación para Windows. Utilice el nombre UnProgramaSimple para este proyecto y haga clic en Aceptar. [Nota: los nombres de archivo deben ajustarse a ciertas reglas. Por ejemplo, no pueden contener símbolos (como: ?, :, *, , # y %) o caracteres de control Unicode® (Unicode es un conjunto especial de caracteres que se describe en el apéndice E). Además, los nombres de archivos no pueden ser nombres reservados para el sistema, como “CON”, “PRN”, “AUX” y “COM1” o “.” y “..”, y no pueden tener una longitud mayor a 256 caracteres.] Anteriormente en este capítulo mencionamos que debe establecer el directorio en el que se guardará este proyecto. En el Visual Studio

2.6 Crear un programa simple que muestra texto e imagen

35

W

Control Label

W

Control PictureBox

Figura 2.27 | Ejecución de un programa simple.

Tipos de plantillas

Escriba el nombre del proyecto

Figura 2.28 | Cuadro de diálogo Nuevo proyecto. completo, puede hacer esto en el cuadro de diálogo Nuevo proyecto. Para especificar el directorio en Visual C# Express, seleccione Archivo > Guardar todo para que aparezca el cuadro de diálogo Guardar proyecto (figura 2.29). Para establecer la ubicación del proyecto haga clic en el botón Examinar…, el cual abrirá el cuadro de diálogo Ubicación del proyecto (figura 2.30). Navegue a través de los directorios, seleccione uno en el que se colocará el proyecto (en nuestro ejemplo, utilizamos un directorio llamado MisProyectos) y haga clic en Aceptar para cerrar el cuadro de diálogo. Asegúrese de que esté seleccionada la casilla de verificación Crear directorio para la solución (figura 2.29). Haga clic en Guardar para cerrar el cuadro de diálogo Guardar proyecto.

36

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Figura 2.29 | Cuadro de diálogo Guardar proyecto.

Ubicación seleccionada para el proyecto

Haga clic para seleccionar la ubicación del proyecto

Figura 2.30 | Establezca la ubicación del proyecto en el cuadro de diálogo Ubicación del proyecto.

La primera vez que comience a trabajar en el IDE, estará en modo de diseño (es decir, el programa se está diseñando y no se encuentra en ejecución). Mientras que el IDE se encuentre en modo de diseño, usted tendrá acceso a todas las ventanas del entorno (por ejemplo, Cuadro de herramientas, Propiedades), menús y barras de herramientas, como verá en breve. 2. Establezca el texto en la barra de título del formulario. El texto en la barra de título del formulario es el valor de la propiedad Text (figura 2.31) del formulario. Si la ventana Propiedades no está abierta, haga clic en el icono de propiedades en la barra de herramientas o seleccione Ver > Ventana Propiedades. Haga clic en cualquier parte del formulario para mostrar las propiedades del mismo en la ventana Propiedades. Haga clic en el cuadro de texto a la derecha del cuadro de la propiedad Text y escriba "Un programa simple", como en la figura 2.31. Oprima Intro cuando termine; la barra de título del formulario se actualizará de inmediato (figura 2.32). 3. Cambie el tamaño del formulario. Haga clic y arrastre uno de los manejadores de tamaño habilitados (los pequeños cuadros blancos que aparecen alrededor del formulario, como se muestra en la figura 2.32). Utilice el ratón para seleccionar y arrastrar el manejador de tamaño para modificar el tamaño del formulario (figura 2.33). 4. Cambie el color de fondo del formulario. La propiedad BackColor especifica el color de fondo de un formulario o control. Al hacer clic en BackColor en la ventana Propiedades aparece un botón con una flecha hacia abajo enseguida del valor de la propiedad (figura 2.34). Al hacer clic en este botón, se despliega un conjunto de otras opciones, que varían dependiendo de la propiedad. En este caso, dicho botón muestra las

2.6 Crear un programa simple que muestra texto e imagen

37

Nombre y tipo del objeto

Propiedad seleccionada

Valor de la propiedad

Descripción de la propiedad

Figura 2.31 | Establezca la propiedad Text del formulario en la ventana Propiedades.

Barra de título

Manejadores de tamaño habilitados

Figura 2.32 | Forma con manejadores de tamaño habilitados.

Figura 2.33 | Formulario con nuevo tamaño.

fichas Personalizado, Web y Sistema (la predeterminada). Haga clic en la ficha Personalizado para mostrar la paleta (una rejilla de colores). Seleccione el cuadro que representa el color azul claro. Una vez que seleccione el color, la paleta se cerrará y el color de fondo del formulario cambiará a azul claro (figura 2.35).

38

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Color actual

Paleta

Botón de flecha hacia abajo

Personalizado

Azul claro

Figura 2.34 | Cambie la propiedad BackColor del formulario.

Nuevo color de fondo

Figura 2.35 | Se aplicó la nueva propiedad BackColor en el formulario. 5. Agregue un control Label al formulario. Si el Cuadro de herramientas ya no se encuentra abierto, seleccione Ver > Cuadro de herramientas para mostrar el conjunto de controles que utilizará para crear sus programas. Para el tipo de programa que crearemos en este capítulo, los controles típicos que utilizaremos se encuentran en la categoría Todos los formularios Windows Forms del Cuadro de herramientas o del grupo Controles comunes. Si se colapsa cualquiera de los dos nombres de grupo, haga clic en el signo más para expandirlo (los grupos Todos los formularios Windows Forms y Controles comunes se muestran cerca de la parte superior de la figura 2.21). A continuación, haga doble clic en el control Label del Cuadro de herramientas. Esta acción hará que aparezca un control Label en la esquina superior izquierda del formulario (figura 2.36). [Nota: si el formulario está detrás del Cuadro de herramientas, tal vez necesite cerrar el Cuadro de herramientas para ver el control Label.] Aunque si hace doble clic sobre cualquier control del Cuadro de herramientas se coloca ese control en el formulario, también puede “arrastrar” los controles del Cuadro de herramientas hacia

2.6 Crear un programa simple que muestra texto e imagen

39

Control Label

Figura 2.36 | Agregue un control Label al formulario. el formulario (tal vez prefiera arrastrar el control, ya que puede colocarlo en la posición que desee). Nuestro control Label muestra el texto label1 de manera predeterminada. Observe que el color de fondo de nuestro control Label es el mismo que el color de fondo del formulario. Cuando se agrega un control al formulario, su propiedad BackColor se hace igual a la propiedad BackColor del formulario. Para cambiar el color de fondo del control Label a un color distinto del formulario, cambie su propiedad BackColor. 6. Personalice la apariencia del control Label. Seleccione el control Label haciendo clic sobre él. Ahora deberán aparecer sus propiedades en la ventana Propiedades (figura 2.37). La propiedad Text del control Label determina el texto (si es que lo hay) que muestra la etiqueta. Tanto el formulario como el control Label tienen su propiedad Text; los formularios y los controles pueden tener los mismos tipos de propiedades (como BackColor y Text) sin conflictos. Escriba el texto ¡Bienvenido a Visual C#! en la propiedad Text del control Label. Observe que el control Label cambia de tamaño para alojar todo el texto que se escribe en una línea. De manera predeterminada, la propiedad AutoSize del control Label se establece en True, lo cual permite al control Label ajustar su tamaño para acomodar a todo el texto, en caso de ser necesario. Establezca la propiedad AutoSize en False (figura 2.37) para que pueda cambiar el tamaño del control Label por su cuenta. Cambie el tamaño del control Label (mediante el uso de los manejadores de tamaño) de manera que pueda verse todo el texto. Desplace el control Label hacia la parte superior central del formulario; para ello arrástrelo o utilice las teclas de flecha izquierda y derecha para ajustar su posición (figura 2.38). De manera alternativa, cuando esté seleccionado el control Label podrá centrarlo horizontalmente si selecciona Formato > Centrar en el formulario > Horizontalmente. 7. Establezca el tamaño de letra del control Label. Para cambiar el tipo de letra y la apariencia del texto del control Label, seleccione el valor de la propiedad Font, con lo cual aparecerá un botón de elipsis ( ) enseguida del valor (figura 2.39). Al hacer clic en el botón de elipsis aparece un cuadro de diálogo que contiene valores adicionales (en este caso, el cuadro de diálogo Fuente (figura 2.40). En este cuadro de diálogo puede seleccionar el nombre de la fuente (por ejemplo, Microsoft Sans Serif, MingLiU, Mistral, Modern No. 20; las opciones pueden variar, dependiendo de su sistema), el estilo (Normal, Cursiva, Negrita, etc.) y el tamaño (16, 18, 20, etc.). El área Ejemplo muestra texto de ejemplo con las opciones de fuente seleccionadas. En Tamaño, seleccione 24 puntos y haga clic en Aceptar. Si el texto del control Label no cabe en una sola línea, se ajusta a la siguiente línea. Cambie el tamaño vertical del control Label si no es lo suficientemente grande como para que aparezca todo el texto.

40

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Propiedad AutoSize

Figura 2.37 | Establezca la propiedad AutoSize del control Label en False.

Control Label centrado, con la propiedad Text actualizada

Manejadores de tamaño

Figura 2.38 | La GUI, una vez que se personalizan el formulario y el control Label.

Botón de elipsis

Figura 2.39 | La ventana Propiedades muestra las propiedades del control Label. 8. Alinee el texto del control Label. Seleccione la propiedad TextAlign del control Label, la cual determina la forma en que se alinea el texto dentro del control Label. A continuación aparecerá una rejilla de tres por tres botones, la cual representa las opciones de alineación (figura 2.41). La posición de cada botón corresponde al lugar en el que aparecerá el texto en el control Label. Para este programa, seleccione la opción MiddleCenter para la propiedad TextAlign en la rejilla de tres por tres; esta selección hará que

2.6 Crear un programa simple que muestra texto e imagen

41

Fuente actual

Ejemplo de la fuente

Figura 2.40 | Cuadro de diálogo Fuente para seleccionar fuentes, tamaños y estilos.

Opciones de alineación de texto

Opción de alineación en la parte media del centro

Figura 2.41 | Centre el texto del control Label. el texto aparezca centrado en la parte media del control Label, con un espaciamiento igual desde el texto hacia todos los lados del control Label. Los demás valores de TextAlign, como TopLeft, TopRight y BottomCenter, se pueden utilizar para posicionar el texto en cualquier parte dentro de un control Label. Ciertos valores de alineación pueden requerir que usted cambie el tamaño del control Label para hacerlo más grande o más pequeño, de manera que se ajuste mejor el texto. 9. Agregue un control PictureBox al formulario. El control PictureBox muestra imágenes. El proceso involucrado en este paso es similar al del paso 5, en donde agregamos un control Label al formulario. Localice el control PictureBox en el menú del Cuadro de herramientas (figura 2.21) y haga doble clic para agregar el control PictureBox al formulario. Cuando aparezca este control, desplácelo por debajo del control Label, ya sea arrastrándolo o mediante las teclas de flecha (figura 2.42). 10. Inserte una imagen. Haga clic en el control PictureBox para mostrar sus propiedades en la ventana Propiedades (figura 2.43). Localice la propiedad Image, la cual muestra una vista previa de la imagen, en caso de que exista. Como no se ha asignado ninguna imagen, el valor de la propiedad Image muestra (ninguno). Haga clic en el botón de elipsis para que aparezca el cuadro de diálogo Seleccionar recurso (figura 2.44). Este cuadro de diálogo se utiliza para importar archivos, como imágenes, en cualquier programa. Haga clic en el botón Importar… para buscar una imagen e insertarla. En nuestro caso, la imagen es insecto.png. En el cuadro de diálogo que aparezca (figura 2.45), haga clic en la imagen con el ratón y después haga clic en Aceptar. Aparecerá una vista previa de la imagen en el cuadro de diálogo Seleccionar recurso (figura 2.45). Haga clic en Aceptar para colocar la imagen en su programa. Los formatos de imagen soportados son PNG (Gráficos portables de red), GIF (Formato de intercambio de gráficos), JPEG (Grupo de expertos en fotografía unidos) y BMP (Mapa de bits de Windows). Para crear una nueva imagen se requiere software de edición de imágenes, como Jasc® Paint Shop Pro™

42

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Control Label actualizado

Control PictureBox

Figura 2.42 | Inserte y alinee un control PictureBox.

Valor de la propiedad Image (no hay ninguna imagen seleccionada)

Figura 2.43 | Propiedad Image del control PictureBox.

Figura 2.44 | En el cuadro de diálogo Seleccionar recurso puede seleccionar una imagen para el control PictureBox.

2.6 Crear un programa simple que muestra texto e imagen

43

Nombre del archivo de la imagen

Figura 2.45 | El cuadro de diálogo Seleccionar recurso muestra una vista previa de una imagen seleccionada. (www.jasc.com), Adobe® Photoshop™ Elements (www.adobe.com) o Microsoft Paint (que se incluye con Windows). Para ajustar el tamaño de la imagen al control PictureBox, modifique la propiedad SizeMode a StretchImage (figura 2.46), con lo cual la imagen se escalará al tamaño del control PictureBox. Cambie el tamaño del control PictureBox para hacerlo más grande (figura 2.47). 11. Guarde el proyecto. Seleccione Archivo > Guardar todo para guardar la solución completa. El archivo de la solución contiene el nombre y la ubicación de su proyecto, y el archivo de proyecto contiene los nombres y las ubicaciones de todos los archivos en el proyecto. 12. Ejecute el proyecto. Recuerde que hasta este punto hemos estado trabajando en el modo de diseño del IDE (es decir, el programa que estamos creando no se está ejecutando). En el modo de ejecución el programa se está ejecutando y usted puede interactuar con algunas cuantas características del IDE; las características no disponibles están deshabilitadas (en color gris). El texto Form1.cs [Diseño] en la ficha (figura 2.48) significa que estamos diseñando el formulario de manera visual, en vez de hacerlo por medio de la programación. Si hubiéramos escrito código, la ficha sólo contendría el texto Form1.cs. Para ejecutar el programa, primero debe generar la solución. Seleccione Generar > Generar solución para compilar el proyecto (figura 2.48). Una vez que genere la solución (el IDE mostrará la leyenda “Generación satisfactoria” en la esquina inferior izquierda del IDE, a la cual se le conoce también como barra de estado), seleccione Depurar > Iniciar depuración para ejecutar el programa. La figura 2.49 muestra el IDE en modo de ejecución (lo cual se indica por el texto de la barra de título UnProgramaSimple (Ejecutando) – Microsoft Visual C# 2005 Express Edition). Observe que muchos iconos y menús de la barra de herramientas están deshabilitados. El programa en ejecución aparecerá en una ventana separada, fuera del IDE (figura 2.49).

Propiedad SizeMode

Propiedad SizeMode establecida en StretchImage

Figura 2.46 | Escale una imagen para ajustarla al tamaño del control PictureBox.

44

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Imagen recién insertada

Figura 2.47 | El control PictureBox muestra una imagen.

Menú Generar

Figura 2.48 | Genere una solución. 13. Termine la ejecución. Para terminar el programa, haga clic en el cuadro Cerrar del programa en ejecución (la X en la esquina superior derecha de la ventana del programa en ejecución). Esta acción detendrá la ejecución del programa y regresará el IDE al modo de diseño.

2.7 Conclusión

45

Formulario

Programa en ejecución

Cuadro Cerrar

Figura 2.49 | El IDE en modo de ejecución, con el programa en ejecución en la ventana principal.

2.7 Conclusión En este capítulo le presentamos las características clave del Entorno Integrado de Desarrollo (IDE) de Visual Studio. Utilizó la técnica de la programación visual para crear un programa funcional en Visual C#, sin necesidad de escribir una sola línea de código. La programación en Visual C# es una mezcla de los dos estilos: la programación visual le permite desarrollar GUIs con facilidad y evitar la tediosa programación de GUIs; la programación convencional (que presentaremos en el capítulo 3) le permite especificar el comportamiento de sus programas. En este capítulo creó una aplicación Windows en Visual C# con un formulario. Trabajó con las ventanas Explorador de soluciones, Cuadro de herramientas y Propiedades, las cuales son esenciales para desarrollar programas en Visual C#. La ventana Explorador de soluciones le permite administrar los archivos de su solución en forma visual. La ventana Cuadro de herramientas contiene una robusta colección de controles para crear GUIs. La ventana Propiedades le permite establecer los atributos de un formulario y de los controles. También exploró las características de ayuda de Visual Studio, incluyendo la ventana Ayuda dinámica y el menú Ayuda. La ventana Ayuda dinámica muestra vínculos relacionados con el elemento sobre el que usted haga clic con el ratón. Aprendió a establecer las opciones de Ayuda para mostrar y utilizar recursos de ayuda. También demostramos cómo utilizar la ayuda sensible al contexto. Utilizó la programación visual para diseñar con rapidez y facilidad las porciones de un programa relacionadas con la GUI, mediante la acción de arrastrar y soltar controles (un Label y un PictureBox) en un formulario (Form) o haciendo doble clic en los controles en el Cuadro de herramientas.

46

Capítulo 2 Introducción al IDE de Visual C# 2005 Express Edition

Al crear el programa UnProgramaSimple utilizó la ventana Propiedades para establecer las propiedades Text y del formulario. Aprendió que los controles Label muestran texto y que los controles PictureBox muestran imágenes. Mostró texto en un control Label y agregó una imagen a un control PictureBox. También trabajó con las propiedades AutoSize, TextAlign y SizeMode de un control Label. En el siguiente capítulo hablaremos sobre la programación “no visual” o “convencional”: usted creará sus primeros programas que contendrán código en Visual C# escrito por usted, en vez de hacer que Visual Studio escriba el código. Estudiará las aplicaciones de consola (programas que muestran texto en pantalla sin utilizar una GUI). También aprenderá acerca de los conceptos de memoria, aritmética, toma de decisiones y cómo utilizar un cuadro de diálogo para mostrar un mensaje. BackColor

2.8 Recursos Web En inglés msdn.microsoft.com/vs2005

El sitio de Microsoft Visual Studio ofrece noticias, documentación, descargas y otros recursos. lab.msdn.microsoft.com/vs2005/

Este sitio ofrece información acerca de la versión más reciente de Visual Studio, incluyendo descargas, información de la comunidad y recursos. www.worldofdotnet.net

Este sitio ofrece noticias sobre Visual Studio y vínculos a grupos de noticias y otros recursos. www.c-sharpcorner.com

Este sitio contiene artículos, reseñas de libros y software, documentación, descargas, vínculos e información acerca de C#. www.devx.com/dotnet

Este sitio tiene una zona dedicada a desarrolladores .NET, que contiene artículos, opiniones, grupos de noticias, código, tips y demás recursos acerca de Visual Studio 2005. www.csharp-station.com

Este sitio proporciona artículos acerca de Visual C#, en especial las actualizaciones del 2005. También incluye tutoriales, descargas y otros recursos. En español www.microsoft.com/spanish/msdn/vs2005/default.mspx

El sitio de Microsoft Visual Studio en español ofrece noticias, documentación, descargas y otros recursos. www.microsoft.com/spanish/msdn/vstudio/express/VCS/default.mspx

En este sitio encontrará la versión Visual C# 2005 Express Edition para descargar, junto con noticias y tutoriales. msdn2.microsoft.com/es-es/library/kx37x362.aspx

Este sitio contiene la documentación de Microsoft Visual Studio 2005, además de ejemplos sobre el uso de Visual C#.

3

Introducción a las aplicaciones de C# ¿Qué hay en un nombre? Lo que llamamos rosa aun con otro nombre mantendría su perfume.

OBJETIVOS

—William Shakespeare

En este capítulo aprenderá lo siguiente:

Cuando me topo con una decisión, siempre pregunto, “¿Qué sería lo más divertido?”

Q

Cómo escribir aplicaciones simples en C# usando código, en lugar de la programación visual.

Q

Escribir instrucciones que envíen y reciban datos hacia/desde la pantalla.

Q

Cómo declarar y utilizar datos de diversos tipos.

Q

Almacenar y recuperar datos de la memoria.

Q

Utilizar los operadores aritméticos.

Q

Determinar el orden en que se aplican los operadores.

Q

Escribir instrucciones de toma de decisiones.

Q

Utilizar los operadores relacionales y de igualdad.

Q

Utilizar cuadros de diálogo de mensaje para mostrar mensajes.

—Peggy Walker

“Toma un poco más de té”, dijo el conejo blanco a Alicia, con gran seriedad. “No he tomado nada todavía.” Alicia contestó en tono ofendido, “Entonces no puedo tomar más”. “Querrás decir que no puedes tomar menos”, dijo el sombrerero loco, “es muy fácil tomar más que nada”. —Lewis Carroll

Pla n g e ne r a l

48

Capítulo 3 Introducción a las aplicaciones de C#

3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10

Introducción Una aplicación simple en C#: mostrar una línea de texto Cómo crear una aplicación simple en Visual C# Express Modificación de su aplicación simple en C# Formato del texto con Console.Write y Console.WriteLine Otra aplicación en C#: suma de enteros Conceptos sobre memoria Aritmética Toma de decisiones: operadores de igualdad y relacionales (Opcional) Caso de estudio de ingeniería de software: análisis del documento de requerimientos del ATM 3.11 Conclusión

3.1 Introducción Ahora presentaremos la programación de aplicaciones en C#, que nos facilita un método disciplinado para su diseño. La mayoría de las aplicaciones de C# que estudiaremos en este libro procesa información y muestra resultados. En este capítulo introduciremos las aplicaciones de consola; éstas envían y reciben texto en una ventana de consola. En Microsoft Windows 95/98/ME, la ventana de consola es el símbolo de MS-DOS. En otras versiones de Microsoft Windows, la ventana de consola es el Símbolo del sistema. Empezaremos con diversos ejemplos que sólo muestran mensajes en la pantalla. Después demostraremos una aplicación que obtiene dos números de un usuario, calcula la suma y muestra el resultado. Aprenderá a realizar diversos cálculos aritméticos y a guardar los resultados para utilizarlos posteriormente. Muchas aplicaciones contienen lógica que requiere que la aplicación tome decisiones; el último ejemplo de este capítulo demuestra los fundamentos de la toma de decisiones al enseñarle cómo comparar números y mostrar mensajes con base en los resultados de las comparaciones. Por ejemplo, la aplicación muestra un mensaje que indica que dos números son iguales sólo si tienen el mismo valor. Analizaremos cuidadosamente cada ejemplo, una línea a la vez.

3.2 Una aplicación simple en C#: mostrar una línea de texto Consideremos ahora una aplicación simple que muestra una línea de texto. (Más adelante en esta sección veremos cómo compilar y ejecutar una aplicación.) La figura 3.1 muestra esta aplicación y su salida. La aplicación ilustra varias características importantes del lenguaje C#. Para su conveniencia, cada uno de los programas que presentamos en este libro incluye números de línea, que no forman parte del verdadero código de C#. En la sección 3.3 le indicaremos cómo mostrar los números de línea para su código de C# en el IDE. Pronto veremos que la línea 10 es la que hace el verdadero trabajo de la aplicación; a saber, muestra en la pantalla la frase ¡Bienvenido a la programación en C#! Ahora analizaremos cada una de las líneas de esta aplicación. La línea 1 // Figura 3.1: Bienvenido1.cs

comienza con //, lo cual indica que el resto de la línea es un comentario. Empezamos cada aplicación con un comentario en el que se indica el número de figura y el nombre del archivo en el que se guarda la aplicación. A un comentario que comienza con // se le llama comentario de una sola línea, ya que termina al final de la línea en la que aparece. Este tipo de comentario también puede comenzar en medio de una línea y continuar hasta el final de la misma (como en las líneas 7, 11 y 12). Los comentarios delimitados como /* Éste es un comentario delimitado. Puede dividirse en muchas líneas*/

3.2 Una aplicación simple en C#: mostrar una línea de texto

1 2 3 4 5 6 7 8 9 10 11 12

49

// Fig. 3.1: Bienvenido1.cs // Aplicación para imprimir texto. using System; public class Bienvenido1 { // El método Main comienza la ejecución de la aplicación de C# public static void Main( string[] args ) { Console.WriteLine( "¡Bienvenido a la programación en C#!" ); } // fin del método Main } // fin de la clase Bienvenido1

¡Bienvenido a la programación en C#!

Figura 3.1 | Aplicación para imprimir texto en pantalla. pueden esparcirse a través de varias líneas. Este tipo de comentario comienza con el delimitador /* y termina con el delimitador */. El compilador ignora todo el texto entre los delimitadores. C# incorporó los comentarios delimitados y los de una sola línea de los lenguajes de programación C y C++, en forma respectiva. En este libro utilizaremos comentarios de una sola línea en nuestros programas. La línea 2 // Aplicación para imprimir texto.

es un comentario de una sola línea, que describe el propósito de la aplicación. La línea 3 using System;

es una directiva using, que ayuda al compilador a localizar una clase que se utiliza en esta aplicación. Uno de los puntos fuertes de C# es su robusto conjunto de clases predefinidas que usted puede utilizar, en lugar de “reinventar la rueda”. Estas clases se organizan bajo espacios de nombres: colecciones con nombre de clases relacionadas. A los espacios de nombres de .NET se les conoce como Biblioteca de Clases del .NET Framework (FCL). Cada directiva using identifica las clases predefinidas que puede utilizar una aplicación de C#. La directiva using en la línea 3 indica que este ejemplo utiliza clases del espacio de nombres System, el cual contiene la clase predefinida Console (que veremos en breve) que se utiliza en la línea 10, y muchas otras clases útiles.

Error común de programación 3.1 Todas las directivas using deben aparecer antes de cualquier otro código (excepto los comentarios) en un archivo de código fuente de C#; en caso contrario se produce un error de compilación.

Tip de prevención de errores 3.1 Si olvida incluir una directiva using para una clase que se utilice en su aplicación, es muy probable que se produzca un error de compilación en el que se muestre un mensaje como "El nombre 'Console' no existe en el contexto actual." Cuando esto ocurra, verifique que haya proporcionado las directivas using apropiadas y que los nombres en las directivas using estén deletreados de forma correcta, incluyendo el uso apropiado de letras mayúsculas y minúsculas.

Para cada nueva clase .NET que utilicemos, debemos indicar el espacio de nombres en el que se encuentra. Esta información es importante, ya que nos ayuda a localizar las descripciones de cada clase en la documentación de .NET. Puede encontrar una versión basada en Web de este documento en las siguientes páginas Web: msdn2.microsoft.com/en-us/library/ms229335 (inglés) msdn2.microsoft.com/es-es/library/ms306608 (español)

50

Capítulo 3 Introducción a las aplicaciones de C#

También puede encontrar esta información en la documentación de Visual C# Express, en el menú Ayuda. Incluso puede colocar el cursor sobre el nombre de cualquier clase o método .NET y después oprimir F1 para obtener más información. La línea 4 es tan sólo una línea en blanco. Las líneas en blanco, los caracteres de espacio y los caracteres de tabulación se conocen como espacio en blanco. (En específico, los caracteres de espacio y los tabuladores se conocen como caracteres de espacio en blanco.) El compilador ignora todo el espacio en blanco. En este capítulo y en algunos de los siguientes hablaremos sobre las convenciones para el uso de espacio en blanco, con el fin mejorar la legibilidad de las aplicaciones. La línea 5 public class Bienvenido1

comienza una declaración de clase para la clase Bienvenido1. Toda aplicación consiste de cuando menos una declaración de clase, la cual usted (como programador) define. A estas clases se les conoce como clases definidas por el usuario. La palabra clave class introduce una declaración de clase y va seguida inmediatamente del nombre de la clase (Bienvenido1). Las palabras clave (conocidas como palabras reservadas) están reservadas para uso exclusivo del lenguaje C# y siempre se escriben en minúsculas. En la figura 3.2 se muestra la lista completa de palabras clave de C#. Por convención, todos los nombres de las clases comienzan con mayúscula y la primera letra de cada palabra que incluyen también se escribe en mayúscula (por ejemplo, NombreClaseEjemplo). Por lo general, a esto se le conoce como estilo de mayúsculas/minúsculas Pascal. El nombre de una clase es un identificador, una serie de caracteres que consiste de letras, dígitos y guiones bajos ( _ ), que no comienza con un dígito y que no contiene espacios. Algunos identificadores válidos son Bienvenido1, identificador, _valor y m_campoEntrada1. El nombre 7boton no es un identificador válido, ya que comienza con un dígito; el nombre campo entrada tampoco lo es, ya que contiene un espacio. Por lo general, un identificador que no comienza con letra mayúscula no es el nombre de una clase. C# es sensible a mayúsculas y minúsculas, es decir, las letras mayúsculas y minúsculas son distintas, por lo que a1 y A1 son identificadores distintos (pero ambos son válidos). También se puede colocar el carácter @ al principio de un identificador. Esto indica que una palabra debe interpretarse como identificador, aun y cuando sea una palabra clave (por ejemplo, @int). De esta forma el código de C# puede utilizar código escrito en otros lenguajes .NET, en los que un identificador podría tener el mismo nombre que una palabra clave de C#.

Palabras clave de C# 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

Figura 3.2 | Palabras clave de C#.

3.2 Una aplicación simple en C#: mostrar una línea de texto

51

Buena práctica de programación 3.1 Por convención, siempre se debe comenzar el identificador del nombre de una clase con letra mayúscula y cada palabra subsiguiente en el identificador se debe comenzar con letra mayúscula.

Error común de programación 3.2 C# es sensible a mayúsculas y minúsculas. Por lo general, si no se utiliza la combinación apropiada de letras mayúsculas y minúsculas para un identificador, se produce un error de compilación.

En los capítulos del 3 al 8, cada una de las clases que definimos comienza con la palabra clave public. Por ahora sólo requeriremos esta palabra clave. En el capítulo 9 aprenderá más acerca de las clases public y no public. Cuando guarda su declaración de clase public en un archivo, por lo general el nombre del archivo es el de la clase, seguido de la extensión de nombre de archivo .cs. Para nuestra aplicación, el nombre de archivo es Bienvenido1.cs.

Buena práctica de programación 3.2 Por convención, un archivo que contiene una sola clase public debe tener un nombre que sea idéntico al nombre de la clase (más la extensión .cs) en términos tanto de ortografía como de uso de mayúsculas y minúsculas. Si nombra sus archivos de esta forma facilitará a los demás programadores (y a usted) el proceso de determinar la ubicación de las clases de una aplicación.

Una llave izquierda (en la línea 6 de la figura 3.1), {, comienza el cuerpo de cualquier declaración de clase. Su correspondiente llave derecha (en la línea 12), }, debe terminar cada declaración de clase. Observe que las líneas de la 7 a la 11 tienen sangría. Esta sangría es una de las convenciones de espaciado que mencionamos antes. Definiremos cada convención de espaciado como una Buena práctica de programación.

Tip de prevención de errores 3.2 Siempre que escriba una llave izquierda de apertura, {, en su aplicación, escriba de inmediato la llave derecha de cierre, }, y después reposicione el cursor entre las llaves y utilice sangría para comenzar a escribir el cuerpo. Esta práctica le ayudará a evitar errores por la omisión de llaves.

Buena práctica de programación 3.3 Aplique sangría a todo el cuerpo de cada declaración de clase; utilice un “nivel” de sangría entre las llaves izquierda y derecha que delimiten el cuerpo de su clase. Este formato enfatiza la estructura de la declaración de la clase y mejora la legibilidad. Para dejar que el IDE aplique formato a su código, seleccione Edición > Avanzado > Dar formato al documento.

Buena práctica de programación 3.4 Establezca una convención para el tamaño de sangría que prefiera y después aplique esa convención de manera uniforme. Puede utilizar la tecla Tab para crear sangrías, pero los marcadores de tabulación varían entre los distintos editores de texto. Le recomendamos que utilice tres espacios para formar cada nivel de sangría. En la sección 3.3 le mostraremos cómo hacer esto.

Error común de programación 3.3 Si las llaves no se escriben en pares se produce un error de sintaxis.

La línea 7 // El método Main comienza la ejecución de la aplicación de C#

es un comentario que indica el propósito de las líneas 8-11 de la aplicación. La línea 8 public static void Main( string[] args )

52

Capítulo 3 Introducción a las aplicaciones de C#

es el punto de inicio de toda aplicación. Los paréntesis después del identificador Main indican que es un bloque de construcción de aplicaciones llamado método. Por lo general, las declaraciones de clases contienen uno o más métodos. Sus nombres siguen casi siempre las convenciones de estilo de mayúsculas/minúsculas Pascal que se utilizan para los nombres de las clases. Para cada aplicación es obligatorio que uno de los métodos en una clase se llame Main (que por lo general se define como se muestra en la línea 8); de no ser así, la aplicación no se ejecutará. Los métodos pueden realizar tareas y devolver información cuando completan su trabajo. La palabra clave void (línea 8) indica que este método no devolverá información después de completar su tarea. Más adelante veremos que muchos métodos sí devuelven información; en los capítulos 4 y 7 aprenderá más acerca de ellos. Por ahora, sólo imite la primera línea de Main en sus aplicaciones. La llave izquierda en la línea 9 comienza el cuerpo de la declaración del método. El cuerpo del método debe terminar con su correspondiente llave derecha (línea 11 de la figura 3.1). Observe que la línea 10 en el cuerpo del método tiene sangría entre las llaves.

Buena práctica de programación 3.5 Al igual que con las declaraciones de clases, debe aplicar sangría a todo el cuerpo de la declaración de cada método; para ello utilice un “nivel” de sangría entre las llaves izquierda y derecha que definen el cuerpo del método. Este formato hace que la estructura del método resalte y mejora su legibilidad.

La línea 10 Console.WriteLine( "¡Bienvenido a la programación en C#!" );

instruye a la computadora para que realice una acción; a saber, imprimir (es decir, mostrar en pantalla) la cadena de caracteres contenida entre los dos símbolos de comillas dobles. A una cadena también se le conoce como cadena de caracteres, un mensaje o una literal de cadena. A los caracteres entre símbolos de comillas dobles les llamaremos simplemente cadenas. El compilador no ignora los caracteres de espacio en blanco en las cadenas. La clase Console proporciona características de entrada/salida estándar, las cuales permiten a las aplicaciones leer y mostrar texto en la ventana de consola desde la cual se ejecuta la aplicación. El método Console. WriteLine muestra (o imprime) una línea de texto en la ventana de consola. La cadena entre paréntesis en la línea 10 es el argumento para el método. La tarea del método Console.WriteLine es mostrar (o enviar de salida) su argumento en la ventana de consola. Cuando Console.WriteLine completa su tarea, posiciona el cursor de la pantalla (el símbolo destellante que indica en dónde se mostrará el siguiente carácter) al principio de la siguiente línea en la ventana de consola. (Este movimiento del cursor es similar a lo que ocurre cuando un usuario oprime Intro mientras escribe en un editor de texto: el cursor se desplaza al principio de la siguiente línea en el archivo.) A toda la línea 10, incluyendo Console.WriteLine, los paréntesis, el argumento "¡Bienvenido a la programación en C#!" entre paréntesis y el punto y coma (;), se le conoce como instrucción. Cada instrucción termina con un punto y coma. Cuando se ejecuta la instrucción de la línea 10, muestra el mensaje ¡Bienvenido a la programación en C#! en la ventana de consola. Por lo general, un método está compuesto de una o más instrucciones, que realizan la tarea de ese método.

Error común de programación 3.4 Si se omite el punto y coma al final de una instrucción se produce un error de sintaxis.

Algunos programadores tienen problemas para asociar las llaves izquierda y derecha ({ y }) que delimitan el cuerpo de la declaración de una clase o de un método cuando leen o escriben una aplicación. Por esta razón, es común incluir un comentario después de cada llave derecha de cierre (}) que termina la declaración de un método, y después de cada llave derecha de cierre que termina la declaración de una clase. Por ejemplo, la línea 11 }

// fin del método Main

especifica la llave derecha de cierre del método Main, y la línea 12 }

// fin de la clase Bienvenido1

3.3 Cómo crear una aplicación simple en Visual C# Express

53

especifica la llave derecha de cierre de la clase Bienvenido1. Cada uno de estos comentarios indica el método o la clase que termina con esa llave derecha. Visual Studio puede ayudarle a localizar las llaves que concuerden en su código. Sólo tiene que colocar el cursor a un lado de una llave y Visual Studio resaltará la otra.

Buena práctica de programación 3.6 Si después de la llave derecha de cierre del cuerpo de un método o de la declaración de una clase se coloca un comentario que indique el método o la declaración de la clase a la cual pertenece esa llave, se mejora la legibilidad de la aplicación.

3.3 Cómo crear una aplicación simple en Visual C# Express Ahora que le hemos presentado nuestra primera aplicación de consola (figura 3.1), veremos una explicación paso a paso de cómo compilarla y ejecutarla mediante el uso de Visual C# Express.

Creación de la aplicación de consola Después de abrir Visual C# 2005 Express, seleccione Archivo > Nuevo proyecto… para que aparezca el cuadro de diálogo Nuevo proyecto (figura 3.3) y luego seleccione la plantilla Aplicación de consola. En el campo Nombre del cuadro de diálogo, escriba Bienvenido1 y haga clic en Aceptar para crear el proyecto. Ahora el IDE debe contener la aplicación de consola, como se muestra en la figura 3.4. Observe que la ventana del editor ya contiene algo de código proporcionado por el IDE. Parte de este código es similar al de la figura 3.1. Otra parte no lo es, ya que utiliza características que no hemos visto todavía. El IDE inserta este código adicional para ayudar a organizar la aplicación y proporcionar acceso a ciertas clases comunes en la Biblioteca de clases del .NET Framework; en este punto del libro, este código no se requiere ni es relevante a la discusión de esta aplicación. Por lo tanto, puede eliminarlo. El esquema de colores del código que utiliza el IDE se llama resaltado de sintaxis por colores; este esquema nos ayuda a diferenciar en forma visual los elementos de la aplicación. Las palabras clave aparecen en color azul y el resto del texto en color negro. Cuando hay comentarios, aparecen en color verde. En este libro sombrearemos nuestro código de manera similar; texto en negrita y cursiva para las palabras clave, cursiva para los comentarios, negrita color gris para las literales y constantes, y color negro para el resto del texto. Un ejemplo de literal es la cadena que se pasa a Console.WriteLine en la línea 10 de la figura 3.1. Para personalizar los colores que se muestran en el editor de código seleccione Herramientas > Opciones… A continuación aparecerá el cuadro de diálogo

Nombre del proyecto

Figura 3.3 | Creación de una Aplicación de consola con el cuadro de diálogo Nuevo proyecto.

54

Capítulo 3 Introducción a las aplicaciones de C#

Ventana del editor

Figura 3.4 | El IDE con una aplicación de consola abierta.

Figura 3.5 | Modificación de las opciones de configuración del IDE. Opciones (figura 3.5). Después haga clic en el signo más (+) que está a un lado colores. Aquí puede modificar los colores para varios elementos de código.

de Entorno y seleccione Fuentes y

Modificación de la configuración del editor para mostrar números de línea Visual C# Express le ofrece muchas formas para personalizar su manera de trabajar con el código. En este paso modificará las configuraciones para que su código concuerde con el de este libro. Para que el IDE muestre los números de línea, seleccione Herramientas > Opciones… En el cuadro de diálogo que se despliega, haga clic en la casilla de verificación Mostrar todas las configuraciones que se encuentra en la parte inferior izquierda, después haga clic en el signo más que está a un lado de Editor de texto en el panel izquierdo y seleccione Todos los lenguajes. En el panel derecho, active la casilla de verificación Números de línea. Mantenga abierto el cuadro de diálogo Opciones.

3.3 Cómo crear una aplicación simple en Visual C# Express

55

Establecer la sangría del código a tres espacios En el cuadro de diálogo Opciones que abrió en el paso anterior (figura 3.5), haga clic en el signo más que está a un lado de C# en el panel izquierdo y seleccione Tabulaciones. Escriba el número 3 en los campos Tamaño de tabulación y Tamaño de sangría. Todo el nuevo código que agregue utilizará ahora tres espacios para cada nivel de sangría. Haga clic en Aceptar para guardar sus nuevas configuraciones, cierre el cuadro de diálogo y regrese a la ventana del editor.

Modificación del nombre del archivo de aplicación Para las aplicaciones que crearemos en este libro, modificaremos el nombre predeterminado del archivo de aplicación (es decir, Program.cs) para ponerle un nombre más descriptivo. Para cambiar el nombre del archivo, haga clic en el nombre Program.cs, en la ventana Explorador de soluciones. A continuación aparecerán las propiedades del archivo de aplicación en la ventana Propiedades (figura 3.6). Cambie la propiedad Nombre de archivo a Bienvenido1.cs.

Escritura de código En la ventana del editor (figura 3.4), escriba el código de la figura 3.1. Después de escribir (en la línea 10) el nombre de la clase y un punto (por ejemplo, Console.) aparecerá una ventana con una barra de desplazamiento (figura 3.7). A esta característica del IDE se le llama IntelliSense; sirve para listar los miembros de una clase, incluyendo los nombres de los métodos. A medida que usted escribe caracteres, Visual C# Express resalta el primer miembro que concuerda con todos los caracteres escritos y después muestra un cuadro de información de herramienta (tool tip), que contiene una descripción de ese miembro. Puede escribir el nombre del miembro completo (por ejemplo, WriteLine), hacer doble clic en el nombre del miembro que se encuentra en la lista u oprimir Tab para completar el nombre. Una vez que se proporciona el nombre completo, la ventana IntelliSense se cierra. Cuando escribe el carácter de paréntesis abierto (, después de Console.WriteLine, aparece la ventana Información de parametros (figura 3.8). Esta ventana contiene información sobre los parámetros del método. Como aprenderá en el capítulo 7, puede haber varias versiones de un método; esto es, una clase puede definir varios métodos que tengan el mismo nombre, siempre y cuando tengan distintos números y/o tipos de parámetros.

Explorador de soluciones

Haga clic en Program.cs para mostrar sus propiedades

Ventana Propiedades

Propiedad Nombre de archivo

Escriba Bienvenido1.cs aquí para cambiar el nombre del archivo

Figura 3.6 | Modificación del nombre del programa en la ventana Propiedades.

56

Capítulo 3 Introducción a las aplicaciones de C#

Se escribió parte del nombre del miembro

Lista de miembros Miembro resaltado

El cuadro de información de herramienta (tool tip) describe el miembro resaltado

Figura 3.7 | Característica IntelliSense de Visual C# Express.

Por lo general, todos estos métodos realizan tareas similares. La ventana Información de parámetros indica cuántas versiones del método seleccionado hay disponibles y proporciona flechas hacia arriba y hacia abajo para desplazarse a través de las distintas versiones. Por ejemplo, hay 19 versiones del método WriteLine; en nuestra aplicación utilizamos una de esas 19 versiones. La ventana Información de parámetros es una de las muchas características que ofrece el IDE para facilitar el desarrollo de aplicaciones. En los siguientes capítulos aprenderá más acerca de la información que se muestra en estas ventanas. La ventana Información de parámetros es útil, en especial, cuando deseamos ver las distintas formas en las que se puede utilizar un método. Del código de la figura 3.1 ya sabemos que nuestra intención es mostrar una cadena con WriteLine, y como sabemos con exactitud cuál versión de WriteLine deseamos utilizar, por el momento sólo hay que cerrar la ventana Información de parámetros mediante la tecla Esc.

Guardar la aplicación Seleccione Archivo > Guardar todo para que aparezca el cuadro de diálogo Guardar proyecto (figura 3.9). En el cuadro de texto Ubicación, especifique el directorio en el que desea guardar el proyecto. Nosotros optamos por guardarlo en el directorio MisProyectos de la unidad C:. Seleccione la casilla de verificación Crear directorio para la solución (para que Visual Studio cree el directorio, en caso de que no exista todavía) y haga clic en Guardar.

Flecha arriba

Flecha arriba

Figura 3.8 | Ventana Información de parámetros.

Ventana Información de parámetros

3.3 Cómo crear una aplicación simple en Visual C# Express

57

Figura 3.9 | Cuadro de diálogo Guardar proyecto.

Compilación y ejecución de la aplicación Ahora está listo para compilar y ejecutar su aplicación. Dependiendo del tipo de aplicación, el compilador puede compilar el código en archivos con una extensión .exe (ejecutable), con una extensión .dll (biblioteca de vínculos dinámicos) o con alguna otra disponible. Dichos archivos se llaman ensamblados y son las unidades de empaquetamiento para el código de C# compilado. Estos ensamblados contienen el código de Lenguaje intermedio de Microsoft (MSIL) para la aplicación. Para compilar la aplicación, seleccione Generar > Generar solución. Si la aplicación no contiene errores de sintaxis, su aplicación de consola se compilara en un archivo ejecutable (llamado Bienvenido1.exe, en el directorio del proyecto). Para ejecutar esta aplicación de consola (es decir, Bienvenido1.exe), seleccione Depurar > Iniciar sin depurar (u oprima F5), con lo cual se invocará el método Main (figura 3.1). La instrucción en la línea 10 de Main muestra ¡Bienvenido a la programación en C#!. La figura 3.10 muestra los resultados de la ejecución de esta aplicación. Observe que los resultados se despliegan en una ventana de consola. Deje la aplicación abierta en Visual C# Express; más adelante en esta sección volveremos a utilizarla.

Ejecución de la aplicación desde el símbolo del sistema Como dijimos al principio del capítulo, puede ejecutar aplicaciones fuera del IDE a través del Símbolo del sistema. Esto es útil cuando sólo desea ejecutar una aplicación, en vez de abrirla para modificarla. Para abrir el Símbolo del sistema, seleccione Inicio > Todos los programas > Accesorios > Símbolo del sistema. [Nota: Los usuarios de Windows 2000 deben sustituir Todos los programas con Programas.] La ventana (figura 3.11) muestra la información de derechos de autor, seguida de un símbolo que indica el directorio actual. De manera predeterminada, el símbolo especifica el directorio actual del usuario en el equipo local (en nuestro caso, C:\Documents and Settings\deitel). En su equipo, el nombre de carpeta deitel se sustituirá con su nombre de usuario. Escriba el comando cd (que significa “cambiar directorio”), seguido del modificador /d (para cambiar de unidad, en caso de ser necesario) y después el directorio en el que se encuentra el archivo .exe de la aplicación (es decir, el directorio Release de su aplicación). Por ejemplo, el comando cd/d C:\MisProyectos\Bienvenido1\Bienvenido1\ bin\Release (figura 3.12) cambia el directorio actual al directorio Release de la aplicación Bienvenido1 en la unidad C:. El siguiente símbolo muestra el nuevo directorio. Después de cambiar al directorio actual, usted puede ejecutar la aplicación compilada con sólo escribir el nombre del archivo .exe (es decir, Bienvenido1). La apli-

Ventana de consola

Figura 3.10 | La ejecución de la aplicación que se muestra en la figura 3.1.

58

Capítulo 3 Introducción a las aplicaciones de C#

cación se ejecutará hasta completarse y después aparecerá de nuevo el símbolo del sistema, esperando el siguiente comando. Para cerrar el Símbolo del sistema, escriba exit (figura 3.12) y oprima Intro. Visual C# 2005 Express mantiene un directorio Debug y un directorio Release en el directorio bin de cada proyecto. El primero contiene una versión de la aplicación que puede utilizarse con el depurador (vea el apéndice C, Uso del depurador de Visual Studio® 2005). El directorio Release contiene una versión optimizada que usted puede proporcionar a sus clientes. En la versión completa de Visual Studio 2005 puede seleccionar la versión específica que desea generar en la lista desplegable Configuraciones de la solución de las barras de herramientas que se encuentran en la parte superior del IDE. La versión predeterminada es Debug. [Nota: muchos entornos muestran ventanas Símbolo del sistema con fondos negros y texto blanco. Nosotros ajustamos estas configuraciones en nuestro entorno para mejorar la legibilidad de las capturas de pantalla.]

Errores de sintaxis, mensajes de error y la ventana Lista de errores Regrese a la aplicación en Visual C# Express. Cuando escribe una línea de código y oprime Intro, el IDE responde ya sea mediante la aplicación de resaltado de sintaxis por colores o mediante la generación de un error de sintaxis, el cual indica una violación a las reglas de Visual C# para crear aplicaciones correctas (es decir, una o más instrucciones no están escritas correctamente). Los errores de sintaxis se producen por diversas razones, como omitir paréntesis y palabras clave mal escritas.

Cuando se abre la ventana Símbolo del sistema aparece el símbolo predeterminado

El usuario escribe el siguiente commando aquí

Figura 3.11 | Ejecución de la aplicación que se muestra en la figura 3.1, desde una ventana Símbolo del sistema.

El símbolo actualizado muestra el nuevo directorio actual

Salida de la aplicación

Cierra la ventana Símbolo del sistema

El símbolo actualizado muestra el nuevo directorio actual

Escriba esto para ejecutar la aplicación Bienvenido1.exe

Figura 3.12 | Ejecución de la aplicación que se muestra en la figura 3.1, desde una ventana Símbolo del sistema.

3.3 Cómo crear una aplicación simple en Visual C# Express

59

El carácter de paréntesis se omitió de manera intencional (error de sintaxis)

Descripción(es) del (los) error(es)

Ventana Lista de errores

El subrayado color rojo indica un error de sintaxis

Figura 3.13 | Errores de sintaxis indicados por el IDE.

Cuando se produce un error de sintaxis, el IDE lo subraya en color rojo y proporciona una descripción del error en la ventana Lista de errores (figura 3.13). Si la ventana no está visible en el IDE, seleccione Ver > Lista de errores para mostrarla. En la figura 3.13 omitimos intencionalmente el primer paréntesis en la línea 10. El primer error contiene el texto "Se esperaba ;" y especifica que el error está en la columna 25 de la línea 10. Este mensaje de error aparece cuando el compilador piensa que la línea contiene una instrucción completa, seguida de un punto y coma, y el comienzo de otra instrucción. El segundo error contiene el mismo texto, pero especifica que está en la columna 54 de la línea 10, ya que el compilador piensa que éste es el final de la segunda instrucción. El tercer error tiene el texto "El término de la expresión ')' no es válido", ya que el compilador está confundido por el paréntesis derecho sin su correspondiente paréntesis izquierdo. Aunque estamos tratando de incluir sólo una instrucción en la línea 10, el paréntesis izquierdo que falta ocasiona que el compilador asuma de manera incorrecta que hay más de una instrucción en esa línea, que malinterprete el paréntesis derecho y genere tres mensajes de error.

Tip de prevención de errores 3.3 Un error de sintaxis puede producir varias entradas en la ventana Lista de errores. Con cada error que usted corrija, podrá eliminar varios mensajes de error subsiguientes al compilar nuevamente su aplicación. Por lo tanto, cuando vea un error que sabe cómo corregir, hágalo y vuelva a compilar el programa; esto puede hacer que desaparezcan varios errores.

60

Capítulo 3 Introducción a las aplicaciones de C#

3.4 Modificación de su aplicación simple en C# En esta sección continuamos con nuestra introducción a la programación en C# con dos ejemplos que modifican el de la figura 3.1, para imprimir texto en una línea mediante el uso de varias instrucciones e imprimir texto en varias líneas mediante el uso de una sola instrucción.

Mostrar una sola línea de texto con varias instrucciones El texto "¡Bienvenido a la programación en C#!" puede mostrarse de varias formas. La clase Bienvenido2, que se muestra en la figura 3.14, utiliza dos instrucciones para producir el mismo resultado que el que se muestra en la figura 3.1. De aquí en adelante resaltaremos las nuevas características clave en cada listado de código, como se muestra en las líneas 10-11 de la figura 3.14. La aplicación es casi idéntica a la figura 3.1. Sólo veremos los cambios. La línea 2 // Impresión de una línea de texto mediante varias instrucciones.

es un comentario que indica el propósito de esta aplicación. La línea 5 comienza la declaración de la clase Bienvenido2.

Las líneas 10-11 del método Main Console.Write( "¡Bienvenido a " ); Console.WriteLine( "la programación en C#!" );

muestran una línea de texto en la ventana de consola. La primera instrucción utiliza el método Write de Console para mostrar una cadena. A diferencia de WriteLine, después de mostrar su argumento, el método Write no posiciona el cursor de la pantalla al comienzo de la siguiente línea en la ventana de consola; el siguiente carácter que muestre la aplicación aparecerá justo después del último carácter que muestre Write. Por ende, la línea 11 posiciona el primer carácter en su argumento (la letra “C”) justo después del último carácter que muestra la línea 10 (el carácter de espacio que va antes del carácter de doble comilla de cierre de la cadena). Cada instrucción Write continúa mostrando caracteres desde donde la última instrucción Write mostró su último carácter.

Mostrar varias líneas de texto mediante una sola instrucción Una sola instrucción puede mostrar varias líneas mediante el uso de caracteres de nueva línea, que indican a los métodos Write y WriteLine de Console cuándo deben posicionar el cursor de la pantalla en el comienzo de la siguiente línea en la ventana de consola. Al igual que los caracteres de espacio y los tabuladores, los caracteres de nueva línea son caracteres de espacio en blanco. La aplicación de la figura 3.15 muestra cuatro líneas de texto mediante el uso de caracteres de nueva línea, para indicar cuándo se debe comenzar cada nueva línea.

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 3.14: Bienvenido2.cs // Impresión de una línea de texto mediante varias instrucciones. using System; public class Bienvenido2 { // El método Main comienza la ejecución de la aplicación de C# public static void Main( string[] args ) { Console.Write( "¡Bienvenido a " ); Console.WriteLine( "la programación en C#!" ); } // fin del método Main } // fin de la clase Bienvenido2

¡Bienvenido a la programación en C#!

Figura 3.14 | Impresión de una línea de texto mediante varias instrucciones.

3.5 Formato del texto con Console.Write y Console.WriteLine

1 2 3 4 5 6 7 8 9 10 11 12

61

// Fig. 3.15: Bienvenido3.cs // Impresión de varias líneas mediante una sola instrucción. using System; public class Bienvenido3 { // El método Main comienza la ejecución de la aplicación de C# public static void Main( string[] args ) { Console.WriteLine( "¡Bienvenido\na la\nprogramación en\nC#!" ); } // fin del método Main } // fin de la clase Bienvenido3

¡Bienvenido a la programación en C#!

Figura 3.15 | Impresión de varias líneas mediante una sola instrucción.

La mayor parte de la aplicación es idéntica a las aplicaciones de las figuras 3.1 y 3.14, por lo que aquí sólo hablaremos de los cambios. La línea 2 // Impresión de varias líneas mediante una sola instrucción.

es un comentario que indica el propósito de esta aplicación. La línea 5 comienza la declaración de la clase Bienvenido3.

La línea 10 Console.WriteLine( "¡Bienvenido\na la\nprogramación en\nC#!" );

muestra cuatro líneas de texto separadas en la ventana de consola. Por lo general, los caracteres en una cadena se muestran en forma exacta a como aparecen en las comillas dobles. Sin embargo, los dos caracteres \ y n (que se repiten tres veces en la instrucción) no aparecen en la pantalla. A la barra diagonal inversa ( \ ) se le conoce como carácter de escape y sirve para indicar a C# que hay un “carácter especial” en la cadena. Cuando aparece una barra diagonal inversa en una cadena de caracteres, C# combina el siguiente carácter con la barra diagonal inversa para formar una secuencia de escape. La secuencia de escape \n representa el carácter de nueva línea. Cuando aparece un carácter de nueva línea en una cadena que se va a imprimir en pantalla mediante métodos de la clase Console, este carácter hace que el cursor de la pantalla se desplace hasta el comienzo de la siguiente línea en la ventana de consola. La figura 3.16 lista varias secuencias de escape comunes y describe cómo afectan a la visualización de caracteres en la ventana de consola.

3.5 Formato del texto con Console.Write y Console.WriteLine Los métodos Write y WriteLine de Console también tienen la capacidad de mostrar datos con formato. La figura 3.17 imprime en pantalla las cadenas "¡Bienvenido a" y "la programación en C#!" mediante el uso de WriteLine. La línea 10 Console.WriteLine( "{0}\n{1}", "¡Bienvenido a", "la programación en C#!" );

llama al método Console.WriteLine para mostrar la salida de la aplicación. La llamada al método especifica tres argumentos. Cuando un método requiere varios argumentos, éstos van separados por comas (,); a lo que se le conoce como lista separada por comas.

62

Capítulo 3 Introducción a las aplicaciones de C#

Secuencia de escape

Descripción

\n

Nueva línea. Posiciona el cursor de la pantalla en el comienzo de la siguiente línea.

\t

Tabulación horizontal. Desplaza el cursor de la pantalla a la siguiente marca de tabulación.

\r

Retorno de carro. Posiciona el cursor de la pantalla en el comienzo de la línea actual; no avanza el cursor a la siguiente línea. Cualquier carácter que se imprima en pantalla después del retorno de carro sobrescribirá a los caracteres que se hayan impreso antes en esa línea.

\\

Barra diagonal inversa. Se utiliza para colocar un carácter de barra diagonal inversa en una cadena.

\"

Doble comilla. Se utiliza para colocar un carácter de doble comilla (") en una cadena. Por ejemplo, Console.Write( "\"entre comillas\"" );

se muestra en pantalla como "entre comillas"

Figura 3.16 | Algunas secuencias de escape comunes.

Buena práctica de programación 3.7 Coloque un espacio después de cada coma (,) en una lista de argumentos para que las aplicaciones tengan mejor legibilidad.

Recuerde que todas las instrucciones terminan con un punto y coma (;). Por lo tanto, la línea 10 sólo representa una instrucción. Las instrucciones largas pueden dividirse en muchas líneas, pero hay algunas restricciones.

Error común de programación 3.5 Si se divide una instrucción a la mitad de un identificador o una cadena, se produce un error de sintaxis.

El primer argumento del método WriteLine es una cadena de formato, la cual puede consistir de texto fijo y elementos de formato. El método WriteLine imprime en pantalla el texto fijo como se demostró en la figura 3.1. Cada elemento de formato es un receptáculo para un valor. Los elementos de formato también pueden incluir información de formato opcional. Los elementos de formato van encerrados entre llaves y contienen una secuencia de caracteres, que indican al método qué argumento debe utilizar y cómo darle formato. Por ejemplo, el elemento de formato {0} es un

1 2 3 4 5 6 7 8 9 10 11 12

// Fig. 3.17: Bienvenido4.cs // Impresión de varias líneas de texto con formato de cadenas. using System; public class Bienvenido4 { // El método Main comienza la ejecución de la aplicación de C# public static void Main( string[] args ) { Console.WriteLine( "{0}\n{1}", "¡Bienvenido a", "la programación en C#!" ); } // fin del método Main } // fin de la clase Bienvenido4

¡Bienvenido a la programación en C#!

Figura 3.17 | Impresión de varias líneas de texto con formato de cadenas.

3.6 Otra aplicación en C#: suma de enteros

63

receptáculo para el primer argumento adicional (ya que C# empieza a contar desde 0), {1} es un receptáculo para el segundo argumento, etc. La cadena de formato en la línea 10 especifica que WriteLine debe imprimir en pantalla dos argumentos y que el primero debe ir seguido de un carácter de nueva línea. Por lo tanto, este ejemplo sustituye la cadena "¡Bienvenido a" por el {0} y la cadena "la programación en C#!" por el {1}. En la salida en pantalla se muestran dos líneas de texto. Como por lo general las llaves en una cadena con formato indican un receptáculo para la sustitución de texto, debe escribir dos llaves izquierdas ({{) o dos llaves derechas (}}) para poder insertar una llave izquierda o derecha respectivamente en una cadena con formato. Conforme sea necesario, iremos presentando características adicionales de formato en nuestros ejemplos.

3.6 Otra aplicación en C#: suma de enteros Nuestra siguiente aplicación lee (o recibe como entrada) dos enteros (números como 22, 7, 0 y 1024) que escribe un usuario en el teclado, calcula la suma de los valores y muestra el resultado. Esta aplicación debe llevar el registro de los números que suministra el usuario para realizar el cálculo más adelante. Las aplicaciones recuerdan números y otros datos en la memoria de la computadora y acceden a ellos a través de ciertos elementos, conocidos como variables. La aplicación de la figura 3.18 muestra estos conceptos. En la salida de ejemplo, resaltamos, en negrita, los datos que escribe el usuario desde el teclado. Las líneas 1-2 // Fig. 3.18: Suma.cs // Muestra la suma de dos números que se introducen desde el teclado.

indican el número de la figura, el nombre del archivo y el propósito de la aplicación. La línea 5 public class Suma

comienza la declaración de la clase Suma. Recuerde que el cuerpo de cada declaración de clase comienza con una llave izquierda de apertura (línea 6) y termina con una llave derecha de cierre (línea 26). La aplicación comienza su ejecución con el método Main (líneas 8-25). La llave izquierda (línea 9) marca el comienzo del cuerpo de Main y la correspondiente llave derecha (línea 25) marca el final. Observe que el método Main tiene un nivel de sangría dentro del cuerpo de la clase Suma y que el código en el cuerpo de Main tiene otro nivel de sangría, para mejorar la legibilidad.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

// Fig. 3.18: Suma.cs // Muestra la suma de dos números que se introducen desde el teclado. using System; public class Suma { // El método Main comienza la ejecución de la aplicación de C# public static void Main( string[] args ) { int numero1; // declara el primer número a sumar int numero2; // declara el segundo número a sumar int suma; // declara la suma de numero1 y numero2 Console.Write( "Escriba el primer entero: " ); // mensaje para el usuario // lee el primer número del usuario numero1 = Convert.ToInt32( Console.ReadLine() ); Console.Write( "Escriba el segundo entero: " ); // mensaje para el usuario // lee el segundo número del usuario

Figura 3.18 | Impresión en pantalla de la suma de dos números introducidos desde el teclado. (Parte 1 de 2).

64

20 21 22 23 24 25 26

Capítulo 3 Introducción a las aplicaciones de C#

numero2 = Convert.ToInt32( Console.ReadLine() ); suma = numero1 + numero2; // suma los números Console.WriteLine( "La suma es {0}", suma ); // muestra la suma } // fin del método Main } // fin de la clase Suma

Escriba el primer entero: 45 Escriba el segundo entero: 72 La suma es 117

Figura 3.18 | Impresión en pantalla de la suma de dos números introducidos desde el teclado. (Parte 2 de 2). La línea 10 int numero1; // declara el primer número a sumar

es una instrucción de declaración de variable (también llamada declaración), la cual especifica el nombre y el tipo de una variable (numero1) que se utilizará en esta aplicación. El nombre de una variable permite que la aplicación acceda al valor de esa variable en memoria; el nombre puede ser cualquier identificador válido (en la sección 3.2 podrá consultar los requerimientos de nomenclatura para los identificadores). El tipo de una variable especifica qué tipo de información se almacena en esa ubicación en memoria. Al igual que las demás instrucciones, las de declaración terminan con un punto y coma (;). La declaración en la línea 10 especifica que la variable llamada numero1 es de tipo int: puede almacenar valores enteros (números como 7, –11, 0 y 31914). El rango de valores para un int es de –2,147,483,648 (int. MinValue) a +2,147,483,647 (int.MaxValue). Pronto hablaremos sobre los tipos float, double y decimal para especificar números reales, y del tipo char para especificar caracteres. Los números reales contienen puntos decimales, como 3.4, 0.0 y -11.19. Las variables de tipo float y double almacenan, en memoria, aproximaciones de números reales. Las variables de tipo decimal almacenan números reales con precisión (hasta 28-29 dígitos significativos), por lo que se utilizan con frecuencia en cálculos monetarios. Las variables de tipo char representan caracteres individuales, como una letra mayúscula (por ejemplo, A), un dígito (por ejemplo, 7), un carácter especial (por ejemplo, * o %) o una secuencia de escape (por ejemplo, el carácter de nueva línea, \n). Por lo general, a los tipos como int, float, double, decimal y char se les conoce como tipos simples. Los nombres de tipo simple son palabras clave y deben aparecer todos en minúscula. En el apéndice I se sintetizan las características de los trece tipos simples (bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double y decimal). Las instrucciones de declaración de variables en las líneas 11-12 int numero2; // declara el segundo número a sumar int suma; // declara la suma de numero1 y numero2

declaran de manera similar las variables numero2 y suma como de tipo int. Las instrucciones de declaración de variables pueden dividirse en varias líneas, con los nombres de las variables separados por comas (es decir, una lista separada por comas de nombres de variables). Pueden declararse distintas variables del mismo tipo en una o en varias declaraciones. Por ejemplo, las líneas 10-12 también pueden escribirse de la siguiente manera: int numero1, // declara el primer número a sumar numero2, // declara el segundo número a sumar suma; // declara la suma de numero1 y numero2

Buena práctica de programación 3.8 Declare cada variable en una línea separada. Este formato permite insertar fácilmente un comentario a continuación de cada declaración.

3.6 Otra aplicación en C#: suma de enteros

65

Buena práctica de programación 3.9 Seleccionar nombres de variables significativos ayuda a que un programa se autodocumente (es decir, que sea más fácil entender el código con sólo leerlo, en lugar de leer manuales o ver un número excesivo de comentarios).

Buena práctica de programación 3.10 Por convención, los identificadores de nombres de variables empiezan con una letra minúscula y cada una de las palabras en el nombre, que van después de la primera, deben empezar con una letra mayúscula. A esta convención se le conoce como estilo de mayúsculas/minúsculas Camel.

La línea 14 Console.Write( "Escriba el primer entero: " ); // mensaje para el usuario

utiliza Console.Write para mostrar el mensaje "Escriba el primer cador, ya que indica al usuario que debe realizar una acción específica. La línea 16

entero: ".

Este mensaje se llama indi-

numero1 = Convert.ToInt32( Console.ReadLine() );

funciona en dos pasos. Primero llama al método ReadLine de Console. Este método espera a que el usuario escriba una cadena de caracteres en el teclado y que oprima Intro para enviar la cadena a la aplicación. Después, la cadena se utiliza como un argumento para el método ToInt32 de la clase Convert, el cual convierte esta secuencia de caracteres en datos de tipo int. Como dijimos antes en este capítulo, algunos métodos realizan una tarea y después devuelven el resultado. En este caso, el método ToInt32 devuelve la representación int de la entrada del usuario. Técnicamente, el usuario puede escribir cualquier cosa como valor de entrada. ReadLine lo aceptará y lo pasará al método ToInt32. Este método asume que la cadena contiene un valor entero válido. En esta aplicación, si el usuario escribe un valor no entero, se producirá un error lógico en tiempo de ejecución y la aplicación terminará. En el capítulo 12, Manejo de excepciones, veremos cómo hacer que sus aplicaciones sean más robustas al permitir que manejen dichos errores y continúen ejecutándose. A esto también se le conoce como hacer que su aplicación sea tolerante a fallas. En la línea 16, el resultado de la llamada al método ToInt32 (un valor int) se coloca en la variable numero1 mediante el uso del operador de asignación, =. La instrucción se lee como “numero1 obtiene el valor devuelto por Convert.ToInt32”. Al operador = se le denomina operador binario, ya que tiene dos operandos: numero1 y el resultado de la llamada al método Convert.ToInt32. Esta instrucción se llama instrucción de asignación, ya que asigna un valor a una variable. Todo lo que esté a la derecha del operador de asignación (=) siempre se evalúa antes de que se realice la asignación.

Buena práctica de programación 3.11 Coloque espacios en ambos lados de un operador binario para hacerlo que resalte y que el código sea más legible.

La línea 18 Console.Write( "Escriba el segundo entero: " ); // mensaje para el usuario

pide al usuario que escriba el segundo entero. La línea 20 numero2 = Convert.ToInt32( Console.ReadLine() );

lee un segundo entero y lo asigna a la variable numero2. La línea 22 suma = numero1 + numero2; // suma los números

es una instrucción de asignación que calcula la suma de las variables numero1 y numero2, y asigna el resultado a la variable suma mediante el uso del operador de asignación, =. La mayoría de los cálculos se realiza en instrucciones

66

Capítulo 3 Introducción a las aplicaciones de C#

de asignación. Cuando la aplicación encuentra el operador de suma, utiliza los valores almacenados en las variables numero1 y numero2 para realizar el cálculo. En la instrucción anterior, el operador de suma es un operador binario; sus dos operandos son numero1 y numero2. A las porciones de las instrucciones que contienen cálculos se les llama expresiones. De hecho, una expresión es cualquier porción de una instrucción que tiene un valor asociado. Por ejemplo, el valor de la expresión numero1 + numero2 es la suma de los números. De manera similar, el valor de la expresión Console.ReadLine() es la cadena de caracteres que escribe el usuario. Después de realizar el cálculo, la línea 24 Console.WriteLine( "La suma es {0}", suma ); // muestra la suma

utiliza el método Console.WriteLine para mostrar la suma. El elemento de formato {0} es un receptáculo para el primer argumento después de la cadena de formato. Aparte del elemento de formato {0}, el resto de los caracteres en la cadena de formato son texto fijo. Por lo tanto, el método WriteLine muestra "La suma es", seguida del valor de suma (en la posición del elemento de formato {0}) y una nueva línea. Los cálculos también pueden realizarse dentro de instrucciones de salida. Podríamos haber combinado las instrucciones en las líneas 22 y 24 en la instrucción Console.WriteLine( "La suma es {0}", ( numero1 + numero2 ) );

Los paréntesis alrededor de la expresión numero1 + numero2 no son requeridos; se incluyen para enfatizar que el valor de la expresión numero1 + numero2 se imprime en pantalla en la posición del elemento de formato {0}.

3.7 Conceptos sobre memoria Los nombres de las variables como numero1, numero2 y suma en realidad corresponden a ubicaciones en la memoria de la computadora. Cada variable tiene un nombre, un tipo, un tamaño y un valor. En la aplicación de suma de la figura 3.18, cuando la instrucción (línea 16) numero1 = Convert.ToInt32( Console.ReadLine() );

se ejecuta, el número escrito por el usuario se coloca en una ubicación de memoria a la que el compilador asigna el nombre numero1. Suponga que el usuario escribe 45. La computadora coloca ese valor entero en la ubicación numero1, como se muestra en la figura 3.19. Siempre que se coloca un valor en una ubicación de memoria, ese valor sustituye al anterior en esa ubicación, y el valor anterior se pierde. Cuando la instrucción (línea 20) numero2 = Convert.ToInt32( Console.ReadLine() );

se ejecuta, suponga que el usuario escribe 72. La computadora coloca ese valor entero en la ubicación numero2. La memoria ahora aparece como se muestra en la figura 3.20. Después de que la aplicación de la figura 3.18 obtiene valores para numero1 y numero2, los suma y coloca el resultado en la variable suma. La instrucción (línea 22) suma = numero1 + numero2; // suma los números

realiza la suma y después sustituye el valor anterior de suma. Después de calcular el valor de suma, la memoria aparece como se muestra en la figura 3.21. Observe que los valores de numero1 y numero2 aparecen en la misma forma como se utilizaron en el cálculo de suma. Estos valores se utilizaron, pero no se destruyeron, cuando la computadora realizó el cálculo; cuando se lee un valor de una ubicación de memoria, el proceso es no destructivo.

numero1

45

Figura 3.19 | Ubicación de memoria que muestra el nombre y el valor de la variable numero1.

3.8 Aritmética

numero1

45

numero2

72

67

Figura 3.20 | Las ubicaciones de memoria después de almacenar valores para numero1 y numero2.

numero1

45

numero2

72

suma

117

Figura 3.21 | Las ubicaciones de memoria, después de calcular y almacenar la suma de numero1 y numero2.

3.8 Aritmética La mayoría de las aplicaciones realiza cálculos aritméticos. Los operadores aritméticos se sintetizan en la figura 3.22. Observe el uso de varios símbolos especiales que no se utilizan en algebra. El asterisco (* ) indica la multiplicación y el signo porcentual (%) es el operador residuo (en algunos lenguajes se le llama módulo), los cuales veremos en breve. Los operadores aritméticos de la figura 3.22 son operadores binarios; por ejemplo, la expresión f + 7 contiene el operador binario + y los dos operandos f y 7. La división de enteros produce un cociente entero; por ejemplo, la expresión 7 / 4 se evalúa como 1 y la expresión 17 / 5 se evalúa como 3. Cualquier parte fraccional en una división de enteros se descarta (es decir, se trunca); no se realiza ningún redondeo. C# cuenta con el operador residuo (%), el cual produce el residuo después de la división. La expresión x % y produce el residuo después de que x se divide entre y. Por lo tanto, 7 % 4 produce 3 y 17 % 5 produce 2. Por lo general este operador se utiliza con operandos enteros, pero también puede utilizarse con operandos tipo float, double y decimal. En capítulos posteriores consideraremos varias aplicaciones interesantes del operador residuo, como determinar si un número es múltiplo de otro. Las expresiones aritméticas deben escribirse en formato de línea recta para facilitar la introducción de aplicaciones en la computadora. Por ende, las expresiones como “a dividida entre b” deben escribirse como a / b, de manera que todas las constantes, variables y operadores aparezcan en una línea recta. Por lo general, los compiladores no aceptan la siguiente notación algebraica: a--b

Operación de C#

Operador aritmético

Expresión algebraica

Expresión en C#

Suma

+

f+7

f + 7

Resta

-

p–c

p - c

Multiplicación

*

b⋅m

b * m

División

/

x / y o –xy o x ÷ y

Residuo

%

R mod s

Figura 3.22 | Operadores aritméticos.

x / y r % s

68

Capítulo 3 Introducción a las aplicaciones de C#

Los paréntesis se utilizan para agrupar términos en expresiones de C#, de la misma forma que en las expresiones algebraicas. Por ejemplo, para multiplicar a por la cantidad b + c, escribimos a * ( b + c )

Si una expresión contiene paréntesis anidados, como ( ( a + b ) * c )

la expresión en el conjunto más interno de paréntesis (a + b en este caso) se evalúa primero. C# aplica los operadores en las expresiones aritméticas en una secuencia precisa, la cual se determina en base a las siguientes reglas de precedencia de operadores, que por lo general son las mismas que las que se siguen en álgebra (figura 3.23): 1. Primero se aplican las operaciones de multiplicación, división y residuo. Si una expresión contiene varias de estas operaciones, los operadores se aplican de izquierda a derecha. Los operadores de multiplicación, división y residuo tienen el mismo nivel de precedencia. 2. Después se aplican las operaciones de suma y resta. Si una expresión contiene varias de esas operaciones, los operadores se aplican de izquierda a derecha. Los operadores de suma y restas tienen el mismo nivel de precedencia. Estas reglas permiten a C# aplicar operadores en el orden correcto. Cuando decimos que los operadores se aplican de izquierda a derecha, nos referimos a su asociatividad. Más adelante verá que algunos operadores se asocian de derecha a izquierda. La figura 3.23 sintetiza estas reglas de precedencia de operadores. Expandiremos la tabla a medida que introduzcamos más operadores. En el apéndice A se incluye una tabla completa de precedencia. Ahora consideremos varias expresiones en vista de las reglas de precedencia de los operadores. Cada ejemplo lista una expresión algebraica y su equivalente en C#. El siguiente es un ejemplo de una media aritmética (promedio) de cinco términos: Álgebra:

+ b+ c + d+ e m = a--------------------------------------------5

C#:

m = ( a + b + c + d + e ) / 5;

Los paréntesis son requeridos, ya que la división tiene mayor precedencia que la suma. Toda la cantidad ( a + b + debe dividirse entre 5. Si se omiten los paréntesis por error, obtenemos a + b + c + d + e / 5, lo cual se evalúa como

c + d + e )

a + b + c + d + --e5

Operador(es)

Operación(es)

Orden de evaluación (asociatividad)

Multiplicación División Residuo

Si hay varios operadores de este tipo, se evalúan de izquierda a derecha.

Suma Resta

Si hay varios operadores de este tipo, se evalúan de izquierda a derecha.

Se evalúa primero * / %

Se evalúa después + -

Figura 3.23 | Precedencia de los operadores aritméticos.

3.9 Toma de decisiones: operadores de igualdad y relacionales

69

A continuación se muestra un ejemplo de la ecuación de una línea recta: Álgebra:

y = mx + b

C#:

y = m * x + b;

No se requieren paréntesis. El operador de multiplicación se aplica primero, ya que la multiplicación tiene mayor precedencia que la suma. La asignación se realiza al último, ya que tiene una menor precedencia que la multiplicación o la suma. El siguiente ejemplo contiene operaciones de residuo (%), multiplicación, división, suma y resta: Álgebra:

z = pr%q + w/x – y

C#:

z

=

p

6

*

r

1

%

q

2

+

w

4

/

x

3

- y; 5

Los números dentro de los círculos bajo la instrucción indican el orden en el que C# aplica los operadores. Las operaciones de multiplicación, residuo y división se evalúan primero, en orden de izquierda a derecha (es decir, se asocian de izquierda a derecha), ya que tienen mayor precedencia que la suma y la resta. Las operaciones de suma y resta se evalúan a continuación. Estas operaciones también se aplican de izquierda a derecha. Para desarrollar una mejor comprensión de las reglas de precedencia de los operadores, considere la evaluación de un polinomio de segundo grado (y = ax 2 + bx + c): y

= 6

a

*

x

1

* 2

x

+ 4

b

* 3

x

+ c; 5

Los números dentro de los círculos indican el orden en el que C# aplica los operadores. Las operaciones de multiplicación se evalúan primero, en orden de izquierda a derecha (es decir, se asocian de izquierda a derecha), ya que tienen mayor precedencia que la suma. Las operaciones de suma se evalúan a continuación y se aplican de izquierda a derecha. Como no hay operador aritmético para la exponenciación en C#, x 2 se representa como x * x. La sección 6.4 muestra una alternativa para realizar la exponenciación en C#. Suponga que a, b, c y x en el polinomio anterior de segundo grado se inicializan (reciben valores) de la siguiente manera: a = 2, b = 3, c = 7 y x = 5. La figura 3.24 ilustra el orden en el que se aplican los operadores. Al igual que en álgebra, es aceptable colocar paréntesis innecesarios en una expresión para mejorar su legibilidad. A éstos se les llama paréntesis redundantes. Por ejemplo, podrían utilizarse paréntesis en la instrucción de asignación anterior para resaltar sus términos de la siguiente manera: y = ( a * x * x ) + ( b * x ) + c;

3.9 Toma de decisiones: operadores de igualdad y relacionales Una condición es una expresión que puede ser verdadera o falsa. En esta sección introducimos una versión simple de la instrucción if de C#, que permite que una aplicación tome una decisión con base en el valor de una condición. Por ejemplo, la condición “calificación es mayor o igual a 60” determina si un estudiante pasó una prueba. Si la condición en una instrucción if es verdadera, se ejecuta el cuerpo de la instrucción if. Si la condición es falsa, el cuerpo no se ejecuta. En breve veremos un ejemplo. Las condiciones en las instrucciones if pueden formarse mediante el uso de los operadores de igualdad (== y !=) y los operadores relacionales (>, = y , = y = 65

lo cual es verdadero si, y sólo si ambas condiciones son verdaderas. Si la condición combinada es verdadera, el cuerpo de la instrucción if incrementa a mujeresMayores en 1. Si una o ambas condiciones simples son falsas, la aplicación omite el incremento. Algunos programadores consideran que la condición combinada anterior es más legible si se agregan paréntesis redundantes, por ejemplo: (

genero == FEMENINO && edad >= 65 )

La tabla de la figura 6.14 sintetiza el uso del operador &&. Esta tabla muestra las cuatro combinaciones posibles de valores false y true para expresión1 y expresión2. A dichas tablas se les conoce como tablas de verdad. C# evalúa todas las expresiones que incluyen operadores relacionales, de igualdad o lógicos como valores bool, los cuales pueden ser true o false.

Operador OR condicional (||) Ahora suponga que deseamos asegurar que cualquiera o ambas condiciones sean verdaderas antes de elegir cierta ruta de ejecución. En este caso, utilizamos el operador || (OR condicional ), como se muestra en el siguiente segmento de una aplicación: if ( ( promedioSemestre >= 90 ) || ( examenFinal >= 90 ) ) Console.WriteLine ( "La calificación del estudiante es A" );

Esta instrucción también contiene dos condiciones simples. La condición promedioSemestre >= 90 se evalúa para determinar si el estudiante merece una A en el curso debido a que tuvo un sólido rendimiento a lo largo del semestre. La condición examenFinal >= 90 se evalúa para determinar si el estudiante merece una A en el

6.8 Operadores lógicos

expresión1

expresión2

expresión1 && expresión2

false

false

false

false

true

false

true

false

false

true

true

true

165

Figura 6.14 | Tabla de verdad del operador && (AND condicional). curso debido a un desempeño sobresaliente en el examen final. Después, la instrucción if considera la condición combinada ( promedioSemestre >= 90 ) || ( examenFinal >= 90 )

y otorga una A al estudiante si una o ambas de las condiciones simples son verdaderas. La única vez que no se imprime el mensaje "La calificación del estudiante es A" es cuando ambas de las condiciones simples son falsas. La figura 6.15 es una tabla de verdad para el operador OR condicional (||). El operador && tiene mayor precedencia que el operador ||. Ambos operadores se asocian de izquierda a derecha.

Evaluación en corto circuito de condiciones complejas Las partes de una expresión que contienen los operadores && o || se evalúan sólo hasta que se sabe si la condición es verdadera o falsa. Por ende, la evaluación de la expresión ( genero == FEMENINO ) && ( edad >= 65 )

se detiene de inmediato si genero no es igual a FEMENINO (es decir, en este punto es evidente que toda la expresión es false) y continúa si genero es igual a FEMENINO (es decir, toda la expresión podría ser aún true si la condición edad >= 65 es true). Esta característica de las expresiones AND y OR condicional se conoce como evaluación en corto circuito.

Error común de programación 6.8 En las expresiones que utilizan el operador &&, una condición (a la cual le llamamos condición dependiente) puede requerir que otra condición sea verdadera para que la evaluación de la condición dependiente tenga significado. En este caso, la condición dependiente debe colocarse después de la otra condición, o podría ocurrir un error. Por ejemplo, en la expresión ( i != 0) && (10 /i = 2), la segunda condición debe aparecer después de la primera condición, o podría ocurrir un error de división entre cero.

Operadores AND lógico booleano (&) y OR lógico booleano ( |)

Los operadores AND lógico booleano (&) y OR inclusivo lógico booleano (|) funcionan en forma idéntica a los operadores && (AND condicional) y || (OR condicional), con una excepción: los operadores lógicos booleanos siempre evalúan ambos operandos (es decir, no realizan una evaluación en corto circuito). Por lo tanto, la expresión ( genero

==

1 ) & ( edad >= 65 )

expresión1

expresión2

expresión1 || expresión2

false

false

false

false

true

true

true

false

true

true

true

true

Figura 6.15 | Tabla de verdad del operador || (OR condicional).

166

Capítulo 6 Instrucciones de control: parte 2

evalúa edad >= 65, sin importar que genero sea igual o no a 1. Esto es útil si el operando derecho del operador AND lógico booleano o del OR inclusivo lógico booleano tiene un efecto secundario requerido: la modificación del valor de una variable. Por ejemplo, la expresión ( cumpleanios == true ) | ( ++edad >= 65 )

garantiza que se evalúe la condición ++edad >= 65. Por ende, la variable edad se incrementa en la expresión anterior, sin importar que la expresión total sea true o false.

Tip de prevención de errores 6.5 Por cuestión de claridad, evite las expresiones con efectos secundarios en las condiciones. Los efectos secundarios pueden tener una apariencia inteligente, pero pueden hacer que el código sea más difícil de entender y pueden producir errores sutiles.

OR exclusivo lógico booleano ( ^)

Una condición compleja que contiene el operador OR exclusivo lógico booleano (^) es true si y sólo si uno de sus operandos es true y el otro es false. Si ambos operandos son true o si ambos son false, toda la condición es false. La figura 6.16 es una tabla de verdad para el operador OR exclusivo lógico booleano (^). También se garantiza que este operador evaluará ambos operandos.

Operador lógico de negación ( !)

El operador ! (negación lógica) le permite “invertir” el significado de una condición. A diferencia de los operadores lógicos &&, ||, &, | y ^, que son operadores binarios que combinan dos condiciones, el operador lógico de negación es un operador unario que sólo tiene una condición como operando. Este operador se coloca antes de una condición para elegir una ruta de ejecución si la condición original (sin el operador lógico de negación) es false, como en el siguiente segmento de código: if ( ! ( calificacion == valorCentinela ) ) Console.WriteLine( "La siguiente calificación es {0}", calificacion );

este segmento ejecuta la llamada a WriteLine sólo si calificacion no es igual a valorCentinela. Los paréntesis alrededor de la condición calificacion == valorCentinela son necesarios, ya que el operador lógico de negación tiene mayor precedencia que el operador de igualdad. En la mayoría de los casos, puede evitar el uso de la negación lógica si expresa la condición en forma distinta, con un operador relacional o de igualdad apropiado. Por ejemplo, la instrucción anterior también puede escribirse de la siguiente manera: if ( calificacion != valorCentinela ) Console.WriteLine( "La siguiente calificación es {0}", calificacion );

Esta flexibilidad le puede ayudar a expresar una condición de una manera más conveniente. La figura 6.17 es una tabla de verdad para el operador lógico de negación.

Ejemplo de los operadores lógicos La figura 6.18 demuestra el uso de los operadores lógicos y de los operadores lógicos booleanos; para ello produce sus tablas de verdad. La salida de esta aplicación muestra la expresión que se evalúo y el resultado bool de esa expresión. Las líneas 10-14 producen la tabla de verdad para el && (AND condicional). Las líneas 17-21 producen

expresión1

expresión2

expresión1 ^ expresión2

false

false

false

false

true

true

true

false

true

true

true

false

Figura 6.16 | Tabla de verdad del operador ^ (OR exclusivo lógico booleano).

6.8 Operadores lógicos

expresión1

!expresión

false

true

true

false

Figura 6.17 | Tabla de verdad del operador ! (negación lógica).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

// Fig. 6.18: OperadoresLogicos.cs // Operadores lógicos. using System; public class OperadoresLogicos { public static void Main( string[] args ) { // crea la tabla de verdad para el operador && (AND conditional) Console.WriteLine( "{0}\n{1}: {2}\n{3}: {4}\n{5}: {6}\n{7}: {8}\n", "AND condicional (&&)", "false && false", ( false && false ), "false && true", ( false && true ), "true && false", ( true && false ), "true && true", ( true && true ) ); // crea la tabla de verdad para el operador || (OR conditional) Console.WriteLine( "{0}\n{1}: {2}\n{3}: {4}\n{5}: {6}\n{7}: {8}\n", "OR condicional (||)", "false || false", ( false || false ), "false || true", ( false || true ), "true || false", ( true || false ), "true || true", ( true || true ) ); // crea la tabla de verdad para el operador & (AND lógico booleano) Console.WriteLine( "{0}\n{1}: {2}\n{3}: {4}\n{5}: {6}\n{7}: {8}\n", "AND lógico booleano (&)", "false & false", ( false & false ), "false & true", ( false & true ), "true & false", ( true & false ), "true & true", ( true & true ) ); // crea la tabla de verdad para el operador | (OR inclusivo lógico booleano) Console.WriteLine( "{0}\n{1}: {2}\n{3}: {4}\n{5}: {6}\n{7}: {8}\n", "OR inclusivo lógico booleano (|)", "false | false", ( false | false ), "false | true", ( false | true ), "true | false", ( true | false ), "true | true", ( true | true ) ); // crea la tabla de verdad para el operador ^ (OR exclusivo lógico booleano) Console.WriteLine( "{0}\n{1}: {2}\n{3}: {4}\n{5}: {6}\n{7}: {8}\n", "OR exclusivo lógico booleano (^)", "false ^ false", ( false ^ false ), "false ^ true", ( false ^ true ), "true ^ false", ( true ^ false ), "true ^ true", ( true ^ true ) ); // crea la tabla de verdad para el operador ! (negación lógica) Console.WriteLine( "{0}\n{1}: {2}\n{3}: {4}", "Negación lógica (!)", "!false", ( !false ), “!true”, ( !true ) );

Figura 6.18 | Operadores lógicos. (Parte 1 de 2).

167

168

50 51

Capítulo 6 Instrucciones de control: parte 2

} // fin de Main } // fin de la clase OperadoresLogicos

AND condicional (&&) false && false: False false && true: False true && false: False true && true: True OR condicional (||) false || false: False false || true: True true || false: True true || true: True AND lógico booleano (&) false & false: False false & true: False true & false: False true & true: True OR inclusivo lógico booleano (|) false | false: False false | true: True true | false: True true | true: True OR exclusivo lógico booleano (^) false ^ false: False false ^ true: True true ^ false: True true ^ true: False Negación lógica (!) !false: True !true: False

Figura 6.18 | Operadores lógicos. (Parte 2 de 2). la tabla de verdad para el || (OR condicional). Las líneas 24-28 producen la tabla de verdad para el & (AND lógico booleano). Las líneas 31-36 producen la tabla de verdad para el | (OR inclusivo lógico booleano). Las líneas 39-44 producen la tabla de verdad para el ^ (OR exclusivo lógico booleano). Las líneas 47-49 producen la tabla de verdad para el ! (negación lógica). La figura 6.19 muestra la precedencia y la asociatividad de los operadores de C# presentados hasta ahora. Los operadores se muestran de arriba hacia abajo, en orden descendente de precedencia.

6.9 (Opcional) Caso de estudio de ingeniería de software: identificación de los estados y actividades de los objetos en el sistema ATM En la sección 5.12 identificamos muchos de los atributos de las clases necesarios para implementar el sistema ATM, y los agregamos al diagrama de clases de la figura 5.16. En esta sección le mostraremos la forma en que estos atributos representan el estado de un objeto. Identificaremos algunos estados clave que pueden ocupar nuestros objetos y hablaremos acerca de cómo cambian de estado, en respuesta a los diversos eventos que ocurren en el sistema. También hablaremos sobre el flujo de trabajo, o actividades, que realizan los diversos objetos en el sistema ATM. En esta sección presentaremos los objetos de transacción SolicitudSaldo y Retiro.

6.9 Identificación de los estados y actividades de los objetos en el sistema ATM

Operadores

Asociatividad

Tipo

(postfijo)

izquierda a derecha

mayor precedencia

(tipo)

derecha a izquierda

prefijo unario

izquierda a derecha

multiplicativo

izquierda a derecha

aditivo

izquierda a derecha

relacional

izquierda a derecha

igualdad

&

izquierda a derecha

AND lógico booleano

^

izquierda a derecha

OR exclusivo lógico booleano

|

izquierda a derecha

OR inclusivo lógico booleano

&&

izquierda a derecha

AND condicional

||

izquierda a derecha

OR condicional

derecha a izquierda

condicional

izquierda a derecha

asignación

.

new

++

++

--

+

*

/

%

+

-

<

(postfijo)

--

-

!

>=

?: =

+=

-=

*=

/=

%=

169

Figura 6.19 | Precedencia/asociatividad de los operadores descritos hasta ahora.

Diagramas de máquina de estado Cada objeto en un sistema pasa a través de una serie de estados discretos. El estado de un objeto en un momento dado en el tiempo se indica mediante los valores de los atributos del objeto en ese momento. Los diagramas de máquina de estado modelan los estados clave de un objeto y muestran bajo qué circunstancias el objeto cambia de estado. A diferencia de los diagramas de clases que presentamos en las secciones anteriores del caso de estudio, que se enfocaban principalmente en la estructura del sistema, los diagramas de máquina de estado modelan parte del comportamiento del sistema. La figura 6.20 es un diagrama de máquina de estado simple, el cual modela dos de los estados en un objeto de la clase ATM. UML representa a cada estado en un diagrama de máquina de estado como un rectángulo redondeado con el nombre del estado dentro de éste. Un círculo relleno con una punta de flecha designa el estado inicial. Recuerde que en el diagrama de clases de la figura 5.16 modelamos esta información de estado como el atributo bool de nombre usuarioAutenticado. Este atributo se inicializa en false, o estado “Usuario no autenticado”, de acuerdo con el diagrama de máquina de estado. Las flechas indican las transiciones entre los estados. Un objeto puede pasar de un estado a otro, en respuesta a los diversos eventos que ocurren en el sistema. El nombre o la descripción del evento que ocasiona una transición se escribe cerca de la línea que corresponde a esa transición. Por ejemplo, el objeto ATM cambia del estado “Usuario no autenticado” al estado “Usuario autenticado”, una vez que la base de datos del banco autentica al usuario. En el documento de requerimientos vimos que para autenticar a un usuario, la base de datos compara el número de cuenta y el NIP introducidos por el usuario con los de la cuenta correspondiente en la base de datos. Si la base de datos indica que el usuario ha introducido un número de cuenta válido y el NIP correcto, el objeto ATM pasa al estado “Usuario autenticado” y cambia su atributo usuarioAutenticado al valor true. Cuando el usuario sale del sistema al seleccionar la opción “salir” del menú principal, el objeto ATM regresa al estado “Usuario no autenticado” para prepararse para el siguiente usuario del ATM.

la base de datos del banco autentica al usuario Usuario no autenticado

Usuario autenticado el usuario sale del sistema

Figura 6.20 | Diagrama de máquina de estado para algunos de los estados del objeto ATM.

170

Capítulo 6 Instrucciones de control: parte 2

Observación de ingeniería de software 6.5 Por lo general, los diseñadores de software no crean diagramas de máquina de estado que muestren todos los posibles estados y transiciones de estados para todos los atributos; simplemente hay demasiados. Lo común es que los diagramas de máquina de estado muestren sólo los estados y las transiciones de estado más importantes o complejas.

Diagramas de actividad Al igual que un diagrama de máquina de estado, un diagrama de actividad modela los aspectos del comportamiento de un sistema. A diferencia de un diagrama de máquina de estado, un diagrama de actividad modela el flujo de trabajo de un objeto (secuencia de eventos) durante la ejecución de una aplicación. Un diagrama de actividad modela las acciones a realizar y en qué orden las realizará el objeto. Si recuerda, utilizamos los diagramas de actividad de UML para ilustrar el flujo de control para las instrucciones de control que presentamos en los capítulos 5 y 6. El diagrama de actividad de la figura 6.21 modela las acciones involucradas en la ejecución de una transacción SolicitudSaldo. Asumimos que ya se ha inicializado un objeto SolicitudSaldo y que ya se ha asignado un número de cuenta válido (el del usuario actual), por lo que el objeto sabe qué saldo extraer de la base de datos. El diagrama incluye las acciones que ocurren después de que el usuario selecciona la opción de solicitud de saldo del menú principal y antes de que el ATM devuelva al usuario al menú principal; un objeto SolicitudSaldo no realiza ni inicia estas acciones, por lo que no las modelamos aquí. El diagrama empieza extrayendo de la base de datos el saldo disponible de la cuenta del usuario. Después, SolicitudSaldo extrae el saldo total de la cuenta. Por último, la transacción muestra los saldos en la pantalla. UML representa una acción en un diagrama de actividad como un estado de acción, el cual se modela mediante un rectángulo en el que sus lados izquierdo y derecho se sustituyen por arcos hacia afuera. Cada estado de acción contiene una expresión de acción (por ejemplo, “obtener de la base de datos el saldo disponible de la cuenta del usuario”); que especifica una acción a realizar. Una flecha conecta dos estados de acción, con lo cual indica el orden en que ocurren las acciones representadas por los estados de acción. El círculo relleno (en la parte superior de la figura 6.21) representa el estado inicial de la actividad: el inicio del flujo de trabajo antes de que el objeto realice las acciones modeladas. En este caso, la transacción primero ejecuta la expresión de acción “obtener de la base de datos el saldo disponible de la cuenta del usuario”. Después, la transacción extrae el saldo total. Por último, la transacción muestra ambos saldos en la pantalla. El círculo relleno encerrado en un círculo sin relleno (en la parte inferior de la figura 6.21) representa el estado final: el fin del flujo de trabajo una vez que el objeto realiza las acciones modeladas. La figura 6.22 muestra un diagrama de actividad para una transacción tipo Retiro. Asumimos que ya se ha asignado un número de cuenta válido a un objeto Retiro. No modelaremos al usuario seleccionando la opción de retiro del menú principal ni al ATM devolviendo al usuario al menú principal, ya que estas acciones no las

obtiene de la base de datos el saldo disponible de la cuenta del usuario

obtiene de la base de datos el saldo total de la cuenta del usuario

muestra los saldos en la pantalla

Figura 6.21 | Diagrama de actividad para una transacción tipo SolicitudSaldo.

6.9 Identificación de los estados y actividades de los objetos en el sistema ATM

171

muestra el menú de montos de retiro y la opción para cancelar

introduce la selección del menú

[el usuario canceló la transacción]

muestra mensaje de cancelación

[el usuario seleccionó una cantidad] atributo del monto establecido

obtiene de la base de datos el saldo disponible de la cuenta del usuario

[monto > saldo disponible]

muestra mensaje de error

[monto efectivo disponible]

muestra mensaje de error

[monto Propiedades de [NombreDelProyecto]… (en donde [NombreDelProyecto] es el nombre de su proyecto) y, desde el cuadro de lista Objeto inicial, seleccione la clase que contenga el método Main que debe ser el punto de entrada.

7.4 Declaración de métodos con múltiples parámetros En los capítulos del 4 al 6 presentamos clases que contenían métodos simples, los cuales tenían cuando mucho un parámetro. A menudo, los métodos requieren más de una pieza de información para realizar sus tareas. Ahora veremos cómo puede escribir sus propios métodos con múltiples parámetros.

180

Capítulo 7 Métodos: un análisis más detallado

La aplicación en las figuras 7.2 y 7.3 utiliza un método, definido por el usuario, llamado Maximo para determinar y devolver el mayor de tres valores double introducidos por el usuario. Cuando la aplicación empieza a ejecutarse, el método Main de la clase PruebaBuscadorMaximo (líneas 6-10 de la figura 7.3) crea un objeto de la clase BuscadorMaximo (línea 8) y llama al método DeterminarMaximo del objeto (línea 9) para producir el resultado de la aplicación. En la clase BuscadorMaximo (figura 7.2), las líneas 11-15 del método DeterminarMaximo piden al usuario que introduzca tres valores double y los lea del usuario (mediante el teclado). La línea 18 llama al método Maximo (declarado en las líneas 25-38) para determinar el mayor de los tres valores double que se pasan como argumentos al método. Cuando el método Maximo devuelve el resultado a la línea 18, la aplicación asigna el valor de retorno de Maximo a la variable local resultado. Después, la línea 21 imprime en pantalla el valor máximo. Al final de esta sección hablaremos sobre el uso del operador + en la línea 21. Considere la declaración del método Maximo (líneas 25-38). La línea 25 indica que el método devuelve un valor double, que el nombre del método es Maximo y que el método requiere tres parámetros double (x, y y z) para realizar su tarea. Cuando un método tiene más de un parámetro, éstos se especifican como una lista separada por comas. Cuando se hace la llamada a Maximo en la línea 18 de la figura 7.2, el parámetro x se inicializa con el valor del argumento numero1, el parámetro y se inicializa con el valor del argumento numero2 y el parámetro z

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

// Fig. 7.2: BuscadorMaximo.cs // Método Maximo definido por el usuario. using System; public class BuscadorMaximo { // obtiene tres valores de punto flotante y determina el valor máximo public void DeterminarMaximo() { // pide y recibe como entrada tres valores de punto flotante Console.WriteLine( "Escriba tres valores de punto flotante,\n" + " oprima 'Intro' después de cada uno: " ); double numero1 = Convert.ToDouble( Console.ReadLine() ); double numero2 = Convert.ToDouble( Console.ReadLine() ); double numero3 = Convert.ToDouble( Console.ReadLine() ); // determina el valor máximo double resultado = Maximo( numero1, numero2, numero3 ); // muestra el valor máximo Console.WriteLine( "Maximo es: " + resultado ); } // fin del método DeterminarMaximo // devuelve el máximo de sus tres parámetros double public double Maximo( double x, double y, double z ) { double valorMaximo = x; // asume que x es el mayor para empezar // determina si y es mayor que valorMaximo if ( y > valorMaximo ) valorMaximo = y; // determina si z es mayor que valorMaximo if ( z > valorMaximo ) valorMaximo = z; return valorMaximo; } // fin del método Maximo } // fin de la clase BuscadorMaximo

Figura 7.2 | Método Maximo definido por el usuario.

7.4 Declaración de métodos con múltiples parámetros

1 2 3 4 5 6 7 8 9 10 11

181

// Fig. 7.3: PruebaBuscadorMaximo.cs // Aplicación para evaluar la clase BuscadorMaximo. public class PruebaBuscadorMaximo { // punto inicial de la aplicación public static void Main( string[] args ) { BuscadorMaximo buscadorMaximo = new BuscadorMaximo(); buscadorMaximo.DeterminarMaximo(); } // fin de Main } // fin de la clase PruebaBuscadorMaximo

Escriba tres valores de punto flotante, oprima 'Intro' después de cada uno: 3.33 2.22 1.11 Maximo es: 3.33

Escriba tres valores de punto flotante, oprima 'Intro' después de cada uno: 2.22 3.33 1.11 Maximo es: 3.33

Escriba tres valores de punto flotante, oprima 'Intro' después de cada uno: 1.11 2.22 867.5309 Maximo es: 867.5309

Figura 7.3 | Aplicación para probar la clase BuscadorMaximo. se inicializa con el valor del argumento numero3. Debe haber un argumento en la llamada al método para cada parámetro (algunas veces conocido como parámetro formal ) en la declaración del método. Además, cada argumento debe ser consistente con el tipo del parámetro correspondiente. Por ejemplo, un parámetro de tipo double puede recibir valores como 7.35 (un double), 22 (un int) o -0.03456 (un double), pero no objetos string como "hola". En la sección 7.7 veremos los tipos de argumentos que pueden proporcionarse en la llamada a un método para cada parámetro de un tipo simple. Para determinar el valor máximo, comenzamos con la suposición de que el parámetro x contiene el valor más grande, por lo que la línea 27 (figura 7.2) declara la variable local valorMaximo y la inicializa con el valor del parámetro x. Desde luego que es posible que el parámetro y o z contenga el valor más grande, por lo que debemos comparar cada uno de estos valores con valorMaximo. La instrucción if en las líneas 30-31 determina si y es mayor que valorMaximo. De ser así, la línea 31 asigna y a valorMaximo. La instrucción if en las líneas 34-35 determina si z es mayor que valorMaximo. De ser así, la línea 35 asigna z a valorMaximo. En este punto, el mayor de los tres valores reside en valorMaximo, por lo que la línea 37 devuelve ese valor a la línea 18. Cuando el control del programa regresa al punto en la aplicación en donde se llamó al método Maximo, los parámetros x, y y z de Maximo ya no están accesibles en la memoria. Observe que los métodos pueden devolver a lo máximo un valor; el valor devuelto puede ser una referencia a un objeto que contenga muchos valores. Observe que resultado es una variable local en el método DeterminarMaximo, ya que se declara en el bloque que representa el cuerpo del método. Las variables deben declararse como campos de una clase (es decir, ya sea como variables de instancia o como variables static de la clase) sólo si se requiere su uso en más de un método de la clase, o si la aplicación debe almacenar sus valores entre las llamadas a los métodos de la clase.

182

Capítulo 7 Métodos: un análisis más detallado

Error común de de programación 7.1 Declarar parámetros del mismo tipo para un método, como float x, y en vez de float x, float y es un error de sintaxis; se requiere un tipo para cada parámetro en la lista de parámetros.

Observación de ingeniería de software 7.4 Un método que tiene muchos parámetros puede estar realizando demasiadas tareas. Considere dividir el método en métodos más pequeños que realizan las tareas separadas. Como lineamiento, trate de ajustar el encabezado del método en una línea, si es posible.

Implementación del método Maximo mediante la reutilización del método Math.Max En la figura 7.1 vimos que la clase Math tiene un método Max, que puede determinar el mayor de dos valores. Todo el cuerpo de nuestro método para encontrar el valor máximo también podría implementarse mediante llamadas anidadas a Math.Max, como se muestra a continuación: return Math.Max( x, Math.Max( y, z ) );

La llamada de más a la izquierda a Math.Max especifica los argumentos x y Math.Max( y, z ). Antes de poder llamar a cualquier método, todos sus argumentos deben evaluarse para determinar sus valores. Si un argumento es una llamada a un método, es necesario realizar esta llamada para determinar su valor de retorno. Por lo tanto, en la instrucción anterior Math.Max( y, z ) se evalúa primero para determinar el máximo entre y y z. Después, el resultado se pasa como el segundo argumento para la otra llamada a Math.Max, que devuelve el mayor de sus dos argumentos. El uso de Math.Max de esta forma es un buen ejemplo de la reutilización de software; buscamos el mayor de los tres valores reutilizando Math.Max, el cual busca el mayor de dos valores. Observe lo conciso de este código, en comparación con las líneas 27-37 de la figura 7.2.

Ensamblado de cadenas mediante la concatenación C# permite crear objetos string mediante el ensamblado de objetos string más pequeños para formar objetos string más grandes, mediante el uso del operador + (o del operador de asignación compuesto +=). A esto se le conoce como concatenación de objetos string. Cuando ambos operandos del operador + son objetos string, el operador + crea un nuevo objeto string en el cual se coloca una copia de los caracteres del operando derecho al final de una copia de los caracteres en el operando izquierdo. Por ejemplo, la expresión "hola" + "a todos" crea el objeto string "hola a todos" sin perturbar los objetos string originales. En la línea 21 de la figura 7.2, la expresión "Maximo es: " + resultado utiliza el operador + con operandos de tipo string y double. Cada valor de un tipo simple en C# tiene una representación string. Cuando uno de los operandos del operador + es un objeto string, el otro se convierte implícitamente en un objeto string y después se concatenan los dos. En la línea 21, el valor double se convierte implícitamente en su representación string y se coloca al final del objeto string "Maximo es: ". Si hay ceros a la derecha en un valor double, éstos se descartan cuando el número se convierte en objeto string. Por ende, el número 9.3500 se representa como 9.35 en el objeto string resultante. Para los valores de tipos simples que se utilizan en la concatenación de objetos string, los valores se convierten en objetos string. Si un valor boolean se concatena con un objeto string, el valor bool se convierte en el objeto string "True" o "False" (observe que cada uno empieza con mayúscula). Todos los objetos tienen un método ToString que devuelve una representación string del objeto. Cuando se concatena un objeto con un string, se hace una llamada implícita al método ToString de ese objeto para obtener la representación string. La línea 21 de la figura 7.2 también podría haberse escrito utilizando el formato de string, como se muestra a continuación: Console.WriteLine( "Maximo es: {0}", resultado );

Al igual que con la concatenación de objetos string, al utilizar un elemento de formato para sustituir un objeto en un string se hace una llamada implícita al método ToString del objeto para obtener su representación string. En el capítulo 8, Arreglos, aprenderá más acerca del método ToString.

7.5 Notas acerca de cómo declarar y utilizar los métodos

183

Cuando se escribe una literal string extensa en el código fuente de una aplicación, se puede dividir en varios objetos string más pequeños para colocarlos en varias líneas y mejorar la legibilidad. Los objetos string pueden reensamblarse mediante el uso de la concatenación o el formato de cadenas. En el capítulo 16, Cadenas, caracteres y expresiones regulares, hablaremos sobre los detalles de los objetos string.

Error común de programación 7.2 Es un error de sintaxis dividir una literal string en varias líneas en una aplicación. Si una literal string no cabe en una línea, divídala en objetos string más pequeños y utilice la concatenación para formar la literal string deseada.

Error común de programación 7.3 Confundir el operador + que se utiliza para la concatenación de cadenas con el operador + que se utiliza para la suma puede producir resultados extraños. El operador + es asociativo a la izquierda. Por ejemplo, si la variable entera y tiene el valor 5, la expresión "y + 2 = " + y + 2 produce la cadena "y + 2 = 52", no "y + 2 = 7", ya que el primer valor de y (5) se concatena con la cadena "y + 2 =" y después el valor 2 se concatena con la nueva cadena "y + 2 = 5" más larga. La expresión "y + 2 =" + (y + 2) produce el resultado deseado "y + 2 = 7".

7.5 Notas acerca de cómo declarar y utilizar los métodos Hemos visto tres formas de llamar a un método: 1. Utilizando el nombre de un método por sí mismo para llamar a un método de la misma clase, como Maximo( numero1, numero2, numero3 ) en la línea 18 de la figura 7.2. 2. Utilizando una variable que contiene una referencia a un objeto, seguida del operador punto (.) y del nombre del método para llamar a un método no static del objeto al que se hace referencia, como la llamada al método en la línea 9 de la figura 7.3, buscadorMaximo.DeterminarMaximo(), con lo cual se llama a un método de la clase BuscadorMaximo desde el método Main de PruebaBuscadorMaximo. 3. Utilizando el nombre de la clase y el operador punto (.) para llamar a un método static de una clase, como Math.Sqrt( 900.0 ) en la sección 7.3. Observe que un método static sólo puede llamar directamente a otros métodos static de la misma clase (es decir, usando el nombre del método por sí mismo) y sólo puede manipular directamente variables static en la misma clase. Para acceder a los miembros no static de la clase, un método static debe hacer referencia a un objeto de esa clase. Recuerde que los métodos static se relacionan con una clase como un todo, mientras que los métodos no static se asocian con una instancia específica (objeto) de la clase y pueden manipular las variables de instancia de ese objeto. Pueden existir muchos objetos de una clase al mismo tiempo, cada uno con sus propias copias de las variables de instancia. Suponga que un método static invoca a un método no static en forma directa. ¿Cómo sabría el método qué variables de instancia manipular de cuál objeto? ¿Qué ocurriría si no existieran objetos de la clase en el momento en el que se invocara el método no static? Por consecuencia, C# no permite que un método static acceda directamente a los miembros no static de la misma clase. Existen tres formas de regresar el control a la instrucción que llama a un método. Si el método no devuelve un resultado, el control regresa cuando el flujo del programa llega a la llave derecha de terminación del método, o cuando se ejecuta la instrucción return;

Si el método devuelve un resultado, la instrucción return expresión;

evalúa la expresión y después devuelve el resultado (y el control) al método que hizo la llamada.

Error común de programación 7.4 Declarar un método fuera del cuerpo de la declaración de una clase, o dentro del cuerpo de otro método es un error de sintaxis.

184

Capítulo 7 Métodos: un análisis más detallado

Error común de programación 7.5 Omitir el tipo de valor de retorno en la declaración de un método es un error de sintaxis.

Error común de programación 7.6 Colocar un punto y coma después del paréntesis derecho que encierra la lista de parámetros de la declaración de un método es un error de sintaxis.

Error común de programación 7.7 Redeclarar el parámetro de un método como una variable local en el cuerpo de ese método es un error de compilación.

Error común de programación 7.8 Olvidar devolver el valor de un método que debe devolver un valor es un error de compilación. Si se especifica un tipo de valor de retorno distinto de void, el método debe contener una instrucción return que devuelva un valor consistente con el tipo de valor de retorno del método. Devolver un valor de un método cuyo tipo de valor de retorno se haya declarado como void es un error de compilación.

7.6 La pila de llamadas a los métodos y los registros de activación Para comprender la forma en que C# realiza las llamadas a los métodos, primero necesitamos considerar una estructura de datos (es decir, una colección de elementos de datos relacionados) conocida como pila. Puede considerar una pila como una analogía de una pila de platos. Cuando se coloca un plato en la pila, por lo general se coloca en la parte superior (lo que se conoce como meter el plato en la pila). De manera similar, cuando se extrae un plato de la pila, siempre se extrae de la parte superior (lo que se conoce como sacar el plato de la pila). Las pilas se denominan estructuras de datos “último en entrar, primero en salir” (LIFO) ; el último elemento que se mete (inserta) en la pila es el primero que se saca (se extrae) de ella. Cuando una aplicación llama a un método, éste debe saber cómo regresar al que lo llamó, por lo que la dirección de retorno del método que hizo la llamada se mete en la pila de ejecución del programa (también conocida como pila de llamadas a los métodos). Si ocurre una serie de llamadas a métodos, las direcciones de retorno sucesivas se meten en la pila, en el orden “último en entrar, primero en salir”, para que cada método pueda regresar al que lo llamó. La pila de ejecución del programa también contiene la memoria para las variables locales que se utilizan en cada invocación de un método durante la ejecución de una aplicación. Estos datos, que se almacenan como una porción de la pila de ejecución del programa, se conocen como el registro de activación o marco de pila de la llamada a un método. Cuando se hace la llamada a un método, el registro de activación para la llamada a ese método se mete en la pila de ejecución del programa. Cuando el método regresa al que lo llamó, el registro de activación para esa llamada al método se saca de la pila y esas variables locales ya no son conocidas para la aplicación. Si una variable local que almacena una referencia a un objeto es la única variable en la aplicación con una referencia a ese objeto, cuando se saca de la pila el registro de activación que contiene a esa variable local, la aplicación ya no puede acceder a ese objeto, el cual se eliminará de memoria en algún momento dado durante la “recolección de basura”. En la sección 9.9 hablaremos sobre la recolección de basura. Desde luego que la cantidad de memoria en una computadora es finita, por lo que sólo puede utilizarse cierta cantidad de memoria para almacenar los registros de activación en la pila de ejecución del programa. Si ocurren más llamadas a métodos de las que se puedan almacenar los registros de activación en la pila de ejecución del programa, se produce un error conocido como desbordamiento de pila.

7.7 Promoción y conversión de argumentos

Otra característica importante de las llamadas a los métodos es la promoción de argumentos: convertir en forma implícita el valor de un argumento al tipo que el método espera recibir en su correspondiente parámetro. Por ejemplo, una aplicación puede llamar al método Sqrt de Math con un argumento entero, aun y cuando el método espera recibir un argumento double (pero no viceversa, como pronto veremos). La instrucción Console.WriteLine( Math.Sqrt( 4 ) );

7.7 Promoción y conversión de argumentos

185

evalúa Math.Sqrt( 4 ) correctamente e imprime el valor 2.0. La lista de parámetros de la declaración del método hace que C# convierta el valor int 4 en el valor double 4.0 antes de pasar ese valor a Sqrt. Tratar de realizar estas conversiones puede ocasionar errores de compilación, si no se satisfacen las reglas de promoción de C#. Las reglas de promoción especifican qué conversiones son permitidas; esto es, qué conversiones pueden realizarse sin perder datos. En el ejemplo anterior de Sqrt, un int se convierte en double sin modificar su valor. No obstante, la conversión de un double a un int trunca la parte fraccional del valor double; por lo tanto, se pierde parte del valor. Además, las variables double pueden almacenar valores mucho mayores (y mucho menores) que las variables int, por lo que asignar un double a un int puede provocar una pérdida de información cuando el valor double no cabe en el valor int. La conversión de tipos de enteros largos a tipos de enteros pequeños (por ejemplo, de long a int) puede también producir valores modificados. Las reglas de promoción se aplican a las expresiones que contienen valores de dos o más tipos simples, y a los valores de tipos simples que se pasan como argumentos para los métodos. Cada valor se promueve al tipo apropiado en la expresión. (En realidad, la expresión utiliza una copia temporal de cada valor; los tipos de los valores originales permanecen sin cambios.) La figura 7.4 lista los tipos simples en orden alfabético, y los tipos a los cuales se puede promover cada uno de ellos. Observe que los valores de todos los tipos simples también pueden convertirse implícitamente al tipo object. En el capítulo 24, Estructuras de datos, demostraremos dichas conversiones implícitas. De manera predeterminada, C# no nos permite convertir implícitamente valores entre los tipos simples, si el tipo de destino no puede representar el valor del tipo original (por ejemplo, el valor int 2000000 no puede representarse como un short, y cualquier número de punto flotante con dígitos después de su punto decimal no pueden representarse en un tipo entero como long, int o short). Por lo tanto, para evitar un error de compilación en casos en los que la información puede perderse debido a una conversión implícita entre los tipos simples, el compilador requiere que utilicemos un operador de conversión (el cual presentamos en la sección 5.7) para forzar explícitamente la conversión. Eso nos permite “tomar el control” del compilador. En esencia decimos, “Sé que esta conversión podría ocasionar pérdida de información, pero para mis fines aquí, eso está bien”. Suponga que crea un método Square que calcula el cuadrado de un entero y por ende requiere un argumento int. Para llamar a Square con un argumento double llamado valorDouble, escribiría la llamada al método como Square( (int) valorDouble ). La llamada a este método convierte explícitamente el valor de valorDouble a un entero, para usarlo en el método Square. Por ende, si el valor de valorDouble es 4.5, el método recibe el valor 4 y devuelve 16, no 20.25 (lo que, por desgracia, resulta en la pérdida de información).

Tipo

Tipo de conversiones

bool

no hay conversiones implícitas posibles a otros tipos simples

byte

ushort, short, uint, int, ulong , long , decimal, float

char

ushort, int, uint, long , ulong , decimal, float

decimal

no hay conversiones implícitas posibles a otros tipos simples

double

no hay conversiones implícitas posibles a otros tipos simples

float

double

int

long , decimal, float

long

decimal, float

sbyte

short, int, long , decimal, float

short

int, long , decimal, float

uint

ulong , long , decimal, float

ulong

decimal, float

ushort

uint, int, ulong , long , decimal, floatx

o double

o double

o double o double

o double o double

o double

Figura 7.4 | Conversiones implícitas entre tipos simples.

o double

o double

186

Capítulo 7 Métodos: un análisis más detallado

Error común de programación 7.9 Convertir un valor de tipo simple a un valor de otro tipo simple puede modificar ese valor si no se permite la promoción. Por ejemplo, convertir un valor de punto flotante a un valor integral puede introducir errores de truncamiento (pérdida de la parte fraccional) en el resultado.

7.8 La Biblioteca de clases del .NET Framework Muchas clases predefinidas se agrupan en categorías de clases relacionadas, llamadas espacios de nombres. En conjunto, estos espacios de nombres se conocen como la Biblioteca de clases del .NET Framework, o FCL. A lo largo de este libro, las directivas using nos permiten utilizar las clases de biblioteca de la FCL sin necesidad de especificar sus nombres completamente calificados. Por ejemplo, la declaración using System;

permite a una aplicación utilizar los nombres de las clases del espacio de nombres System sin tener que calificar completamente sus nombres. Esto nos permite utilizar el nombre de clase descalificado Console, en vez del nombre de clase completamente calificado System.Console, en el código. Una gran ventaja de C# es el extenso número de clases en los espacios de nombres de la FCL. En la figura 7.5 se describen algunos espacios de nombres clave de la FCL, que representan sólo una pequeña porción de las clases reutilizables en la FCL. En su proceso de aprendizaje de C#, le recomendamos invertir tiempo en explorar los espacios de nombres y las clases en la documentación .NET (en inglés: msdn2.microsoft.com/en-us/library/ms229335; en español: msdn.microsoft. com/library/spa/default.asp?url=/library/SPA/cpref/html/cpref_start.asp). El conjunto de espacios de nombres disponibles en la FCL es bastante extenso. Además de los espacios de nombres que se sintetizan en la figura 7.5, la FCL contiene espacios de nombres para gráficos complejos, interfaces gráficas de usuario avanzadas, impresión, redes avanzadas, seguridad, procesamiento de bases de datos, multimedia, accesibilidad (para personas con discapacidad) y muchas otras capacidades. El URL anterior de la documentación .NET proporciona las generalidades acerca de los espacios de nombres de la Biblioteca de clases del .NET Framework.

Espacio de nombres

Descripción

System.Windows.Forms

Contiene las clases requeridas para crear y manipular GUIs. (En el capítulo 13, Conceptos de interfaz gráfica de usuario, parte 1, y en el capítulo 14, Conceptos de interfaz gráfica de usuario, parte 2, hablaremos sobre varias de las clases en este espacio de nombres.)

System.IO

Contiene clases que permiten las operaciones de entrada/salida de datos en los programas. (En el capítulo 18, Archivos y flujos, aprenderá más acerca de este espacio de nombres.)

System.Data

Contiene clases que permiten a los programas acceder a las bases de datos (es decir, colecciones organizadas de datos) y manipularlas. (En el capítulo 20, Bases de datos, SQL y ADO.NET, aprenderá más acerca de este espacio de nombres.)

System.Web

Contiene clases que se utilizan para crear y mantener aplicaciones Web, a las cuales se accede a través de Internet. (En el capítulo 21, ASP.NET 2.0, formularios Web Forms y controles Web, aprenderá más acerca de este espacio de nombres.)

System.Xml

Contiene clases para crear y manipular datos de XML. Los datos pueden leerse o escribirse en archivos de XML. (En el capítulo 19, Lenguaje de marcado extensible (XML), aprenderá más acerca de este espacio de nombres.)

Figura 7.5 | Espacios de nombres de la FCL (un subconjunto). (Parte 1 de 2).

7.9 Caso de estudio: generación de números aleatorios

187

Espacio de nombres

Descripción

System.Collections System.Collections.Generic

Contiene clases que definen estructuras de datos para mantener colecciones de datos. (En el capítulo 26, Colecciones, aprenderá más acerca de este espacio de nombres.)

System.Net

Contiene clases que permiten que los programas se comuniquen a través de redes de computadoras como Internet. (En el capítulo 23, Redes: Sockets basados en flujos y datagramas, aprenderá más acerca de este espacio de nombres.)

System.Text

Contiene clases e interfaces que permiten a los programas manipular caracteres y cadenas. (En el capítulo 16, Cadenas, caracteres y expresiones regulares, aprenderá más acerca de este espacio de nombres.)

System.Threading

Contiene clases que permiten a los programas realizar varias tareas al mismo tiempo. (En el capítulo 15, Subprocesamiento múltiple, aprenderá más acerca de este espacio de nombres.)

System.Drawing

Contiene clases que permiten a los programas realizar el procesamiento básico de gráficos, tal como mostrar figuras y arcos. (En el capítulo 17, Gráficos y multimedia, aprenderá más acerca de este espacio de nombres.)

Figura 7.5 | Espacios de nombres de la FCL (un subconjunto). (Parte 2 de 2). En la Referencia de la Biblioteca de clases del .NET Framework podrá encontrar información adicional acerca de los métodos de las clases predefinidas en C#. Cuando visite este sitio, verá un listado alfabético de todos los espacios de nombres en la FCL. Localice el espacio de nombres y haga clic en su vínculo para poder ver un listado alfabético de todas sus clases, con una breve descripción de cada una de ellas. Haga clic en el vínculo de una clase para ver una descripción más completa de la misma. Haga clic en el vínculo Métodos (Methods) de la columna izquierda para ver un listado de los métodos de la clase.

Buena práctica de programación 7.2 La documentación en línea del .NET Framework es fácil de explorar y proporciona muchos detalles acerca de cada clase. A medida que conozca cada una de las clases en este libro, es conveniente que revise la clase en la documentación en línea para obtener información adicional.

7.9 Caso de estudio: generación de números aleatorios En esta sección y en la siguiente, desarrollaremos una aplicación de juego muy bien estructurada, con varios métodos. La aplicación utiliza la mayoría de las instrucciones de control presentadas hasta ahora en el libro; además introduce varios conceptos nuevos de programación. Hay algo en el aire de un casino que vigoriza a las personas; desde los grandes apostadores en las lujosas mesas de dados de caoba y fieltro, hasta las ranuras que expulsan monedas en los “bandidos de un solo brazo”. Es el elemento de probabilidad, la posibilidad de que la suerte convierta unos cuantos pesos en una montaña de riqueza. El elemento de probabilidad puede introducirse en una aplicación mediante un objeto de la clase Random (del espacio de nombres System). Los objetos de la clase Random pueden producir valores byte, int y double aleatorios. En los siguientes ejemplos utilizaremos objetos de la clase Random para producir números aleatorios. Se puede crear un nuevo objeto generador de números aleatorios de la siguiente manera: Random numerosAleatorios = new Random();

Así, el objeto generador de números aleatorios puede utilizarse para generar valores byte, int y double aleatorios; aquí sólo veremos valores int aleatorios. Para obtener más información sobre la clase Random, visite el sitio en inglés msdn2.microsoft.com/en-us/library/ts6se2ek, y el sitio en español msdn.microsoft.com/ library/spa/default.asp?url=/library/SPA/cpref/html/cpref_start.asp.

188

Capítulo 7 Métodos: un análisis más detallado Considere la siguiente instrucción: int valorAleatorio = numerosAleatorios.Next();

El método Next de la clase Random genera un valor int aleatorio en el rango de 0 a +2,147,483,646, inclusive. Si en verdad el método Next produce valores al azar, entonces todos los valores en ese rango deberían tener la misma oportunidad (o probabilidad) de ser elegidos cada vez que se hace una llamada al método Next. En realidad, los valores devueltos por Next son números seudoaleatorios (una secuencia de valores producidos por un cálculo matemático complejo. El cálculo utiliza la hora actual del día (que, por supuesto, cambia en forma constante) para sembrar el generador de números aleatorios, de tal forma que cada ejecución de una aplicación produzca una secuencia distinta de valores aleatorios. Con frecuencia, el rango de valores que produce directamente el método Next difiere del rango de valores requeridos en una aplicación específica de C#. Por ejemplo, una aplicación que simule el lanzamiento de una moneda podría requerir sólo 0 para “águila” y 1 para “sol”. Una aplicación que simule el tiro de un dado de seis lados podría requerir enteros aleatorios en el rango de 1 a 6. Un videojuego que adivine en forma aleatoria la siguiente nave espacial (de cuatro posibilidades distintas) que volará a lo largo del horizonte podría requerir enteros aleatorios en el rango de 1 a 4. Para casos como éstos, la clase Random cuenta con otras versiones del método Next. Una de ellas recibe un argumento int y devuelve un valor de 0 hasta, pero sin incluir, el valor del argumento. Por ejemplo, podría utilizar la instrucción int valorAleatorio = numerosAleatorios.Next( 6 );

que devuelve 0, 1, 2, 3, 4 o 5. El argumento 6 (llamado el factor de escala) representa el número de valores únicos que debe producir Next (en este caso, seis: 0, 1, 2, 3, 4 y 5). A esta manipulación se le conoce como escalar el rango de valores producidos por el método Next de Random. Suponga que deseamos simular un dado de seis caras que tiene los números del 1 al 6 en sus caras, no del 0 al 5. No basta con sólo escalar el rango de valores, sino que tenemos que desplazar el rango de números producidos. Para ello podríamos agregar un valor de desplazamiento (en este caso, 1) al resultado del método Next, como en cara = 1 + numerosAleatorios.Next( 6 );

El valor de desplazamiento (1) especifica el primer valor en el conjunto deseado de enteros aleatorios. La instrucción anterior asigna a cara un entero aleatorio en el rango del 1 al 6. La tercera alternativa del método Next proporciona una manera más intuitiva de expresar tanto el desplazamiento como la acción de escalar. Este método recibe dos argumentos int y devuelve un valor desde el valor del primer argumento hasta, pero sin incluir, el valor del segundo argumento. Podríamos utilizar este método para escribir una instrucción equivalente a nuestra instrucción anterior, como en cara = numerosAleatorios.Next( 1, 7 );

Tirar un dado de seis caras Para demostrar el uso de los números aleatorios, desarrollaremos una aplicación que simula 20 tiros de un dado de seis caras y muestra el valor de cada tiro. La figura 7.6 muestra dos ejecuciones de ejemplo, que confirman que los resultados del cálculo anterior son enteros en el rango del 1 al 6 y que cada ejecución de la aplicación puede producir una secuencia distinta de números aleatorios. La directiva using en la línea 3 nos permite utilizar la clase Random sin necesidad de calificar completamente su nombre. La línea 9 crea el objeto numerosAleatorios de la clase Random para producir valores aleatorios. La línea 16 se ejecuta 20 veces en un ciclo para tirar el dado. La instrucción if (líneas 21-22) en el ciclo inicia una nueva línea de salida después de cada cinco números, para poder presentar los resultados en varias líneas.

Tirar un dado de seis caras 6000 veces Para mostrar que los números producidos por Next ocurren con una probabilidad aproximadamente igual, simularemos 6000 tiros de un dado (figura 7.7). Cada entero del 1 al 6 debe aparecer aproximadamente 1000 veces. Como muestran los dos resultados de ejemplo, los valores producidos por el método Next permiten a la aplicación simular en forma realista el tiro de un dado de seis caras. La aplicación utiliza instrucciones de control

7.9 Caso de estudio: generación de números aleatorios

189

anidadas (el switch está anidado dentro del for) para determinar el número de veces que se tiró cada una de las caras del dado. La instrucción for (líneas 21-47) itera 6000 veces. Durante cada iteración, la línea 23 produce un valor aleatorio del 1 al 6. Este valor de cara se utiliza después como la expresión del switch (línea 26) en la

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

// Fig. 7.6: EnterosAleatorios.cs // Desplazar y escalar enteros aleatorios. using System; public class EnterosAleatorios { public static void Main( string[] args ) { Random numerosAleatorios = new Random(); // generador de números aleatorios int cara; // almacena cada entero aleatorio que se genera // itera 20 veces for ( int contador = 1; contador califAlta ) califAlta = calificacion; } // fin de foreach return califAlta; // devuelve la calificación más alta } // fin del método ObtenerMaxima // determina la calificación promedio para un estudiante específico public double ObtenerPromedio( int estudiante ) { // obtiene el número de calificaciones por estudiante int monto = calificaciones.GetLength( 1 ); int total = 0; // inicializa el total // suma las calificaciones para un estudiante for ( int examen = 0; examen < monto; examen++ ) total += calificaciones[ estudiante, examen ]; // devuelve el promedio de las calificaciones return ( double ) total / monto; } // fin del método ObtenerPromedio // imprime gráfico de barras que muestra la distribución total de calificaciones public void ImprimirGraficoBarras() { Console.WriteLine( "Distribución total de calificaciones:" ); // almacena la frecuencia de las calificaciones en cada rango de 10 calificaciones int[] frecuencia = new int[ 11 ]; // para cada calificacion en LibroCalificaciones, incrementa la frecuencia apropiada foreach ( int calificacion in calificaciones ) { ++frecuencia[ calificacion / 10 ];

Figura 8.20 | Libro de calificaciones que utiliza un arreglo rectangular para almacenar calificaciones. (Parte 2 de 3).

8.11 Caso de estudio: la clase LibroCalificaciones que usa un arreglo rectangular

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160

249

} // fin de foreach // para cada frecuencia de calificacion, imprime la barra en el gráfico for ( int cuenta = 0; cuenta < frecuencia.Length; cuenta++ ) { // imprime etiqueta de barras ( "00-09: ", ..., "90-99: ", "100: " ) if ( cuenta == 10 ) Console.Write( " 100: " ); else Console.Write( "{0:D2}-{1:D2}: ", cuenta * 10, cuenta * 10 + 9 ); // imprime barra de asteriscos for ( int estrellas = 0; estrellas < frecuencia[ cuenta ]; estrellas++ ) Console.Write( "*" ); Console.WriteLine(); // inicia una nueva línea de salida } // fin de for externo } // fin del método ImprimirGraficoBarras // imprime el contenido del arreglo calificaciones public void ImprimirCalificaciones() { Console.WriteLine( "Las calificaciones son:\n" ); Console.Write( " " ); // alinea encabezados de columna // crea un encabezado de columna para cada una de las pruebas for ( int prueba = 0; prueba < calificaciones.GetLength( 1 ); prueba++ ) Console.Write( "Prueba {0} ", prueba + 1 ); Console.WriteLine( "Promedio" ); // encabezado de columna de promedio de estudiante // crea filas/columnas de texto que representan el arreglo calificaciones for ( int estudiante = 0; estudiante < calificaciones.GetLength( 0 ); estudiante++ ) { Console.Write( "Estudiante {0,2}", estudiante + 1 ); // imprime las calificaciones de los estudiantes for ( int calificacion = 0; calificacion < calificaciones.GetLength( 1 ); calificacion++ ) Console.Write( "{0,8}", calificaciones[ estudiante, calificacion ] ); // llama al método ObtenerPromedio para calcular la calificación promedio de los estudiantes; // pasa el número de fila como argumento para ObtenerPromedio Console.WriteLine( "{0,9:F2}", ObtenerPromedio( estudiante ) ); } // fin de for externo } // fin del método ImprimirCalificaciones } // fin de la clase LibroCalificaciones

Figura 8.20 | Libro de calificaciones que utiliza un arreglo rectangular para almacenar calificaciones. (Parte 3 de 3).

imprime un gráfico de barras de la distribución de todas las calificaciones de los estudiantes durante el semestre. El método ImprimirCalificaciones (líneas 135-159) imprime el arreglo bidimensional en formato tabular, junto con el promedio semestral de cada estudiante. Cada uno de los métodos ObtenerMinimo, ObtenerMaximo e ImprimirGraficoBarras itera a través del arreglo calificaciones mediante el uso de la instrucción foreach; por ejemplo, la instrucción foreach del método ObtenerMinimo (líneas 60-65). Para buscar la calificación más baja, esta instrucción foreach itera

250

Capítulo 8 Arreglos

a través del arreglo rectangular calificaciones y compara cada elemento con la variable califBaja. Si una calificación es menor que califBaja, a califBaja se le asigna esa calificación. Cuando la instrucción foreach recorre los elementos del arreglo calificaciones, analiza cada elemento de la primera fila en orden por índice, después cada elemento de la segunda fila en orden por índice, y así en lo sucesivo. La instrucción foreach en las líneas 60-65 recorre los elementos de calificacion en el mismo orden que el siguiente código equivalente, expresado con instrucciones for anidadas: for ( int fila = 0; fila < calificaciones.GetLength( 0 ); fila++ ) for ( int columna = 0; columna < calificaciones.GetLength( 1 ); columna++ ) { if ( calificaciones[ fila, columna ] < califBaja ) califBaja = calificaciones[ fila, columna ]; }

Cuando se completa la instrucción foreach, califBaja contiene la calificación más baja en el arreglo rectangular. El método ObtenerMaximo funciona de manera similar al método ObtenerMinimo. El método ImprimirGraficoBarras en la figura 8.20 muestra la distribución de calificaciones en forma de un gráfico de barras. Observe que la sintaxis de la instrucción foreach (líneas 111-114) es idéntica para los arreglos unidimensionales y bidimensionales. El método ImprimirCalificaciones (líneas 135-159) utiliza instrucciones for anidadas para imprimir valores del arreglo calificaciones, además del promedio semestral de cada estudiante. La salida en la figura 8.21 muestra el resultado, que se asemeja al formato tabular del libro de calificaciones real de un instructor. Las líneas 141-142 imprimen los encabezados de columna para cada prueba. Aquí utilizamos la instrucción for en vez de la instrucción foreach, para poder identificar cada prueba con un número. De manera similar, la instrucción for en las líneas 147-158 imprime primero una etiqueta de fila mediante el uso de una variable contador para identificar a cada estudiante (línea 149). Aunque los índices de los arreglos empiezan en 0, observe que las líneas 142 y 149 imprimen prueba + 1 y estudiante + 1 en forma respectiva, para producir números de prueba y estudiante que empiecen en 1 (vea la figura 8.21). La instrucción for interna en las líneas 152-153 utiliza la variable contador estudiante de la instrucción for externa para iterar a través de una fila específica del arreglo calificaciones e imprime la calificación de la prueba de cada estudiante. Por último, la línea 157 obtiene el promedio semestral de cada estudiante, para lo cual pasa el índice de fila de calificaciones (es decir, estudiante) al método ObtenerPromedio. El método ObtenerPromedio (líneas 88-100) recibe un argumento: el índice de fila para un estudiante específico. Cuando la línea 157 llama a ObtenerPromedio, el argumento es el valor int de estudiante, el cual especifica la fila particular del arreglo rectangular calificaciones. El método ObtenerPromedio calcula la suma de los elementos del arreglo en esta fila, divide el total entre el número de resultados de la prueba y devuelve el resultado de punto flotante como un valor double (línea 99).

Clase PruebaLibroCalificaciones que demuestra la clase LibroCalificaciones La aplicación en la figura 8.21 crea un objeto de la clase LibroCalificaciones (figura 8.20) mediante el uso del arreglo bidimensional de valores int llamado arregloCalificaciones (el cual se declara y se inicializa en las líneas 9-18). Las líneas 20-21 pasan el nombre de un curso y arregloCalificaciones al constructor de LibroCalificaciones. Después, las líneas 22-23 invocan a los métodos MostrarMensaje y ProcesarCalificaciones de miLibroCalificaciones para mostrar un mensaje de bienvenida y obtener un reporte que sintetice las calificaciones de los estudiantes para el semestre, en forma respectiva.

8.12 Listas de argumentos de longitud variable

Las listas de argumentos de longitud variable le permiten crear métodos que reciben un número arbitrario de argumentos. Un argumento tipo arreglo unidimensional que va precedido por la palabra clave params en la lista de parámetros del método indica que éste recibe un número variable de argumentos con el tipo de los elementos del arreglo. Este uso de un modificador params puede ocurrir sólo en la última entrada de la lista de parámetros. Aunque podemos utilizar la sobrecarga de métodos y el paso de arreglos para realizar gran parte de lo que se logra con los “varargs” (otro nombre para las listas de argumentos de longitud variable), es más conciso utilizar el modificador params.

8.12 Listas de argumentos de longitud variable

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

251

// Fig. 8.21: PruebaLibroCalificaciones.cs // Crea objeto LibroCalificaciones que utiliza un arreglo rectangular de calificaciones. public class PruebaLibroCalificaciones { // el método Main comienza la ejecución de la aplicación public static void Main( string[] args ) { // arreglo rectangular de calificaciones de estudiantes int[ , ] arregloCalificaciones = { { 87, 96, 70 }, { 68, 87, 90 }, { 94, 100, 90 }, { 100, 81, 82 }, { 83, 65, 85 }, { 78, 87, 65 }, { 85, 75, 83 }, { 91, 94, 100 }, { 76, 72, 84 }, { 87, 93, 73 } }; LibroCalificaciones miLibroCalificaciones = new LibroCalificaciones( "CS101 Introducción a la programación en C#", arregloCalificaciones ); miLibroCalificaciones.MostrarMensaje(); miLibroCalificaciones.ProcesarCalificaciones(); } // fin de Main } // fin de la clase PruebaLibroCalificaciones

¡Bienvenido al libro de calificaciones para CS101 Introducción a la programación en C#! Las calificaciones son:

Estudiante 1 Estudiante 2 Estudiante 3 Estudiante 4 Estudiante 5 Estudiante 6 Estudiante 7 Estudiante 8 Estudiante 9 Estudiante 10

Prueba 1 Prueba 2 Prueba 3 Promedio 87 96 70 84.33 68 87 90 81.67 94 100 90 94.67 100 81 82 87.67 83 65 85 77.67 78 87 65 76.67 85 75 83 81.00 91 94 100 95.00 76 72 84 77.33 87 93 73 84.33

La calificación más baja en el libro de calificaciones es 65 La calificación más alta en el libro de calificaciones es 100 Distribución total de calificaciones: 00-09: 10-19: 20-29: 30-39: 40-49: 50-59: 60-69: *** 70-79: ****** 80-89: *********** 90-99: ******* 100: ***

Figura 8.21 | Crea un objeto LibroCalificaciones que utiliza un arreglo rectangular de calificaciones.

252

Capítulo 8 Arreglos

La figura 8.22 demuestra el método Promedio (líneas 8-17), que recibe una secuencia de longitud variable de valores double (línea 8). C# trata a la lista de argumentos de longitud variable como un arreglo unidimensional cuyos elementos son todos del mismo tipo. Así, el cuerpo del método puede manipular el parámetro numeros como un arreglo de valores double. Las líneas 13-14 utilizan el ciclo foreach para recorrer el arreglo y calcular el total de los valores double en el arreglo. La línea 16 accede a numeros.Length para obtener el tamaño del arreglo numeros y usarlo en el cálculo del promedio. Las líneas 31, 33 y 35 en Main llaman al método Promedio con dos, tres y cuatro argumentos, en forma respectiva. El método Promedio tiene una lista de argumentos de longitud variable, por lo que puede promediar todos los argumentos double que le pase el método que hace la llamada. La salida revela que cada llamada al método Promedio devuelve el valor correcto.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 d1 d2 d3 d4

// Fig. 8.22: PruebaVarArgs.cs // Uso de listas de argumentos de longitud variable. using System; public class PruebaVarArgs { // calcula el promedio public static double Promedio( params double[] numeros ) { double total = 0.0; // inicializa el total // calcula el total usando la instrucción foreach foreach ( double d in numeros ) total += d; return total / numeros.Length; } // fin del método Promedio public static void Main( string[] args ) { double d1 = 10.0; double d2 = 20.0; double d3 = 30.0; double d4 = 40.0; Console.WriteLine( "d1 = {0:F1}\nd2 = {1:F1}\nd3 = {2:F1}\nd4 = {3:F1}\n", d1, d2, d3, d4 ); Console.WriteLine( "El promedio de d1 y d2 es {0:F1}", Promedio( d1, d2 ) ); Console.WriteLine( "El promedio de d1, d2 y d3 es {0:F1}", Promedio( d1, d2, d3 ) ); Console.WriteLine( "El promedio de d1, d2, d3 y d4 es {0:F1}", Promedio( d1, d2, d3, d4 ) ); } // fin de Main } // fin de la clase PruebaVarArgs = = = =

10.0 20.0 30.0 40.0

El promedio de d1 y d2 es 15.0 El promedio de d1, d2 y d3 es 20.0 El promedio de d1, d2, d3 y d4 es 25.0

Figura 8.22 | Uso de listas de argumentos de longitud variable.

8.13 Uso de argumentos de línea de comandos

253

Error común de programación 8.4 Utilizar el modificador params con un parámetro en medio de la lista de parámetros de un método es un error de sintaxis. El modificador params puede usarse sólo con el último parámetro de la lista de parámetros.

8.13 Uso de argumentos de línea de comandos

En muchos sistemas es posible pasar argumentos desde la línea de comandos (estos se conocen como argumentos de línea de comandos) a una aplicación; para ello se incluye un parámetro de tipo string[] (es decir, un arreglo de objetos string) en la lista de parámetros de Main, de la misma forma exacta que como lo hemos hecho en cada aplicación de este libro. Por convención, este parámetro se llama args (figura 8.23, línea 7). Cuando se ejecuta una aplicación desde el Símbolo del sistema, el entorno de ejecución pasa los argumentos de línea de comandos que aparecen después del nombre de la aplicación al método Main de la aplicación, como objetos string en el arreglo unidimensional args. El número de argumentos que se pasan desde la línea de comandos se obtiene al acceder a la propiedad Length del arreglo. Por ejemplo, el comando "MiAplicacion a b" pasa dos argumentos de línea de comandos a la aplicación MiAplicacion. Observe que los argumentos de línea de comandos se separan por espacio en blanco, no por comas. Cuando se ejecuta el comando anterior, el punto de entrada del método Main recibe el arreglo de dos elementos args (es decir, args.Length es 2), en donde args[ 0 ] contiene el objeto string "a" y args[ 1 ] contiene el objeto string "b". Los usos comunes de los argumentos de línea de comandos incluyen el paso de opciones y nombres de archivo a las aplicaciones.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

// Fig. 8.23: InicArreglo.cs // Uso de argumentos de línea de comandos para inicializar un arreglo. using System; public class InicArreglo { public static void Main( string[] args ) { // revisa el número de argumentos de línea de comandos if ( args.Length != 3 ) Console.WriteLine( "Error: por favor reintroduzca todo el comando, incluyendo\n" + "un tamaño de arreglo, valor inicial e incremento." ); else { // obtiene tamaño del arreglo del primer argumento de línea de comandos int longitudArreglo = Convert.ToInt32( args[ 0 ] ); int[] arreglo = new int[ longitudArreglo ]; // crea el arreglo // obtiene valor inicial e incremento del argumento de línea de comandos int valorInicial = Convert.ToInt32( args[ 1 ] ); int incremento = Convert.ToInt32( args[ 2 ] ); // calcula el valor para cada elemento del arreglo for ( int contador = 0; contador < arreglo.Length; contador++ ) arreglo[ contador ] = valorInicial + incremento * contador; Console.WriteLine( "{0}{1,8}", "Índice", "Valor" ); // muestra subíndice y valor del arreglo for ( int contador = 0; contador < arreglo.Length; contador++ ) Console.WriteLine( "{0,5}{1,8}", contador, arreglo[ contador ] ); } // fin de else

Figura 8.23 | Uso de argumentos de línea de comandos para inicializar un arreglo. (Parte 1 de 2).

254

34 35

Capítulo 8 Arreglos

} // fin de Main } // fin de la clase InicArreglo

C:\Ejemplos\cap08\fig08_21>InicArreglo.exe Error: por favor reintroduzca todo el comando, incluyendo un tamaño de arreglo, valor inicial e incremento.

C:\Ejemplos\cap08\fig08_21>InicArreglo.exe 5 0 4 Índice Valor 0 0 1 4 2 8 3 12 4 16

C:\Ejemplos\cap08\fig08_21>InicArreglo.exe 10 1 2 Índice Valor 0 1 1 3 2 5 3 7 4 9 5 11 6 13 7 15 8 17 9 19

Figura 8.23 | Uso de argumentos de línea de comandos para inicializar un arreglo. (Parte 2 de 2). La figura 8.23 utiliza tres argumentos de línea de comandos para inicializar un arreglo. Cuando se ejecuta la aplicación, si args.Length no es 3, la aplicación imprime un mensaje de error y termina (líneas 10-13). En caso contrario, las líneas 16-32 inicializan e imprimen en pantalla el arreglo, con base en los valores de los argumentos de línea de comandos. Estos argumentos están disponibles para Main como objetos string en args. La línea 17 obtiene args[ 0 ] (un objeto string que especifica el tamaño del arreglo) y lo convierte en un valor int, que la aplicación utiliza para crear el arreglo en la línea 18. El método static ToInt32 de la clase Convert convierte su argumento string en un int. Las líneas 21-22 convierten los argumentos de línea de comandos args[ 1 ] y args[ 2 ] en valores int y los almacena en valorInicial e incremento, respectivamente. Las líneas 25-26 calculan el valor para cada elemento del arreglo. La salida de la primera ejecución de ejemplo indica que la aplicación recibió un número insuficiente de argumentos de línea de comandos. La segunda ejecución de ejemplo utiliza los argumentos de línea de comandos 5, 0 y 4 para especificar el tamaño del arreglo (5), el valor del primer elemento (0) y el incremento de cada valor en el arreglo (4), respectivamente. La salida correspondiente indica que estos valores crearon un arreglo que contiene los enteros 0, 4, 8, 12 y 16. La salida de la tercera ejecución de ejemplo ilustra que los argumentos de línea de comando 10, 1 y 2 producen un arreglo cuyos 10 elementos son los enteros impares no negativos del 1 al 19.

8.14 (Opcional) Caso de estudio de ingeniería de software: colaboración entre los objetos en el sistema ATM

Cuando dos objetos se comunican entre sí para realizar una tarea, se dice que colaboran. Una colaboración es cuando un objeto de una clase envía un mensaje a un objeto de otra clase. En C#, los mensajes se envían a

8.14 Colaboración entre los objetos en el sistema ATM

255

través de llamadas a métodos. En esta sección nos concentramos en las colaboraciones (interacciones) entre los objetos en nuestro sistema ATM. En la sección 7.15 determinamos muchas de las operaciones de las clases en nuestro sistema. En esta sección nos concentramos en los mensajes que invocan a estas operaciones. Para identificar las colaboraciones en el sistema, regresamos al documento de requerimientos de la sección 3.10. Recuerde que este documento especifica las actividades que ocurren durante una sesión con el ATM (por ejemplo, autenticar un usuario, realizar transacciones). Los pasos que se utilizan para describir la forma en que el sistema debe realizar cada una de estas tares son nuestra primera indicación de las colaboraciones en nuestro sistema. A medida que avancemos a través de estas últimas secciones del Caso de estudio de ingeniería de software, tal vez descubramos colaboraciones adicionales.

Identificar las colaboraciones en un sistema Para empezar a identificar las colaboraciones en el sistema, leeremos con cuidado las secciones del documento de requerimientos que especifican lo que debe hacer el ATM para autenticar a un usuario y para realizar cada tipo de transacción. Para cada acción o paso descrito en el documento de requerimientos, decidimos qué objetos en nuestro sistema deben interactuar para lograr el resultado deseado. Identificamos un objeto como el emisor (es decir, el objeto que envía el mensaje) y otro como el receptor (es decir, el objeto que ofrece esa operación a los clientes de la clase). Después seleccionamos una de las operaciones del objeto receptor (identificadas en la sección 7.15) que el objeto emisor debe invocar para producir el comportamiento apropiado. Por ejemplo, el ATM muestra un mensaje de bienvenida cuando está inactivo. Sabemos que un objeto de la clase Pantalla muestra un mensaje al usuario a través de su operación MostrarMensaje. Por ende, decidimos que el sistema puede mostrar un mensaje de bienvenida si empleamos una colaboración entre el ATM y la Pantalla, en donde el ATM envía un mensaje MostrarMensaje a la Pantalla mediante la invocación de la operación MostrarMensaje de la clase Pantalla. [Nota: para evitar repetir la frase “un objeto de la clase…”, nos referiremos a cada objeto sólo utilizando su nombre de clase, precedido por un artículo (por ejemplo, “un”, “una”, “el” o “la”); por ejemplo, “el ATM” hace referencia a un objeto de la clase ATM.] La figura 8.24 lista las colaboraciones que pueden derivarse del documento de requerimientos. Para cada objeto emisor, listamos las colaboraciones en el orden en el que se describen en el documento de requerimientos. Listamos cada colaboración en la que se involucre un emisor único, un mensaje y un receptor sólo una vez, aun y cuando la colaboración puede ocurrir varias veces durante una sesión con el ATM. Por ejemplo, la primera fila en la figura 8.24 indica que el objeto ATM colabora con el objeto Pantalla cada vez que el ATM necesita mostrar un mensaje al usuario. Consideraremos las colaboraciones en la figura 8.24. Antes de permitir que un usuario realice transacciones, el ATM debe pedirle que introduzca un número de cuenta y que después introduzca un NIP. Para realizar cada una de estas tareas envía un mensaje a través de MostrarMensaje a la Pantalla. Ambas acciones se refieren a la misma colaboración entre el ATM y la Pantalla, que ya se listan en la figura 8.24. El ATM obtiene la entrada en respuesta a un indicador, mediante el envío de un mensaje ObtenerEntrada al Teclado. A continuación, el ATM debe determinar si el número de cuenta especificado por el usuario y el NIP concuerdan con los de una cuenta en la base de datos. Para ello envía un mensaje AutenticarUsuario a la BaseDatosBanco. Recuerde que BaseDatosBanco no puede autenticar a un usuario en forma directa; sólo la Cuenta del usuario (es decir, la Cuenta que contiene el número de cuenta especificado por el usuario) puede acceder al NIP del usuario para autenticarlo. Por lo tanto, la figura 8.24 lista una colaboración en la que BaseDatosBanco envía un mensaje ValidarNIP a una Cuenta. Una vez autenticado el usuario, el ATM muestra el menú principal enviando una serie de mensajes MostrarMensaje a la Pantalla y obtiene la entrada que contiene una selección de menú; para ello envía un mensaje ObtenerEntrada al Teclado. Ya hemos tomado en cuenta estas colaboraciones. Una vez que el usuario selecciona un tipo de transacción a realizar, el ATM ejecuta la transacción enviando un mensaje Execute a un objeto de la clase de transacción apropiada (es decir, un objeto SolicitudSaldo, Retiro o Deposito). Por ejemplo, si el usuario elije realizar una solicitud de saldo, el ATM envía un mensaje Execute a un objeto SolicitudSaldo. Un análisis más a fondo del documento de requerimientos revela las colaboraciones involucradas en la ejecución de cada tipo de transacción. Un objeto SolicitudSaldo extrae la cantidad de dinero disponible en la cuenta del usuario, al enviar un mensaje ObtenerSaldoDisponible al objeto BaseDatosBanco, el cual envía un mensaje get a la propiedad SaldoDisponible de un objeto Cuenta para acceder al saldo disponible. De manera similar, el objeto SolicitudSaldo extrae la cantidad de dinero depositado al enviar un mensaje ObtenerSaldo-

256

Capítulo 8 Arreglos

Un objeto de la clase… ATM

SolicitudSaldo

Retiro

Deposito

BaseDatosBanco

envía el mensaje…

a un objeto de la clase…

MostrarMensaje

Pantalla

ObtenerEntrada

Teclado

AutenticarUsuario

BaseDatosBanco

Execute

SolicitudSaldo

Execute

Retiro

Execute

Deposito

ObtenerSaldoDisponible

BaseDatosBanco

ObtenerSaldoTotal

BaseDatosBanco

MostrarMensaje

Pantalla

MostrarMensaje

Pantalla

ObtenerEntrada

Teclado

ObtenerSaldoDisponible

BaseDatosBanco

HaySuficienteEfectivoDisponible

DispensadorEfectivo

Cargar

BaseDatosBanco

DispensarEfectivo

DispensadorEfectivo

MostrarMensaje

Pantalla

ObtenerEntrada

Teclado

SeRecibioSobreDeposito

RanuraDeposito

Abonar

BaseDatosBanco

ValidarNIP

Cuenta

SaldoDisponible (get)

Cuenta

SaldoTotal (get)

Cuenta

Cargar

Cuenta

Abonar

Cuenta

Figura 8.24 | Colaboraciones en el sistema ATM. Total al objeto BaseDatosBanco, el cual envía un mensaje get a la propiedad SaldoTotal de un objeto Cuenta para acceder al saldo total depositado. Para mostrar en pantalla ambas cantidades del saldo del usuario al mismo tiempo, el objeto SolicitudSaldo envía mensajes MostrarMensaje a la Pantalla. Un objeto Retiro envía mensajes MostrarMensaje a la Pantalla para mostrar un menú de montos estándar de retiro (es decir, $20, $40, $60, $100, $200). El objeto Retiro envía un mensaje ObtenerEntrada al Teclado para obtener la selección del menú elegida por el usuario. A continuación, el objeto Retiro determina si el monto de retiro solicitado es menor o igual al saldo de la cuenta del usuario. Para obtener el monto de dinero disponible en la cuenta del usuario, el objeto Retiro envía un mensaje ObtenerSaldoDisponible al objeto BaseDatosBanco. Después el objeto Retiro evalúa si el dispensador contiene suficiente efectivo enviando un mensaje HaySuficienteEfectivoDisponible al DispensadorEfectivo. Un objeto Retiro envía un mensaje Cargar al objeto BaseDatosBanco para reducir el saldo de la cuenta del usuario. El objeto BaseDatosBanco envía a su vez el mismo mensaje al objeto Cuenta apropiado. Recuerde que al hacer un cargo a una Cuenta se reduce tanto el saldo total como el saldo disponible. Para dispensar la cantidad solicitada de efectivo, el objeto Retiro envía un mensaje DispensarEfectivo al objeto DispensadorEfectivo. Por último, el objeto Retiro envía un mensaje MostrarMensaje a la Pantalla, instruyendo al usuario para que tome el efectivo. Para responder a un mensaje Execute, un objeto Deposito primero envía un mensaje MostrarMensaje a la Pantalla para pedir al usuario que introduzca un monto a depositar. El objeto Deposito envía un mensaje ObtenerEntrada al Teclado para obtener la entrada del usuario. Después, el objeto Deposito envía un mensaje MostrarMensaje a la Pantalla para pedir al usuario que inserte un sobre de depósito. Para determinar si la ranura de depósito recibió un sobre de depósito entrante, el objeto Deposito envía un mensaje SeRecibioSobreDeposito al objeto RanuraDeposito. El objeto Deposito actualiza la cuenta del usuario enviando un mensaje Abonar al objeto BaseDatosBanco, el cual a su vez envía un mensaje Abonar al objeto Cuenta del usuario. Recuerde que al abonar a una Cuenta se incrementa el saldo total, pero no el saldo disponible.

8.14 Colaboración entre los objetos en el sistema ATM

257

Diagramas de interacción Ahora que identificamos un conjunto de posibles colaboraciones entre los objetos en nuestro sistema ATM, modelaremos en forma gráfica estas interacciones. UML cuenta con varios tipos de diagramas de interacción, que para modelar el comportamiento de un sistema modelan la forma en que los objetos interactúan entre sí. El diagrama de comunicación enfatiza cuáles objetos participan en las colaboraciones. [Nota: los diagramas de comunicación se llamaban diagramas de colaboración en versiones anteriores de UML.] Al igual que el diagrama de comunicación, el diagrama de secuencia muestra las colaboraciones entre los objetos, pero enfatiza cuándo se deben enviar los mensajes entre los objetos.

Diagramas de comunicación La figura 8.25 muestra un diagrama de comunicación que modela la forma en que el ATM ejecuta una SolicitudSaldo. Los objetos se modelan en UML como rectángulos que contienen nombres de la forma nombreObjeto : NombreClase. En este ejemplo, que involucra sólo a un objeto de cada tipo, descartamos el nombre del objeto y listamos sólo un signo de dos puntos (:) seguido del nombre de la clase. Se recomienda especificar el nombre de cada objeto en un diagrama de comunicación cuando se modelan varios objetos del mismo tipo. Los objetos que se comunican se conectan con líneas sólidas y los mensajes se pasan entre los objetos a lo largo de estas líneas, en la dirección mostrada por las flechas rellenas. El nombre del mensaje, que aparece enseguida de la flecha, es el nombre de una operación (es decir, un método) que pertenece al objeto receptor; considere el nombre como un servicio que el objeto receptor proporciona a los objetos emisores (sus “clientes”). La flecha rellena en la figura 8.25 representa un mensaje (o llamada síncrona) en UML y una llamada a un método en C#. Esta flecha indica que el flujo de control va desde el objeto emisor (el ATM) hasta el objeto receptor (una SolicitudSaldo). Como ésta es una llamada síncrona, el objeto emisor no puede enviar otro mensaje, ni hacer cualquier otra cosa, hasta que el objeto receptor procese el mensaje y devuelva el control (y posiblemente un valor de retorno) al objeto emisor. El emisor sólo espera. Por ejemplo, en la figura 8.25 el objeto ATM llama al método Execute de un objeto SolicitudSaldo y no puede enviar otro mensaje sino hasta que Execute termine y devuelva el control al objeto ATM. [Nota: si ésta fuera una llamada asíncrona, representada por una flecha, el objeto emisor no tendría que esperar a que el objeto receptor devolviera el control; continuaría enviando mensajes adicionales inmediatamente después de la llamada asíncrona. Dichas llamadas se encuentran fuera del alcance de este libro.]

Secuencia de mensajes en un diagrama de comunicación La figura 8.26 muestra un diagrama de comunicación que modela las interacciones entre los objetos en el sistema, cuando se ejecuta un objeto de la clase SolicitudSaldo. Asumimos que el atributo numeroCuenta del objeto contiene el número de cuenta del usuario actual. Las colaboraciones en la figura 8.26 empiezan después de que el objeto ATM envía un mensaje Execute a un objeto SolicitudSaldo (es decir, la interacción modelada en la figura 8.25). El número a la izquierda del nombre de un mensaje indica el orden en el que éste se pasa. La secuencia de mensajes en un diagrama de comunicación progresa en orden numérico, de menor a mayor. En este diagrama, la numeración comienza con el mensaje 1 y termina con el mensaje 3. El objeto SolicitudSaldo envía primero un mensaje ObtenerSaldoDisponible al objeto BaseDatosBanco (mensaje 1), después envía un mensaje ObtenerSaldoTotal al objeto BaseDatosBanco (mensaje 2). Dentro de los paréntesis que van después del nombre de un mensaje, podemos especificar una lista separada por comas de los nombres de los argumentos que se envían con el mensaje (es decir, los argumentos en una llamada a un método en C#); el objeto SolicitudSaldo pasa el atributo numeroCuenta con sus mensajes al objeto BaseDatosBanco para indicar de cuál objeto Cuenta se extraerá la información del saldo. En la figura 7.22 vimos que las operaciones ObtenerSaldoDisponible y ObtenerSaldoTotal de la clase BaseDatosBanco requieren cada uno de ellos un parámetro para identificar una cuenta. El objeto SolicitudSaldo muestra a continuación el saldo disponible y el saldo total al usuario; para

Ejecutar() : ATM

: SolicitudSaldo

Figura 8.25 | Diagrama de comunicación del ATM, ejecutando una solicitud de saldo.

258

Capítulo 8 Arreglos

: Pantalla

3: MostrarMensaje(mensaje)

: SolicitudSaldo

1: ObtenerSaldoDisponible(numeroCuenta) 2: ObtenerSaldoTotal(numeroCuenta)

: BaseDatosBanco 1.1: SaldoDisponible 2.1: SaldoTotal

: Cuenta

Descriptores de acceso de la propiedad get

Figura 8.26 | Diagrama de comunicación para ejecutar una SolicitudSaldo.

ello pasa un mensaje MostrarMensaje a la Pantalla (mensaje 3) que incluye un parámetro, el cual indica el mensaje a mostrar. Observe que la figura 8.26 modela dos mensajes adicionales que se pasan del objeto BaseDatosBanco a un objeto Cuenta (mensaje 1.1 y mensaje 2.1). Para proveer al ATM los dos saldos de la Cuenta del usuario (según lo solicitado por los mensajes 1 y 2), el objeto BaseDatosBanco debe enviar mensajes get a las propiedades SaldoDisponible y SaldoTotal del objeto Cuenta. Un mensaje que se pasa dentro del manejo de otro mensaje se llama mensaje anidado. UML recomienda utilizar un esquema de numeración decimal para indicar mensajes anidados. Por ejemplo, el mensaje 1.1 es el primer mensaje anidado en el mensaje 1; el objeto BaseDatosBanco envía el mensaje get a la propiedad SaldoDisponible del objeto Cuenta durante el procesamiento de un mensaje ObtenerSaldoDisponible por parte del objeto BaseDatosBanco. [Nota: Si el objeto BaseDatosBanco necesita pasar un segundo mensaje anidado mientras procesa el mensaje 1, el segundo mensaje se numera como 1.2.] Un mensaje puede pasarse sólo cuando se han pasado ya todos los mensajes anidados del mensaje anterior. Por ejemplo, el objeto SolicitudSaldo pasa el mensaje 3 a la Pantalla sólo hasta que se han pasado los mensajes 2 y 2.1, en ese orden. El esquema de numeración anidado que se utiliza en los diagramas de comunicación ayuda a aclarar con precisión cuándo y en qué contexto se pasa cada mensaje. Por ejemplo, si numeramos los cinco mensajes de la figura 8.26 usando un esquema de numeración plano (es decir, 1, 2, 3, 4, 5), podría ser posible que alguien que viera el diagrama no pudiera determinar que el objeto BaseDatosBanco pasa el mensaje get a la propiedad SaldoDisponible de un objeto Cuenta (mensaje 1.1) durante el procesamiento del mensaje 1 por parte del objeto BaseDatosBanco, en vez de hacerlo después de completar el procesamiento del mensaje 1. Los números decimales anidados hacen ver que el mensaje get (mensaje 1.1) se pasa a la propiedad SaldoDisponible de un objeto Cuenta dentro del manejo del mensaje ObtenerSaldoDisponible (mensaje 1) por parte del objeto BaseDatosBanco.

Diagramas de secuencia Los diagramas de comunicación enfatizan los participantes en las colaboraciones, pero modelan su sincronización de una forma bastante extraña. Un diagrama de secuencia ayuda a modelar la sincronización de las colaboraciones con más claridad. La figura 8.27 muestra un diagrama de secuencia que modela la secuencia de las interacciones que ocurren cuando se ejecuta un Retiro. La línea punteada que se extiende hacia abajo desde el rectángulo de un objeto es la línea de vida de ese objeto, la cual representa la evolución en el tiempo. Por lo general, las acciones ocurren a lo largo de la línea de vida de un objeto, en orden cronológico de arriba hacia abajo; una acción cerca de la parte superior ocurre antes que una cerca de la parte inferior.

8.14 Colaboración entre los objetos en el sistema ATM

259

El paso de mensajes en los diagramas de secuencia es similar al paso de mensajes en los diagramas de comunicación. Una flecha con punta rellena, que se extiende desde el objeto emisor hasta el objeto receptor, representa un mensaje entre dos objetos. La punta de flecha apunta a una activación en la línea de vida del objeto receptor. Una activación, que se muestra como un rectángulo vertical delgado, indica que se está ejecutando un objeto. Cuando un objeto devuelve el control, un mensaje de retorno (representado como una línea punteada con una punta de flecha) se extiende desde la activación del objeto que devuelve el control hasta la activación del objeto que envió originalmente el mensaje. Para eliminar el desorden, omitimos las flechas de los mensajes de retorno; UML permite esta práctica para que los diagramas sean más legibles. Al igual que los diagramas de comunicación, los diagramas de secuencia pueden indicar parámetros de mensaje entre los paréntesis que van después del nombre de un mensaje. La secuencia de mensajes de la figura 8.27 empieza cuando un objeto Retiro pide al usuario que seleccione un monto de retiro; para ello envía un mensaje MostrarMensaje a la Pantalla. Después el objeto Retiro envía un mensaje ObtenerEntrada al Teclado, el cual obtiene los datos de entrada del usuario. En el diagrama de la figura 6.22 ya hemos modelado la lógica de control involucrada en un objeto Retiro, por lo que no mostraremos esta lógica en el diagrama de secuencia de la figura 8.27. En vez de ello modelaremos el escenario para el mejor

: Retiro

: Teclado

: Pantalla

: Cuenta

: BaseDatosBanco

MostrarMensaje(mensaje)

ObtenerEntrada()

ObtenerSaldoDisponible(numeroCuenta) SaldoDisponible

HaySuficienteEfectivoDisponible(monto)

Cargar(numeroCuenta, monto) Cargar(monto)

DispensarEfectivo(monto)

MostrarMensaje(mensaje)

Figura 8.27 | Diagrama de secuencia que modela la ejecución de un Retiro.

: DispensadorEfectivo

260

Capítulo 8 Arreglos

caso, en el cual el saldo de la cuenta del usuario es mayor o igual al monto de retiro seleccionado, y el dispensador de efectivo contiene un monto de efectivo suficiente como para satisfacer la solicitud. Para obtener información acerca de cómo modelar la lógica de control en un diagrama de secuencia, consulte los recursos Web y las lecturas recomendadas que se listan al final de la sección 3.10. Después de obtener un monto de retiro, el objeto Retiro envía un mensaje ObtenerSaldoDisponible al objeto BaseDatosBanco, el cual a su vez envía un mensaje get a la propiedad SaldoDisponible del objeto Cuenta. Suponiendo que la cuenta del usuario tiene suficiente dinero disponible para permitir la transacción, el objeto Retiro envía a continuación un mensaje HaySuficienteEfectivoDisponible al objeto DispensadorEfectivo. Suponiendo que hay suficiente efectivo disponible, el objeto Retiro reduce el saldo de la cuenta del usuario (tanto el saldo total como el saldo disponible) enviando un mensaje Cargar al objeto BaseDatosBanco. Este objeto responde enviando un mensaje Cargar al objeto Cuenta del usuario. Por último, el objeto Retiro envía un mensaje DispensarEfectivo al usuario para extraer el efectivo de la máquina. Hemos identificado las colaboraciones entre los objetos en el sistema ATM, y modelamos algunas de estas colaboraciones usando los diagramas de interacción de UML: los diagramas de comunicación y los diagramas de secuencia. En la siguiente sección del Caso de estudio de ingeniería de software (sección 9.17), mejoraremos la estructura de nuestro modelo para completar un diseño orientado a objetos preliminar, cuando empecemos a implementar el sistema ATM en C#.

Ejercicios de autoevaluación del Caso de estudio de ingeniería de software 8.1

Un(a) __________ consiste en que un objeto de una clase envía un mensaje a un objeto de otra clase. a) asociación b) agregación c) colaboración d) composición

8.2 ¿Cuál forma de diagrama de interacción es la que enfatiza qué colaboraciones se llevan a cabo? ¿Cuál forma enfatiza cuándo ocurren las interacciones? 8.3 Cree un diagrama de secuencia para modelar las interacciones entre los objetos del sistema ATM, que ocurran cuando se ejecute un Deposito con éxito. Explique la secuencia de los mensajes modelados por el diagrama.

Respuestas a los ejercicios de autoevaluación del Caso de estudio de ingeniería de software 8.1

c.

8.2 Los diagramas de comunicación enfatizan qué colaboraciones se llevan a cabo. Los diagramas de secuencia enfatizan cuándo ocurren las colaboraciones. 8.3 La figura 8.28 presenta un diagrama de secuencia que modela las interacciones entre objetos en el sistema ATM, que ocurren cuando un Deposito se ejecuta con éxito. La figura 8.28 indica que un Deposito primero envía un mensaje MostrarMensaje a la Pantalla (para pedir al usuario que introduzca un monto de depósito). A continuación, el Deposito envía un mensaje ObtenerEntrada al Teclado para recibir el monto que el usuario depositará. Después, el Deposito pide al usuario que inserte un sobre de depósito; para ello envía un mensaje MostrarMensaje a la Pantalla. Luego, el Deposito envía un mensaje SeRecibioSobreDeposito al objeto RanuraDeposito para confirmar que el ATM haya recibido el sobre de depósito. Por último, el objeto Deposito incrementa el saldo total (pero no el saldo disponible) de la Cuenta del usuario, enviando un mensaje Abonar al objeto BaseDatosBanco. El objeto BaseDatosBanco responde enviando el mismo mensaje a la Cuenta del usuario.

8.15 Conclusión En este capítulo empezó nuestra introducción a las estructuras de datos. Aquí exploramos el uso de los arreglos para almacenar datos y extraerlos de listas y tablas de valores. Los ejemplos de este capítulo demostraron cómo declarar arreglos, inicializarlos y hacer referencia a elementos individuales de los arreglos. Se introdujo la instrucción foreach como un medio adicional (además de la instrucción for) para iterar a través de los arreglos. Le mostramos cómo pasar arreglos a los métodos, y cómo declarar y manipular arreglos multidimensionales. Por último, en este capítulo se demostró cómo escribir métodos que utilizan listas de argumentos de longitud variable, y cómo leer argumentos que se pasan a una aplicación desde la línea de comandos.

8.15 Conclusión

: Deposito

: Teclado

: Pantalla

261

: BaseDatosBanco

: RanuraDeposito

: Cuenta

MostrarMensaje(mensaje)

ObtenerEntrada()

MostrarMensaje(mensaje)

SeRecibioSobreDeposito()

Abonar(numeroCuenta, monto) Abonar(monto)

Figura 8.28 | Diagrama de secuencia que modela la ejecución de un Deposito. En el capítulo 24, Estructuras de datos, continuaremos nuestra cobertura de las estructuras de datos. Presentaremos las estructuras dinámicas de datos, como listas, colas, pilas y árboles, que pueden aumentar y reducir su tamaño a medida que se ejecuta una aplicación. En el capítulo 25, Genéricos, presentaremos una de las nuevas características de C#: los genéricos, que proporcionan los medios para crear modelos generales de métodos y clases que pueden declararse una vez, pero utilizarse con muchos tipos de datos distintos. En el capítulo 26, Colecciones, veremos una introducción a las clases de estructuras de datos que proporciona el .NET Framework, algunas de las cuales utilizan genéricos para permitirle especificar los tipos exactos de objetos que almacenará una estructura de datos específica. Puede utilizar estas estructuras de datos predefinidas en vez de crear las suyas. En el capítulo 26 hablaremos sobre muchas clases de estructuras de datos, incluyendo Hashtable y ArrayList, que son estructuras de datos similares a los arreglos y pueden aumentar y reducir su tamaño en respuesta al cambio en los requerimientos de almacenamiento de una aplicación. El .NET Framework también proporciona la clase Array, que contiene métodos utilitarios para la manipulación de arreglos. El capítulo 26 utiliza varios métodos static de la clase Array para realizar manipulaciones tales como ordenar y buscar en los datos de un arreglo. Ya le hemos presentado los conceptos básicos de las clases, los objetos, las instrucciones de control, los métodos y los arreglos. En el capítulo 9 analizaremos con más detalle las clases y los objetos.

9

Clases y objetos: un análisis más detallado En vez de esta absurda división entre sexos, deberían clasificar a las personas como estáticas y dinámicas.

OBJETIVOS

—Evelyn Waugh

En este capítulo aprenderá lo siguiente:

¿Es éste un mundo en el cual se deben ocultar las virtudes?

Q

Acerca del encapsulamiento y el ocultamiento de información.

Q

Los conceptos de abstracción de datos y tipos de datos abstractos (ADTs).

Q

Utilizar la palabra clave this.

Q

A utilizar indexadores para acceder a los miembros de una clase.

Q

Cómo usar variables y métodos static.

Q

Utilizar campos readonly.

Q

Sacar ventaja de las características de administración de memoria de C#.

Q

Crear una biblioteca de clases.

Q

Cuándo utilizar el modificador de acceso internal.

—William Shakespeare

Por encima de todo: hay que ser sinceros con nosotros mismos. —William Shakespeare

No hay que ser “duros”, sino simplemente sinceros. —Oliver Wendell Holmes, Jr.

Pla n g e ne r a l

264

Capítulo 9 Clases y objetos: un análisis más detallado

9.1 9.2 9.3 9.4 9.5 9.6 9.7 9.8 9.9 9.10 9.11 9.12 9.13 9.14 9.15 9.16 9.17

Introducción Caso de estudio de la clase Tiempo Control del acceso a los miembros Referencias a los miembros del objeto actual mediante this Indexadores Caso de estudio de la clase Tiempo: constructores sobrecargados Constructores predeterminados y sin parámetros Composición Recolección de basura y destructores Miembros de clase static Variables de instancia readonly Reutilización de software Abstracción de datos y encapsulamiento Caso de estudio de la clase Tiempo: creación de bibliotecas de clases Acceso internal Vista de clases y Examinador de objetos (Opcional) Caso de estudio de ingeniería de software: inicio de la programación de las clases del sistema ATM 9.18 Conclusión

9.1 Introducción En los capítulos anteriores hablamos acerca de las aplicaciones orientadas a objetos, presentamos muchos conceptos básicos y terminología en relación con la programación orientada a objetos (POO) en C#. También hablamos sobre nuestra metodología para desarrollar aplicaciones: seleccionamos variables y métodos apropiados para cada aplicación y especificamos la manera en que un objeto de nuestra clase debería colaborar con los objetos de las clases en la Biblioteca de clases del .NET Framework para realizar los objetivos generales de la aplicación. En este capítulo analizaremos, de manera más detallada, la creación de clases, el control del acceso a los miembros de una clase y la creación de constructores. Hablaremos sobre la composición: una capacidad que permite a una clase tener referencias a objetos de otras clases como miembros. Analizaremos nuevamente el uso de las propiedades y exploraremos los indexadores como una notación alternativa para acceder a los miembros de una clase. Además, hablaremos a detalle sobre los miembros de clase static y las variables de instancia readonly. Investigaremos cuestiones como la reutilización de software, la abstracción de datos y el encapsulamiento. Por último, explicaremos cómo organizar las clases en ensamblados, para ayudar en la administración de aplicaciones extensas y promover la reutilización; después mostraremos una relación especial entre clases dentro del mismo ensamblado. En el capítulo 10, Programación orientada a objetos: herencia, y en el capítulo 11, Polimorfismo, interfaces y sobrecarga de operadores, presentaremos dos tecnologías clave de programación orientada a objetos.

9.2 Caso de estudio de la clase Tiempo Declaración de la clase Tiempo1 Nuestro primer ejemplo consiste en dos clases: Tiempo1 (figura 9.1) y PruebaTiempo1 (figura 9.2). La clase Tiempo1 representa la hora del día. La clase PruebaTiempo1 es una clase de prueba en la que el método Main crea un objeto de la clase Tiempo1 e invoca a sus métodos. El resultado de esta aplicación aparece en la figura 9.2. La clase Tiempo1 contiene tres variables de instancia private de tipo int (figura 9.1, líneas 5-7): hora, minuto y segundo. Estas variables representan la hora en formato de tiempo universal (formato de reloj de 24 horas, en el cual las horas se encuentran en el rango de 0 a 23). La clase Tiempo1 contiene los métodos public

9.2 Caso de estudio de la clase Tiempo

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

265

// Fig. 9.1: Tiempo1.cs // La declaración de la clase Tiempo1 mantiene la hora en formato de 24 horas. public class Tiempo1 { private int hora; // 0 - 23 private int minuto; // 0 - 59 private int segundo; // 0 - 59 // establece un nuevo valor de tiempo usando la hora universal; asegura que // los datos permanezcan consistentes al establecer los valores inválidos a cero public void EstablecerTiempo( int h, int m, int s ) { hora = ( ( h >= 0 && h < 24 ) ? h : 0 ); // valida la hora minuto = ( ( m >= 0 && m < 60 ) ? m : 0 ); // valida los minutos segundo = ( ( s >= 0 && s < 60 ) ? s : 0 ); // valida los segundos } // fin del método EstablecerTiempo // convierte en string, en formato de hora universal (HH:MM:SS) public string AStringUniversal() { return string.Format( "{0:D2}:{1:D2}:{2:D2}", hora, minuto, segundo ); } // fin del método AStringUniversal // convierte en string, en formato de hora estándar (H:MM:SS AM o PM) public override string ToString() { return string.Format( "{0}:{1:D2}:{2:D2} {3}", ( ( hora == 0 || hora == 12 ) ? 12 : hora % 12 ), minuto, segundo, ( hora < 12 ? "AM" : "PM" ) ); } // fin del método ToString } // fin de la clase Tiempo1

Figura 9.1 | La declaración de la clase Tiempo1 mantiene la hora en formato de 24 horas.

EstablecerTiempo (líneas 11-16), AStringUniversal (líneas 19-23) y ToString (líneas 26-31). Estos métodos son los servicios public o la interfaz public que proporciona la clase a sus clientes. En este ejemplo, la clase Tiempo1 no declara un constructor, por lo que tiene uno predeterminado que le suministra el compilador. Cada variable de instancia recibe en forma implícita el valor predeterminado 0 para un int. Observe que cuando las variables de instancia se declaran en el cuerpo de la clase, pueden inicializarse

mediante la misma sintaxis de inicialización que la de una variable local. El método EstablecerTiempo (líneas 11-16) es public, declara tres parámetros int y los utiliza para establecer la hora. Una expresión condicional evalúa cada argumento para determinar si el valor se encuentra en un rango especificado. Por ejemplo, el valor de hora (línea 13) debe ser mayor o igual que 0 y menor que 24, ya que el formato de hora universal representa las horas como enteros de 0 a 23 (por ejemplo, la 1 PM es la hora 13 y las 11 PM son la hora 23; medianoche es la hora 0 y mediodía es la hora 12). De manera similar, los valores de minuto y segundo (líneas 14 y 15) deben ser mayores o iguales que 0 y menores que 60. Cualquier valor fuera de rango se establece como 0 para asegurar que un objeto Tiempo1 siempre contenga datos consistentes; esto es, los valores de datos del objeto siempre se mantienen en rango, aun si los valores que se proporcionan como argumentos para el método EstablecerTiempo son incorrectos. En este ejemplo, 0 es un valor consistente para hora, minuto y segundo. Un valor que se pasa a EstablecerTiempo es correcto si se encuentra dentro del rango permitido para el miembro que va a inicializar. Por lo tanto, cualquier número en el rango de 0 a 23 sería un valor correcto para la hora. Un valor correcto siempre es un valor consistente. Sin embargo, un valor consistente no necesariamente es un valor correcto. Si EstablecerTiempo establece hora a 0 debido a que el argumento que recibió se encontraba fuera del rango, entonces EstablecerTiempo está recibiendo un valor incorrecto y lo hace consistente, para que el objeto permanezca en un estado consistente en todo momento. En este caso, la aplicación podría indicar que el objeto es

266

Capítulo 9 Clases y objetos: un análisis más detallado

incorrecto. En el capítulo 12, Manejo de excepciones, aprenderá técnicas que permitirán a sus clases indicar cuándo se reciben valores incorrectos.

Observación de ingeniería de software 9.1 Los métodos y las propiedades que modifican los valores de variables private deben verificar que los nuevos valores que se les pretende asignar sean apropiados. Si no lo son, deben colocar las variables private en un estado consistente apropiado.

El método AStringUniversal (líneas 19-23) no recibe argumentos y devuelve un objeto string en formato de hora universal, el cual consta de seis dígitos: dos para la hora, dos para los minutos y dos para los segundos. Por ejemplo, si la hora es 1:30:07 PM, el método AStringUniversal devuelve 13:30:07. La instrucción return (líneas 21-22) utiliza el método static Format de la clase string para devolver un objeto string que contiene los valores con formato de hora, minuto y segundo, cada uno con dos dígitos y, en donde sea necesario, un 0 a la izquierda (el cual se especifica con el especificador de formato D2, para rellenar el entero con 0s si tiene menos de dos dígitos). El método Format es similar al formato de objetos string en el método Console.Write, sólo que Format devuelve un objeto string con formato, en vez de mostrarlo en una ventana de consola. El método AStringUniversal devuelve el objeto string con formato. El método ToString (líneas 26-31) no recibe argumentos y devuelve un objeto String en formato de hora estándar, que consiste en los valores de hora, minuto y segundo separados por signos de dos puntos (:), y seguido de un indicador AM o PM (por ejemplo, 1:27:06 PM). Al igual que el método AStringUniversal, el método ToString utiliza el método static string Format para dar formato a los valores de minuto y segundo como valores de dos dígitos con 0s a la izquierda, en caso de ser necesario. La línea 29 utiliza un operador condicional (?:) para determinar el valor de hora en la cadena; si hora es 0 o 12 (AM o PM), aparece como 12; en cualquier otro caso, aparece como un valor de 1 a 11. El operador condicional en la línea 30 determina si se devolverá AM o PM como parte del objeto string. En la sección 7.4 vimos que todos los objetos en C# tienen un método ToString que devuelve una representación string del objeto. Optamos por devolver un objeto string que contiene la hora en formato estándar. El método ToString se llama en forma implícita cuando la representación string de un objeto se imprime en pantalla con un elemento de formato, en una llamada a Console.Write. Recuerde que para especificar la representación string de una clase, necesitamos declarar el método ToString con la palabra clave override; la razón de esto se aclarará cuando hablemos sobre la herencia en el capítulo 10.

Uso de la clase Tiempo1 Como aprendió en el capítulo 4, cada clase que se declara representa un nuevo tipo en C#. Por lo tanto, después de declarar la clase Tiempo1 podemos utilizarla como un tipo en las declaraciones como Tiempo1 puestasol; // puestasol puede guardar una referencia a un objeto Tiempo1

La clase de la aplicación PruebaTiempo1 (figura 9.2) utiliza la clase Tiempo1. La línea 10 crea un objeto Tiempo1 y lo asigna a la variable local tiempo. Observe que new invoca al constructor predeterminado de la clase Tiempo1, ya que Tiempo1 no declara constructores. Las líneas 13-17 imprimen en pantalla la hora, primero en formato universal (mediante la invocación al método AStringUniversal en la línea 14) y después en formato estándar (mediante la invocación explícita del método ToString de tiempo en la línea 16) para confirmar que el objeto Tiempo1 se haya inicializado en forma apropiada. 1 2 3 4 5 6 7 8

// Fig. 9.2: PruebaTiempo1.cs // Uso de un objeto Tiempo1 en una aplicación. using System; public class PruebaTiempo1 { public static void Main( string[] args ) {

Figura 9.2 | Uso de un objeto Tiempo1 en una aplicación. (Parte 1 de 2).

9.2 Caso de estudio de la clase Tiempo

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

267

// crea e inicializa un objeto Tiempo1 Tiempo1 tiempo = new Tiempo1(); // invoca al constructor de Tiempo1 // imprime representaciones de cadena de la hora Console.Write( "La hora universal inicial es: " ); Console.WriteLine( tiempo.AStringUniversal() ); Console.Write( "La hora inicial estándar es: " ); Console.WriteLine( tiempo.ToString() ); Console.WriteLine(); // imprime una línea en blanco // cambia la hora e imprime la hora actualizada tiempo.EstablecerTiempo( 13, 27, 6 ); Console.Write( "La hora universal después de EstablecerTiempo es: " ); Console.WriteLine( tiempo.AStringUniversal() ); Console.Write( "La hora estándar después de EstablecerTiempo es: " ); Console.WriteLine( tiempo.ToString() ); Console.WriteLine(); // imprime una línea en blanco // establece la hora con valores inválidos; imprime la hora actualizada tiempo.EstablecerTiempo( 99, 99, 99 ); Console.WriteLine( "Después de tratar de asignar configuraciones inválidas:" ); Console.Write( "Hora universal: " ); Console.WriteLine( tiempo.AStringUniversal() ); Console.Write( "Hora estándar: " ); Console.WriteLine( tiempo.ToString() ); } // fin de Main } // fin de la clase PruebaTiempo1

La hora universal inicial es: 00:00:00 La hora inicial estándar es: 12:00:00 AM La hora universal después de EstablecerTiempo es: 13:27:06 La hora estándar después de EstablecerTiempo es: 1:27:06 PM Después de tratar de asignar configuraciones inválidas: Hora universal: 00:00:00 Hora estándar: 12:00:00 AM

Figura 9.2 | Uso de un objeto Tiempo1 en una aplicación. (Parte 2 de 2). La línea 20 invoca al método EstablecerTiempo del objeto tiempo para cambiar la hora. Después, las líneas 21-25 imprimen en pantalla la hora otra vez en ambos formatos, para confirmar que se haya ajustado en forma apropiada. Para ilustrar que el método EstablecerTiempo mantiene el objeto en un estado consistente, la línea 28 llama al método EstablecerTiempo con los argumentos inválidos de 99 para la hora, el minuto y el segundo. Las líneas 29-33 imprimen nuevamente el tiempo en ambos formatos, para confirmar que EstablecerTiempo mantenga el estado consistente del objeto, y después la aplicación se termina. Las últimas dos líneas de la salida de la aplicación muestran que el tiempo se reestablece a medianoche (el valor inicial de un objeto Tiempo1) si tratamos de establecer el tiempo con valores fuera de rango.

Notas acerca de la declaración de la clase Tiempo1 Es necesario considerar diversas cuestiones sobre el diseño de clases, en relación con la clase Tiempo1. Las variables de instancia hora, minuto y segundo se declaran como private. La representación de datos que se utilice dentro de la clase no concierne a los clientes de la misma. Por ejemplo, sería perfectamente razonable que Tiempo1 representara el tiempo internamente como el número de segundos transcurridos a partir de medianoche, o el número de minutos y segundos transcurridos a partir de medianoche. Los clientes podrían usar los mismos métodos y propiedades public para obtener los mismos resultados, sin tener que preocuparse por lo anterior.

268

Capítulo 9 Clases y objetos: un análisis más detallado

Observación de ingeniería de software 9.2 Las clases simplifican la programación, ya que el cliente sólo puede utilizar los miembros public expuestos por la clase. Dichos miembros por lo general están orientados a los clientes, en vez de estar orientados a la implementación. Los clientes nunca se percatan de (ni se involucran en) la implementación de una clase; por lo general se preocupan acerca de lo que ésta hace, pero no cómo lo hace. (Desde luego que los clientes se encargan de que la clase opere en forma correcta y eficiente.)

Observación de ingeniería de software 9.3 Las interfaces cambian con menos frecuencia que las implementaciones. Cuando cambia una implementación, el código dependiente de esa ella debe cambiar de manera acorde. El ocultamiento de la implementación reduce la posibilidad de que otras partes de la aplicación dependan de los detalles de la implementación de la clase.

9.3 Control del acceso a los miembros Los modificadores de acceso public y private controlan el acceso a las variables y métodos de una clase (en la sección 9.15 y en el capítulo 10, presentaremos los modificadores de accesos adicionales internal y protected, respectivamente). Como dijimos en la sección 9.2, el principal propósito de los métodos public es presentar a los clientes de la clase una vista de los servicios que proporciona (la interfaz pública de la clase). Los clientes de la clase no necesitan preocuparse por la forma en que ésta realiza sus tareas. Por esta razón, las variables y métodos private de la clase (es decir, los detalles de implementación de la clase) no son directamente accesibles para los clientes. La figura 9.3 demuestra que los miembros de la clase private no son directamente accesibles fuera de la clase. Las líneas 9-11 tratan de acceder en forma directa a las variables de instancia private hora, minuto y segundo del objeto tiempo de Tiempo1. Al compilar esta aplicación, el compilador genera mensajes de error que indican que estos miembros private no son accesibles. [Nota: esta aplicación asume que se utiliza la clase Tiempo1 de la figura 9.1.]

Error común de programación 9.1 Cuando un método que no es miembro de una clase trata de acceder a uno private de esa clase, se produce un error de compilación.

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 9.3: PruebaAccesoMiembros.cs // Los miembros privados de la clase Tiempo1 no son accesibles. public class PruebaAccesoMiembros { public static void Main( string[] args ) { Tiempo1 tiempo = new Tiempo1(); // crea e inicializa un objeto Tiempo1 tiempo.hora = 7; // error: hora tiene acceso privado en Tiempo1 tiempo.minuto = 15; // error: minuto tiene acceso privado en Tiempo1 tiempo.segundo = 30; // error: segundo tiene acceso privado en Tiempo1 } // fin de Main } // fin de la clase PruebaAccesoMiembros

Figura 9.3 | Los miembros privados de la clase Tiempo1 no son accesibles.

9.4 Referencias a los miembros del objeto actual mediante this

269

Observe que los miembros de una clase (por ejemplo, los métodos y las variables de instancia) no necesitan declararse explícitamente como private. Si el miembro de una clase no se declara con un modificador de acceso, tiene acceso private de manera predeterminada. Nosotros siempre declaramos los miembros private en forma explícita.

9.4 Referencias a los miembros del objeto actual mediante this Cada objeto puede acceder a una referencia a sí mismo mediante la palabra clave this (también conocida como referencia). Cuando se hace una llamada a un método no static para un objeto específico, el cuerpo del método utiliza en forma implícita la palabra clave this para hacer referencia a las variables de instancia y los demás métodos del objeto. Como verá en la figura 9.4, puede utilizar también la palabra clave this explícitamente en el cuerpo de un método no static. Las secciones 9.5 y 9.6 muestran dos usos más interesantes de la palabra clave this; la sección 9.10 explica por qué no puede usarse esta palabra clave en un método static. Ahora demostraremos el uso implícito y explícito de la referencia this para permitir al método Main de la clase PruebaThis que muestre en pantalla los datos private de un objeto de la clase TiempoSimple (figura 9.4). Por cuestión de brevedad, declaramos dos clases en un archivo; la clase PruebaThis se declara en las líneas 5-12 y la clase TiempoSimple se declara en las líneas 15-48. La clase TiempoSimple (líneas 15-48) declara tres variables de instancia private: hora, minuto y segundo (líneas 17-19). El constructor (líneas 24-29) recibe tres argumentos int para inicializar un objeto TiempoSimple. Observe que para el constructor utilizamos nombres de parámetros idénticos a los nombres de las variables de instancia de la clase (líneas 17-19). No recomendamos esta práctica, pero lo hicimos aquí para ocultar las variables de instancia correspondientes y así poder ilustrar el uso explícito de la referencia this. En la sección 7.11 vimos que si un método contiene una variable local con el mismo nombre que el de un campo, hará referencia a la variable local y no al campo. En este caso, la variable local oculta el campo en el alcance del método. No obstante, el método puede utilizar la referencia this para hacer referencia a la variable de instancia oculta de manera explícita, como se muestra en las líneas 26-28 para las variables de instancia ocultas de TiempoSimple. El método CrearString (líneas 32-37) devuelve un objeto string creado por una instrucción que utiliza la referencia this en forma explícita e implícita. La línea 35 utiliza la referencia this en forma explícita para

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

// Fig. 9.4: PruebaThis.cs // Uso implícito y explícito de this para hacer referencia a los miembros de un objeto. using System; public class PruebaThis { public static void Main( string[] args ) { TiempoSimple tiempo = new TiempoSimple( 15, 30, 19 ); Console.WriteLine( tiempo.CrearString() ); } // fin de Main } // fin de la clase PruebaThis // clase TiempoSimple para demostrar la referencia "this" public class TiempoSimple { private int hora; // 0-23 private int minuto; // 0-59 private int segundo; // 0-59 // si el constructor usa nombres de parámetros idénticos a los // nombres de las variables de instancia, se requiere la referencia "this" // para diferenciar entre ellos public TiempoSimple( int hora, int minuto, int segundo ) {

Figura 9.4 | Uso implícito y explícito de this para hacer referencia a los miembros de un objeto. (Parte 1 de 2).

270

26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

Capítulo 9 Clases y objetos: un análisis más detallado

this.hora = hora; // establece la variable de instancia hora de este ("this") objeto this.minuto = minuto; // establece el minuto de este ("this") objeto this.segundo = segundo; // establece el segundo de este ("this") objeto } // fin del constructor de TiempoSimple // usa "this" en forma explícita e implícita para llamar a AStringUniversal public string CrearString() { return string.Format( "{0,24}: {1}\n{2,24}: {3}", "this.AStringUniversal()", this.AStringUniversal(), "AStringUniversal()", AStringUniversal() ); } // fin del método CrearString // convierte a string en formato de hora universal (HH:MM:SS) public string AStringUniversal() { // "this" no se requiere aquí para acceder a las variables de instancia, // porque el método no tiene variables locales con los mismos // nombres que las variables de instancia return string.Format( "{0:D2}:{1:D2}:{2:D2}", this.hora, , this.segundo ); , this.minuto, } // fin del método AStringUniversal } // fin de la clase TiempoSimple

this.AStringUniversal(): 15:30:19 AStringUniversal(): 15:30:19

Figura 9.4 | Uso implícito y explícito de this para hacer referencia a los miembros de un objeto. (Parte 2 de 2). llamar al método AStringUniversal. La línea 36 la utiliza en forma implícita para llamar al mismo método. Observe que ambas líneas realizan la misma tarea. Por lo general, los programadores no utilizan la referencia this en forma explícita para hacer referencia a otros métodos en el objeto actual. Además, observe que la línea 46 en el método AStringUniversal utiliza en forma explícita la referencia this para acceder a cada variable de instancia. Esto no es necesario aquí, ya que el método no tiene variables locales que oculten las variables de instancia de la clase.

Error común de programación 9.2 A menudo se produce un error lógico cuando un método contiene un parámetro o variable local con el mismo nombre que una variable de instancia de la clase. En tal caso, use la referencia this si desea acceder a la variable de instancia de la clase; de no ser así, se hará referencia al parámetro o variable local del método.

Tip de prevención de errores 9.1 Evite los nombres de los parámetros o variables locales que tengan conflicto con los nombres de los campos. Esto ayuda a evitar errores sutiles, difíciles de localizar.

La clase PruebaThis (figura 9.4, líneas 5-12) demuestra el uso de la clase TiempoSimple. La línea 9 crea una instancia de la clase TiempoSimple e invoca a su constructor. La línea 10 invoca al método CrearString del objeto y después muestra los resultados en pantalla.

Tip de rendimiento 9.1 Para conservar la memoria, C# mantiene sólo una copia de cada método por clase; todos los objetos de la clase invocan a este método. Por otro lado, cada objeto tiene su propia copia de las variables de instancia de la clase (es decir, las variables no static). Cada método de la clase utiliza en forma implícita la referencia this para determinar el objeto específico de la clase que se manipulará.

9.5 Indexadores

271

9.5 Indexadores En el capítulo 4 se introdujeron las propiedades como una forma para acceder a los datos private de una clase en forma controlada, a través de los descriptores de acceso get y set. En ocasiones, una clase encapsula listas de datos como arreglos. Dicha clase puede utilizar la palabra clave this para definir miembros de la clase similares a las propiedades, a los cuales se les conoce como indexadores; éstos permiten el acceso indexado a las listas de elementos, al estilo de los arreglos. Con los arreglos “convencionales” de C#, el índice (o subíndice) debe ser un valor entero. Un beneficio de los indexadores es que se pueden definir subíndices enteros y no enteros. Por ejemplo, podríamos permitir que el código cliente manipule datos mediante el uso de objetos string como índices que representen los nombres o las descripciones de los elementos de datos. Al manipular elementos de arreglos “convencionales” en C#, el operador de acceso al elemento de un arreglo siempre devuelve un valor del mismo tipo; es decir, el tipo del arreglo. Los indexadores son más flexibles: pueden devolver cualquier tipo, incluso uno que sea distinto del de los datos subyacentes. Aunque el operador de acceso a los elementos de un indexador se utiliza igual que un operador de acceso a los elementos de un arreglo, los indexadores se definen como las propiedades en una clase. A diferencia de las propiedades, para las cuales se puede elegir un nombre de propiedad apropiado, los indexadores deben definirse con la palabra clave this. Los indexadores tienen la siguiente forma general: modificadorAcceso tipoRetorno this[ TipoIndice1 nombre1, TipoIndice2 nombre2, … ] { get { // utiliza nombre1, nombre2,



aquí para obtener los datos

} set { // usa nombre1, nombre2,



aquí para establecer los datos

} }

Los parámetros TipoIndice que se especifican en los corchetes ([ ]) son accesibles para los descriptores de acceso get y set. Estos descriptores de acceso definen cómo se utilizará el índice (o índices) para extraer o modificar el miembro de datos apropiado. Al igual que con las propiedades, el descriptor de acceso get del indexador debe devolver (return) un valor de tipo tipoRetorno y el descriptor de acceso set puede utilizar el parámetro implícito value para hacer referencia al valor que debe asignarse al elemento.

Error común de programación 9.3 Declarar los indexadores como static es un error de sintaxis.

La aplicación de las figuras 9.5-9.6 contiene dos clases: la clase Caja representa un cuadro con una longitud, una anchura y una altura, y la clase PruebaCaja demuestra el uso de los indexadores de la clase Caja.

1 2 3 4 5 6 7 8 9 10 11

// Fig. 9.5: Caja.cs // La definición de la clase Caja representa una caja con dimensiones de // longitud, anchura y altura con indexadores. public class Caja { private string[] nombres = { "longitud", "anchura", "altura" }; private double[] medidas = new double[ 3 ]; // constructor public Caja( double longitud, double anchura, double altura ) {

Figura 9.5 | La definición de la clase Caja representa una caja con medidas de longitud, anchura y altura con indexadores. (Parte 1 de 2).

272

12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

Capítulo 9 Clases y objetos: un análisis más detallado

medidas[ 0 ] = longitud; medidas[ 1 ] = anchura; medidas[ 2 ] = altura; } // indexador para acceder a medidas por el número de índice entero public double this[ int indice ] { get { // valida indice a obtener if ( ( indice < 0 ) || ( indice >= medidas.Length ) ) return -1; else return medidas[ indice ]; } // fin de get set { if ( indice >= 0 && indice < medidas.Length ) medidas[ indice ] = value; } // fin de set } // fin de indexador numérico // indexador para acceder a medidas por sus nombres tipo string public double this[ string nombre ] { get { // localiza elemento a obtener int i = 0; while ( ( i < nombres.Length ) && ( nombre.ToLower() != nombres[ i ] ) ) i++; return ( i == nombres.Length ) ? -1 : medidas[ i ]; } // fin de get set { // localiza elemento a establecer int i = 0; while ( ( i < nombres.Length ) && ( nombre.ToLower() != nombres[ i ] ) ) i++; if ( i != nombres.Length ) medidas[ i ] = value; } // fin de set } // fin de indexador de string } // fin de la clase Caja

Figura 9.5 | La definición de la clase Caja representa una caja con medidas de longitud, anchura y altura con indexadores. (Parte 2 de 2). Los miembros de datos private de la clase Caja son el arreglo string llamado nombres (línea 6), que contiene los nombres (es decir, "longitud", "anchura" y "altura") para las medidas de una Caja, y el arreglo double llamado medidas (línea 7), que contiene el tamaño de cada medida. Cada elemento en el arreglo nombres corresponde a un elemento en el arreglo medidas (por ejemplo, medidas[ 2 ] contiene la altura de la Caja).

9.5 Indexadores Caja define dos indexadores (líneas 18-33 double que representa el tamaño de la medida

273

y 36-59), cada uno de los cuales devuelve (return) un valor especificada por el parámetro del indexador. Los indexadores pueden sobrecargarse de igual forma que los métodos. El primer indexador utiliza un índice int para manipular un elemento en el arreglo medidas. El segundo indexador utiliza un índice string, que representa el nombre de la medida para manipular un elemento en el arreglo medidas. Cada indexador devuelve -1 si su descriptor de acceso get encuentra un subíndice inválido. El descriptor de acceso set de cada indexador asigna value al elemento apropiado de medidas, sólo si el índice es válido. Por lo general, un indexador lanza una excepción si recibe un índice inválido. En el capítulo 12, Manejo de excepciones, veremos cómo lanzar excepciones. Observe que el indexador string utiliza una estructura while para buscar un string que concuerde en el arreglo nombres (líneas 42-44 y 52-54). Si hay una concordancia, el indexador manipula el elemento correspondiente en el arreglo medidas (líneas 46 y 57). La clase PruebaCaja (figura 9.6) manipula los miembros de datos private de la clase Caja, a través de los indexadores de Caja. La variable local caja se declara en la línea 10 y se inicializa con una nueva instancia de la clase Caja. Utilizamos el constructor de Caja para inicializar caja con las medidas 30, 30 y 30. Las líneas 14-16 utilizan el indexador declarado con el parámetro int para obtener las tres medidas de caja, y las muestra en pantalla mediante WriteLine. La expresión caja[ 0 ] (línea 14) llama en forma implícita al descriptor de acceso get del indexador para obtener el valor de la variable de instancia private medidas[ 0 ] de caja. De manera similar, la asignación a caja[ 0 ] en la línea 20 llama en forma implícita al descriptor de acceso set en las líneas 28-32 de la figura 9.5. El descriptor de acceso set establece en forma implícita su parámetro value a 10 y después establece medidas[ 0 ] a value (10). Las líneas 24 y 28-30 en la figura 9.6 realizan acciones similares, usando el indexador sobrecargado con un parámetro string para manipular los mismos datos. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

// Fig. 9.6: PruebaCaja.cs // Los indexadores proporcionan acceso a los miembros de un objeto Caja. using System; public class PruebaCaja { public static void Main( string[] args ) { // crea una caja Caja caja = new Caja( 30, 30, 30 ); // muestra medidas Console.WriteLine( Console.WriteLine( Console.WriteLine( Console.WriteLine(

con indexadores numéricos "Se creó una caja con las medidas:" ); "caja[ 0 ] = {0}", caja[ 0 ] ); "caja[ 1 ] = {0}", caja[ 1 ] ); "caja[ 2 ] = {0}", caja[ 2 ] );

// establece una medida con el indexador numérico Console.WriteLine( "\nSe estableció caja[ 0 ] a 10...\n" ); caja[ 0 ] = 10; // establece una medida con el indexador string Console.WriteLine( "Se estableció caja[ \"anchura\" ] a 20...\n" ); caja[ "anchura" ] = 20; // muestra medidas con indexadores string Console.WriteLine( "Ahora la caja tiene las medidas:" ); Console.WriteLine( "caja[ \"longitud\" ] = {0}", caja[ "longitud" ] ); Console.WriteLine( "caja[ \"anchura\" ] = {0}", caja[ "anchura" ] ); Console.WriteLine( "caja[ \"altura\" ] = {0}", caja[ "altura" ] ); } // fin del método Main } // fin de la clase PruebaCaja

Figura 9.6 | Los indexadores proporcionan acceso a los miembros de un objeto. (Parte 1 de 2).

274

Capítulo 9 Clases y objetos: un análisis más detallado

Se creó caja[ 0 caja[ 1 caja[ 2

una ] = ] = ] =

caja con las medidas: 30 30 30

Se estableció caja[ 0 ] a 10... Se estableció caja[ “anchura” ] a 20... Ahora caja[ caja[ caja[

la caja tiene las medidas: “longitud” ] = 10 “anchura” ] = 20 “altura” ] = 30

Figura 9.6 | Los indexadores proporcionan acceso a los miembros de un objeto. (Parte 2 de 2).

9.6 Caso de estudio de la clase Tiempo: constructores sobrecargados Como sabe, puede declarar su propio constructor para especificar cómo deben inicializarse los objetos de una clase. A continuación demostraremos una clase con varios constructores sobrecargados, que permiten a los objetos inicializarse de distintas formas. Para sobrecargar los constructores, sólo hay que proporcionar varias declaraciones del constructor con distintas firmas. En la sección 7.12 vimos que el compilador diferencia las firmas en base al número, tipos y orden de los parámetros en cada firma.

Clase Tiempo2 con constructores sobrecargados De manera predeterminada, las variables de instancia hora, minuto y segundo de la clase Tiempo1 (figura 9.1) se inicializan a sus valores predeterminados de 0 (medianoche en formato de hora universal). La clase Tiempo1 no permite que sus clientes inicialicen la hora con valores específicos distintos de cero. La clase Tiempo2 (figura 9.7) contiene cinco constructores sobrecargados para inicializar de manera conveniente sus objetos en una variedad de formas. Los constructores aseguran que cada objeto Tiempo2 comience en un estado consistente. En esta aplicación, cuatro de los constructores invocan un quinto constructor, el cual a su vez llama al método EstablecerTiempo. El método EstablecerTiempo invoca los descriptores de acceso set de las propiedades Hora, Minuto y Segundo, que aseguran que el valor suministrado para hora se encuentre en el rango de 0 a 23, y que los valores para minuto y segundo se encuentren cada uno en el rango de 0 a 59. Si un valor está fuera de rango, se establece a 0 mediante la propiedad 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

// Fig. 9.7: Tiempo2.cs // Declaración de la clase Tiempo2 con constructores sobrecargados. public class Tiempo2 { private int hora; // 0 - 23 private int minuto; // 0 - 59 private int segundo; // 0 - 59 // constructor de Tiempo2 sin argumentos: inicializa cada variable de instancia // a cero; asegura que los objetos Tiempo2 empiecen en un estado consistente public Tiempo2() : this( 0, 0, 0 ) { } // constructor de Tiempo2: se suministra hora, minuto y segundo con valor predet. 0 public Tiempo2( int h ) : this( h, 0, 0 ) { } // constructor de Tiempo2: se suministran hora y minuto, segundo con valor predet. 0 public Tiempo2( int h, int m ) : this( h, m, 0 ) { } // constructor de Tiempo2: se suministran hora, minuto y segundo

Figura 9.7 | Declaración de la clase Tiempo2 con constructores sobrecargados. (Parte 1 de 3).

9.6 Caso de estudio de la clase Tiempo: constructores sobrecargados

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77

275

public Tiempo2( int h, int m, int s ) { EstablecerTiempo( h, m, s ); // invoca a EstablecerTiempo para validar el tiempo } // fin de constructor de Tiempo2 con tres argumentos // constructor de Tiempo2: se suministra otro objeto Tiempo2 public Tiempo2( Tiempo2 tiempo ) : this( tiempo.Hora, tiempo.Minuto, tiempo.Segundo ) { } // establece un nuevo valor de tiempo usando hora universal; asegura que // los datos permanezcan consistentes al establecer los valores inválidos a cero public void EstablecerTiempo( int h, int m, int s ) { Hora = h; // establece la propiedad Hora Minuto = m; // establece la propiedad Minuto Segundo = s; // establece la propiedad Segundo } // fin del método EstablecerTiempo // Propiedades para obtener y establecer // propiedad que obtiene (get) y establece (set) la hora public int Hora { get { return hora; } // fin de get // hacer la escritura inaccesible fuera de la clase private set { hora = ( ( value >= 0 && value < 24 ) ? value : 0 ); } // fin de set } // fin de la propiedad Hora // propiedad que obtiene (get) y establece (set) el minuto public int Minuto { get { return minuto; } // fin de get // hacer la escritua inaccesible fuera de la clase private set { minuto = ( ( value >= 0 && value < 60 ) ? value : 0 ); } // fin de set } // fin de la propiedad Minuto // propiedad que obtiene (get) y establece (set) el segundo public int Segundo { get { return segundo; } // fin de get // hacer la escritura inaccesible fuera de la clase private set { segundo = ( ( value >= 0 && value < 60 ) ? value : 0 );

Figura 9.7 | Declaración de la clase Tiempo2 con constructores sobrecargados. (Parte 2 de 3).

276

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

Capítulo 9 Clases y objetos: un análisis más detallado

} // fin de set } // fin de la propiedad Segundo // convierte a string en formato de hora universal (HH:MM:SS) public string AStringUniversal() { return string.Format( "{0:D2}:{1:D2}:{2:D2}", Hora, Minuto, Segundo ); } // fin del método AStringUniversal // convierte a string en formato de hora estándar (H:MM:SS AM o PM) public override string ToString() { return string.Format( "{0}:{1:D2}:{2:D2} {3}", ( ( Hora == 0 || Hora == 12 ) ? 12 : Hora % 12 ), Minuto, Segundo, ( Hora < 12 ? "AM" : "PM" ) ); } // fin del método ToString } // fin de la clase Tiempo2

Figura 9.7 | Declaración de la clase Tiempo2 con constructores sobrecargados. (Parte 3 de 3). correspondiente (una vez más se asegura que cada variable de instancia permanezca en un estado consistente). Para invocar el constructor apropiado, el compilador relaciona el número y los tipos de los argumentos especificados en la llamada al constructor con el número y los tipos de los parámetros especificados en la declaración de cada constructor. Observe que la clase Tiempo2 también proporciona propiedades para cada variable de instancia.

Constructores de la clase Tiempo2

La línea 11 declara un constructor sin parámetros, es decir, que se invoca sin argumentos. Observe que este constructor tiene un cuerpo vacío, como se indica mediante el conjunto vacío de llaves después del encabezado. En vez de un cuerpo, presentamos un uso de la referencia this que se permite sólo en el encabezado del constructor. En la línea 11, el encabezado usual del constructor va seguido de un signo de dos puntos (:) y de la palabra clave this. La referencia this se utiliza en la sintaxis de llamada a un método (junto con los tres argumentos int) para invocar al constructor de Tiempo2 que recibe tres argumentos int (líneas 20-23). El constructor sin parámetros pasa los valores de 0 para hora, minuto y segundo al constructor con tres parámetros int. El uso de la referencia this que se muestra aquí se denomina inicializador de constructor. Estos inicializadores son una forma popular de reutilizar el código de inicialización que proporciona uno de los constructores de la clase, en vez de definir código similar en el cuerpo de otro constructor. Utilizamos esta sintaxis en cuatro de los cinco constructores de Tiempo2 para que la clase sea más fácil de mantener. Si necesitamos cambiar la forma en que se inicializan los objetos de la clase Tiempo2, sólo hay que modificar el constructor al que necesitan llamar los demás constructores de la clase. Incluso ese constructor podría no requerir de modificación; simplemente llama al método TiempoSimple para realizar la verdadera inicialización, por lo que es posible que los cambios que requiera la clase se localicen en este método. La línea 14 declara un constructor de Tiempo2 con un solo parámetro int que representa la hora, que se pasa con 0 para minuto y segundo al constructor de las líneas 20-23. La línea 17 declara un constructor Tiempo2 que recibe dos parámetros int, que representan la hora y el minuto, que se pasan con un 0 para segundo al constructor de las líneas 20-23. Al igual que el constructor sin parámetros, cada uno de estos constructores invoca al constructor en las líneas 20-23 para minimizar la duplicación de código. Las líneas 20-23 declaran el constructor Tiempo2 que recibe tres parámetros int, que representan la hora, el minuto y el segundo. Este constructor llama a EstablecerTiempo para inicializar las variables de instancia con valores consistentes. A su vez, EstablecerTiempo invoca a los descriptores de acceso set de las propiedades Hora, Minuto y Segundo.

Error común de programación 9.4 Un constructor puede llamar a los métodos de la clase. Tenga en cuenta que tal vez las variables de instancia no se encuentren aún en un estado consistente, ya que el constructor está en el proceso de inicializar el objeto. El uso de variables de instancia antes de inicializarlas en forma apropiada es un error lógico.

9.6 Caso de estudio de la clase Tiempo: constructores sobrecargados

277

Las líneas 26-27 declaran un constructor de Tiempo2 que recibe una referencia Tiempo2 a otro objeto Tiempo2. En este caso, los valores del argumento Tiempo2 se pasan al constructor de tres parámetros en las líneas 20-23 para inicializar hora, minuto y segundo. Observe que la línea 27 podría haber accedido en forma directa a las variables de instancia hora, minuto y segundo del argumento tiempo del constructor mediante las expresiones tiempo.hora, tiempo.minuto y tiempo.segundo, aun y cuando hora, minuto y segundo se declaran como variables private de la clase Tiempo2.

Observación de ingeniería de software 9.4 Cuando un objeto de una clase tiene una referencia a otro objeto de la misma clase, el primer objeto puede acceder a todos los datos y métodos del segundo objeto (incluyendo los que sean private).

Observaciones en relación con los métodos, propiedades y constructores de la clase Tiempo2 Observe que se accede a las propiedades de Tiempo2 a través del cuerpo de la clase. En especial, el método EstablecerTiempo asigna valores a las propiedades Hora, Minuto y Segundo en las líneas 33-35, y los métodos AStringUniversal y ToString usan las propiedades Hora, Minuto y Segundo en la línea 85 y en las líneas 92 y 93, respectivamente. En cada caso, estos métodos podrían haber accedido a los datos private de la clase en forma directa, sin necesidad de usar las propiedades. Ahora, considere la acción de cambiar la representación del tiempo, de tres valores int (que requieren 12 bytes de memoria) a un solo valor int que represente el número total de segundos transcurridos a partir de medianoche (que requiere sólo 4 bytes de memoria). Si hacemos ese cambio, sólo tendrían que cambiar los cuerpos de los métodos que acceden directamente a los datos private; en especial, las propiedades individuales Hora, Minuto y Segundo. No habría necesidad de modificar los cuerpos de los métodos EstablecerTiempo, AStringUniversal o ToString, ya que no acceden a los datos private en forma directa. Si se diseña la clase de esta forma se reduce la probabilidad de que se produzcan errores de programación al momento de alterar la implementación de la clase. De manera similar, cada constructor de Tiempo2 podría escribirse de forma que incluya una copia de las instrucciones apropiadas del método EstablecerTiempo. Esto sería un poco más eficiente, ya que se eliminan la llamada extra al constructor y la llamada a EstablecerTiempo. No obstante, duplicar las instrucciones en varios métodos o constructores dificulta más el proceso de modificar la representación interna de datos de la clase, además de que aumenta la probabilidad de que haya errores. Si hacemos que los constructores de Tiempo2 llamen al constructor con tres parámetros (o que incluso llamen a EstablecerTiempo directamente), cualquier modificación a la implementación de EstablecerTiempo sólo tendrá que hacerse una vez.

Observación de ingeniería de software 9.5 Al implementar un método de una clase, use las propiedades de la clase para acceder a sus datos simplifica el mantenimiento del código y reduce la probabilidad de errores.

private.

Esto

Observe además que la clase Tiempo2 aprovecha los modificadores de acceso para asegurar que los clientes de la clase utilicen los métodos y propiedades apropiados para acceder a los datos private. En especial, las propiedades Hora, Minuto y Segundo declaran los descriptores de acceso private set (líneas 47, 61 y 75, respectivamente) para restringir el uso de los descriptores de acceso set a los miembros de la clase. Declaramos estos descriptores como private por las mismas razones que declaramos las variables de instancia private: para simplificar el mantenimiento del código y asegurar que los datos permanezcan en un estado consistente. Aunque los métodos en la clase Tiempo2 aún tienen todas las ventajas de usar los descriptores de acceso set para realizar la validación, los clientes de la clase deben utilizar el método EstablecerTiempo para modificar estos datos. Los descriptores de acceso get de las propiedades Hora, Minuto y Segundo se declaran en forma implícita como public, ya que sus propiedades se declaran public; cuando no hay un modificador de acceso antes de un descriptor de acceso get o set, el descriptor hereda el modificador de acceso que va antes del nombre de la propiedad.

Uso de los constructores sobrecargados de la clase Tiempo2 La clase PruebaTiempo2 (figura 9.8) crea seis objetos Tiempo2 (líneas 9-14) para invocar a los constructores sobrecargados de Tiempo2. La línea 9 muestra que para invocar el constructor sin parámetros (línea 11 de la

278

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

Capítulo 9 Clases y objetos: un análisis más detallado

// Fig. 9.8: PruebaTiempo2.cs // Uso de constructores sobrecargados para inicializar objetos Tiempo2. using System; public class PruebaTiempo2 { public static void Main( string[] args ) { Tiempo2 t1 = new Tiempo2(); // 00:00:00 Tiempo2 t2 = new Tiempo2( 2 ); // 02:00:00 Tiempo2 t3 = new Tiempo2( 21, 34 ); // 21:34:00 Tiempo2 t4 = new Tiempo2( 12, 25, 42 ); // 12:25:42 Tiempo2 t5 = new Tiempo2( 27, 74, 99 ); // 00:00:00 Tiempo2 t6 = new Tiempo2( t4 ); // 12:25:42 Console.WriteLine( Console.WriteLine( Console.WriteLine( Console.WriteLine(

"Se construyó con:\n" ); "t1: todos los argumentos con valor predeterminado" ); " {0}", t1.AStringUniversal() ); // 00:00:00 " {0}\n", t1.ToString() ); // 12:00:00 AM

Console.WriteLine( "t2: se especificó hora; minuto y segundo con valores predeterminados" ); Console.WriteLine( " {0}", t2.AStringUniversal() ); // 02:00:00 Console.WriteLine( " {0}\n", t2.ToString() ); // 2:00:00 AM Console.WriteLine( "t3: se especificaron hora y minuto; segundo con valor predeterminado" ); Console.WriteLine( " {0}", t3.AStringUniversal() ); // 21:34:00 Console.WriteLine( " {0}\n", t3.ToString() ); // 9:34:00 PM Console.WriteLine( "t4: se especificaron hora, minuto y segundo" ); Console.WriteLine( " {0}", t4.AStringUniversal() ); // 12:25:42 Console.WriteLine( " {0}\n", t4.ToString() ); // 12:25:42 PM Console.WriteLine( "t5: se especificaron todos los valores inválidos" ); Console.WriteLine( " {0}", t5.AStringUniversal() ); // 00:00:00 Console.WriteLine( " {0}\n", t5.ToString() ); // 12:00:00 AM Console.WriteLine( "t6: se especificó objeto t4 de Tiempo2" ); Console.WriteLine( " {0}", t6.AStringUniversal() ); // 12:25:42 Console.WriteLine( " {0}", t6.ToString() ); // 12:25:42 PM } // fin de Main } // fin de la clase PruebaTiempo2

Se construyó con: t1: todos los argumentos con valor predeterminado 00:00:00 12:00:00 AM t2: se especificó hora; minuto y segundo con valores predeterminados 02:00:00 2:00:00 AM t3: se especificaron hora y minuto; segundo con valor predeterminado 21:34:00 9:34:00 PM

(continúa…)

Figura 9.8 | Uso de constructores sobrecargados para inicializar objetos Tiempo2. (Parte 1 de 2).

9.8 Composición

279

t4: se especificaron hora, minuto y segundo 12:25:42 12:25:42 PM t5: se especificaron todos los valores inválidos 00:00:00 12:00:00 AM t6: se especificó objeto t4 de Tiempo2 12:25:42 12:25:42 PM

Figura 9.8 | Uso de constructores sobrecargados para inicializar objetos Tiempo2. (Parte 2 de 2). figura 9.7) se coloca un conjunto vacío de paréntesis después del nombre de la clase, cuando se asigna un objeto Tiempo2 mediante new. Las líneas 10-14 de la aplicación demuestran el paso de argumentos a los demás constructores de Tiempo2. Para invocar el constructor sobrecargado apropiado, C# relaciona el número y los tipos de los argumentos especificados en la llamada al constructor con el número y los tipos de los parámetros especificados en la declaración de cada constructor. La línea 10 invoca el constructor de la línea 14 de la figura 9.7. La línea 11 invoca el constructor de la línea 17 de la figura 9.7. Las líneas 12-13 invocan el constructor de las líneas 20-23 de la figura 9.7. La línea 14 invoca el constructor de las líneas 26-27 de la figura 9.7. La aplicación muestra en pantalla la representación string de cada objeto Tiempo2 inicializado, para confirmar que cada uno de ellos se haya inicializado en forma apropiada.

9.7 Constructores predeterminados y sin parámetros Toda clase debe tener cuando menos un constructor. En la sección 4.9 vimos que si no se proporcionan constructores en la declaración de una clase, el compilador crea uno predeterminado que no recibe argumentos cuando se le invoca. En la sección 10.4.1 aprenderá que el constructor predeterminado realiza de manera implícita una tarea especial. Toda clase debe tener cuando menos un constructor. El compilador no creará un constructor predeterminado para una clase que declare cuando menos un constructor en forma explícita. En este caso, si desea poder invocar al constructor sin argumentos, debe declarar un constructor sin parámetros (como en la línea 11 de la figura 9.7). Al igual que un constructor predeterminado, un constructor sin parámetros se invoca con paréntesis vacíos. Observe que el constructor sin parámetros de Tiempo2 inicializa en forma explícita un objeto Tiempo2; para ello pasa un 0 a cada parámetro del constructor de tres parámetros. Como 0 es el valor predeterminado para las variables de instancia int, el constructor sin parámetros en este ejemplo podría omitir el inicializador del constructor. En este caso, cada variable de instancia recibiría su valor predeterminado al momento de crear el objeto. Si omitiéramos el constructor sin parámetros, los clientes de esta clase no podrían crear un objeto Tiempo2 con la expresión new Tiempo2().

Error común de programación 9.5 Si una clase tiene constructores, pero ninguno de los constructores public son sin parámetros, y si una aplicación intenta llamar a un constructor sin parámetros para inicializar un objeto de esa clase, se produce un error de compilación. Se puede llamar a un constructor sin argumentos sólo cuando la clase no tiene constructores (en cuyo caso se llama al constructor predeterminado), o si la clase tiene un constructor public sin parámetros.

Error común de programación 9.6 Sólo los constructores pueden tener el mismo nombre que la clase. Declarar un método, propiedad o campo con el mismo nombre que la clase es un error de compilación.

9.8 Composición Una clase puede tener referencias a objetos de otras clases como miembros. A dicha capacidad se le conoce como composición y algunas veces como relación “tiene un”. Por ejemplo, un objeto de la clase RelojAlarma necesita

280

Capítulo 9 Clases y objetos: un análisis más detallado

saber la hora actual y la hora en la que se supone sonará su alarma, por lo que es razonable incluir dos referencias a objetos Tiempo como miembros del objeto RelojAlarma.

Observación de ingeniería de software 9.6 La composición es una forma de reutilización de software, en donde una clase tiene como miembros referencias a objetos de otras clases.

Nuestro ejemplo de composición contiene tres clases: Fecha (figura 9.9), Empleado (figura 9.10) y PruebaEmpleado (figura 9.11). La clase Fecha (figura 9.9) declara las variables de instancia mes, dia y anio (líneas 7-9) para representar una fecha. El constructor recibe tres parámetros int. La línea 15 invoca en forma implícita el descriptor de acceso set de la propiedad Mes (líneas 41-50) para validar el mes; un valor fuera de rango se establece en 1 para mantener un estado consistente. La línea 16 utiliza de manera similar la propiedad Anio para establecer el año; pero observe que el descriptor de acceso set de Anio (líneas 28-31) asume que el valor de anio es correcto y no lo valida. La línea 17 utiliza la propiedad Dia (líneas 54-78), la cual valida y asigna el valor de dia con base en el mes y anio actuales (mediante el uso de las propiedades Mes y Anio para poder obtener los valores de mes y anio). Observe que el orden de inicialización es importante, ya que el descriptor de acceso set de la propiedad Dia valida el valor para dia, con base en la suposición de que mes y anio son correctos. La línea 66 determina si el día es correcto, con base en el número de días en el Mes especificado. Si el día no es correcto, las líneas 69-70 determinan si el Mes es Febrero, el día es 29 y el Anio es bisiesto. En caso contrario, si el parámetro value no contiene un valor correcto para dia, la línea 75 establece dia en 1 para mantener la Fecha en un estado consistente. Observe que la línea 18 en el constructor muestra en pantalla la referencia this como un objeto string. Como this es una referencia al objeto Fecha actual, el método ToString del objeto (líneas 81-84) se llama en forma implícita para obtener la representación string del objeto. La clase Empleado (figura 9.10) tiene las variables de instancia primerNombre, apellido, fechaNacimiento y fechaContratacion. Los miembros fechaNacimiento y fechaContratacion (líneas 7-8) son referencias

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

// Fig. 9.9: Fecha.cs // Declaración de la clase Fecha. using System; public class Fecha { private int mes; // 1-12 private int dia; // 1-31 dependiendo del mes private int anio; // cualquier año (se podría validar) // constructor: usa la propiedad Mes para confirmar el valor apropiado para mes; // usa la propiedad Dia para confirmar el valor apropiado para dia public Fecha( int elMes, int elDia, int elAnio ) { Mes = elMes; // valida el mes Anio = elAnio; // podría validar el año Dia = elDia; // valida el día Console.WriteLine( "Constructor de objeto Fecha para fecha {0}", this ); } // fin del constructor de Fecha // propiedad que obtiene (get) y establece (set) el año public int Anio { get { return anio; } // fin de get

Figura 9.9 | Declaración de la clase Fecha. (Parte 1 de 2).

9.8 Composición

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

281

private set // hace la escritura inaccesible fuera de la clase { anio = value; // podría validar } // fin de set } // fin de la propiedad Anio // propiedad que obtiene (get) y establece (set) el mes public int Mes { get { return mes; } // fin de get private set // hace la escritura inaccesible fuera de la clase { if ( value > 0 && value 0 && value = 0 && m < 60 ) ? m : 0 ); // valida minuto segundo = ( ( s >= 0 && s < 60 ) ? s : 0 ); // valida segundo } // fin del método EstablecerTiempo // convierte a string en formato de hora universal (HH:MM:SS) public string AStringUniversal() { return string.Format( "{0:D2}:{1:D2}:{2:D2}", hora, minuto, segundo ); } // fin del método AStringUniversal // convierte a string en formato de hora estándar (H:MM:SS AM o PM) public override string ToString() { return string.Format( "{0}:{1:D2}:{2:D2} {3}", ( ( hora == 0 || hora == 12 ) ? 12 : hora % 12 ), minuto, segundo, ( hora < 12 ? "AM" : "PM" ) ); } // fin del método ToString } // fin de la clase Tiempo1 } // fin del espacio de nombres Capitulo09

Figura 9.16 | Declaración de la clase Tiempo1 en un espacio de nombres. usarse los nombres completamente calificados de las clases para diferenciarlas en la aplicación y evitar un conflicto de nombres (lo que también se conoce como colisión de nombres). Sólo las declaraciones namespace, las directivas using, los comentarios y los atributos de C# (que utilizaremos en el capítulo 18) pueden aparecer fuera de las llaves de una declaración de tipo (por ejemplo, clases y enumeraciones). Sólo las clases que se declaren como public podrán reutilizarse por los clientes de la biblioteca de clases. Por lo general, las clases no public se colocan en una biblioteca para dar soporte a las clases public reutilizables de esa biblioteca.

Paso 3: compilar la biblioteca de clases El paso 3 es compilar la clase en una biblioteca de clases. Para crear una biblioteca de clases en Visual C# Express, debemos crear un nuevo proyecto haciendo clic en el menú Archivo, después seleccionamos Nuevo proyecto… y elegimos Biblioteca de clases de la lista de plantillas, como se muestra en la figura 9.17. Después agregamos el código de la figura 9.16 en el nuevo proyecto (puede copiar el código de los ejemplos del libro o escribirlo usted mismo). En los proyectos que ha creado hasta ahora, el compilador de C# creaba un archivo .exe ejecutable, que contenía la aplicación. Al compilar un proyecto Biblioteca de clases, el compilador crea un archivo .dll, al cual se le conoce como biblioteca de vínculos dinámicos: un tipo de ensamblado al que podemos hacer referencias desde otras aplicaciones.

Paso 4: agregar una referencia a la biblioteca de clases Una vez que la clase se compila y se guarda en el archivo de biblioteca de clases, se puede hacer referencia a la biblioteca desde cualquier aplicación; para ello hay que indicar al IDE de Visual C# Express en dónde puede

294

Capítulo 9 Clases y objetos: un análisis más detallado

Figura 9.17 | Creación de un proyecto Biblioteca de clases. encontrar el archivo de biblioteca (paso 4). Cree un nuevo proyecto (vacío) y haga clic derecho sobre el nombre del proyecto en la ventana Explorador de soluciones. Seleccione Agregar referencia… del menú contextual que aparezca. El cuadro de diálogo que aparece a continuación contiene una lista de bibliotecas de clases del .NET Framework. Algunas bibliotecas de clases, como la que contiene el espacio de nombres System, son tan comunes que se agregan a su aplicación de manera implícita. Las que se encuentran en esta lista no se agregan así. En el cuadro de diálogo Agregar referencia…, haga clic en la ficha Examinar. En la sección 3.3 vimos que cuando se crea una aplicación, Visual C# 2005 coloca el archivo .exe en la carpeta bin\Release del directorio de su aplicación. Cuando usted crea una biblioteca de clases, Visual C# coloca el archivo .dll en el mismo lugar. En la ficha Examinar puede navegar hasta el directorio que contiene el archivo de biblioteca de clases que usted creó en el paso 3, como se muestra en la figura 9.18. Seleccione el archivo .dll y haga clic en Aceptar.

Paso 5: usar la clase desde una aplicación Agregue un nuevo archivo de código a su aplicación e introduzca el código para la clase PruebaEspacioNombresTiempo1 (figura 9.19). Ahora que ha agregado una referencia a su biblioteca de clases en esta aplicación, PruebaEspacioNombresTiempo1 puede utilizar su clase Tiempo1 (paso 5) sin tener que agregar el archivo de código fuente Tiempo1.cs al proyecto. En la figura 9.19, la directiva using en la línea 3 especifica que deseamos utilizar la(s) clase(s) de espacio de nombres Capitulo09 en este archivo. La clase PruebaEspacioNombres es el espacio de nombres global de esta aplicación, ya que el archivo de la clase no contiene una declaración namespace. Como las dos clases se encuentran en espacios de nombres distintos, la directiva using en la línea 3 permite a la clase PruebaEspacioNombresTiempo1 utilizar la clase Tiempo1, como si estuviera en el mismo espacio de nombres.

Figura 9.18 | Agregar una referencia.

9.15 Acceso internal

295

En la sección 4.4 vimos que podíamos omitir la dirección using en la línea 4, si nos referíamos siempre a la clase Console por su nombre de clase completamente calificado, System.Console. De manera similar, podríamos omitir la directiva using en la línea 3 para el espacio de nombres Capitulo09 si cambiáramos la declaración de Tiempo1 en la línea 11 de la figura 9.19 para que utilizara el nombre completamente calificado de la clase Tiempo1, como se muestra a continuación: Capitulo09.Tiempo1 tiempo = new Capitulo09.Tiempo1();

9.15 Acceso internal Las clases pueden declararse con sólo dos modificadores de acceso: public e internal. Si no hay un modificador de acceso en la declaración de la clase, el valor predeterminado es el acceso internal. Esto permite que la clase 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

// Fig. 9.19: PruebaEspacioNombresTiempo1.cs // Uso del objeto Tiempo1 en una aplicación. using Capitulo09; using System; public class PruebaEspacioNombresTiempo1 { public static void Main( string[] args ) { // crea e inicializa un objeto Tiempo1 Tiempo1 tiempo = new Tiempo1(); // llama al constructor de Tiempo1 // imprime en pantalla representaciones string del tiempo Console.Write( "La hora universal inicial es: " ); Console.WriteLine( tiempo.AStringUniversal() ); Console.Write( "La hora estándar inicial es: " ); Console.WriteLine( tiempo.ToString() ); Console.WriteLine(); // imprime una línea en blanco // cambia tiempo e imprime en pantalla tiempo actualizado tiempo.EstablecerTiempo( 13, 27, 6 ); Console.Write( "La hora universal después de EstablecerTiempo es: " ); Console.WriteLine( tiempo.AStringUniversal()); Console.Write( "La hora estándar después de EstablecerTiempo es: " ); Console.WriteLine( tiempo.ToString() ); Console.WriteLine(); // imprime una línea en blanco // establece tiempo con valores inválidos; imprime el tiempo actualizado tiempo.EstablecerTiempo( 99, 99, 99 ); Console.WriteLine( "Después de probar configuraciones inválidas:" ); Console.Write( "Hora universal: " ); Console.WriteLine( tiempo.AStringUniversal() ); Console.Write( "Hora estándar: " ); Console.WriteLine( tiempo.ToString() ); } // fin de Main } // fin de la clase PruebaEspacioNombresTiempo1

La hora universal inicial es: 00:00:00 La hora estándar inicial es: 12:00:00 AM La hora universal después de EstablecerTiempo es: 13:27:06 La hora estándar después de EstablecerTiempo es: 1:27:06 PM Después de probar configuraciones inválidas: Hora universal: 00:00:00 Hora estándar: 12:00:00 AM

Figura 9.19 | Uso de un objeto Tiempo1 en una aplicación.

296

Capítulo 9 Clases y objetos: un análisis más detallado

pueda utilizarse por todo el código en el mismo ensamblado que la clase, pero no por el código en otros ensamblados. Dentro del mismo ensamblado que la clase, esto equivale al acceso public. No obstante, si se hace referencia a una biblioteca de clases desde una aplicación, las clases internal de la biblioteca estarán inaccesibles desde el código de la aplicación. De manera similar, los métodos, las variables de instancia y otros miembros de una clase que se declara como internal son accesibles para todo el código compilado en el mismo ensamblado, pero no para el código en otros ensamblados. La aplicación de la figura 9.20 demuestra el acceso internal. Esta aplicación contiene dos clases en un archivo de código fuente: la clase de aplicación PruebaAccesoInternal (líneas 6-22) y la clase DatosInternal (líneas 25-43). En la declaración de la clase DatosInternal, las líneas 27-28 declaran las variables de instancia numero y mensaje con el modificador de acceso internal; la clase DatosInternal tiene el acceso internal de manera predeterminada, por lo que no hay necesidad de un modificador de acceso. El método static Main de PruebaAccesoInternal crea una instancia de la clase DatosInternal (línea 10) para demostrar cómo se modifican las variables de instancia de DatosInternal de manera directa (como se muestra en las líneas 16-17). Dentro del mismo ensamblado, el acceso internal es equivalente al acceso public. Los resultados se pueden ver en la ventana de resultados. Si compilamos esta clase en un archivo .dll de biblioteca de clases y la referenciamos desde una nueva aplicación, esa aplicación tendrá acceso a la clase public PruebaAccesoInternal, pero no a la clase internal DatosInternal o a sus miembros internal.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

// Fig. 9.20: PruebaAccesoInternal.cs // Los miembros que se declaran como internal en una clase, son accesibles para // otras clases en el mismo ensamblado. using System; public class PruebaAccesoInternal { public static void Main(string[] args) { DatosInternal datosInternal = new DatosInternal(); // imprmime representación string de datosInternal Console.WriteLine( "Después de instanciar:\n{0}", datosInternal ); // modifica los datos de acceso internal en datosInternal datosInternal.numero = 77; datosInternal.mensaje = “Adiós”; // imprime representación string de datosInternal Console.WriteLine( "\nDespués de modificar valores:\n{0}", datosInternal ); } // fin de Main } // fin de la clase PruebaAccesoInternal // clase con variables de instancia con acceso internal class DatosInternal { internal int numero; // variable de instancia con acceso internal internal string mensaje; // variable de instancia con acceso internal // constructor public DatosInternal() { numero = 0; mensaje = "Hola"; } // fin del constructor de DatosInternal

Figura 9.20 | Los miembros que se declaren como internal en una clase son accesibles para otras clases en el mismo ensamblado. (Parte 1 de 2).

9.16 Vista de clases y Examinador de objetos

37 38 39 40 41 42 43

297

// devuelve representación string del objeto DatosInternal public override string ToString() { return string.Format( “numero: {0}; mensaje: {1}”, numero, mensaje); } // fin del método ToString } // fin de la clase DatosInternal

Después de instanciar: numero: 0; mensaje: Hola Después de modificar valores: numero: 77; mensaje: Adiós

Figura 9.20 | Los miembros que se declaren como internal en una clase son accesibles para otras clases en el mismo ensamblado. (Parte 2 de 2).

9.16 Vista de clases y Examinador de objetos

Ahora que hemos introducido los conceptos clave de la programación orientada a objetos, le presentaremos dos características que proporciona Visual Studio para facilitar el diseño de las aplicaciones orientadas a objetos: Vista de clases y Examinador de objetos.

Uso de la ventana Vista de clases La ventana Vista de clases muestra los campos y los métodos para todas las clases en un proyecto. Para acceder a esta característica, seleccione Vista de clases del menú Ver. La figura 9.21 muestra la Vista de clases para el proyecto Tiempo1 de la figura 9.1 (clase Tiempo1) y la figura 9.2 (clase PruebaTiempo1). La vista sigue una estructura jerárquica, en donde posiciona el nombre del proyecto (Tiempo1) como la raíz e incluye una serie de nodos que representan las clases, variables y métodos en el proyecto. Si aparece un signo más (+) a la izquierda de un nodo, ese nodo se puede expandir para mostrar otros. Si aparece un signo menos (-) a la izquierda de un nodo, ese nodo se puede contraer. De acuerdo con la Vista de clases el proyecto Tiempo1 contiene la clase Tiempo1 y la clase PruebaTiempo1 como hijos. Cuando se selecciona la clase Tiempo1, aparecen sus miembros en la mitad inferior de la ventana. La clase Tiempo1 contiene los métodos EstablecerTiempo, ToString y AStringUniversal (los cuales se indican mediante cuadros color púrpura) y las variables de instancia hora, minuto y segundo (que se indican mediante cuadros color azul). Los iconos de candado, que se encuentran a la izquierda de los iconos de cuadros color azul para las variables de instancia, especifican que estas variables son private. La clase PruebaTiempo1 contiene el método Main. Observe que tanto la clase Tiempo1 como la clase PruebaTiempo1 contienen

Figura 9.21 | La Vista de clases de la clase Tiempo1 (figura 9.1) y de la clase PruebaTiempo1 (figura 9.2).

298

Capítulo 9 Clases y objetos: un análisis más detallado

el nodo Tipos base. Si expande este nodo, podrá ver la clase Object en cada caso, ya que cada clase hereda de la clase System.Object (que veremos en el capítulo 10).

Uso del Examinador de objetos El Examinador de objetos de Visual C# Express lista todas las clases en la biblioteca de C#. Puede utilizar el Examinador de objetos para aprender acerca de la funcionalidad que ofrece una clase específica. Para abrir el Examinador de objetos, seleccione Otras ventanas del menú Ver y haga clic en Examinador de objetos. La figura 9.22 ilustra el Examinador de objetos cuando el usuario navega a la clase Math en el espacio de nombres System, en el ensamblado mscorlib (Microsoft Core Library). [Nota: tenga cuidado de no confundir el espacio de nombres System con el ensamblado llamado System. El ensamblado System describe a otros miembros del espacio de nombres System, pero la clase System.Math está en mscorlib.] El Examinador de objetos lista todos los métodos que proporciona la clase Math en el marco superior derecho; esto le ofrece un “acceso instantáneo” a la información relacionada con la funcionalidad de varios objetos. Si hace clic en el nombre de un miembro en el marco superior derecho, aparece una descripción de ese miembro en el marco inferior derecho. Observe también que el Examinador de objetos lista en el marco izquierdo a todas las clases de la FCL. El Examinador de objetos puede ser un mecanismo rápido para aprender acerca de una clase o del método de una clase. Recuerde que también puede ver la descripción completa de una clase o de un método en la documentación en línea disponible a través del menú Ayuda en Visual C# Express.

9.17 (Opcional) Caso de estudio de ingeniería de software: inicio de la programación de las clases del sistema ATM En las secciones del Caso de estudio de ingeniería de software de los capítulos 1 y del 3 al 8, introducimos los fundamentos de la orientación a objetos y desarrollamos un diseño orientado a objetos para nuestro sistema ATM. En los capítulos del 4 al 7 presentamos la programación orientada a objetos en C#. En el capítulo 8 vimos un análisis más detallado acerca de los detalles de la programación con clases. Ahora comenzaremos a implementar nuestro diseño orientado a objetos; para ello convertiremos los diagramas de clases en código de C#. En la sección final del Caso de estudio de ingeniería de software (sección 11.9) modificaremos el código para incorporar los conceptos orientados a objetos de herencia y polimorfismo. En el apéndice J presentamos la implementación completa del código en C#.

Visibilidad Ahora vamos a aplicar modificadores de acceso a los miembros de nuestras clases. En el capítulo 4 presentamos los modificadores de acceso public y private. Los modificadores de acceso determinan la visibilidad, o accesibilidad, de los atributos y operaciones de un objeto para otros objetos. Antes de empezar a implementar nuestro diseño, debemos considerar cuáles atributos y métodos de nuestras clases deben ser public y cuáles deben ser private.

Figura 9.22 | Examinador de objetos para la clase Math.

9.17 Inicio de la programación de las clases del sistema ATM

299

En el capítulo 4 observamos que por lo general los atributos deben ser private y que los métodos invocados por los clientes de una clase deben ser public. No obstante, los métodos que se llaman sólo por otros métodos de la clase como “funciones utilitarias” deben ser private. UML emplea marcadores de visibilidad para modelar la visibilidad de los atributos y las operaciones. La visibilidad pública se indica mediante la colocación de un signo más (+) antes de una operación o atributo; un signo menos (-) indica una visibilidad privada. La figura 9.23 muestra nuestro diagrama de clases actualizado, en el cual se incluyen los marcadores de visibilidad. [Nota: no incluimos parámetros de operación en la figura 9.23. Esto es perfectamente normal. Agregar los marcadores de visibilidad no afecta a los parámetros que ya están modelados en los diagramas de clases de las figuras 7.20 a 7.24.]

Navegabilidad Antes de empezar a implementar nuestro diseño en C#, presentaremos una notación adicional de UML. El diagrama de clases de la figura 9.24 refina aún más las relaciones entre las clases del sistema ATM, al agregar flechas de navegabilidad a las líneas de asociación. Las flechas de navegabilidad (representadas como flechas con puntas delgadas en el diagrama de clases) indican en qué dirección puede recorrerse una asociación, y se basan en las colaboraciones modeladas en los diagramas de comunicación y de secuencia (vea la sección 8.14). Al implementar un sistema diseñado mediante el uso de UML, los programadores utilizan flechas de navegabilidad para ayudar a determinar cuáles objetos necesitan referencias a otros objetos. Por ejemplo, la flecha de navegabilidad que apunta de la clase ATM a la clase BaseDatosBanco indica que podemos navegar de una a la otra, con lo cual se permite a la clase ATM invocar a las operaciones de BaseDatosBanco. No obstante, como la figura 9.24 no

!4¶ jhjVg^d6jiZci^XVYd/Wdda2[Vahd

3OLICITUD3ALDO ¶ cjbZgd8jZciV/^ci :_ZXjiVg 2ETIRO

#UENTA ¶ cjbZgd8jZciV/^ci ¶ c^e/^ci ®egde^ZYVY¯HVaYd9^hedc^WaZ/ YZX^bVa pgZVYDcanr ®egde^ZYVY¯HVaYdIdiVa/ YZX^bVa pgZVYDcanr KVa^YVC>E/Wdda 6WdcVg 8Vg\Vg

¶ cjbZgd8jZciV/^ci ¶ bdcid/YZX^bVa :_ZXjiVg

0ANTALLA BdhigVgBZchV_Z

$EPOSITO 4ECLADO

¶ cjbZgd8jZciV/^ci ¶ bdcid/YZX^bVa :_ZXjiVg "ASE$ATOS"ANCO

DWiZcZg:cigVYV/^ci $ISPENSADOR%FECTIVO ¶ XjZciV7^aaZiZh/^ci2*%%

6jiZci^XVgJhjVg^d/Wdda DWiZcZgHVaYd9^hedc^WaZ/YZX^bVa DWiZcZgHVaYdIdiVa/YZX^bVa 6WdcVg 8Vg\Vg

9^heZchVg:[ZXi^kd =VnHj[^X^ZciZ:[ZXi^kd9^hedc^WaZ/Wdda 2ANURA$EPOSITO HZGZX^W^dHdWgZ9Zedh^id/Wdda

Figura 9.23 | Diagrama de clases con marcadores de visibilidad.

300

Capítulo 9 Clases y objetos: un análisis más detallado

& 4ECLADO

&

&

$ISPENSADOR%FECTIVO

2ANURA$EPOSITO

&

0ANTALLA

&

&

& &

&

&

&

!4-

%##& :_ZXjiV &

%##&

%##&

%##&

2ETIRO

&

%##&

6jiZci^XVVajhjVg^dXdcigV & & "ASE$ATOS"ANCO

6XXZYZV$bdY^[^XVjchVaYdYZXjZciV

6XXZYZV$bdY^[^XVjchVaYd YZXjZciVVigVk‚hYZ

& %##

#UENTA

Figura 9.24 | Diagrama de clases con flechas de navegabilidad. contiene una flecha de navegabilidad que apunte de la clase BaseDatosBanco a la clase ATM, la clase BaseDatosno puede acceder a las operaciones de la clase ATM. Observe que las asociaciones en un diagrama de clases que tiene flechas de navegabilidad en ambos extremos, o que no tiene ninguna flecha de navegabilidad, indica una navegabilidad bidireccional: la navegación puede proceder en cualquier dirección a lo largo de la asociación. Al igual que el diagrama de clases de la figura 4.24, el de la figura 9.24 omite las clases SolicitudSaldo y Deposito para simplificarlo. La navegabilidad de las asociaciones en las que participan estas dos clases se asemeja mucho a la navegabilidad de las asociaciones de la clase Retiro. En la sección 4.11 vimos que SolicitudSaldo tiene una asociación con la clase Pantalla. Podemos navegar de la clase SolicitudSaldo a la clase Pantalla a lo largo de esta asociación, pero no podemos navegar de la clase Pantalla a la clase SolicitudSaldo. Por ende, si modeláramos la clase SolicitudSaldo en la figura 9.24, colocaríamos una flecha de navegabilidad en el extremo de la clase Pantalla de esta asociación. Recuerde también que la clase Deposito se asocia con las clases Pantalla, Teclado y RanuraDeposito. Podemos navegar de la clase Deposito a cada una de estas clases, pero no al revés. Por lo tanto, podríamos colocar flechas de navegabilidad en los extremos de las clases Pantalla, Teclado y RanuraDeposito de estas asociaciones. [Nota: modelaremos estas clases y asociaciones adicionales en nuestro diagrama de clases final en la sección 11.9, una vez que hayamos simplificado la estructura de nuestro sistema al incorporar el concepto orientado a objetos de la herencia.] Banco

Implementación del sistema ATM a partir de su diseño de UML Ahora estamos listos para empezar a implementar el sistema ATM. Primero convertiremos las clases de los diagramas de las figuras 9.23 y 9.24 en código de C#. Este código representará el “esqueleto” del sistema. En el capítulo 11 modificaremos el código para incorporar el concepto orientado a objetos de la herencia. En el apéndice J, Código del caso de estudio del ATM, presentaremos el código de C# completo y funcional que implementa nuestro diseño orientado a objetos. Como ejemplo, empezaremos a desarrollar el código para la clase Retiro a partir de nuestro diseño de la clase Retiro en la figura 9.23. Utilizamos esta figura para determinar los atributos y operaciones de la clase. Usamos el modelo de UML en la figura 9.24 para determinar las asociaciones entre las clases. Seguiremos estos cuatro lineamientos para cada clase:

9.17 Inicio de la programación de las clases del sistema ATM

301

1. Use el nombre que se localiza en el primer compartimiento de una clase en un diagrama de clases, para declarar la clase como public con un constructor sin parámetros vacío; incluimos este constructor tan sólo como un receptáculo para recordarnos que la mayoría de las clases necesitará uno o más constructores. En el apéndice J, en donde completamos una versión funcional de esta clase, agregaremos todos los argumentos y el código necesarios al cuerpo del constructor. Al principio, la clase Retiro produce el código de la figura 9.25. [Nota: si encontramos que las variables de instancia de la clase sólo requieren la inicialización predeterminada, eliminaremos el constructor sin parámetros vacío, ya que es innecesario.] 2. Use los atributos que se localizan en el segundo compartimiento de la clase para declarar las variables de instancia. Los atributos private numeroCuenta y monto de la clase Retiro producen el código de la figura 9.26. 3. Use las asociaciones descritas en el diagrama de clases para declarar las referencias a otros objetos. De acuerdo con la figura 9.24, Retiro puede acceder a un objeto de la clase Pantalla, a un objeto de la clase Teclado, a un objeto de la clase DispensadorEfectivo y a un objeto de la clase BaseDatosBanco. La clase Retiro debe mantener referencias a estos objetos y enviarles mensajes, por lo que las líneas 10-13 de la figura 9.27 declaran las referencias apropiadas como variables de instancia private. En la implementación de la clase Retiro en el apéndice J, un constructor inicializa estas variables de instancia con referencias a los objetos actuales. 4. Use las operaciones que se localizan en el tercer compartimiento de la figura 9.23 para declarar las armazones de los métodos. Si todavía no hemos especificado un tipo de valor de retorno para una operación, declaramos el método con el tipo de retorno void. Consulte los diagramas de clases de las figuras 7.20 a 7.24 para declarar cualquier parámetro necesario. Al agregar la operación public Ejecutar (que tiene una lista de parámetros vacía) en la clase Retiro se produce el código en las líneas 23-26 de la figura 9.28. [Nota: codificaremos los cuerpos de los métodos cuando implementemos el sistema ATM completo en el apéndice J.]

1 2 3 4 5 6 7 8 9 10

// Fig. 9.25: Retiro.cs // La clase Retiro representa una transacción de retiro del ATM public class Retiro { // constructor sin parámetros public Retiro() { // código del cuerpo del constructor } // fin del constructor } // fin de la clase Retiro

Figura 9.25 | Código de C# inicial para la clase Retiro, con base en las figuras 9.23 y 9.24. 1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 9.26: Retiro.cs // La clase Retiro representa una transacción de retiro del ATM public class Retiro { // atributos private int numeroCuenta; // cuenta de la que se van a retirar los fondos private decimal monto; // monto que se va a retirar de la cuenta // constructor sin parámetros public Retiro() { // código del cuerpo del constructor } // fin del constructor } // fin de la clase Retiro

Figura 9.26 | C# incorpora variables private para la clase Retiro, con base en las figuras 9.23 y 9.24.

302

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

Capítulo 9 Clases y objetos: un análisis más detallado

// Fig. 9.27: Retiro.cs // La clase Retiro representa una transacción de retiro del ATM public class Retiro { // atributos private int numeroCuenta; // cuenta de la que se van a retirar los fondos private decimal monto; // monto a retirar // referencias a los objetos asociados private Pantalla pantalla; // pantalla del ATM private Teclado teclado; // teclado del ATM private DispensadorEfectivo dispensadorEfectivo; // dispensador de efectivo del ATM private BaseDatosBanco baseDatosBanco; // base de datos de información de las cuentas // constructor sin parámetros public Retiro() { // código del cuerpo del constructor } // fin del constructor } // fin de la clase Retiro

Figura 9.27 | Código de C# que incorpora manejadores de referencias private para las asociaciones de la clase Retiro, con base en las figuras 9.23 y 9.24. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

// Fig. 9.28: Retiro.cs // La clase Retiro representa una transacción de retiro del ATM public class Retiro { // atributos private int numeroCuenta; // cuenta de la que se van a retirar los fondos private decimal monto; // monto a retirar // referencias a los objetos asociados private Pantalla pantalla; // pantalla del ATM private Teclado teclado; // teclado del ATM private DispensadorEfectivo dispensadorEfectivo; // dispensador de efectivo del ATM private BaseDatosBanco baseDatosBanco; // base de datos de información de las cuentas // constructor sin parámetros public Retiro() { // código del cuerpo del constructor } // fin del constructor // operaciones // realiza una transacción public void Ejecutar() { // código del cuerpo del método Ejecutar } // fin del método Ejecutar } // fin de la clase Retiro

Figura 9.28 | Código de C# que incorpora el método Ejecutar en la clase Retiro, con base en las figuras 9.23 y 9.24.

Observación de ingeniería de software 9.13 Muchas herramientas de modelado de UML pueden convertir los diseños basados en UML en código de C#, con lo que se agiliza en forma considerable el proceso de implementación. Para obtener más información acerca de estos generadores “automáticos” de código, consulte los recursos Web que se listan al final de la sección 3.10.

9.17 Inicio de la programación de las clases del sistema ATM

303

Esto concluye nuestra discusión acerca de los fundamentos de la generación de archivos de clases a partir de diagramas de UML. En la sección final del Caso de estudio de ingeniería de software (sección 11.9), demostraremos cómo modificar el código en la figura 9.28 para incorporar los conceptos orientados a objetos de la herencia y el polimorfismo, que presentaremos en los capítulos 10 y 11, respectivamente.

Ejercicios de autoevaluación del Caso de estudio de ingeniería de software 9.1 Indique si el siguiente enunciado es verdadero o falso, y si es falso, explique por qué: si un atributo de una clase se marca con un signo menos (-) en un diagrama de clases, el atributo no es directamente accesible fuera de la clase. 9.2

En la figura 9.24, la asociación entre los objetos ATM y Pantalla indica: a) que podemos navegar de la Pantalla al ATM. b) que podemos navegar del ATM a la Pantalla. c) a y b; la asociación es bidireccional. d) Ninguna de las anteriores.

9.3

Escriba código en C# para empezar a implementar el diseño para la clase Cuenta.

Respuestas a los ejercicios de autoevaluación del Caso de estudio de ingeniería de software 9.1

Verdadero. El signo negativo (-) indica una visibilidad privada.

9.2

b.

9.3 El diseño para la clase Cuenta produce el código en la figura 9.29. Observe que incluimos las variables de instancia private llamadas saldoDisponible y saldoTotal para almacenar los datos que manipularán las propiedades SaldoDisponible y SaldoTotal, y los métodos Abonar y Cargar.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

// Fig. 9.29: Cuenta.cs // La clase Cuenta representa una cuenta bancaria. public class Cuenta { private int numeroCuenta; // número de cuenta private int nip; // NIP para la autenticación private decimal saldoDisponible; // monto de retiro disponible private decimal saldoTotal; // fondos disponibles + depósito pendiente // constructor sin parámetros public Cuenta() { // código del cuerpo del constructor } // fin del constructor // valida NIP del usuario public bool ValidarNIP() { // código del cuerpo del método ValidarNIP } // fin del método ValidarNIP // propiedad de sólo lectura que obtiene el saldo disponible public decimal SaldoDisponible { get { // código del cuerpo del descriptor de acceso get de SaldoDisponible } // fin de get } // fin de la propiedad SaldoDisponible

Figura 9.29 | Código de C# para la clase Cuenta, con base en las figuras 9.23 y 9.24. (Parte 1 de 2).

304

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

Capítulo 9 Clases y objetos: un análisis más detallado

// propiedad de sólo lectura que obtiene el saldo total public decimal SaldoTotal { get { // código del cuerpo del descriptor de acceso get de SaldoTotal } // fin de get } // fin de la propiedad SaldoTotal // abona a la cuenta public void Abonar() { // código del cuerpo del método Abonar } // fin del método Abonar // carga a la cuenta public void Cargar() { // código del cuerpo del método Cargar } // fin del método Cargar } // fin de la clase Cuenta

Figura 9.29 | Código de C# para la clase Cuenta, con base en las figuras 9.23 y 9.24. (Parte 2 de 2).

9.18 Conclusión En este capítulo hablamos sobre conceptos adicionales de las clases. El caso de estudio de la clase Tiempo presentó una declaración de clase completa que consiste de datos private, constructores public sobrecargados para flexibilidad en la inicialización, propiedades para manipular los datos y métodos de la clase que devuelven representaciones string de un objeto Tiempo en dos formatos distintos. Aprendió que toda clase puede declarar un método ToString que devuelve una representación string de un objeto de la clase, y que este método puede invocarse en forma implícita cuando se imprime en pantalla un objeto de una clase en la forma de un objeto string. Aprendió que la referencia this se utiliza en forma implícita en los métodos no static de una clase para acceder a las variables de instancia de la clase y a otros métodos no static. Vio usos explícitos de la referencia this para acceder a los miembros de la clase (incluyendo los campos ocultos) y aprendió a utilizar la palabra clave this en un constructor para llamar a otro constructor de la clase. También aprendió a declarar indexadores con la palabra clave this, con lo cual se puede acceder a los datos de un objeto en forma muy parecida a la manera en que se accede a los elementos de un arreglo. Vio que una clase puede tener referencias a los objetos de otras clases como miembros; un concepto conocido como composición. Aprendió acerca de la capacidad de recolección de basura de C# y cómo reclama la memoria de los objetos que ya no se utilizan. Explicamos la motivación para las variables static en una clase, y le demostramos cómo declarar y utilizar variables y métodos static en sus propias clases. También aprendió a declarar e inicializar variables readonly. Le mostramos cómo crear una biblioteca de clases para reutilizar componentes, y cómo utilizar las clases de la biblioteca en una aplicación. Aprendió que las clases que se declaran sin un modificador de acceso reciben un acceso internal de manera predeterminada. Vio que las clases en un ensamblado pueden acceder a los miembros con acceso internal de las otras clases dentro del mismo ensamblado. También le mostramos cómo utilizar las ventanas Vista de clases y Examinador de objetos de Visual Studio para navegar por las clases de la FCL y sus propias aplicaciones, para descubrir información acerca de esas clases. En el siguiente capítulo aprenderá otra tecnología clave de programación orientada a objetos: la herencia. En ese capítulo verá que todas las clases en C# se relacionan en forma directa o indirecta con la clase Object. También empezará a comprender cómo la herencia le permite crear aplicaciones más poderosas en menos tiempo.

10

Programación orientada a objetos: herencia No digas que conoces a otro por completo, sino hasta que hayas dividido una herencia con él.

OBJETIVOS

—Johann Kasper Lavater

En este capítulo aprenderá lo siguiente:

Este método es definir como el número de una clase, la clase de todas las clases similares a la clase dada.

Q

Cómo la herencia promueve la reutilización de código.

Q

Los conceptos de las clases bases y las clases derivadas.

Q

Crear una clase derivada que herede los atributos y comportamientos de una clase base.

—Bertrand Russell

Q

Utilizar el modificador de acceso protected para otorgar a los métodos de la clase derivada el acceso a los miembros de la clase base.

Es bueno heredar una biblioteca, pero es mejor coleccionar una.

Q

Cómo acceder a los miembros de la clase base con base.

—Augustine Birrell

Q

Cómo se utilizan los constructores en las jerarquías de herencias.

Q

Los métodos de la clase object, la clase base directa o indirecta de todas las clases.

Preserva la autoridad base de los libros de otros. —William Shakespeare

Pla n g e ne r a l

306

Capítulo 10 Programación orientada a objetos: herencia

10.1 10.2 10.3 10.4

10.5 10.6 10.7 10.8

Introducción Clases base y clases derivadas Miembros protected Relación entre las clases base y las clases derivadas 10.4.1 Creación y uso de una clase EmpleadoPorComision 10.4.2 Creación de una clase EmpleadoBaseMasComision sin usar la herencia 10.4.3 Creación de una jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision 10.4.4 La jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision mediante el uso de variables de instancia protected 10.4.5 La jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision mediante el uso de variables de instancia private Los constructores en las clases derivadas Ingeniería de software mediante la herencia La clase object Conclusión

10.1 Introducción En este capítulo continuaremos nuestra discusión acerca de la programación orientada a objetos (POO), analizaremos una de sus características principales: la herencia, que es una forma de reutilización de software, en la cual para crear una nueva clase se absorben los miembros de una clase existente y se mejoran con capacidades nuevas o modificadas. Con la herencia, los programadores ahorran tiempo durante el desarrollo de aplicaciones, al reutilizar software de alta calidad, probado y depurado. Esto también incrementa la probabilidad de que el sistema se implemente en forma efectiva. Al crear una clase, en lugar de declarar miembros completamente nuevos, puede hacer que la nueva clase herede los miembros de una ya existente. La clase existente se llama clase base, y la clase nueva es la clase derivada. Esta última puede convertirse en la clase base para clases derivadas en el futuro. Por lo general, una clase derivada agrega sus propios campos y métodos. Por lo tanto, es más específica que su clase base y representa un grupo más especializado de objetos. Es común que la clase derivada exhiba los comportamientos de su clase base y comportamientos adicionales, específicos para la clase derivada. Cuando la clase base hereda, en forma explícita, a partir de una clase derivada se conoce como clase base directa. Por otro lado, una clase base indirecta es la que se encuentra por encima de la clase base directa en la jerarquía de clases, que define las relaciones de herencia entre las clases. La jerarquía de clases empieza con la clase object (que es el alias en C# para System.Object en la Biblioteca de clases del .NET Framework), de la cual toda clase se extiende (o “hereda de”), ya sea en forma directa o indirecta. La sección 10.7 lista los métodos de la clase object, que todas las demás clases heredan. En el caso de la herencia simple, una clase se deriva de una clase base directa. A diferencia de C++, C# no soporta la herencia múltiple (que ocurre cuando una clase se deriva de más de una clase base directa). En el capítulo 11, Polimorfismo, interfaces y sobrecarga de operadores, le explicaremos cómo puede utilizar las interfaces para que se dé cuenta de los muchos de los beneficios de la herencia múltiple, y al mismo tiempo evite los problemas asociados. La experiencia en la creación de sistemas de software indica qué cantidades considerables de código tratan con casos especiales muy relacionados. Cuando los programadores se preocupan por los casos especiales, los detalles pueden oscurecer el panorama en general. Con la programación orientada a objetos, los programadores pueden (cuando sea apropiado) enfocarse en las características comunes entre los objetos en el sistema, en vez de los casos especiales. Hay una diferencia entre la relación “es un” y la relación “tiene un”. “Es un” representa la herencia. En una relación del tipo “es un”, un objeto de una clase derivada también puede tratarse como un objeto de su clase base. Por ejemplo, un auto es un vehículo, y un camión es un vehículo. En contraste, “tiene un” representa la composición (vea el capítulo 9). En una relación del tipo “tiene un”, un objeto contiene como miembros refe-

10.2 Clases base y clases derivadas

307

rencias a otros objetos. Por ejemplo, un auto tiene un volante, y un objeto auto tiene una referencia a un objeto volante. Las nuevas clases pueden heredar de las clases en bibliotecas de clases. Las organizaciones desarrollan sus propias bibliotecas de clases y pueden aprovechar las otras bibliotecas disponibles en todo el mundo. Algún día, es probable que la mayoría del software se construya a partir de componentes reutilizables estándar, así como se construyen hoy en día los automóviles y la mayoría del hardware de computadora. Esto facilitará el desarrollo de software más poderoso, abundante y económico.

10.2 Clases base y clases derivadas A menudo, un objeto de una clase es un objeto de otra clase también. Por ejemplo, en geometría, un rectángulo es un cuadrilátero (así como los cuadrados, los paralelogramos y los trapezoides). Por ende, se puede decir que la clase Rectangulo hereda de la clase Cuadrilatero. En este contexto, la Cuadrilatero es una clase base y Rectangulo es una clase derivada. Un rectángulo es un tipo específico de cuadrilátero, pero es incorrecto decir que todo cuadrilátero es un rectángulo; el cuadrilátero podría ser un paralelogramo o cualquier otra figura. La figura 10.1 lista varios ejemplos simples de las clases base y las clases derivadas; observe que las primeras tienden a ser “más generales”; y las segundas, “más específicas”. Como todo objeto de una clase derivada es un objeto de su clase base, y una clase base puede tener muchas clases derivadas, el conjunto de objetos representados por una clase base es por lo general mayor que el conjunto de objetos representado por cualquiera de sus clases derivadas. Por ejemplo, la clase base Vehiculo representa a todos los vehículos: autos, camiones, barcos, bicicletas, etc. En contraste, la clase derivada Auto representa un subconjunto más pequeño y específico de vehículos. Las relaciones de herencia forman estructuras jerárquicas similares a los árboles (figuras 10.2 y 10.3). Una clase base existe en una relación jerárquica con sus clases derivadas. Cuando varias clases participan en relaciones de herencia, se “afilian” con otras. Una clase se convierte ya sea en una clase base, suministrando miembros a las otras clases, o en una clase derivada, heredando sus miembros de otra clase. En algunos casos, una clase es tanto base como derivada. Desarrollaremos una jerarquía de clases simple, también llamada jerarquía de herencia (figura 10.2). El diagrama de clases de UML de la figura 10.2 muestra una comunidad universitaria que tiene muchos tipos de miembros, incluyendo empleados, estudiantes y ex alumnos. Los empleados pueden ser docentes o administrativos. Los miembros docentes pueden ser administradores (como los decanos y los directores de departamento) o maestros. Observe que la jerarquía podría contener muchas otras clases. Por ejemplo, los estudiantes pueden ser graduados o no graduados. Los estudiantes no graduados pueden ser de primer año, de segundo, de tercero o de cuarto año. Cada flecha con una punta triangular hueca en el diagrama de jerarquía representa una relación “es un”. Al seguir las flechas en esta jerarquía de clases podemos decir, por ejemplo, que “un Empleado es un MiembroDeLaComunidad” y que “un Maestro es un miembro Docente”. MiembroDeLaComunidad es la clase base directa de Empleado, Estudiante y ExAlumno, y es una clase base indirecta de todas las demás clases en el diagrama. Empezando desde la parte inferior del diagrama, usted puede seguir las flechas y aplicar la relación “es un” hasta la clase base superior. Por ejemplo, un Administrador es un miembro Docente, es un Empleado y es un MiembroDeLaComunidad.

Clase base

Clases derivadas

Estudiante

EstudianteGraduado, EstudianteNoGraduado

Figura

Circulo, Triangulo, Rectangulo

Prestamo

PrestamoAuto, PrestamoMejoraHogar, PrestamoHipotecario

Empleado

Docente, Administrativo, TrabajadorPorHoras, TrabajadorPorComision

CuentaBanco

CuentaCheques, CuentaAhorros

Figura 10.1 | Ejemplos de herencia.

308

Capítulo 10 Programación orientada a objetos: herencia

MiembroDeLaComunidad

Empleado

Docente

Administrador

Estudiante

ExAlumno

Administrativo

Maestro

Figura 10.2 | Diagrama de clases de UML que muestra una jerarquía de herencia para cada MiembroDeLaComunidad de una universidad.

Ahora considere la jerarquía de herencia para Figura en la figura 10.3. Esta jerarquía comienza con la clase base Figura, que se extiende mediante las clases derivadas FiguraBidimensional y FiguraTridimensional (un objeto Figura puede ser objeto Bidimensional u objeto Tridimensional). El tercer nivel de esta jerarquía contiene algunos objetos FiguraBidimensional y FiguraTridimensional específicos. Al igual que en la figura 10.2, podemos seguir las flechas desde la parte inferior del diagrama de clases hasta la clase base superior en esta jerarquía de clases, para identificar varias relaciones del tipo es un. Por ejemplo, un Triangulo es una FiguraBidimensional y es una Figura, mientras que una Esfera es una FiguraTridimensional y es una Figura. Observe que esta jerarquía podría contener muchas otras clases. Por ejemplo, las elipses y los trapezoides son objetos de la clase FiguraBidimensional. No todas las relaciones de clases son relaciones de herencia. En el capítulo 9 hablamos sobre la relación “tiene un”, en la cual las clases tienen miembros que son referencias a objetos de otras clases. Dichas relaciones crean clases mediante la composición de clases existentes. Por ejemplo, dadas las clases Empleado, FechaNacimiento y NumeroTelefonico, sería impropio decir que un Empleado es una FechaNacimiento, o que un Empleado es un NumeroTelefonico. No obstante, un Empleado tiene una FechaNacimiento y tiene un NumeroTelefonico. Es posible tratar a los objetos de una clase base y a los de una clase derivada de manera similar; sus características comunes se expresan en los miembros de la clase base. Los objetos de todas las clases que extiendan a una clase base común pueden tratarse como objetos de esa clase base (es decir, dichos objetos tienen una relación del tipo “es un” con la clase base). No obstante, los objetos de la clase base no pueden tratarse como objetos de sus clases derivadas. Por ejemplo, todos los autos son vehículos, pero no todos los vehículos son autos (los otros vehículos podrían ser camiones, aviones o bicicletas, por ejemplo). En este capítulo y en el capítulo 11, Polimorfismo, interfaces y sobrecarga de operadores, consideraremos muchos ejemplos de las relaciones “es un”.

Figura

FiguraBidimensional

Círculo

Cuadrado

FiguraTridimensional

Triángulo

Esfera

Cubo

Tetraedro

Figura 10.3 | Diagrama de clases que muestra una jerarquía de herencia para objetos Figura.

10.4 Relación entre las clases base y las clases derivadas

309

Uno de los problemas con la herencia es que una clase derivada puede heredar métodos que no necesita, o no debe tener. Aun cuando el método de una clase base sea apropiado para una clase derivada, esa clase derivada necesita por lo general una versión personalizada del método. En dichos casos, la clase derivada puede redefinir el método de la clase base con una implementación apropiada, como veremos con frecuencia en los ejemplos de código del capítulo.

10.3 Miembros protected En el capítulo 9 vimos los modificadores de acceso public, private e internal. Los miembros public de una clase son accesibles en cualquier parte en donde la aplicación tenga una referencia a un objeto de esa clase, o a una de sus clases derivadas. Los miembros private de una clase son accesibles sólo dentro de la misma clase. Los miembros private de una clase base son heredados por sus clases derivadas, pero no son directamente accesibles por los métodos y las propiedades de la clase derivada. En esta sección presentaremos el modificador de acceso protected. El uso del acceso protected ofrece un nivel intermedio de acceso entre public y private. Se puede acceder a los miembros protected de una clase base a través de los miembros de esa clase base y de los miembros de sus clases derivadas (los miembros de una clase también pueden declararse como protected internal. Es posible acceder a los miembros protected internal de una clase base a través de los miembros de esa clase base, por los miembros de sus clases derivadas y por cualquier clase dentro del mismo ensamblado). Todos los miembros no private de la clase base retienen su modificador de acceso original cuando se convierten en miembros de la clase derivada (es decir, los miembros public de la clase base se convierten en miembros public de la clase derivada, y los miembros protected de la clase base se convierten en miembros protected de la clase derivada). Los métodos de una clase derivada pueden hacer referencia a los miembros public y protected heredados de la clase base con sólo usar los nombres de los miembros. Cuando un método de la clase derivada redefine a un método de la clase base, se puede acceder a la versión de la clase base del método desde la clase derivada si se antepone al nombre del método de la clase base la palabra clave base y el operador punto (.). En la sección 10.4 hablaremos sobre cómo acceder a los miembros redefinidos de la clase base.

Observación de ingeniería de software 10.1 Los métodos de una clase derivada no pueden acceder en forma directa a los miembros private de la clase base. Una clase derivada puede modificar el estado de los campos private de la clase base sólo a través de los métodos y propiedades no private que se proporcionan en la clase base.

Observación de ingeniería de software 10.2 Declarar campos private en una clase base le ayuda a probar, depurar y modificar sistemas en forma correcta. Si una clase derivada puede acceder a los campos private de su clase base, las clases que hereden de esa clase base podrían acceder también a los campos. Esto propagaría el acceso a los que deberían ser campos private, y se perderían los beneficios del ocultamiento de la información.

10.4 Relación entre las clases base y las clases derivadas En esta sección usaremos una jerarquía de herencia que contiene tipos de empleados en la aplicación de nómina de una compañía, para hablar sobre la relación entre una clase base y sus clases derivadas. En esta compañía, a los empleados por comisión (que se representan como objetos de una clase base) se les paga un porcentaje de sus ventas, mientras que los empleados por comisión con salario base (que se representan como objetos de una clase derivada) reciben un salario base, más un porcentaje de sus ventas. Dividiremos nuestra discusión sobre la relación entre los empleados por comisión y los empleados por comisión con salario base en cinco ejemplos. El primero declara la clase EmpleadoPorComision, que hereda directamente de la clase object y declara como variables de instancia private el primer nombre, el apellido paterno, el número de seguro social, la tarifa de comisión y el monto de ventas en bruto (es decir, total). El segundo ejemplo declara la clase EmpleadoBaseMasComision, que también hereda directamente de la clase object y declara como variables de instancia private el primer nombre, el apellido paterno, el número de seguro social, la tarifa de comisión, el monto de ventas en bruto y el salario base. Para crear esta última clase,

310

Capítulo 10 Programación orientada a objetos: herencia

escribiremos cada línea de código que ésta requiera; pronto veremos que es mucho más eficiente crear esta clase haciendo que herede de la clase EmpleadoPorComision. El tercer ejemplo declara una clase EmpleadoBaseMasComision2 separada, la cual extiende a la clase EmpleadoPorComision (es decir, un EmpleadoBasePorComision2 es un EmpleadoPorComision que también tiene un salario base). Demostraremos que los métodos de la clase base deben declararse explícitamente como virtual, si van a ser redefinidos por los métodos en las clases derivadas. La clase EmpleadoBaseMasComision2 trata de acceder a los miembros private de la clase EmpleadoPorComision; esto produce errores de compilación, ya que una clase derivada no puede acceder a las variables de instancia private de su clase base. El cuarto ejemplo muestra que si las variables de instancia de la clase base EmpleadoPorComision se declaran como protected, una clase EmpleadoBaseMasComision3 que extiende a la clase EmpleadoPorComision2 puede acceder a los datos de manera directa. Para este fin, declaramos la clase EmpleadoPorComision2 con variables de instancia protected. Todas las clases EmpleadoBasePorComision contienen una funcionalidad idéntica, pero le mostraremos que la clase EmpleadoBaseMasComision3 es más fácil de crear y manipular. Una vez que hablemos sobre la conveniencia de utilizar variables de instancia protected, crearemos el quinto ejemplo, que establece las variables de instancia de EmpleadoPorComision de vuelta a private en la clase EmpleadoPorComision3, para hacer cumplir la buena ingeniería de software. Después le mostraremos cómo una clase EmpleadoBaseMasComision4 separada, que extiende a la clase EmpleadoPorComision3, puede utilizar los métodos públicos de EmpleadoPorComision3 para manipular las variables de instancia private de EmpleadoPorComision3.

10.4.1 Creación y uso de una clase EmpleadoPorComision Comenzaremos por declarar la clase EmpleadoPorComision (figura 10.4). La línea 3 empieza la declaración de la clase. El signo de dos puntos (:) seguido por el nombre de clase object al final del encabezado de la declaración indica que la clase EmpleadoPorComision extiende a la clase object (System.Object en la FCL). Los programadores de C# utilizan la herencia para crear clases a partir de clases existentes. De hecho, todas las clases en C# (excepto object) extienden a una clase existente. Como la clase EmpleadoPorComision extiende la clase object, la clase EmpleadoPorComision hereda los métodos de la clase object; la clase object no tiene campos. Cada clase en C# hereda en forma directa o indirecta los métodos de object. Si una clase no especifica que hereda de otra clase, la nueva clase hereda de object en forma implícita. Por esta razón, es común que los programadores no incluyan “: object” en su código; en nuestro ejemplo lo haremos por fines demostrativos.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

// Fig. 10.4: EmpleadoPorComision.cs // La clase EmpleadoPorComision representa a un empleado por comisión. public class EmpleadoPorComision : object { private string primerNombre; private string apellidoPaterno; private string numeroSeguroSocial; private decimal ventasBrutas; // ventas semanales totales private decimal tarifaComision; // porcentaje de comisión // constructor con cinco parámetros public EmpleadoPorComision( string nombre, string apellido, string nss, decimal ventas, decimal tarifa ) { // la llamada implícita al constructor del objeto ocurre aquí primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; VentasBrutas = ventas; // valida las ventas brutas mediante una propiedad TarifaComision = tarifa; // valida la tarifa de comisión mediante una propiedad } // fin del constructor de EmpleadoPorComision con cinco parámetros

Figura 10.4 | La clase EmpleadoPorComision representa a un empleado por comisión. (Parte 1 de 3).

10.4 Relación entre las clases base y las clases derivadas

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78

311

// propiedad de sólo lectura que obtiene el primer nombre del empleado por comisión public string PrimerNombre { get { return primerNombre; } // fin de get } // fin de la propiedad PrimerNombre // propiedad de sólo lectura que obtiene el apellido paterno del empleado por comisión public string ApellidoPaterno { get { return apellidoPaterno; } // fin de get } // fin de la propiedad ApellidoPaterno // propiedad de sólo lectura que obtiene el // número de seguro social del empleado por comisión public string NumeroSeguroSocial { get { return numeroSeguroSocial; } // fin de get } // fin de la propiedad NumeroSeguroSocial // propiedad que obtiene y establece las ventas brutas del empleado por comisión public decimal VentasBrutas { get { return ventasBrutas; } // fin de get set { ventasBrutas = ( value < 0 ) ? 0 : value; } // fin de set } // fin de la propiedad VentasBrutas // propiedad que obtiene y establece la tarifa de comisión del empleado por comisión public decimal TarifaComision { get { return tarifaComision; } // fin de get set { tarifaComision = ( value > 0 && value < 1 ) ? value : 0; } // fin de set } // fin de la propiedad TarifaComision // calcula el salario del empleado por comisión public decimal Ingresos()

Figura 10.4 | La clase EmpleadoPorComision representa a un empleado por comisión. (Parte 2 de 3).

312

79 80 81 82 83 84 85 86 87 88 89 90 91 92

Capítulo 10 Programación orientada a objetos: herencia

{ return tarifaComision * ventasBrutas; } // fin del método Ingresos // devuelve representación string del objeto EmpleadoPorComision public override string ToString() { return string.Format( "{0}: {1} {2}\n{3}: {4}\n{5}: {6:C}\n{7}: {8:F2}", "empleado por comisión", PrimerNombre, ApellidoPaterno, "número de seguro social", NumeroSeguroSocial, "ventas brutas", VentasBrutas, "tarifa de comisión", TarifaComision ); } // fin del método ToString } // fin de la clase EmpleadoPorComision

Figura 10.4 | La clase EmpleadoPorComision representa a un empleado por comisión. (Parte 3 de 3).

Observación de ingeniería de software 10.3 El compilador establece la clase base de una clase a object cuando la declaración de la clase no extiende explícitamente una clase base.

Los servicios public de la clase EmpleadoPorComision incluyen un constructor (líneas 12-21) y los métodos Ingresos (líneas 78-81) y ToString (líneas 84-91). Las líneas 24-75 declaran propiedades public para manipular las variables de instancia primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision de la clase (las cuales se declaran en las líneas 5-9). La clase EmpleadoPorComision declara cada una de sus variables de instancia como private, por lo que los objetos de otras clases no pueden acceder directamente a estas variables. Declarar las variables de instancia como private y proporcionar propiedades public para manipular y validar estas variables ayuda a cumplir con la buena ingeniería de software. Por ejemplo, los descriptores de acceso set de las propiedades VentasBrutas y TarifaComision validan sus argumentos antes de asignar los valores a las variables de instancia ventasBrutas y tarifaComision, en forma respectiva. Los constructores no se heredan, por lo que la clase EmpleadoPorComision no hereda el constructor de la clase object. Sin embargo, el constructor de la clase EmpleadoPorComision llama al constructor de la clase object de manera implícita. De hecho, la primer tarea del constructor de cualquier clase derivada es llamar al constructor de su clase base directa, ya sea en forma explícita o implícita (si no se especifica una llamada al constructor), para asegurar que las variables de instancia heredadas de la clase base se inicialicen en forma apropiada. En la sección 10.4.3 hablaremos sobre la sintaxis para llamar al constructor de una clase base en forma explícita. Si el código no incluye una llamada explícita al constructor de la clase base, el compilador genera una llamada implícita al constructor predeterminado o sin parámetros de la clase base. El comentario en la línea 15 de la figura 10.4 indica en dónde se hace la llamada implícita al constructor predeterminado de la clase base object (usted no necesita escribir el código para esta llamada). El constructor predeterminado (vacío) de la clase object no hace nada. Observe que aun si una clase no tiene constructores, el constructor predeterminado que declara el compilador de manera implícita para la clase llamará al constructor predeterminado o sin parámetros de la clase base. La clase object es la única clase que no tiene una clase base. Una vez que se realiza la llamada implícita al constructor de object, las líneas 16-20 del constructor de EmpleadoPorComision asignan valores a las variables de instancia de la clase. Observe que no validamos los valores de los argumentos nombre, apellido y nss antes de asignarlos a las variables de instancia correspondientes. Sin duda podríamos validar el nombre y el apellido; tal vez asegurarnos de que tengan una longitud razonable. De manera similar, podría validarse un número de seguro social, para garantizar que contenga nueve dígitos, con o sin guiones cortos (por ejemplo, 123-45-6789 o 123456789). El método Ingresos (líneas 78-81) calcula los ingresos de un EmpleadoPorComision. La línea 80 multiplica la tarifaComision por las ventasBrutas y devuelve el resultado. El método ToString (líneas 84-91) es especial: es uno de los métodos que hereda cualquier clase de manera directa o indirecta de la clase object, que es la raíz de la jerarquía de clases de C#. La sección 10.7 muestra un

10.4 Relación entre las clases base y las clases derivadas

313

resumen de los métodos de la clase object. El método ToString devuelve un string que representa a un objeto. Una aplicación llama a este método de manera implícita cada vez que un objeto debe convertirse en una representación string, como en el método Write de Console o en el método string Format, en el que se utiliza un elemento de formato. El método ToString de la clase object devuelve un string que incluye el nombre de la clase del objeto. En esencia, es un receptáculo que puede redefinirse (y por lo general así es) por una clase derivada para especificar una representación string apropiada de los datos en un objeto de la clase derivada. El método ToString de la clase EmpleadoPorComision redefine al método ToString de la clase object. Al invocarse, el método ToString de EmpleadoPorComision usa el método string Format para devolver un string que contiene información acerca del EmpleadoPorComision. Utilizamos el especificador de formato C para dar formato a ventasBrutas como cantidad monetaria, y el especificador de formato F2 para dar formato a tarifaComision con dos dígitos de precisión a la derecha del punto decimal. Para redefinir a un método de una clase base, una clase derivada debe declarar al método con la palabra clave override y con la misma firma (nombre del método, número de parámetros y tipos de los parámetros) y tipo de valor de retorno que el método de la clase base; el método ToString de object no recibe parámetros y devuelve el tipo string, por lo que EmpleadoPorComision declara a ToString sin parámetros y con el tipo de valor de retorno string.

Error común de programación 10.1 Es un error de compilación redefinir a un método con un modificador de acceso distinto. Tenga en cuenta que si se redefine un método con un modificador de acceso más restrictivo se romperá la relación “es un”. Si un método public pudiera redefinirse como protected o private, los objetos de la clase derivada no podrían responder a las mismas llamadas a métodos que los objetos de la clase base. Una vez que se declara un método en una clase base, el método debe tener el mismo modificador de acceso para todas las clases derivadas en forma directa o indirecta de esa clase.

La figura 10.5 prueba la clase EmpleadoPorComision. Las líneas 10-11 crean una instancia de un objeEmpleadoPorComision e invocan a su constructor (líneas 12-21 de la figura 10.4) para inicializarlo con "Sue" como el primer nombre, "Jones" como el apellido, "222-22-2222" como el número de seguro social, 10000.00M como el monto de ventas brutas y .06M como la tarifa de comisión. Adjuntamos el sufijo M al monto de ventas brutas y a la tarifa de comisión para indicar que deben interpretarse como literales decimal, y no como double. Las líneas 16-25 utilizan las propiedades de EmpleadoPorComision para extraer los valores de las variato

bles de instancia del objeto e imprimirlas en pantalla. Las líneas 26-27 imprimen en pantalla el monto calculado por el método Ingresos. Las líneas 29-30 invocan a los descriptores de acceso set de las propiedades VentasBrutas y TarifaComision del objeto para modificar los valores de las variables de instancia ventasBrutas y tarifaComision. Las líneas 32-33 imprimen en pantalla la representación string del EmpleadoPorComision actualizado. Observe que cuando se imprime un objeto en pantalla utilizando un elemento de formato, se invoca de manera implícita al método ToString del objeto para obtener su representación string. La línea 34 imprime en pantalla los ingresos otra vez.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 10.5: PruebaEmpleadoPorComision.cs // Prueba de la clase EmpleadoPorComision. using System; public class PruebaEmpleadoPorComision { public static void Main( string[] args ) { // crea instancia de objeto EmpleadoPorComision EmpleadoPorComision empleado = new EmpleadoPorComision( "Sue", "Jones", "222-22-2222", 10000.00M, .06M ); // muestra datos del empleado por comisión Console.WriteLine(

Figura 10.5 | Prueba de la clase EmpleadoPorComision. (Parte 1 de 2).

314

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

Capítulo 10 Programación orientada a objetos: herencia

“Información del empleado obtenida por las propiedades y los métodos: \n” ); Console.WriteLine( "{0} {1}", "El primer nombre es", empleado.PrimerNombre ); Console.WriteLine( "{0} {1}", "El apellido es", empleado.ApellidoPaterno ); Console.WriteLine( "{0} {1}", "El número de seguro social es", empleado.NumeroSeguroSocial ); Console.WriteLine( "{0} {1:C}", "Las ventas brutas son", empleado.VentasBrutas ); Console.WriteLine( "{0} {1:F2}", "La tarifa de comisión es", empleado.TarifaComision ); Console.WriteLine( "{0} {1:C}", "Los ingresos son", empleado.Ingresos() ); empleado.VentasBrutas = 5000.00M; // establece las ventas brutas empleado.TarifaComision = .1M; // establece la tarifa de comisión Console.WriteLine( "\n{0}:\n\n{1}", "Se actualizó la información del empleado obtenida por ToString", empleado); Console.WriteLine( "ingresos: {0:C}", empleado.Ingresos() ); } // fin de Main } // fin de la clase PruebaEmpleadoPorComision

Información del empleado obtenida por las propiedades y los métodos: El primer nombre es Sue El apellido es Jones El número de seguro social es 222-22-2222 Las ventas brutas son $10,000.00 La tarifa de comisión es 0.06 Los ingresos son: $600.00 Se actualizó la información del empleado obtenida por ToString: empleado por comisión: Sue Jones número de seguro social: 222-22-2222 ventas brutas: $5,000.00 tarifa de comisión: 0.10 ingresos: $500.00

Figura 10.5 | Prueba de la clase EmpleadoPorComision. (Parte 2 de 2).

10.4.2 Creación de una clase EmpleadoBaseMasComision sin usar la herencia Ahora hablaremos sobre la segunda parte de nuestra introducción a la herencia, mediante la declaración y prueba de la clase (completamente nueva e independiente) EmpleadoBaseMasComision (figura 10.6), la cual contiene los siguientes datos: primer nombre, apellido paterno, número de seguro social, monto de ventas brutas, tarifa de comisión y salario base. Los servicios public de la clase EmpleadoBaseMasComision incluyen un constructor (líneas 14-24), y los métodos Ingresos (líneas 99-102) y ToString (líneas 105-113). Las líneas 28-96 declaran propiedades public para las variables de instancia private primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas, tarifaComision y salarioBase para la clase (las cuales se declaran en las líneas 6-11). Estas variables, propiedades y métodos encapsulan todas las características necesarias de un empleado por comisión con sueldo base. Observe la similitud entre esta clase y la clase EmpleadoPorComision (figura 10.4); en este ejemplo, no explotamos todavía esa similitud. Observe que la clase EmpleadoBaseMasComision no especifica que extiende a object con la sintaxis “: object” en la línea 4, por lo que se deduce que la clase extiende a object en forma implícita. Observe además que, al igual que el constructor de la clase EmpleadoPorComision (línea 12-21 de la figura 10.4), el constructor de la

10.4 Relación entre las clases base y las clases derivadas

315

clase EmpleadoBaseMasComision invoca al constructor predeterminado de la clase object en forma implícita, como se indica en el comentario de la línea 17 de la figura 10.6. El método Ingresos de la clase EmpleadoBaseMasComision (líneas 99-102) calcula los ingresos de un empleado por comisión con salario base. La línea 101 devuelve el resultado de la suma del salario base del empleado con el producto de la tarifa de comisión y las ventas brutas del empleado. La clase EmpleadoBaseMasComision redefine el método ToString de object para que devuelva un string que contiene la información del EmpleadoBaseMasComision (líneas 105-113). Una vez más, utilizamos el especificador de formato C para dar formato a las ventas brutas y al salario base como cantidades monetarias, y el especificador de formato F2 para dar formato a la tarifa de comisión con dos dígitos de precisión a la derecha del punto decimal (línea 108).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

// Fig. 10.6: EmpleadoBaseMasComision.cs // La clase EmpleadoBaseMasComision representa a un empleado que recibe // un salario base, además de una comisión. public class EmpleadoBaseMasComision { private string primerNombre; private string apellidoPaterno; private string numeroSeguroSocial; private decimal ventasBrutas; // ventas semanales totales private decimal tarifaComision; // porcentaje de comisión private decimal salarioBase; // salario base por semana // constructor con seis parámetros public EmpleadoBaseMasComision( string nombre, string apellido, string nss, decimal ventas, decimal tarifa, decimal salario ) { // la llamada implícita al constructor del objeto se realiza aquí primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; VentasBrutas = ventas; // valida las ventas brutas a través de una propiedad TarifaComision = tarifa; // valida la tarifa de comisión a través de una propiedad SalarioBase = salario; // valida el salario base a través de una propiedad } // fin del constructor de EmpleadoBaseMasComision con seis parámetros // propiedad de sólo lectura que obtiene // el primer nombre del empleado por comisión con salario base public string PrimerNombre { get { return primerNombre; } // fin de get } // fin de la propiedad PrimerNombre // propiedad de sólo lectura que obtiene // el apellido paterno del empleado por comisión con salario base public string ApellidoPaterno { get { return apellidoPaterno;

Figura 10.6 | La clase EmpleadoBaseMasComision representa a un empleado que recibe un salario base, además de una comisión. (Parte 1 de 3).

316

43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

Capítulo 10 Programación orientada a objetos: herencia

} // fin de get } // fin de la propiedad ApellidoPaterno // propiedad de sólo lectura que obtiene // el número de seguro social del empleado por comisión con salario base public string NumeroSeguroSocial { get { return numeroSeguroSocial; } // fin de get } // fin de la propiedad NumeroSeguroSocial // propiedad que obtiene y establece las // ventas brutas del empleado por comisión con salario base public decimal VentasBrutas { get { return ventasBrutas; } // fin de get set { ventasBrutas = ( value < 0 ) ? 0 : value; } // fin de set } // fin de la propiedad VentasBrutas // propiedad que obtiene y establece la // tarifa por comisión del empleado por comisión con salario base public decimal TarifaComision { get { return tarifaComision; } // fin de get set { tarifaComision = ( value > 0 && value < 1 ) ? value : 0; } // fin de set } // fin de la propiedad TarifaComision // propiedad que obtiene y establece // el salario base del empleado por comisión con salario base public decimal SalarioBase { get { return salarioBase; } // fin de get set { salarioBase = ( value < 0 ) ? 0 : value; } // fin de set } // fin de la propiedad SalarioBase // calcula los ingresos public decimal Ingresos()

Figura 10.6 | La clase EmpleadoBaseMasComision representa a un empleado que recibe un salario base, además de una comisión. (Parte 2 de 3).

10.4 Relación entre las clases base y las clases derivadas

317

100 { 101 return SalarioBase + ( TarifaComision * VentasBrutas ); 102 } // fin del método Ingresos 103 104 // devuelve representación string de EmpleadoBaseMasComision 105 public override string ToString() 106 { 107 return string.Format( 108 "{0}: {1} {2}\n{3}: {4}\n{5}: {6:C}\n{7}: {8:F2}\n{9}: {10:C}", 109 "empleado por comisión con salario base", PrimerNombre, ApellidoPaterno, 110 "número de seguro social", NumeroSeguroSocial, 111 "ventas brutas", VentasBrutas, "tarifa de comisión", TarifaComision, 112 "salario base", SalarioBase ); 113 } // fin del método ToString 114 } // fin de la clase EmpleadoBaseMasComision

Figura 10.6 | La clase EmpleadoBaseMasComision representa a un empleado que recibe un salario base, además de una comisión. (Parte 3 de 3). La figura 10.7 prueba la clase EmpleadoBaseMasComision. Las líneas 10-12 crean una instancia de un objeto EmpleadoBaseMasComision y pasan los parámetros "Bob", "Lewis", "333-33-3333", 5000.00M, .04M y 300.00M al constructor como el primer nombre, apellido paterno, número de seguro social, ventas brutas, tarifa de comisión y salario base, respectivamente. Las líneas 17-30 utilizan las propiedades y métodos de EmpleadoBaseMasComision para extraer los valores de las variables de instancia del objeto y calcular los ingresos para imprimirlos en pantalla. La línea 32 invoca a la propiedad SalarioBase del objeto para modificar el salario base. El descriptor de acceso set de la propiedad SalarioBase (figura 10.6, líneas 92-95) asegura que no se le asigne a la variable SalarioBase un valor negativo, ya que el salario base de un empleado no puede ser negativo. Las líneas 34-35 de la figura 10.7 invocan en forma implícita al método ToString del objeto, para obtener su representación string.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

// Fig. 10.7: PruebaEmpleadoBaseMasComision.cs // Prueba de la clase EmpleadoBaseMasComision. using System; public class PruebaEmpleadoBaseMasComision { public static void Main( string[] args ) { // crea instancia de un objeto EmpleadoBaseMasComision EmpleadoBaseMasComision empleado = new EmpleadoBaseMasComision( "Bob", "Lewis", "333-33-3333", 5000.00M, .04M, 300.00M ); // muestra en pantalla los datos del empleado por comisión con salario base Console.WriteLine( "Información del empleado obtenida por las propiedades y los métodos: \n" ); Console.WriteLine( "{0} {1}", "El primer nombre es", empleado.PrimerNombre ); Console.WriteLine( "{0} {1}", "El apellido paterno es", empleado.ApellidoPaterno ); Console.WriteLine( "{0} {1}", "El número de seguro social es", empleado.NumeroSeguroSocial ); Console.WriteLine( "{0} {1:C}", "Las ventas brutas son", empleado.VentasBrutas );

Figura 10.7 | Prueba de la clase EmpleadoBaseMasComision. (Parte 1 de 2).

318

25 26 27 28 29 30 31 32 33 34 35 36 37 38

Capítulo 10 Programación orientada a objetos: herencia

Console.WriteLine( "{0} {1:F2}", "La tarifa de comisión es", empleado.TarifaComision ); Console.WriteLine( "{0} {1:C}", "Los ingresos son", empleado.Ingresos() ); Console.WriteLine( "{0} {1:C}", "El salario base es", empleado.SalarioBase ); empleado.SalarioBase = 1000.00M; // establece el salario base Console.WriteLine( "\n{0}:\n\n{1}", "Información actualizada del empleado, obtenida mediante ToString", empleado ); Console.WriteLine( "ingresos: {0:C}", empleado.Ingresos() ); } // fin de Main } // fin de la clase PruebaEmpleadoBaseMasComision

Información del empleado obtenida por las propiedades y los métodos: El primer nombre es Bob El apellido paterno es Lewis El número de seguro social es 333-33-3333 Las ventas brutas son $5,000.00 La tarifa de comisión es 0.04 Los ingresos son $500.00 El salario base es $300.00 Información actualizada del empleado, obtenida mediante ToString: empleado por comisión con salario base: Bob Lewis número de seguro social: 333-33-3333 ventas brutas: $5,000.00 tarifa de comisión: 0.04 salario base: $1,000.00 ingresos: $1,200.00

Figura 10.7 | Prueba de la clase EmpleadoBaseMasComision. (Parte 2 de 2). Observe que la mayor parte del código para la clase EmpleadoBaseMasComision (figura 10.6) es similar, si no es que idéntico, al código para la clase EmpleadoPorComision (figura 10.4). Por ejemplo, en la clase EmpleadoBaseMasComision, las variables de instancia private primerNombre y apellidoPaterno, y las propiedades PrimerNombre y ApellidoPaterno son idénticas a las de la clase EmpleadoPorComision. Las clases EmpleadoPorComision y EmpleadoBasePorComision también contienen las variables de instancia private numeroSeguroSocial, tarifaComision y ventasBrutas, así como propiedades para manipular estas variables. Además, el constructor de EmpleadoBasePorComision es casi idéntico al de la clase EmpleadoPorComision, sólo que el constructor de EmpleadoBaseMasComision también establece el salarioBase. Las demás adiciones a la clase EmpleadoBaseMasComision son: la variable de instancia private salarioBase y la propiedad SalarioBase. El método ToString de la clase EmpleadoBaseMasComision es casi idéntico al de la clase EmpleadoPorComision, excepto que el método ToString de EmpleadoBasePorComision también da formato al valor de la variable de instancia salarioBase como cantidad monetaria. Literalmente hablando, copiamos todo el código de la clase EmpleadoPorComision y lo pegamos en la clase EmpleadoBaseMasComision, después modificamos esta clase para incluir un salario base, y los métodos y propiedades que manipulan ese salario base. A menudo, este método de “copiar y pegar” está propenso a errores y consume mucho tiempo. Peor aún, se pueden esparcir muchas copias físicas del mismo código a lo largo de un sistema, con lo que el mantenimiento del código se convierte en una pesadilla. ¿Existe alguna manera de “absorber” los miembros de una clase, para que formen parte de otras clases sin tener que copiar el código? En los siguientes ejemplos responderemos a esta pregunta, utilizando un método más elegante para crear clases: la herencia.

10.4 Relación entre las clases base y las clases derivadas

319

Tip de prevención de errores 10.1 Copiar y pegar código de una clase a otra puede esparcir los errores a través de varios archivos de código fuente. Para evitar la duplicación de código (y posiblemente los errores) en situaciones en las que se desea que una clase “absorba” los miembros de otra, usamos la herencia en vez del método de “copiar y pegar”.

Observación de ingeniería de software 10.4 Con la herencia, los miembros comunes de todas las clases en la jerarquía se declaran en una clase base. Cuando se requieren modificaciones para estas características comunes, sólo hay que realizar los cambios en la clase base; así las clases derivadas heredan los cambios. Sin la herencia, habría que modificar todos los archivos de código fuente que contengan una copia del código en cuestión.

10.4.3 Creación de una jerarquía de herencia EmpleadoPorComisionEmpleadoBaseMasComision Ahora declararemos la clase EmpleadoBaseMasComision2 (figura 10.8), que extiende a la clase EmpleadoPorComision (figura 10.4). Un objeto EmpleadoBaseMasComision es un EmpleadoPorComision (ya que la herencia traspasa las capacidades de la clase EmpleadoPorComision), pero la clase EmpleadoBaseMasComision2 también tiene la variable de instancia salarioBase (figura 10.8, línea 5). El signo de dos puntos (:) en la línea 3 de la declaración de la clase indica la herencia. Como clase derivada, EmpleadoBaseMasComision2 hereda los miembros de la clase EmpleadoPorComision y puede acceder a los miembros que no son private. El constructor de la clase EmpleadoPorComision no se hereda. Por lo tanto, los servicios public de EmpleadoBaseMasComision2 incluyen su constructor (líneas 9-14), los métodos y las propiedades public heredadas de la clase EmpleadoPorComision, la propiedad SalarioBase (líneas 18-28), el método Ingresos (líneas 31-35) y el método ToString (líneas 38-47).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

// Fig. 10.8: EmpleadoBaseMasComision2.cs // EmpleadoBaseMasComision2 hereda de la clase EmpleadoPorComision. public class EmpleadoBaseMasComision2 : EmpleadoPorComision { private decimal salarioBase; // salario base por semana // constructor de la clase derivada con seis parámetros // con una llamada al constructor de la clase base EmpleadoPorComision public EmpleadoBaseMasComision2( string nombre, string apellido, string nss, decimal ventas, decimal tarifa, decimal salario ) : base( nombre, apellido, nss, ventas, tarifa ) { SalarioBase = salario; // valida el salario base a través de una propiedad } // fin del constructor de EmpleadoBaseMasComision2 con seis parámetros // propiedad que obtiene y establece // el salario base del empleado por comisión con salario base public decimal SalarioBase { get { return salarioBase; } // fin de get set { salarioBase = ( value < 0 ) ? 0 : value; } // fin de set } // fin de la propiedad SalarioBase

Figura 10.8 |

EmpleadoBaseMasComision2

hereda de la clase EmpleadoPorComision. (Parte 1 de 2).

320

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

Capítulo 10 Programación orientada a objetos: herencia

// calcula los ingresos public override decimal Ingresos() { // no se permite: tarifaComision y ventasBrutas son private en la clase base return salarioBase + ( tarifaComision * ventasBrutas ); } // fin del método Ingresos // devuelve representación string de EmpleadoBaseMasComision2 public override string ToString() { // no se permite: trata de acceder a los miembros private de la clase base return string.Format( "{0}: {1} {2}\n{3}: {4}\n{5}: {6:C}\n{7}: {8:F2}\n{9}: {10:C}", "empleado por comisión con salario base", primerNombre, apellidoPaterno, "número de seguro social", numeroSeguroSocial, "ventas brutas", ventasBrutas, "tarifa de comisión", tarifaComision, "salario base", salarioBase ); } // fin del método ToString } // fin de la clase EmpleadoBaseMasComision2

Figura 10.8 |

EmpleadoBaseMasComision2

hereda de la clase EmpleadoPorComision. (Parte 2 de 2).

Cada constructor de clase derivada debe llamar en forma implícita o explícita al constructor de su clase base, para asegurar que las variables de instancia heredadas de la clase base se inicialicen en forma apropiada. El constructor de EmpleadoBaseMasComision2 con seis parámetros llama en forma explícita al constructor de la clase EmpleadoPorComision con cinco parámetros, para inicializar la porción correspondiente a la clase base de un objeto EmpleadoBaseMasComision2 (es decir, las variables primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision). La línea 11 en el encabezado del constructor de EmpleadoBaseMasComision2 con seis parámetros invoca al constructor de EmpleadoPorComision con cinco parámetros (declarado en las líneas 12-21 de la figura 10.4) mediante el uso de un inicializador de constructor. En la sección 9.6 utilizamos inicializadores de constructores con la palabra clave this para llamar a los constructores sobrecargados en la misma clase. En la línea 11 de la figura 10.8, usamos un inicializador de constructor con la palabra clave base para invocar al constructor base. Los argumentos nombre, apellido, nss, ventas y tarifa se utilizan para inicializar a los miembros primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision de la clase base, respectivamente. Si el constructor de EmpleadoBaseMasComision2 no invocara al constructor de EmpleadoPorComision de manera explícita, C# trataría de invocar al constructor predeterminado o sin parámetros de la clase EmpleadoPorComision; pero como la clase no tiene un constructor así, el compilador generaría un error. Cuando una clase base contiene un constructor sin parámetros, podemos usar base() en el inicializador del constructor para llamar a ese constructor de manera explícita, pero esto se hace muy raras veces.

Error común de programación 10.2 Si el constructor de una clase derivada llama a uno de los constructores de su clase base con argumentos que no concuerdan con el número y el tipo de los parámetros especificados en una de las declaraciones del constructor de la clase base, se produce un error de compilación.

10.4 Relación entre las clases base y las clases derivadas

321

Figura 10.9 | Errores de compilación generados por EmpleadoBaseMasComision2 (figura 10.8), después de declarar el método Ingresos en la figura 10.4 con la palabra clave virtual. Las líneas 31-35 de la figura 10.8 declaran el método Ingresos usando la palabra clave override para redefinir el método de EmpleadoPorComision, como hicimos con el método ToString en ejemplos anteriores. La línea 31 produce un error de compilación, que indica que no podemos redefinir el método Ingresos de la clase, ya que no está “marcado como virtual, abstract u override” en forma explícita. Las palabras clave virtual y abstract indican que un método de la clase base puede redefinirse en clases derivadas (como aprenderá en la sección 11.4, los métodos abstract son implícitamente virtuales). El modificador override declara que el método de una clase derivada redefine a un método virtual o abstract de la clase base. Este modificador también declara en forma implícita el método de la clase derivada como virtual y le permite ser redefinido en las clases derivadas, en niveles más abajo en la jerarquía de herencia. Si agregamos la palabra clave virtual a la declaración del método Ingresos en la figura 10.4 y compilamos nuevamente el programa, aparecerán otros errores de compilación. Como se muestra en la figura 10.9, el compilador genera errores adicionales para la línea 34 de la figura 10.8, ya que las variables de instancia tarifaComision y ventasBrutas de la clase base EmpleadoPorComision son private; no se permite que los métodos de la clase derivada EmpleadoBaseMasComision2 accedan a las variables de instancia private de la clase base EmpleadoPorComision. Observe que utilizamos texto en negritas en la figura 10.8 para indicar el código erróneo. El compilador genera errores adicionales en las líneas 43-45 del método ToString de EmpleadoBaseMasComision2 por la misma razón. Los errores en EmpleadoBaseMasComision2 podrían haberse prevenido mediante el uso de las propiedades public heredadas de la clase EmpleadoPorComision. Por ejemplo, la línea 34 podría haber invocado a los descriptores de acceso get de las propiedades TarifaComision y VentasBrutas para acceder a las variables de instancia private tarifaComision y ventasBrutas de EmpleadoPorComision, respectivamente. Las líneas 43-45 también podrían haber usado las propiedades apropiadas para extraer los valores de las variables de instancia de la clase base.

10.4.4 La jerarquía de herencia EmpleadoPorComisionEmpleadoBaseMasComision mediante el uso de variables de instancia protected Para permitir que la clase EmpleadoBaseMasComision acceda directamente a las variables de instancia primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision de la clase base, podemos declarar esos miembros como protected en la clase base. Como vimos en la sección 10.3, los miembros protected de una clase base se heredan por todas las clases derivadas de esa clase base. La clase EmpleadoPorComision2 (figura 10.10) es una modificación de la clase EmpleadoPorComision (figura 10.4), que declara las variables de instancia primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision como protected, en vez de private (figura 10.10, líneas 5-9). Como vimos en la sección 10.4.3, también declaramos el método Ingresos como virtual en la línea 78, de manera que EmpleadoBaseMasComision pueda redefinir el método. Además del cambio en el nombre de la clase (y por ende el cambio en el nombre del

322

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56

Capítulo 10 Programación orientada a objetos: herencia

// Fig. 10.10: EmpleadoPorComision2.cs // EmpleadoPorComision2 con variables de instancia protected. public class EmpleadoPorComision2 { protected string primerNombre; protected string apellidoPaterno; protected string numeroSeguroSocial; protected decimal ventasBrutas; // ventas semanales totales protected decimal tarifaComision; // porcentaje de comisión // constructor con cinco parámetros public EmpleadoPorComision2( string nombre, string apellido, string nss, decimal ventas, decimal tarifa ) { // la llamada implícita al constructor del objeto se realiza aquí primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; VentasBrutas = ventas; // valida las ventas brutas a través de una propiedad TarifaComision = tarifa; // valida la tarifa de comisión a través de una propiedad } // fin del constructor de EmpleadoPorComision2 con cinco parámetros // propiedad de sólo lectura que obtiene el primer nombre del empleado por comisión public string PrimerNombre { get { return primerNombre; } // fin de get } // fin de la propiedad PrimerNombre // propiedad de sólo lectura que obtiene el apellido paterno del empleado por comisión public string ApellidoPaterno { get { return apellidoPaterno; } // fin de get } // fin de la propiedad ApellidoPaterno // propiedad de sólo lectura que obtiene // el número de seguro social del empleado por comisión public string NumeroSeguroSocial { get { return numeroSeguroSocial; } // fin de get } // fin de la propiedad NumeroSeguroSocial // propiedad que obtiene y establece las ventas brutas del empleado por comisión public decimal VentasBrutas { get { return ventasBrutas;

Figura 10.10 |

EmpleadoPorComision2

con las variables de instancia protected. (Parte 1 de 2).

10.4 Relación entre las clases base y las clases derivadas

57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92

323

} // fin de get set { ventasBrutas = ( value < 0 ) ? 0 : value; } // fin de set } // fin de la propiedad VentasBrutas // propiedad que obtiene y establece la tarifa de comisión del empleado por comisión public decimal TarifaComision { get { return tarifaComision; } // fin de get set { tarifaComision = ( value > 0 && value < 1 ) ? value : 0; } // fin de set } // fin de la propiedad TarifaComision // calcula el sueldo del empleado por comisión public virtual decimal Ingresos() { return tarifaComision * ventasBrutas; } // fin del método Ingresos // devuelve representación string del objeto EmpleadoPorComision public override string ToString() { return string.Format( "{0}: {1} {2}\n{3}: {4}\n{5}: {6:C}\n{7}: {8:F2}", "empleado por comisión", primerNombre, apellidoPaterno, "número de seguro social", numeroSeguroSocial, "ventas brutas", ventasBrutas, "tarifa de comisión", tarifaComision ); } // fin del método ToString } // fin de la clase EmpleadoPorComision2

Figura 10.10 |

EmpleadoPorComision2

con las variables de instancia protected. (Parte 2 de 2).

constructor) a EmpleadoPorComision2, el resto de la declaración de la clase en la figura 10.10 es idéntico al de la figura 10.4. Podríamos haber declarado las variables de instancia primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision de la clase base EmpleadoPorComision2 como public para permitir que la clase derivada EmpleadoBaseMasComision2 pueda acceder a las variables de instancia de la clase base. No obstante, declarar variables de instancia public es una mala ingeniería de software, ya que permite el acceso sin restricciones a las variables de instancia, lo que incrementa considerablemente la probabilidad de errores. Con las variables de instancia protected, la clase derivada obtiene acceso a las variables de instancia, pero las clases que no se derivan de la clase base no pueden acceder a sus variables en forma directa. La clase EmpleadoBaseMasComision3 (figura 10.11) es una modificación de la clase EmpleadoBaseMasComision2 (figura 10.8), que extiende a EmpleadoPorComision2 (línea 4) en vez de la clase EmpleadoPorComision. Los objetos de la clase EmpleadoBaseMasComision3 heredan las variables de instancia protected primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision de EmpleadoPorComision2; ahora todas estas variables son miembros protected de EmpleadoBaseMasComision3. Como resultado, el compilador no genera errores al compilar la línea 34 del método Ingresos y las líneas 42-44 del método ToString. Si otra clase extiende a EmpleadoBasePorComision3, la nueva clase derivada también hereda los miembros protected.

324

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

Capítulo 10 Programación orientada a objetos: herencia

// Fig. 10.11: EmpleadoBaseMasComision3.cs // EmpleadoBaseMasComision3 hereda de EmpleadoPorComision2 y tiene // acceso a los miembros protected de EmpleadoPorComision2. public class EmpleadoBaseMasComision3 : EmpleadoPorComision2 { private decimal salarioBase; // salario base por semana // constructor de la clase derivada con seis parámetros // con una llamada al constructor de la clase bases EmpleadoPorComision public EmpleadoBaseMasComision3( string nombre, string apellido, string nss, decimal ventas, decimal tarifa, decimal salario ) : base( nombre, apellido, nss, ventas, tarifa ) { SalarioBase = salario; // valida el salario base a través de una propiedad } // fin del constructor de EmpleadoBaseMasComision3 con seis parámetros // propiedad que obtiene y establece // el salario base del empleado por comisión con salario base public decimal SalarioBase { get { return salarioBase; } // fin de get set { salarioBase = ( value < 0 ) ? 0 : value; } // fin de set } // fin de la propiedad SalarioBase // calcula los ingresos public override decimal Ingresos() { return salarioBase + ( tarifaComision * ventasBrutas ); } // fin del método Ingresos // devuelve representación string de EmpleadoBaseMasComision3 public override string ToString() { return string.Format( "{0}: {1} {2}\n{3}: {4}\n{5}: {6:C}\n{7}: {8:F2}\n{9}: {10:C}", "empleado por comisión con salario base", primerNombre, apellidoPaterno, "número de seguro social", numeroSeguroSocial, "ventas brutas", ventasBrutas, "tarifa de comisión", tarifaComision, "salario base", salarioBase ); } // fin del método ToString } // fin de la clase EmpleadoBaseMasComision3

Figura 10.11 | EmpleadoBaseMasComision3 hereda de EmpleadoPorComision2 y tiene acceso a los miembros protected de EmpleadoPorComision2.

La clase EmpleadoBaseMasComision3 no hereda el constructor de la clase EmpleadoPorComision2. No obstante, el constructor de la clase EmpleadoBaseMasComision3 con seis parámetros (líneas 10-15) llama al constructor de la clase EmpleadoPorComision2 con cinco parámetros, mediante un inicializador de constructor. El constructor de EmpleadoBaseMasComision3 con seis parámetros debe llamar en forma explícita al constructor de la clase EmpleadoPorComision2, ya que EmpleadoPorComision2 no proporciona un constructor sin parámetros que pueda invocarse en forma implícita.

10.4 Relación entre las clases base y las clases derivadas

325

La figura 10.12 utiliza un objeto EmpleadoBaseMasComision3 para realizar las mismas tareas que realizó la figura 10.7 con un objeto EmpleadoBaseMasComision (figura 10.6). Observe que los resultados de las dos aplicaciones son idénticos. Aunque declaramos la clase EmpleadoBaseMasComision sin utilizar la herencia, y declaramos la clase EmpleadoBaseMasComision3 utilizando la herencia, ambas clases proporcionan la misma funcionalidad. El código fuente para la clase EmpleadoBaseMasComision3, que tiene 47 líneas, es mucho más corto que el de la clase EmpleadoBaseMasComision, que tiene 114 líneas, debido a que la clase EmpleadoBaseMasComision3 hereda la mayor parte de su funcionalidad de EmpleadoPorComision2, mientas que la clase EmpleadoBaseMasComision sólo hereda la funcionalidad de la clase object. Además, ahora sólo hay una copia de la funcionalidad del empleado por comisión declarada en la clase EmpleadoPorComision2. Esto hace que el código sea más fácil de mantener, modificar y depurar, ya que el código relacionado con un empleado por comisión sólo existe en la clase EmpleadoPorComision2. En este ejemplo, declaramos las variables de instancia de la clase base como protected, para que la clase derivada pudiera heredarlas. Al heredar variables de instancia protected se incrementa un poco el rendimiento, ya que podemos acceder directamente a las variables en la clase derivada, sin incurrir en la sobrecarga de invocar a los descriptores de acceso set o get de la propiedad correspondiente. No obstante, en la mayoría de los casos es mejor utilizar variables de instancia private, para cumplir con la ingeniería de software apropiada, y dejar las cuestiones relacionadas con la optimización al compilador. Su código será más fácil de mantener, modificar y depurar. El uso de variables de instancia protected crea varios problemas potenciales. En primer lugar, el objeto de la clase derivada puede establecer el valor de una variable heredada directamente sin utilizar el descriptor de acceso set de la propiedad. Por lo tanto, un objeto de la clase derivada puede asignar un valor inválido a la variable, con lo cual el objeto queda en un estado inconsistente. Por ejemplo, si declaramos la variable de instancia ventasBrutas de EmpleadoPorComision3 como protected, un objeto de una clase derivada (por ejemplo, EmpleadoBaseMasComision) podría entonces asignar un valor negativo a ventasBrutas.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

// Fig. 10.12: PruebaEmpleadoBaseMasComision3.cs // Prueba de la clase EmpleadoBaseMasComision3. using System; public class PruebaEmpleadoBaseMasComision3 { public static void Main( string[] args ) { // crea instancia de un objeto EmpleadoBaseMasComision3 EmpleadoBaseMasComision3 empleadoBaseMasComision = new EmpleadoBaseMasComision3( "Bob", "Lewis", "333-33-3333", 5000.00M, .04M, 300.00M ); // muestra en pantalla los datos del empleado por comisión con salario base Console.WriteLine( "Información del empleado obtenida por las propiedades y los métodos: \n" ); Console.WriteLine( "{0} {1}", "El primer nombre es", empleadoBaseMasComision.PrimerNombre ); Console.WriteLine( "{0} {1}", "El apellido paterno es", empleadoBaseMasComision.ApellidoPaterno ); Console.WriteLine( "{0} {1}", "El número de seguro social es", empleadoBaseMasComision.NumeroSeguroSocial ); Console.WriteLine( "{0} {1:C}", "Las ventas brutas son", empleadoBaseMasComision.VentasBrutas ); Console.WriteLine( "{0} {1:F2}", "La tarifa de comisión es", empleadoBaseMasComision.TarifaComision ); Console.WriteLine( "{0} {1:C}", "Los ingresos son", empleadoBaseMasComision.Ingresos() );

Figura 10.12 | Prueba de la clase EmpleadoBaseMasComision3. (Parte 1 de 2).

326

29 30 31 32 33 34 35 36 37 38 39 40

Capítulo 10 Programación orientada a objetos: herencia

Console.WriteLine( "{0} {1:C}", "El salario base es", empleadoBaseMasComision.SalarioBase ); empleadoBaseMasComision.SalarioBase = 1000.00M; // establece el salario base Console.WriteLine( "\n{0}:\n\n{1}", "Información actualizada del empleado, obtenida por ToString", empleadoBaseMasComision ); Console.WriteLine( "ingresos: {0:C}", empleadoBaseMasComision.Ingresos() ); } // fin de Main } // fin de la clase PruebaEmpleadoBaseMasComision3

Información del empleado obtenida por las propiedades y los métodos: El primer nombre es Bob El apellido paterno es Lewis El número de seguro social es 333-33-3333 Las ventas brutas son $5,000.00 La tarifa de comisión es 0.04 Los ingresos son $500.00 El salario base es $300.00 Información actualizada del empleado, obtenida por ToString: empleado por comisión con salario base: Bob Lewis número de seguro social: 333-33-3333 ventas brutas: $5,000.00 tarifa de comisión: 0.04 salario base: $1,000.00 ingresos: $1,200.00

Figura 10.12 | Prueba de la clase EmpleadoBaseMasComision3. (Parte 2 de 2). El segundo problema con el uso de variables de instancia protected es que hay más probabilidad de que los métodos de la clase derivada se escriban de manera que dependan de la implementación de datos de la clase base. En la práctica, las clases derivadas sólo deben depender de los servicios de la clase base (es decir, métodos y propiedades que no sean private) y no en la implementación de datos de la clase base. Si hay variables de instancia protected en la clase base, tal vez necesitemos modificar todas las clases derivadas de la clase base si cambia la implementación de ésta. Por ejemplo, si por alguna razón tuviéramos que cambiar los nombres de las variables de instancia primerNombre y apellidoPaterno por nombre y apellido, entonces tendríamos que hacerlo para todas las ocurrencias en las que una clase derivada haga referencia directa a las variables de instancia primerNombre y apellidoPaterno de la clase base. En tal caso, se dice que el software es frágil o quebradizo, ya que un pequeño cambio en la clase base puede “quebrar” la implementación de la clase derivada. Lo óptimo es poder modificar la implementación de la clase base sin dejar de proporcionar los mismos servicios a las clases derivadas. Desde luego que, si cambian los servicios de la clase base, debemos reimplementar nuestras clases derivadas.

Observación de ingeniería de software 10.5 Al declarar variables de instancia private (a diferencia de protected) en la clase base se permite que la implementación de la clase base para estas variables de instancia cambie sin afectar la implementación de la clase derivada.

10.4.5 La jerarquía de herencia EmpleadoPorComisionEmpleadoBaseMasComision mediante el uso de variables de instancia private Ahora vamos a reexaminar nuestra jerarquía una vez más, pero ahora utilizaremos las mejores prácticas de ingeniería de software. La clase EmpleadoPorComision3 (figura 10.13) declara las variables de instancia primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision como private

10.4 Relación entre las clases base y las clases derivadas

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

327

// Fig. 10.13: EmpleadoPorComision3.cs // La clase EmpleadoPorComision3 representa a un empleado por comisión. public class EmpleadoPorComision3 { private string primerNombre; private string apellidoPaterno; private string numeroSeguroSocial; private decimal ventasBrutas; // ventas semanales totales private decimal tarifaComision; // porcentaje de comisión // constructor con cinco parámetros public EmpleadoPorComision3( string nombre, string apellido, string nss, decimal ventas, decimal tarifa ) { // la llamada implícita al constructor de object se realiza aquí primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; VentasBrutas = ventas; // valida las ventas brutas a través de una propiedad TarifaComision = tarifa; // valida la tarifa de comisión a través de una propiedad } // fin del constructor de EmpleadoPorComision3 con cinco parámetros // propiedad de sólo lectura que obtiene el primer nombre del empleado por comisión public string PrimerNombre { get { return primerNombre; } // fin de get } // fin de la propiedad PrimerNombre // propiedad de sólo lectura que obtiene el apellido paterno del empleado por comisión public string ApellidoPaterno { get { return apellidoPaterno; } // fin de get } // fin de la propiedad ApellidoPaterno // propiedad de sólo lectura que obtiene el // número de seguro social del empleado por comisión public string NumeroSeguroSocial { get { return numeroSeguroSocial; } // fin de get } // fin de la propiedad NumeroSeguroSocial // propiedad que obtiene y establece las ventas brutas del empleado por comisión public decimal VentasBrutas { get { return ventasBrutas; } // fin de get set

Figura 10.13 | La clase EmpleadoPorComision3 representa a un empleado por comisión. (Parte 1 de 2).

328

59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92

Capítulo 10 Programación orientada a objetos: herencia

{ ventasBrutas = ( value < 0 ) ? 0 : value; } // fin de set } // fin de la propiedad VentasBrutas // propiedad que obtiene y establece la tarifa de comisión del empleado por comisión public decimal TarifaComision { get { return tarifaComision; } // fin de get set { tarifaComision = ( value > 0 && value < 1 ) ? value : 0; } // fin de set } // fin de la propiedad TarifaComision // calcula el salario del empleado por comisión public virtual decimal Ingresos() { return TarifaComision * VentasBrutas; } // fin del método Ingresos // devuelve representación string del objeto EmpleadoPorComision public override string ToString() { return string.Format( "{0}: {1} {2}\n{3}: {4}\n{5}: {6:C}\n{7}: {8:F2}", "empleado por comisión", PrimerNombre, ApellidoPaterno, "número de seguro social", NumeroSeguroSocial, "ventas brutas", VentasBrutas, "tarifa de comisión", TarifaComision ); } // fin del método ToString } // fin de la clase EmpleadoPorComision3

Figura 10.13 | La clase EmpleadoPorComision3 representa a un empleado por comisión. (Parte 2 de 2).

(líneas 5-9) y proporciona las propiedades public PrimerNombre, ApellidoPaterno, NumeroSeguroSocial, VentasBrutas y TarifaComision para manipular estos valores. Observe que los métodos Ingresos (líneas 78-81) y ToString (líneas 84-91) utilizan las propiedades de la clase para obtener los valores de sus variables de instancia. Si decidimos modificar los nombres de las variables de instancia, no habrá que modificar las declaraciones de Ingresos y de ToString; sólo habrá que modificar los cuerpos de las propiedades que manipulan directamente estas variables de instancia. Observe que estos cambios ocurren sólo dentro de la clase base; no se necesitan cambios en la clase derivada. La localización de los efectos de los cambios como éste es una buena práctica de ingeniería de software. La clase derivada EmpleadoBaseMasComision4 (figura 10.14) hereda los miembros no private de EmpleadoPorComision3 y puede acceder a los miembros private de su clase base a través de sus propiedades public. La clase EmpleadoBaseMasComision4 (figura 10.14) tiene varios cambios en las implementaciones de sus métodos, que la diferencian de la clase EmpleadoBaseMasComision3 (figura 10.11). Los métodos Ingresos (figura 10.14, líneas 33-36) y ToString (líneas 39-43) invocan cada uno al descriptor de acceso get de la propiedad SalarioBase para obtener el valor del salario base, en vez de acceder en forma directa a salarioBase. Si decidimos cambiar el nombre de la variable de instancia salarioBase, sólo tendrá que cambiar el cuerpo de la propiedad SalarioBase. El método Ingresos de la clase EmpleadoBaseMasComision4 (figura 10.14, líneas 33-36) redefine al método Ingresos de la clase EmpleadoPorComision3 (figura 10.13, líneas 78-81) para calcular los ingresos de un empleado por comisión con sueldo base. La nueva versión obtiene la porción de los ingresos del empleado,

10.4 Relación entre las clases base y las clases derivadas

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

329

// Fig. 10.14: EmpleadoBaseMasComision4.cs // EmpleadoBaseMasComision4 hereda de EmpleadoPorComision3 y tiene // acceso a los datos private de EmpleadoPorComision3 a través de // sus propiedades public. public class EmpleadoBaseMasComision4 : EmpleadoPorComision3 { private decimal salarioBase; // salario base por semana // constructor de la clase derivada con seis parámetros // con una llamada al constructor de la clase base EmpleadoPorComision3 public EmpleadoBaseMasComision4( string nombre, string apellido, string nss, decimal ventas, decimal tarifa, decimal salario ) : base( nombre, apellido, nss, ventas, tarifa ) { SalarioBase = salario; // valida el salario base a través de una propiedad } // fin del constructor de EmpleadoBaseMasComision4 con seis parámetros // propiedad que obtiene y establece el // salario base del empleado por comisión con salario base public decimal SalarioBase { get { return salarioBase; } // fin de get set { salarioBase = ( value < 0 ) ? 0 : value; } // fin de set } // fin de la propiedad SalarioBase // calcula los ingresos public override decimal Ingresos() { return SalarioBase + base.Ingresos(); } // fin del método Ingresos // devuelve representación string de EmpleadoBaseMasComision4 public override string ToString() { return string.Format( "{0} {1}\n{2}: {3:C}", "salario base +", base.ToString(), "salario base", SalarioBase ); } // fin del método ToString } // fin de la clase EmpleadoBaseMasComision4

Figura 10.14 | EmpleadoBaseMasComision4 hereda de EmpleadoPorComision3 y tiene acceso a los datos private de EmpleadoPorComision3 a través de sus propiedades public.

con base en la comisión solamente, mediante una llamada al método Ingresos de EmpleadoPorComision3 con la expresión base.Ingresos() (figura 10.14, línea 35). El método Ingresos de EmpleadoBasePorComision4 agrega después el salario base a este valor, para calcular los ingresos totales del empleado. Observe la sintaxis utilizada para invocar un método de clase base redefinido desde una clase derivada: coloque la palabra clave base y el operador punto (.) antes del nombre del método de la clase base. Esta forma de invocar métodos es una buena práctica de ingeniería de software; al hacer que el método Ingresos de EmpleadoBaseMasComision4 invoque al método Ingresos de EmpleadoPorComision3 para calcular parte de los ingresos del objeto EmpleadoBaseMasComision4, evitamos duplicar el código y se reducen los problemas de mantenimiento del mismo.

330

Capítulo 10 Programación orientada a objetos: herencia

Error común de programación 10.3 Cuando se redefine un método de la clase base en una clase derivada, por lo general la versión correspondiente a la clase derivada llama a la versión de la clase base para que realice una parte del trabajo. Si no se anteponen al nombre del método de la clase base la palabra clave base y el operador punto (.) cuando se hace referencia al método de la clase base, el método de la clase derivada se llama a sí mismo, creando un error conocido como recursividad infinita. La recursividad, si se utiliza en forma correcta, es una poderosa herramienta, como vimos en la sección 7.13, Recursividad.

Error común de programación 10.4 El uso de referencias base “encadenadas” para hacer referencia a un miembro (un método, propiedad o variable) que esté varios niveles arriba en la jerarquía [como en base.base.Ingresos()] es un error de compilación.

De manera similar, el método ToString de EmpleadoBaseMasComision4 (figura 10.14, líneas 39-43) redefine el método ToString de la clase EmpleadoPorComision3 (figura 10.13, líneas 84-91) para devolver una representación string apropiada para un empleado por comisión con salario base. La nueva versión crea parte de la representación string de un objeto EmpleadoBaseMasComision4 (es decir, la cadena “empleado por comisión con sueldo base” y los valores de las variables de instancia private de la clase EmpleadoPorComision3), mediante una llamada al método ToString de EmpleadoPorComision3 con la expresión base.ToString() (figura 10.14, línea 42). Después, el método ToString de EmpleadoBaseMasComision4 imprime en pantalla el resto de la representación string de un objeto EmpleadoBaseMasComision4 (es decir, el valor del salario base de la clase EmpleadoBaseMasComision4). La figura 10.15 realiza las mismas manipulaciones sobre un objeto EmpleadoBaseMasComision4 que las de las figuras 10.7 y 10.12 sobre objetos de las clases EmpleadoBaseMasComision y EmpleadoBaseMasComision3, respectivamente. Aunque cada clase de “empleado por comisión con salario base” se comporta en forma idéntica, la clase EmpleadoBaseMasComision4 es la mejor diseñada. Mediante el uso de la herencia y las propiedades que ocultan los datos y aseguran la consistencia, hemos construido una clase bien diseñada con eficiencia y efectividad. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

// Fig. 10.15: PruebaEmpleadoBaseMasComision4.cs // Prueba de la clase EmpleadoBaseMasComision4. using System; public class PruebaEmpleadoBaseMasComision4 { public static void Main( string[] args ) { // crea instancia de un objeto EmpleadoBaseMasComision4 EmpleadoBaseMasComision4 empleado = new EmpleadoBaseMasComision4( "Bob", "Lewis", "333-33-3333", 5000.00M, .04M, 300.00M ); // muestra los datos del empleado por comisión con salario base Console.WriteLine( "Información del empleado obtenida por las propiedades y los métodos: \n" ); Console.WriteLine( "{0} {1}", "El primer nombre es", empleado.PrimerNombre ); Console.WriteLine( "{0} {1}", "El apellido paterno es", empleado.ApellidoPaterno ); Console.WriteLine( "{0} {1}", "El número de seguro social es", empleado.NumeroSeguroSocial ); Console.WriteLine( "{0} {1:C}", "Las ventas brutas son", empleado.VentasBrutas ); Console.WriteLine( "{0} {1:F2}", "La tarifa de comisión es", empleado.TarifaComision ); Console.WriteLine( "{0} {1:C}", "Los ingresos son",

Figura 10.15 | Prueba de la clase EmpleadoBaseMasPorComision4. (Parte 1 de 2).

10.5 Los constructores en las clases derivadas

28 29 30 31 32 33 34 35 36 37 38

331

empleado.Ingresos() ); Console.WriteLine( "{0} {1:C}", "El salario base es", empleado.SalarioBase ); empleado.SalarioBase = 1000.00M; // establece el salario base Console.WriteLine( "\n{0}:\n\n{1}", "Información actualizada del empleado, obtenida por ToString", empleado ); Console.WriteLine( "ingresos: {0:C}", empleado.Ingresos() ); } // fin de Main } // fin de la clase PruebaEmpleadoBaseMasComision4

Información del empleado obtenida por las propiedades y los métodos: El primer nombre es Bob El apellido paterno es Lewis El número de seguro social es 333-33-3333 Las ventas brutas son $5,000.00 La tarifa de comisión es 0.04 Los ingresos son $500.00 El salario base es $300.00 Información actualizada del empleado, obtenida por ToString: salario base + empleado por comisión: Bob Lewis número de seguro social: 333-33-3333 ventas brutas: $5,000.00 tarifa de comisión: 0.04 salario base: $1,000.00 ingresos: $1,200.00

Figura 10.15 | Prueba de la clase EmpleadoBaseMasPorComision4. (Parte 2 de 2).

En esta sección vio la evolución de un conjunto de ejemplos diseñados cuidadosamente para enseñar las capacidades clave de la buena ingeniería de software mediante el uso de la herencia. Aprendió a crear una clase derivada mediante la herencia, a utilizar miembros protected de la clase base para permitir que una clase derivada acceda a las variables de instancia heredadas de una clase base y cómo redefinir los métodos de la clase base para proporcionar versiones más apropiadas para los objetos de las clases derivadas. Además, aplicó las técnicas de ingeniería de software de los capítulos 4, 9 y este capítulo para crear clases que sean fáciles de mantener, modificar y depurar.

10.5 Los constructores en las clases derivadas Como explicamos en la sección anterior, al crear una instancia de un objeto de clase derivada se empieza una cadena de llamadas a los constructores, en los que el constructor de la clase derivada, antes de realizar sus propias tareas, invoca al constructor de su clase base directa, ya sea en forma explícita (por medio de un inicializador de constructor con la referencia base) o implícita (llamando al constructor predeterminado o sin parámetros de la clase base). De manera similar, si la clase base se deriva de otra clase (como sucede con cualquier clase, excepto object), el constructor de la clase base invoca al constructor de la siguiente clase que se encuentre a un nivel más arriba en la jerarquía, y así en lo sucesivo. El último constructor que se llama en la cadena es siempre el de la clase object. El cuerpo del constructor original de la clase derivada termina de ejecutarse al último. El constructor de cada clase base manipula las variables de instancia de la clase base que hereda el objeto de la clase derivada. Por ejemplo, considere de nuevo la jerarquía EmpleadoPorComision3–EmpleadoBaseMasComision4 de las figuras 10.13 y 10.14. Cuando una aplicación crea un objeto EmpleadoBaseMasComision4, se hace una llamada al constructor de EmpleadoBaseMasComision4. Ese constructor llama al constructor de la clase EmpleadoPor-

332

Capítulo 10 Programación orientada a objetos: herencia

Comision3, que a su vez llama en forma implícita al constructor de object. El constructor de la clase object tiene un cuerpo vacío, por lo que devuelve de inmediato el control al constructor de EmpleadoPorComision3, el cual inicializa las variables de instancia private de EmpleadoPorComision3 que son parte del objeto EmpleadoBaseMasComision4. Cuando este constructor termina de ejecutarse, devuelve el control al constructor de EmpleadoBaseMasComision4, el cual inicializa el salarioBase del objeto EmpleadoBaseMasComision4.

Observación de ingeniería de software 10.6 Cuando una aplicación crea un objeto de una clase derivada, el constructor de la clase derivada llama de inmediato al constructor de la clase base (ya sea en forma explícita, mediante base, o implícita). El cuerpo del constructor de la clase base se ejecuta para inicializar las variables de instancia de la clase base que forman parte del objeto de la clase derivada, después se ejecuta el cuerpo del constructor de la clase derivada para inicializar las variables de instancia que son parte sólo de la clase derivada. Aun si un constructor no asigna un valor a una variable de instancia, la variable de todas formas se inicializa con su valor predeterminado (es decir, 0 para los tipos numéricos simples, false para bool y null para las referencias).

En nuestro siguiente ejemplo volvemos a utilizar la jerarquía de empleado por comisión, al declarar las clases (figura 10.16) y EmpladoBaseMasComision5 (figura 10.17). El constructor de cada clase imprime un mensaje cuando se le invoca, lo cual nos permite observar el orden en el que se ejecutan los constructores en la jerarquía. La clase EmpleadoPorComision4 (figura 10.16) contiene las mismas características que la versión de la clase que se muestra en la figura 10.13. Se modificó el constructor (líneas 14-25) para desplegar el texto cuando es llamado. Observe que si concatenamos la palabra clave this con una literal string (línea 24) se invoca en forma implícita al método ToString del objeto que se está creando, para obtener la representación string de ese objeto. EmpleadoPorComision4

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

// Fig. 10.16: EmpleadoPorComision4.cs // La clase EmpleadoPorComision4 representa a un empleado por comisión. using System; public class EmpleadoPorComision4 { private string primerNombre; private string apellidoPaterno; private string numeroSeguroSocial; private decimal ventasBrutas; // ventas semanales totales private decimal tarifaComision; // porciento de comisión // constructor con cinco parámetros public EmpleadoPorComision4( string nombre, string apellido, string nss, decimal ventas, decimal tarifa ) { // la llamada implícita al constructor del objeto se realiza aquí primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; VentasBrutas = ventas; // valida las ventas brutas a través de una propiedad TarifaComision = tarifa; // valida la tarifa de comisión a través de una propiedad Console.WriteLine( "\nConstructor de EmpleadoPorComision4:\n" + this ); } // fin del constructor de EmpleadoPorComision4 con cinco parámetros // propiedad de sólo lectura que obtiene el primer nombre del empleado por comisión public string PrimerNombre

Figura 10.16 | La clase EmpleadoPorComision4 representa a un empleado por comisión. (Parte 1 de 3).

10.5 Los constructores en las clases derivadas

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

333

{ get { return primerNombre; } // fin de get } // fin de la propiedad PrimerNombre // propiedad de sólo lectura que obtiene el apellido paterno del empleado por comisión public string ApellidoPaterno { get { return apellidoPaterno; } // fin de get } // fin de la propiedad ApellidoPaterno // propiedad de sólo lectura que obtiene el // número de seguro social del empleado por comisión public string NumeroSeguroSocial { get { return numeroSeguroSocial; } // fin de get } // fin de la propiedad NumeroSeguroSocial // propiedad que obtiene y establece las ventas brutas del empleado por comisión public decimal VentasBrutas { get { return ventasBrutas; } // fin de get set { ventasBrutas = ( value < 0 ) ? 0 : value; } // fin de set } // fin de la propiedad VentasBrutas // propiedad que obtiene y establece la tarifa de comisión de un empleado por comisión public decimal TarifaComision { get { return tarifaComision; } // fin de get set { tarifaComision = ( value > 0 && value < 1 ) ? value : 0; } // fin de set } // fin de la propiedad TarifaComision // calcula el salario del empleado por comisión public virtual decimal Ingresos() { return TarifaComision * VentasBrutas; } // fin del método Ingresos

Figura 10.16 | La clase EmpleadoPorComision4 representa a un empleado por comisión. (Parte 2 de 3).

334

86 87 88 89 90 91 92 93 94 95 96

Capítulo 10 Programación orientada a objetos: herencia

// devuelve representación string del objeto EmpleadoPorComision public override string ToString() { return string.Format( "{0}: {1} {2}\n{3}: {4}\n{5}: {6:C}\n{7}: {8:F2}", "empleado por comisión", PrimerNombre, ApellidoPaterno, "número de seguro social", NumeroSeguroSocial, "ventas brutas", VentasBrutas, "tarifa de comisión", TarifaComision ); } // fin del método ToString } // fin de la clase EmpleadoPorComision4

Figura 10.16 | La clase EmpleadoPorComision4 representa a un empleado por comisión. (Parte 3 de 3). La clase EmpleadoBaseMasComision5 (figura 10.17) es casi idéntica a EmpleadoBaseMasComision4 (figura 10.14), sólo que el constructor de EmpleadoBaseMasComision5 imprime texto cuando se invoca. Al igual que en EmpleadoPorComision4 (figura 10.16), concatenamos la palabra clave this con una literal string para obtener de manera implícita la representación string del objeto.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

// Fig. 10.17: EmpleadoBaseMasComision5.cs // Declaración de la clase EmpleadoBaseMasComision5. using System; public class EmpleadoBaseMasComision5 : EmpleadoPorComision4 { private decimal salarioBase; // salario base por semana // constructor de la clase derivada con seis parámetros // con una llamada al constructor de la clase base EmpleadoPorComision4 public EmpleadoBaseMasComision5( string nombre, string apellido, string nss, decimal ventas, decimal tarifa, decimal salario ) : base( nombre, apellido, nss, ventas, tarifa ) { SalarioBase = salario; // valida el salario base a través de una propiedad Console.WriteLine( "\nConstructor de EmpleadoBaseMasComision5:\n" + this ); } // fin del constructor de EmpleadoBaseMasComision5 con seis parámetros // propiedad que obtiene y establece el // salario base del empleado por comisión con salario base public decimal SalarioBase { get { return salarioBase; } // fin de get set { salarioBase = ( value < 0 ) ? 0 : value; } // fin de set } // fin de la propiedad SalarioBase // calcula los ingresos

Figura 10.17 | Declaración de la clase EmpleadoBaseMasComision5. (Parte 1 de 2).

10.5 Los constructores en las clases derivadas

36 37 38 39 40 41 42 43 44 45 46 47

335

public override decimal Ingresos() { return SalarioBase + base.Ingresos(); } // fin del método Ingresos // devuelve representación string de EmpleadoBaseMasComision5 public override string ToString() { return string.Format( "{0} {1}\n{2}: {3:C}", "salario base +", base.ToString(), "salario base", SalarioBase ); } // fin del método ToString } // fin de la clase EmpleadoBaseMasComision5

Figura 10.17 | Declaración de la clase EmpleadoBaseMasComision5. (Parte 2 de 2). La figura 10.18 demuestra el orden en el que se llaman los constructores para los objetos de las clases que forman parte de una jerarquía de herencia. El método Main empieza por crear una instancia del objeto empleado1 de la clase EmpleadoPorComision4 (líneas 10-11). A continuación, las líneas 14-16 crean una instancia del objeto empleado2 de EmpleadoBaseMasComision5. Esto invoca al constructor de EmpleadoPorComision4, el cual imprime los resultados con los valores que recibe del constructor de EmpleadoBaseMasComision5 y después imprime los resultados especificados en el constructor de EmpleadoBaseMasComision5. Después, las líneas 19-21 crean una instancia del objeto empleado3 de EmpleadoBaseMasComision5. De nuevo, se hacen llamadas a los constructores de EmpleadoPorComision4 y EmpleadoBaseMasComision5. En cada caso, el cuerpo del constructor de EmpleadoPorComision4 se ejecuta antes que el cuerpo del constructor de EmpleadoBaseMasComision5. Observe que empleado2 se crea por completo antes que empiece el constructor de empleado3.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// Fig. 10.18: PruebaConstructor.cs // Muestra el orden en el que se llaman los constructores de // la clase base y la clase derivada. using System; public class PruebaConstructor { public static void Main( string[] args ) { EmpleadoPorComision4 empleado1 = new EmpleadoPorComision4( "Bob", "Lewis", "333-33-3333", 5000.00M, .04M ); Console.WriteLine(); EmpleadoBaseMasComision5 empleado2 = new EmpleadoBaseMasComision5( "Lisa", "Jones", "555-55-5555", 2000.00M, .06M, 800.00M ); Console.WriteLine(); EmpleadoBaseMasComision5 empleado3 = new EmpleadoBaseMasComision5( "Mark", "Sands", "888-88-8888", 8000.00M, .15M, 2000.00M ); } // fin de Main } // fin de la clase PruebaConstructor

Figura 10.18 | Orden de visualización en el que se llaman los constructores de la clase base y la clase derivada. (Parte 1 de 2).

336

Capítulo 10 Programación orientada a objetos: herencia

Constructor de EmpleadoPorComision4: empleado por comisión: Bob Lewis número de seguro social: 333-33-3333 ventas brutas: $5,000.00 tarifa de comisión: 0.04 Constructor de EmpleadoPorComision4: salario base + empleado por comisión: Lisa Jones número de seguro social: 555-55-5555 ventas brutas: $2,000.00 tarifa de comisión: 0.06 salario base: $0.00 Constructor de EmpleadoBaseMasComision5: salario base + empleado por comisión: Lisa Jones número de seguro social: 555-55-5555 ventas brutas: $2,000.00 tarifa de comisión: 0.06 salario base: $800.00 Constructor de EmpleadoPorComision4: salario base + empleado por comisión: Mark Sands número de seguro social: 888-88-8888 ventas brutas: $8,000.00 tarifa de comisión: 0.15 salario base: $0.00 Constructor de EmpleadoBaseMasComision5: salario base + empleado por comisión: Mark Sands número de seguro social: 888-88-8888 ventas brutas: $8,000.00 tarifa de comisión: 0.15 salario base: $2,000.00

Figura 10.18 | Orden de visualización en el que se llaman los constructores de la clase base y la clase derivada. (Parte 2 de 2).

10.6 Ingeniería de software mediante la herencia En esta sección hablaremos sobre la personalización del software existente mediante la herencia. Cuando una nueva clase extiende a una clase existente, la nueva clase hereda los miembros de la clase existente. Podemos personalizar la nueva clase para cumplir nuestras necesidades, mediante la inclusión de miembros adicionales y la redefinición de miembros de la clase base. Para hacer esto, el programador de la clase derivada no tiene que modificar el código fuente de la clase base. C# sólo requiere el acceso al código compilado de la clase base, para poder compilar y ejecutar cualquier aplicación que utilice o extienda la clase base. Esta poderosa capacidad es atractiva para los distribuidores independientes de software (ISVs), quienes pueden desarrollar clases propietarias para vender o licenciar, y ponerlas a disposición de los usuarios en bibliotecas de clases. Después, los usuarios pueden derivar con rapidez nuevas clases a partir de estas clases de biblioteca, sin necesidad de acceder al código fuente propietario del ISV.

Observación de ingeniería de software 10.7 A pesar del hecho de que al heredar de una clase no se requiere acceso a su código fuente, los desarrolladores insisten con frecuencia en ver el código fuente para comprender cómo está implementada la clase. Por ejemplo, tal vez quieran asegurarse de que están extendiendo una clase que se desempeña bien y que se implementa en forma segura.

Algunas veces, los estudiantes tienen dificultad para apreciar el alcance de los problemas a los que se enfrentan los diseñadores que trabajan en proyectos de software a gran escala en la industria. Las personas experimentadas

10.7 La clase object

337

con esos proyectos dicen que la reutilización efectiva del software mejora el proceso de desarrollo del mismo. La programación orientada a objetos facilita la reutilización de software, con lo que se obtiene una potencial reducción en el tiempo de desarrollo. La disponibilidad de bibliotecas de clases extensas y útiles produce los máximos beneficios de la reutilización de software a través de la herencia. Las bibliotecas de clases de la FCL que utiliza C# tienden a ser más de propósito general. Existen muchas bibliotecas de clases de propósito especial y muchas más están en proceso de crearse.

Observación de ingeniería de software 10.8 En la etapa de diseño de un sistema orientado a objetos, el diseñador encuentra comúnmente que ciertas clases están muy relacionadas. Es conveniente que el diseñador “factorice” los miembros comunes y los coloque en una clase base. Después debe usar la herencia para desarrollar clases derivadas, especializándolas con herramientas que estén más allá de las heredadas de parte de la clase base.

Observación de ingeniería de software 10.9 Declarar una clase derivada no afecta el código fuente de la clase base. La herencia preserva la integridad de la clase base.

Observación de ingeniería de software 10.10 Así como los diseñadores de sistemas no orientados a objetos deben evitar la proliferación de métodos, los diseñadores de sistemas orientados a objetos deben evitar la proliferación de clases. Dicha proliferación crea problemas administrativos y puede obstaculizar la reutilización de software, ya que en una biblioteca de clases enorme es difícil para un cliente localizar las clases más apropiadas. La alternativa es crear menos clases que proporcionen una funcionalidad más sustancial, pero dichas clases podrían volverse incómodas.

Tip de rendimiento 10.1 Si las clases derivadas son más grandes de lo necesario (es decir, que contengan demasiada funcionalidad), podrían desperdiciarse los recursos de memoria y de procesamiento. Extienda la clase base que contenga la funcionalidad que esté más cerca de lo que necesita.

Puede ser confuso leer las declaraciones de las clases derivadas, ya que los miembros heredados no se declaran de manera explícita en las clases derivadas, pero sin embargo están presentes en ellas. Hay un problema similar a la hora de documentar los miembros de las clases derivadas.

10.7 La clase object Como vimos al principio en este capítulo, todas las clases heredan ya sea en forma directa o indirecta de la clase object (System.Object en la FCL), por lo que todas las demás clases heredan sus siete métodos. La figura 10.19 muestra un resumen de los métodos de object. A lo largo de este libro hablaremos sobre varios métodos de object (como se indica en la tabla). También puede aprender más acerca de los métodos de object en la documentación en línea de object en la Referencia de la Biblioteca de clases del .NET Framework, en: msdn2.microsoft.com/en-us/library/system.object_members (inglés) msdn.microsoft.com/library/spa/default.asp?url=/library/SPA/cpref/html/frlrfSystemObjectClassTopic.asp (español)

Todos los tipos de arreglos heredan en forma implícita de la clase Array en el espacio de nombres System, que a su vez extiende a la clase object. Como resultado y al igual que otros objetos, un arreglo hereda los miembros de la clase object. Para obtener más información acerca de la clase Array, consulte la documentación para esta clase en la Referencia de la FCL: msdn2.microsoft.com/en-us/library/system.array_members (inglés) msdn.microsoft.com/library/spa/default.asp?url=/library/SPA/cpref/html/frlrfsystemarrayclasstopic.asp (español)

338

Capítulo 10 Programación orientada a objetos: herencia

Método

Descripción

Equals

Este método compara la igualdad entre dos objetos; devuelve true si son iguales y false en caso contrario. El método recibe cualquier objeto como argumento. Cuando debe compararse la igualdad entre objetos de una clase en particular, la clase debe redefinir el método Equals para comparar el contenido de los dos objetos. La implementación de este método debe cumplir los siguientes requerimientos: • Debe devolver false si el argumento es null. • Debe devolver true si un objeto se compara consigo mismo, como en objeto1.Equals( objeto1 ). • Debe devolver true sólo si tanto objeto1.Equals( objeto2.Equals( objeto1 ) devuelven true.

objeto2 )

como

• Para tres objetos, si objeto1.Equals( objeto2 ) devuelve true y objeto3.Equals( objeto3 ) devuelve true, entonces objeto.Equals( objeto ) también debe devolver true. • Una clase que redefina el método Equals también debe redefinir el método GetHashCode para asegurar que los objetos iguales tengan códigos hash idénticos. La implementación predeterminada de Equals sólo determina si dos referencias hacen referencia al mismo objeto en memoria. Finalize

Este método no puede declararse o llamarse en forma explícita. Cuando una clase contiene un destructor, el compilador lo renombra en forma implícita para que redefina el método protected Finalize, que sólo el recolector de basura puede llamar antes de reclamar la memoria de un objeto. Como no se garantiza que el recolector de basura reclame a un objeto, tampoco se garantiza que se vaya a ejecutar el método Finalize de un objeto. Cuando se ejecuta el método Finalize de una clase derivada, éste realiza su tarea y después invoca al método Finalize de la clase base. La implementación predeterminada de Finalize es un receptáculo que tan sólo invoca el método Finalize de la clase base.

GetHashCode

Una tabla hash es una estructura de datos que relaciona un objeto (conocido como la clave) con otro objeto (conocido como el valor). En el capítulo 26, Colecciones, hablaremos sobre Hashtable. Cuando se inserta al principio un valor en una tabla hash, se hace una llamada al método GetHashCode de la clave. La tabla hash utiliza el valor de código hash devuelto para determinar la ubicación en la que se debe insertar el valor correspondiente. La tabla hash también utiliza el código hash de la clave para localizar el valor correspondiente de la clave.

GetType

Cada objeto conoce su propio tipo en tiempo de ejecución. El método GetType (que utilizaremos en la sección 11.5) devuelve un objeto de la clase Type (espacio de nombres System) que contiene información acerca del tipo del objeto, como su nombre de clase (que se obtiene de la propiedad FullName de Type).

Memberwise-

Este método protected, que no recibe argumentos y devuelve una referencia object, crea una copia del objeto desde el cual se llamó. La implementación de este método realiza una copia superficial: los valores de las variables de instancia en un objeto se copian a otro objeto del mismo tipo. Para los tipos de referencia, sólo se copian las referencias.

Clone

ReferenceEquals ToString

Este método static recibe dos argumentos object y devuelve true si dos objetos son la misma instancia, o si son referencias null. En cualquier otro caso, devuelve false. Este método (presentado en la sección 7.4) devuelve una representación string de un objeto. La implementación predeterminada de este método devuelve el espacio de nombres, seguido de un punto y del nombre de la clase del objeto.

Figura 10.19 | Los métodos de object que todas las clases heredan en forma directa o indirecta.

10.8 Conclusión

339

10.8 Conclusión En este capítulo se introdujo el concepto de la herencia: la habilidad de crear clases mediante la absorción de los miembros de una clase existente, mejorándolos con nuevas capacidades. Aprendió las nociones de las clases base y las clases derivadas, y creó una clase derivada que hereda miembros de una clase base. En este capítulo se introdujo también el modificador de acceso protected; los métodos de clases derivadas pueden acceder a los miembros protected de la clase base. Aprendió también cómo acceder a los miembros de la clase base mediante base. Vio además cómo se utilizan los constructores en las jerarquías de herencia. Por último, aprendió acerca de los métodos de la clase object, la clase base directa o indirecta de todas las clases. En el capítulo 11, Polimorfismo, interfaces y sobrecarga de operadores, continuaremos con nuestra discusión sobre la herencia al introducir el polimorfismo: un concepto orientado a objetos que nos permite escribir aplicaciones que puedan manipular, de una forma más general, objetos de una amplia variedad de clases relacionadas por la herencia. Después de estudiar el capítulo 11, estará familiarizado con las clases, los objetos, el encapsulamiento, la herencia y el polimorfismo: los aspectos más esenciales de la programación orientada a objetos.

11

Polimorfismo, interfaces y sobrecarga de operadores

OBJETIVOS En este capítulo aprenderá lo siguiente:

Un anillo para gobernarlos a todos, un anillo para encontrarlos, un anillo para traerlos a todos y en la oscuridad enlazarlos. —John Ronald Reuel Tolkien

Q

El concepto de polimorfismo y cómo le permite “programar en general”.

Las proposiciones generales no deciden casos concretos.

Q

Cómo utilizar métodos redefinidos para efectuar el polimorfismo.

—Oliver Wendell Holmes

Q

Diferenciar entre las clases concretas y abstractas.

Q

Declarar métodos abstractos para crear clases abstractas.

Q

Cómo el poliformismo hace que los sistemas sean extensibles y administrables.

Q

Determinar el tipo de un objeto en tiempo de ejecución.

Q

Crear métodos y clases sealed.

Q

Declarar e implementar interfaces.

Q

Sobrecargar operadores para permitirles manipular objetos.

Un filósofo de imponente estatura no piensa en un vacío. Incluso, sus ideas más abstractas son, en cierta medida, condicionadas por lo que se conoce o no en el tiempo en que vive. —Alfred North Whitehead

¿Por qué, alma mía, desfalleces y te agitas por mí? —Salmos 42:5

Pla n g e ne r a l

342

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

11.1 11.2 11.3 11.4 11.5

11.6 11.7

11.8 11.9 11.10

Introducción Ejemplos de polimorfismo Demostración del comportamiento polimórfico Clases y métodos abstractos Caso de estudio: sistema de nómina utilizando polimorfismo 11.5.1 Creación de la clase base abstracta Empleado 11.5.2 Creación de la clase derivada concreta EmpleadoAsalariado 11.5.3 Creación de la clase derivada concreta EmpleadoPorHoras 11.5.4 Creación de la clase derivada concreta EmpleadoPorComision 11.5.5 Creación de la clase derivada concreta indirecta EmpleadoBaseMasComision 11.5.6 El procesamiento polimórfico, el operador is y la conversión descendente 11.5.7 Resumen de las asignaciones permitidas entre variables de la clase base y de la clase derivada Métodos y clases sealed Caso de estudio: creación y uso de interfaces 11.7.1 Desarrollo de una jerarquía IPorPagar 11.7.2 Declaración de la interfaz IPorPagar 11.7.3 Creación de la clase Factura 11.7.4 Modificación de la clase Empleado para implementar la interfaz IPorPagar 11.7.5 Modificación de la clase EmpleadoAsalariado para usarla en la jerarquía IPorPagar 11.7.6 Uso de la interfaz IPorPagar para procesar objetos Factura y Empleado mediante el polimorfismo 11.7.7 Interfaces comunes de la Biblioteca de clases del .NET Framework Sobrecarga de operadores (Opcional) Caso de estudio de ingeniería de software: incorporación de la herencia y el polimorfismo en el sistema ATM Conclusión

11.1 Introducción

Ahora continuaremos nuestro estudio de la programación orientada a objetos, explicando y demostrando el polimorfismo con las jerarquías de herencia. El polimorfismo nos permite “programar en forma general”, en vez de “programar en forma específica”. En especial, nos permite escribir aplicaciones que procesen objetos de clases que formen parte de la misma jerarquía de clases, como si todos fueran objetos de la clase. Considere el siguiente ejemplo de polimorfismo. Suponga que crearemos una aplicación que simula el movimiento de varios tipos de animales para un estudio biológico. Las clases Pez, Rana y Ave representan los tres tipos de animales bajo investigación. Imagine que cada una de estas clases extiende a la clase base Animal, que contiene un método llamado Mover y mantiene la posición actual de un animal, en forma de coordenadas x-y. Cada clase derivada implementa el método Mover. Nuestra aplicación mantiene un arreglo de referencias a objetos de las diversas clases derivadas de Animal. Para simular los movimientos de los animales, la aplicación envía a cada objeto el mismo mensaje una vez por segundo; a saber, Mover. No obstante, cada tipo específico de Animal responde a un mensaje Mover de manera única; un Pez podría nadar tres pies, una Rana podría saltar cinco pies y un Ave podría volar 10 pies. La aplicación envía el mismo mensaje (es decir, Mover) a cada objeto animal en forma genérica, pero cada objeto sabe cómo modificar sus coordenadas x-y en forma apropiada para su tipo específico de movimiento. Confiar en que cada objeto sepa cómo “hacer lo correcto” (es decir, lo que sea apropiado para ese tipo de objeto) en respuesta a la llamada al mismo método es el concepto clave del polimorfismo. El mismo mensaje (en este caso, Mover) que se envía a una variedad de objetos tiene “muchas formas” de resultados; de aquí que se utilice el término polimorfismo.

11.2 Ejemplos de polimorfismos

343

Con el polimorfismo podemos diseñar e implementar sistemas que puedan extenderse con facilidad; pueden agregarse nuevas clases con sólo modificar un poco (o nada) las porciones generales de la aplicación, siempre y cuando las nuevas clases sean parte de la jerarquía de herencia que la aplicación procesa en forma genérica. Las únicas partes de una aplicación que deben alterarse para alojar nuevas clases son las que requieren un conocimiento directo de las nuevas clases que el programador va a agregar a la jerarquía. Por ejemplo, si extendemos la clase Animal para crear la clase Tortuga (que podría responder a un mensaje Mover caminando una pulgada), necesitamos escribir sólo la clase Tortuga y la parte de la simulación que crea una instancia de un objeto Tortuga. Las porciones de la simulación que procesan a cada Animal en forma genérica pueden permanecer iguales. Este capítulo se divide en varias partes. Primero hablaremos sobre los ejemplos comunes del polimorfismo. Después proporcionaremos un ejemplo de código activo, en el que se demuestra el comportamiento polimórfico. Como pronto verá, utilizará referencias a la clase base para manipular tanto los objetos de la clase base como los de las clases derivadas mediante el polimorfismo. Después presentaremos un caso de estudio en el que volveremos a utilizar la jerarquía de empleados de la sección 10.4.5. Desarrollaremos una aplicación simple de nómina que, mediante el polimorfismo, calcula el salario semanal de varios tipos de empleados, usando el método Ingresos de cada empleado. Aunque los ingresos de cada tipo de empleado se calculan de una manera específica, el polimorfismo nos permite procesar a los empleados “en general”. En el caso de estudio ampliaremos la jerarquía para incluir dos nuevas clases: EmpleadoAsalariado (para las personas que reciben un salario semanal fijo) y EmpleadoPorHoras (para las personas que reciben un salario por horas y “tiempo y medio” por el tiempo extra). Declararemos un conjunto común de funcionalidad para todas las clases en la jerarquía actualizada en una clase “abstracta” llamada Empleado, a partir de la cual las clases EmpleadoAsalariado, EmpleadoPorHoras y EmpleadoPorComision heredan en forma directa, y la clase EmpleadoBaseMasComision4 hereda en forma indirecta. Como veremos a continuación, cuando invocamos el método Ingresos de cada empleado desde una referencia a la clase base Empleado, se realiza el cálculo correcto de los ingresos debido a las capacidades polimórficas de C#. Algunas veces, cuando se lleva a cabo el procesamiento del polimorfismo, es necesario programar “en forma específica”. Nuestro caso de estudio con Empleado demuestra que una aplicación puede determinar el tipo de un objeto en tiempo de ejecución, y actuar sobre ese objeto de manera acorde. En el caso de estudio utilizamos estas capacidades para determinar si cierto objeto empleado es un EmpleadoBaseMasComision. Si es así, incrementamos el salario base de ese empleado en 10 por ciento. El capítulo continúa con una introducción a las interfaces en C#. Una interfaz describe a un conjunto de métodos y propiedades que pueden llamarse en un objeto, pero no proporciona implementaciones concretas para ellos. Los programadores pueden declarar clases que implementen a (es decir, que proporcionen implementaciones concretas para los métodos y propiedades de) una o más interfaces. Cada miembro de interfaz debe declararse en todas las clases que implementen a la interfaz. Una vez que una clase implementa a una interfaz, todos los objetos de esa clase tienen una relación es un con el tipo de la interfaz, y se garantiza que todos los objetos de la clase proporcionarán la funcionalidad descrita por la interfaz. Esto se aplica también para todas las clases derivadas de esa clase. En especial, las interfaces son útiles para asignar la funcionalidad común a clases que posiblemente no estén relacionadas. Esto permite que los objetos de clases no relacionadas se procesen en forma polimórfica; los objetos de las clases que implementan la misma interfaz pueden responder a las mismas llamadas a los métodos. Para demostrar la creación y el uso de interfaces, modificaremos nuestra aplicación de nómina para crear una aplicación general de cuentas por pagar, que puede calcular los pagos vencidos por los ingresos de los empleados de la compañía y los montos de las facturas a pagar por los bienes comprados. Como verá, las interfaces permiten capacidades polimórficas similares a las que permite la herencia. Este capítulo termina con una introducción a la sobrecarga de operadores. En capítulos anteriores, declaramos nuestras propias clases y utilizamos métodos para realizar tareas con objetos de esas clases. La sobrecarga de operadores nos permite definir el comportamiento de los operadores integrados, como +, - y = 0 ) ? value : 0 ); // validación } // fin de set } // fin de la propiedad SalarioSemanal // calcula los ingresos; redefine el método abstracto Ingresos en Empleado public override decimal Ingresos() { return SalarioSemanal; } // fin del método Ingresos // devuelve representación string del objeto EmpleadoAsalariado public override string ToString() { return string.Format( "empleado asalariado: {0}\n{1}: {2:C}", base.ToString(), "salario semanal", SalarioSemanal ); } // fin del método ToString } // fin de la clase EmpleadoAsalariado

Figura 11.5 | La clase EmpleadoAsalariado que extiende a Empleado. devolvería el nombre completo del empleado y su número de seguro social, lo cual no representa en forma adecuada a un EmpleadoAsalariado. Para producir una representación string completa de EmpleadoAsalariado, el método ToString de la clase derivada devuelve "empleado asalariado: ", seguido de la información específica de la clase base Empleado (es decir, el primer nombre, el apellido paterno y el número de seguro social) que se obtiene al invocar el método ToString de la clase base (línea 37); éste es un excelente ejemplo de reutilización de código. La representación string de un EmpleadoAsalariado también contiene el salario semanal del empleado, el cual se obtiene mediante el uso de la propiedad SalarioSemanal de la clase.

11.5.3 Creación de la clase derivada concreta EmpleadoPorHoras La clase EmpleadoPorHoras (figura 11.6) también extiende a la clase Empleado (línea 3). La clase incluye un constructor (líneas 9-15) que recibe como argumentos un primer nombre, un apellido paterno, un número de seguro social, un sueldo por horas y el número de horas trabajadas. Las líneas 18-28 y 31-42 declaran las propiedades Sueldo y Horas para las variables de instancia sueldo y horas, respectivamente. El descriptor de acceso set en la propiedad Sueldo (líneas 24-27) asegura que sueldo sea no negativo, y el descriptor de acceso set en la propiedad Horas (líneas 37-41) asegura que horas se encuentre en el rango de 0-168 (el número total de horas

354

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

en la semana). La clase EmpleadoPorHoras también incluye el método Ingresos (líneas 45-51) para calcular los ingresos de un EmpleadoPorHoras; y el método ToString (líneas 54-59), que devuelve el tipo del empleado, a saber, "empleado por horas: ", e información específica para ese empleado. Observe que el constructor de EmpleadoPorHoras, al igual que el constructor de EmpleadoAsalariado, pasa el primer nombre, el apellido paterno y el número de seguro social al constructor de la clase base Empleado (línea 11) para inicializar las variables de instancia private de la clase base. Además, el método ToString llama al método ToString de la clase base (línea 58) para obtener la información específica del Empleado (es decir, primer nombre, apellido paterno y número de seguro social); éste es otro excelente ejemplo de reutilización de código.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

// Fig. 11.6: EmpleadoPorHoras.cs // La clase EmpleadoPorHoras que extiende a Empleado. public class EmpleadoPorHoras : Empleado { private decimal sueldo; // sueldo por hora private decimal horas; // horas trabajadas durante la semana // constructor con cinco parámetros public EmpleadoPorHoras( string nombre, string apellido, string nss, decimal sueldoPorHoras, decimal horasTrabajadas ) : base( nombre, apellido, nss ) { Sueldo = sueldoPorHoras; // valida el sueldo por horas a través de una propiedad Horas = horasTrabajadas; // valida las horas trabajadas a través de una propiedad } // fin del constructor de EmpleadoPorHoras con cinco parámetros // propiedad que obtiene y establece el sueldo del empleado por horas public decimal Sueldo { get { return sueldo; } // fin de get set { sueldo = ( value >= 0 ) ? value : 0; // validación } // fin de set } // fin de la propiedad Sueldo // propiedad que obtiene y establece las horas del empleado por horas public decimal Horas { get { return horas; } // fin de get set { horas = ( ( value >= 0 ) && ( value = 0 ) ? value : 0; // validación } // fin de set } // fin de la propiedad VentasBrutas // calcula los ingresos; redefine el método abstracto Ingresos en Empleado public override decimal Ingresos() { return TarifaComision * VentasBrutas; } // fin del método Ingresos // devuelve representación string del objeto EmpleadoPorComision public override string ToString() { return string.Format( "{0}: {1}\n{2}: {3:C}\n{4}: {5:F2}", "empleado por comisión", base.ToString(), "ventas brutas", VentasBrutas, "tarifa de comisión", TarifaComision ); } // fin del método ToString } // fin de la clase EmpleadoPorComision

Figura 11.7 | La clase EmpleadoPorHoras que extiende a Empleado. (Parte 2 de 2).

El constructor de EmpleadoPorComision también pasa el primer nombre, el apellido y el número de seguro social al constructor de Empleado (línea 10) para inicializar las variables de instancia private de Empleado. El método ToString llama al método ToString de la clase base (línea 53) para obtener la información específica del Empleado (es decir, primer nombre, apellido paterno y número de seguro social).

11.5.5 Creación de la clase derivada concreta indirecta EmpleadoBaseMasComision La clase EmpleadoBaseMasComision (figura 11.8) extiende a la clase EmpleadoPorComision (línea 3) y, por lo tanto, es una clase derivada indirecta de la clase Empleado. La clase EmpleadoBaseMasComision tiene un constructor (líneas 8-13) que recibe como argumentos un primer nombre, un apellido paterno, un número de seguro social, un monto de ventas, una tarifa de comisión y un salario base. Después pasa el primer nombre, el apellido paterno, el número de seguro social, el monto de ventas y la tarifa de comisión al constructor de EmpleadoPorComision (línea 10) para inicializar los miembros de datos private de la clase base. EmpleadoBaseMasComision también contiene la propiedad SalarioBase (líneas 17-27) para manipular la variable de instancia salarioBase. El método Ingresos (líneas 30-33) calcula los ingresos de un EmpleadoBaseMasComision. Observe que la línea 32 en el método Ingresos llama al método Ingresos de la clase base EmpleadoPorComision para calcular la porción basada en la comisión de los ingresos del empleado. Éste es otro buen ejemplo de reutilización de código. El método ToString de EmpleadoBaseMasComision (líneas 36-40) crea una representación string de un EmpleadoBaseMasComision, la cual contiene "salario base +", seguida del objeto string que se obtiene al invocar el método ToString de la clase base EmpleadoPorComision (otro buen ejemplo de reutilización de código), y después el salario base. El resultado es un objeto string que empieza con "salario base + empleado por comisión", seguido del resto de la información de EmpleadoBaseMasComision. Recuerde que el método ToString de EmpleadoPorComision obtiene el primer nombre, el apellido paterno y el número de seguro social del empleado mediante la invocación del método ToString de su clase base (es decir, Empleado); otro ejemplo

11.5 Caso de estudio: sistema de nómina utilizando polimorfismo

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

357

// Fig. 11.8: EmpleadoBaseMasComision.cs // La clase EmpleadoBaseMasComision que extiende a EmpleadoPorComision. public class EmpleadoBaseMasComision : EmpleadoPorComision { private decimal salarioBase; // salario base por semana // constructor con seis parámetros public EmpleadoBaseMasComision( string nombre, string apellido, string nss, decimal ventas, decimal tarifa, decimal salario ) : base( nombre, apellido, nss, ventas, tarifa ) { SalarioBase = salario; // valida el salario base a través de una propiedad } // fin del constructor de EmpleadoBaseMasComision con seis parámetros // propiedad que obtiene y establece // el salario base del empleado por comisión con salario base public decimal SalarioBase { get { return salarioBase; } // fin de get set { salarioBase = ( value >= 0 ) ? value : 0; // validación } // fin de set } // fin de la propiedad SalarioBase // calcula los ingresos; redefine el método Ingresos en EmpleadoPorComision public override decimal Ingresos() { return SalarioBase + base.Ingresos(); } // fin del método Ingresos // devuelve representación string del objeto EmpleadoBaseMasComision public override string ToString() { return string.Format( "{0} {1}; {2}: {3:C}", "salario base +", base.ToString(), "salario base", SalarioBase ); } // fin del método ToString } // fin de la clase EmpleadoBaseMasComision

Figura 11.8 | La clase EmpleadoBaseMasComision que extiende a EmpleadoPorComision.

más de reutilización de código. Observe que el método ToString de EmpleadoBaseMasComision inicia una cadena de llamadas a métodos que abarcan los tres niveles de la jerarquía de Empleado.

11.5.6 El procesamiento polimórfico, el operador is y la conversión descendente Para probar nuestra jerarquía de Empleado, la aplicación en la figura 11.9 crea un objeto de cada una de las cuatro clases concretas EmpleadoAsalariado, EmpleadoPorHoras, EmpleadoPorComision y EmpleadoBaseMasComision. La aplicación manipula estos objetos, primero mediante variables del mismo tipo de cada objeto y después mediante el polimorfismo, utilizando un arreglo de variables Empleado. Al procesar los objetos mediante el polimorfismo, la aplicación incrementa el salario base de cada EmpleadoBaseMasComision en 10% (desde luego que para esto se requiere determinar el tipo del objeto en tiempo de ejecución). Por último, la aplicación determina e imprime en forma polimórfica el tipo de cada objeto en el arreglo Empleado. Las líneas 10-20 crean objetos de cada una de las cuatro clases concretas derivadas de Empleado. Las líneas 24-32 imprimen en pantalla

358

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

la representación string y los ingresos de cada uno de estos objetos. Observe que Write llama en forma implícita al método ToString de cada objeto, cuando éste se imprime en pantalla como un objeto string con elementos de formato.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

// Fig. 11.9: PruebaSistemaNomina.cs // Aplicación de prueba de la jerarquía de Empleado. using System; public class PruebaSistemaNomina { public static void Main( string[] args ) { // crea objetos de clases derivadas EmpleadoAsalariado empleadoAsalariado = new EmpleadoAsalariado( "John", "Smith", "111-11-1111", 800.00M ); EmpleadoPorHoras empleadoPorHoras = new EmpleadoPorHoras( "Karen", "Price", "222-22-2222", 16.75M, 40.0M ); EmpleadoPorComision empleadoPorComision = new EmpleadoPorComision( "Sue", "Jones", "333-33-3333", 10000.00M, .06M ); EmpleadoBaseMasComision empleadoBaseMasComision = new EmpleadoBaseMasComision( "Bob", "Lewis", "444-44-4444", 5000.00M, .04M, 300.00M ); Console.WriteLine( "Empleados procesados en forma individual:\n" ); Console.WriteLine( "{0}\n{1}: {2:C}\n", empleadoAsalariado, "ingresos", empleadoAsalariado.Ingresos() ); Console.WriteLine( "{0}\n{1}: {2:C}\n", empleadoPorHoras, "ingresos", empleadoPorHoras.Ingresos() ); Console.WriteLine( "{0}\n{1}: {2:C}\n", empleadoPorComision, "ingresos", empleadoPorComision.Ingresos() ); Console.WriteLine( "{0}\n{1}: {2:C}\n", empleadoBaseMasComision, "ingresos", empleadoBaseMasComision.Ingresos() ); // crea arreglo Empleado de cuatro elementos Empleado[] empleados = new Empleado[ 4 ]; // inicializa arreglo con objetos Empleado de tipos derivados empleados[ 0 ] = empleadoAsalariado; empleados[ 1 ] = empleadoPorHoras; empleados[ 2 ] = empleadoPorComision; empleados[ 3 ] = empleadoBaseMasComision; Console.WriteLine( "Empleados procesados en forma polimórfica:\n" ); // procesa en forma generica cada elemento en el arreglo de empleados foreach ( Empleado empleadoActual in empleados ) { Console.WriteLine( empleadoActual ); // invoca a ToString // determina si el elemento es un EmpleadoBaseMasComision if ( empleadoActual is EmpleadoBaseMasComision ) {

Figura 11.9 | Aplicación de prueba de la jerarquía de Empleado. (Parte 1 de 3).

11.5 Caso de estudio: sistema de nómina utilizando polimorfismo

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73

// conversión descendente de referencia de Empleado a // referencia de EmpleadoBaseMasComision EmpleadoBaseMasComision empleado = ( EmpleadoBaseMasComision ) empleadoActual; empleado.SalarioBase *= 1.10M; Console.WriteLine( "nuevo salario base con incremento del 10%: {0:C}", empleado.SalarioBase ); } // fin de if Console.WriteLine( "ingresos {0:C}\n", empleadoActual.Ingresos() ); } // fin de foreach // obtiene el nombre del tipo de cada objeto en el arreglo de empleados for ( int j = 0; j < empleados.Length; j++ ) Console.WriteLine( "Empleado {0} es un {1}", j, empleados[ j ].GetType() ); } // fin de Main } // fin de la clase PruebaSistemaNomina

Empleados procesados en forma individual: empleado asalariado: John Smith número de seguro social: 111-11-1111 salario semanal: $800.00 ingresos: $800.00 empleado por horas: Karen Price número de seguro social: 222-22-2222 sueldo por horas: $16.75; horas trabajadas: 40.00 ingresos: $670.00 empleado por comisión: Sue Jones número de seguro social: 333-33-3333 ventas brutas: $10,000.00 tarifa de comisión: 0.06 ingresos: $600.00 salario base + empleado por comisión: Bob Lewis número de seguro social: 444-44-4444 ventas brutas: $5,000.00 tarifa de comisión: 0.04; salario base: $300.00 ingresos: $500.00 Empleados procesados en forma polimórfica: empleado asalariado: John Smith número de seguro social: 111-11-1111 salario semanal: $800.00 ingresos $800.00 empleado por horas: Karen Price número de seguro social: 222-22-2222 sueldo por horas: $16.75; horas trabajadas: 40.00 ingresos $670.00

Figura 11.9 | Aplicación de prueba de la jerarquía de Empleado. (Parte 2 de 3).

359

360

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

empleado por comisión: Sue Jones número de seguro social: 333-33-3333 ventas brutas: $10,000.00 tarifa de comisión: 0.06 ingresos $600.00 salario base + empleado por comisión: Bob Lewis número de seguro social: 444-44-4444 ventas brutas: $5,000.00 tarifa de comisión: 0.04; salario base: $300.00 nuevo salario base con incremento del 10%: $330.00 ingresos $530.00 Empleado Empleado Empleado Empleado

0 1 2 3

es es es es

un un un un

EmpleadoAsalariado EmpleadoPorHoras EmpleadoPorComision EmpleadoBaseMasComision

Figura 11.9 | Aplicación de prueba de la jerarquía de Empleado. (Parte 3 de 3).

La línea 35 declara a empleados y le asigna un arreglo de cuatro variables Empleado. Las líneas 38-41 asignan un objeto EmpleadoAsalariado, un objeto EmpleadoPorHoras, un objeto EmpleadoPorComision y un objeto EmpleadoBaseMasComision a empleados[ 0 ], empleados[ 1 ], empleados[ 2 ] y empleados[ 3 ], respectivamente. Cada asignación es permitida, ya que un EmpleadoAsalariado es un Empleado, un EmpleadoPorHoras es un Empleado, un EmpleadoPorComision es un Empleado y un EmpleadoBaseMasComision es un Empleado. Por lo tanto, podemos asignar las referencias de los objetos EmpleadoAsalariado, EmpleadoPorHoras, EmpleadoPorComision y EmpleadoBaseMasComision a variables de la clase base Empleado, aun cuando ésta es una clase abstracta. Las líneas 46-66 iteran a través del arreglo empleados e invocan los métodos ToString e Ingresos con la variable empleadoActual de Empleado, a la cual se le asigna la referencia a un Empleado distinto en el arreglo, durante cada iteración. Los resultados ilustran que en definitivo se invocan los métodos apropiados para cada clase. Todas las llamadas a los métodos ToString e Ingresos se resuelven en tiempo de ejecución, con base en el tipo del objeto al que empleadoActual hace referencia. Este proceso se conoce como vinculación dinámica o vinculación postergada. Por ejemplo, la línea 48 invoca en forma implícita al método ToString del objeto al que empleadoActual hace referencia. Como resultado de la vinculación dinámica, el CLR decide qué método ToString de cuál clase va a llamar en tiempo de ejecución, en vez de hacerlo en tiempo de compilación. Observe que sólo los métodos de la clase Empleado pueden llamarse a través de una variable Empleado; y desde luego que Empleado incluye los métodos de la clase object, como ToString. (En la sección 10.7 vimos el conjunto de métodos que todas las clases heredan de la clase object). Una referencia a la clase base puede utilizarse para invocar sólo a métodos de la clase base. Realizamos un procesamiento especial en los objetos EmpleadoBasePorComision; a medida que los encontramos, incrementamos su salario base en 10%. Cuando procesamos objetos en forma polimórfica, por lo general no necesitamos preocuparnos por los “detalles específicos”, pero para ajustar el salario base, tenemos que determinar el tipo específico de cada objeto Empleado en tiempo de ejecución. La línea 51 utiliza el operador is para determinar si el tipo de cierto objeto Empleado es EmpleadoBaseMasComision. La condición en la línea 51 es verdadera si el objeto al que hace referencia empleadoActual es un EmpleadoBaseMasComision. Esto también sería verdadero para cualquier objeto de una clase derivada de EmpleadoBaseMasComision (si hubiera alguna), debido a la relación es un que tiene una clase derivada con su clase base. Las líneas 55-65 realizan una conversión descendente en empleadoActual, del tipo Empleado al tipo EmpleadoBaseMasComision; esta conversión se permite sólo si el objeto tiene una relación es un con EmpleadoBaseMasComision. La condición en la línea 51 asegura que éste sea el caso. Esta conversión se requiere si vamos a utilizar la propiedad SalarioBase de la clase derivada EmpleadoBaseMasComision en el objeto Empleado actual; si tratamos de invocar a un método que pertenezca sólo a la clase derivada en una referencia a la clase base, se produce un error de compilación.

11.5 Caso de estudio: sistema de nómina utilizando polimorfismo

361

Error común de programación 11.3 Asignar una variable de la clase base a una variable de la clase derivada (sin una conversión descendente explícita) es un error de compilación.

Observación de ingeniería de software 11.4 Si en tiempo de ejecución se asigna la referencia a un objeto de la clase derivada a una variable de una de sus clases base directas o indirectas, es aceptable convertir la referencia almacenada en esa variable de la clase base, de vuelta a una referencia del tipo de la clase derivada. Antes de realizar dicha conversión, use el operador is para asegurar que el objeto sea de un tipo de clase derivada apropiado.

Error común de programación 11.4 Al realizar una conversión descendente sobre un objeto, se produce una excepción InvalidCastException (del espacio de nombres System) si, en tiempo de ejecución, el objeto no tiene una relación “es un” con el tipo especificado en el operador de conversión. Un objeto puede convertirse sólo a su propio tipo o al tipo de una de sus clases base.

Si la expresión is en la línea 51 es true, la instrucción if (líneas 51-62) realiza el procesamiento especial requerido para el objeto EmpleadoBaseMasComision. Usando la variable empleado de EmpleadoBaseMasComision, la línea 58 utiliza la propiedad SalarioBase, que sólo pertenece a la clase derivada, para extraer y actualizar el salario base del empleado con el aumento del 10 por ciento. Las líneas 64-65 invocan al método Ingresos en empleadoActual, el cual llama al método Ingresos del objeto de la clase derivada apropiada en forma polimórfica. Observe que al obtener en forma polimórfica los ingresos del EmpleadoAsalariado, el EmpleadoPorHoras y el EmpleadoPorComision en las líneas 64-65, se produce el mismo resultado que obtener los ingresos de estos empleados en forma individual, en las líneas 24-32. No obstante, el monto de los ingresos obtenidos para el EmpleadoBaseMasComision en las líneas 64-65 es más alto que el que se obtiene en las líneas 30-32, debido al aumento del 10% en su salario base. Las líneas 69-71 imprimen en pantalla el tipo de cada empleado, como un objeto string. Todos los objetos en C# conocen su propia clase y pueden acceder a esta información a través del método GetType, que todas las clases heredan de la clase object. El método GetType devuelve un objeto de la clase Type (del espacio de nombres System), el cual contiene información acerca del tipo del objeto, incluyendo el nombre de su clase, los nombres de sus métodos public y el nombre de su clase base. La línea 71 invoca al método GetType en el objeto para obtener su clase en tiempo de ejecución (es decir, un objeto Type que representa el tipo del objeto). Después el método ToString se invoca en forma implícita en el objeto devuelto por GetType. El método ToString de la clase Type devuelve el nombre de la clase. En el ejemplo anterior, evitamos varios errores de compilación mediante la conversión descendente de la variable Empleado a una variable EmpleadoBaseMasComision en las líneas 55-56. Si eliminamos el operador de conversión (EmpleadoBaseMasComision) de la línea 56 y tratamos de asignar la variable empleadoActual de Empleado directamente a la variable empleado de EmpleadoBaseMasComision, recibiremos un error de compilación del tipo “No se puede convertir implícitamente al tipo”. Este error indica que el intento de asignar la referencia del objeto empleadoPorComision de la clase base a la variable empleadoBaseMasComision de la clase derivada no se permite sin un operador de conversión apropiado. El compilador evita esta asignación debido a que un EmpleadoPorComision no es un EmpleadoBaseMasComision; de nuevo, la relación es un se aplica sólo entre la clase derivada y sus clases base, no viceversa. De manera similar, si las líneas 58 y 61 utilizan la variable empleadoActual de la clase base, en vez de la variable empleado de la clase derivada, para usar la propiedad SalarioBase que pertenece sólo a la clase derivada, recibiríamos un error de compilación del tipo “'Empleado' no contiene una definición para 'SalarioBase'” en cada una de estas líneas. No se permite tratar de invocar métodos que pertenezcan sólo a la clase derivada en una referencia a la clase base. Mientras que las líneas 58 y 61 se ejecutan sólo si la instrucción is en la línea 51 devuelve true para indicar que a empleadoActual se le asignó una referencia a un objeto EmpleadoBaseMasComision, no podemos tratar de usar la propiedad SalarioBase de la clase derivada EmpleadoBaseMasComision con la referencia empleadoActual de la clase base Empleado. El compilador generaría errores en las líneas 58 y 61, ya que SalarioBase no es miembro de la clase base y no puede utilizarse con una variable de la clase base. Aunque el método que se vaya a llamar en realidad depende del tipo del objeto en tiempo de ejecución, puede

362

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

utilizarse una variable para invocar sólo a los métodos que sean miembros del tipo de esa variable, lo cual verifica el compilador. Si utilizamos una variable Empleado de la clase base, sólo podemos invocar a los métodos y propiedades que se encuentran en la clase Empleado: los métodos Ingresos y ToString, y las propiedades PrimerNombre, ApellidoPaterno y NumeroSeguroSocial.

11.5.7 Resumen de las asignaciones permitidas entre variables de la clase base y de la clase derivada Ahora que hemos visto una aplicación completa que procesa diversos objetos de clases derivadas en forma polimórfica, sintetizaremos lo que puede y no puede hacer con los objetos y variables de las clases base y las clases derivadas. Aunque un objeto de una clase derivada también es un objeto de su clase base, los dos objetos son, sin embargo, distintos. Como vimos antes, los objetos de una clase derivada pueden tratarse como su fueran objetos de la clase base. No obstante, la clase derivada puede tener miembros adicionales que sólo pertenezcan a esa clase. Por esta razón, no se permite asignar una referencia de clase base a una variable de clase derivada sin una conversión explícita; dicha asignación dejaría los miembros de la clase derivada indefinidos para un objeto de la clase base. Hemos visto cuatro maneras de asignar referencias de clase base y de clase derivada a las variables de los tipos de clase base y clase derivada: 1. Asignar una referencia de clase base a una variable de clase base es un proceso simple y directo. 2. Asignar una referencia de clase derivada a una variable de clase derivada es un proceso simple y directo. 3. Asignar una referencia de clase derivada a una variable de clase base es seguro, ya que el objeto de la clase derivada es un objeto de su clase base. No obstante, esta referencia puede usarse para referirse sólo a los miembros de la clase base. Si este código hace referencia a los miembros que pertenezcan sólo a la clase derivada, a través de la variable de la clase base, el compilador reporta errores. 4. Tratar de asignar una referencia de clase base a una variable de clase derivada es un error de compilación. Para evitar este error, la referencia de clase base debe convertirse en forma explícita a un tipo de clase derivada. En tiempo de ejecución, si el objeto al que se refiere la referencia no es un objeto de la clase derivada, se producirá una excepción. (Para más información sobre el manejo de excepciones, vea el capítulo 12, Manejo de excepciones). El operador is puede utilizarse para asegurar que dicha conversión se realice sólo si el objeto es de la clase derivada.

11.6 Métodos y clases sealed En la sección 10.4 vimos que sólo pueden redefinirse en las clases derivadas los métodos que se declaran como virtual, override o abstract. Un método que se declara como sealed en una clase base no puede redefinirse en una clase derivada. Los métodos que se declaran como private son sealed de manera implícita, ya que es imposible redefinirlos en una clase derivada (aunque la clase derivada puede declarar un nuevo método con la misma firma que el método private en la clase base). Los métodos que se declaran como static también son sealed de manera implícita, ya que los métodos static tampoco pueden redefinirse. Un método de clase derivada que se declare tanto override como sealed puede redefinir a un método de la clase base, pero no puede redefinirse en clases derivadas que se encuentren a niveles más bajos de la jerarquía de herencia. La declaración de un método sealed no puede cambiar nunca, por lo que todas las clases derivadas utilizan la misma implementación del método, y las llamadas a los métodos sealed se resuelven en tiempo de compilación; a esto se le conoce como vinculación estática. Como el compilador sabe que los métodos sealed no pueden ser redefinidos, a menudo puede optimizar el código eliminando llamadas a los métodos sealed y sustituyéndolas con el código expandido de sus declaraciones en cada ubicación de llamada al método; a esta técnica se le conoce como poner el código en línea.

Tip de rendimiento 11.1 El compilador puede decidir si va a poner en línea la llamada a un método sealed, y lo hará para los métodos sealed pequeños y simples. Poner en línea el código de una llamada a un método no viola el encapsulamiento ni el ocultamiento de información y aumenta el rendimiento, ya que elimina la sobrecarga en la que se incurre al hacer una llamada a un método.

11.7 Caso de estudio: creación y uso de interfaces

363

Una clase que se declara como sealed no puede ser una clase base (es decir, una clase no puede extender a una clase sealed). Todos los métodos en una clase sealed son sealed de manera implícita. La clase string es una clase sealed. Esta clase no puede extenderse, por lo que las aplicaciones que utilizan objetos string pueden depender de la funcionalidad de los objetos string que se especifica en la FCL.

Error común de programación 11.5 Tratar de declarar una clase derivada de una clase sealed es un error de compilación.

Observación de ingeniería de software 11.5 En la FCL, la vasta mayoría de clases no se declaran como sealed. Esto permite el uso de la herencia y el polimorfismo: las herramientas fundamentales de la programación orientada a objetos.

11.7 Caso de estudio: creación y uso de interfaces En nuestro siguiente ejemplo (figuras 11.11 a 11.15) utilizaremos otra vez el sistema de nómina de la sección 11.5. Suponga que la compañía involucrada desea realizar varias operaciones de contabilidad en una sola aplicación de cuentas por pagar; además de calcular los ingresos de nómina que deben pagarse a cada empleado, la compañía debe también calcular el pago vencido en cada una de varias facturas (por los bienes comprados). Aunque se aplican a cosas no relacionadas (es decir, empleados y facturas), ambas operaciones tienen que ver con el cálculo de algún tipo de monto a pagar. Para un empleado, el pago se refiere a los ingresos del empleado. Para una factura, el pago se refiere al costo total de los bienes listados en la factura. ¿Podemos calcular esas cosas distintas, tales como los pagos vencidos para los empleados y las facturas, en forma polimórfica en una sola aplicación? ¿Ofrece C# una capacidad que requiera que las clases no relacionadas implementen un conjunto de métodos comunes (por ejemplo, un método que calcule un monto a pagar)? Las interfaces de C# ofrecen exactamente esta capacidad. Las interfaces definen y estandarizan las formas en que pueden interactuar las personas y los sistemas entre sí. Por ejemplo, los controles en un radio sirven como una interfaz entre los usuarios del radio y sus componentes internos. Los controles permiten a los usuarios realizar un conjunto limitado de operaciones (por ejemplo, cambiar la estación, ajustar el volumen, seleccionar AM o FM), y distintos radios pueden implementar los controles de distintas formas (por ejemplo, el uso de botones, perillas, comandos de voz). La interfaz especifica qué operaciones debe permitir el radio que realicen los usuarios, pero no especifica cómo deben realizarse las operaciones. De manera similar, la interfaz entre un conductor y un auto con transmisión manual incluye el volante, la palanca de cambios, el pedal del embrague, el pedal del acelerador y el pedal del freno. Esta misma interfaz se encuentra en casi todos los autos de transmisión manual, lo que permite que alguien que sabe cómo manejar cierto auto de transmisión manual sepa cómo manejar casi cualquier auto de transmisión manual. Los componentes de cada auto individual pueden tener una apariencia ligeramente distinta, pero el propósito general es el mismo; permitir que las personas conduzcan el auto. Los objetos de software también se comunican a través de interfaces. Una interfaz de C# describe un conjunto de métodos que pueden llamarse sobre un objeto, para indicar al objeto que realice cierta tarea, o que devuelva cierta pieza de información, por ejemplo. El siguiente ejemplo introduce una interfaz llamada IPorPagar, la cual describe la funcionalidad de cualquier objeto que deba ser capaz de recibir un pago y, por lo tanto, debe ofrecer un método para determinar el monto de pago vencido apropiado. La declaración de una interfaz empieza con la palabra clave interface y sólo puede contener métodos, propiedades, indexadores y eventos (hablaremos sobre los eventos en el capítulo 13, Conceptos de interfaz gráfica de usuario: parte I) abstractos. Todos los miembros de la interfaz se declaran en forma implícita como public y abstract. Además, cada interfaz puede extender a otra interfaz o más interfaces, para crear una interfaz más elaborada que otras clases puedan implementar.

Error común de programación 11.6 Es un error de compilación declarar un miembro de la interfaz public o abstract en forma explícita, debido a que es redundante en las declaraciones de miembros de interfaces. También es un error de compilación especificar cualquier detalle de implementación, tal como las declaraciones de métodos concretos, en una interfaz.

364

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

Para utilizar una interfaz, una clase debe especificar que implementa a esa interfaz mediante un listado de esa interfaz después del signo de dos puntos (:) en la declaración de la clase. Observe que ésta es la misma sintaxis que se utiliza para indicar la herencia de una clase base. Una clase concreta que implementa a esta interfaz debe declarar cada miembro de la interfaz con la firma especificada en la declaración de la interfaz. Una clase que implemente a una interfaz, pero que no implementa a todos los miembros de la misma, es una clase abstracta; debe declararse como abstract y debe contener una declaración abstract para cada miembro de la interfaz que no implemente. Implementar una interfaz es como firmar un contrato con el compilador que diga, “Proporcionaré una implementación para todos los miembros especificados por la interfaz, o los declararé como abstract”.

Error común de programación 11.7 Si no declaramos ningún miembro de una interfaz, en una clase que implemente a esa interfaz, se produce un error de compilación.

Por lo general, una interfaz se utiliza cuando clases dispares (es decir, no relacionadas) necesitan compartir métodos comunes. Esto permite que los objetos de clases no relacionadas se procesen en forma polimórfica; los objetos de clases que implementan la misma interfaz pueden responder a las mismas llamadas a métodos. Los programadores pueden crear una interfaz que describa la funcionalidad deseada y después implementar esta interfaz en cualquier clase que requiera esa funcionalidad. Por ejemplo, en la aplicación de cuentas por pagar que desarrollaremos en esta sección, implementamos la interfaz IPorPagar en cualquier clase que deba tener la capacidad de calcular el monto de un pago (por ejemplo, Empleado, Factura). A menudo, una interfaz se utiliza en vez de una clase abstract cuando no hay una implementación predeterminada que heredar; esto es, no hay campos ni implementaciones de métodos predeterminadas. Al igual que las clases public abstract, las interfaces son comúnmente de tipo public, por lo que se declaran en archivos por sí solas con el mismo nombre que la interfaz, y la extensión de archivo .cs.

11.7.1 Desarrollo de una jerarquía IPorPagar Para crear una aplicación que pueda determinar los pagos para los empleados y facturas por igual, primero vamos a crear una interfaz llamada IPorPagar. Esta interfaz contiene el método ObtenerMontoPago, el cual devuelve un monto decimal que debe pagarse para un objeto de cualquier clase que implemente a la interfaz. El método ObtenerMontoPago es una versión de propósito general del método Ingresos de la jerarquía de Empleado; el método Ingresos calcula un monto de pago específicamente para un Empleado, mientras que ObtenerMontoPago puede aplicarse a un amplio rango de objetos no relacionados. Después de declarar la interfaz IPorPagar presentaremos la clase Factura, la cual implementa a la interfaz IPorPagar. Luego modificaremos la clase Empleado de tal forma que también implemente a la interfaz IPorPagar. Por último, actualizaremos la clase EmpleadoAsalariado derivada de Empleado para “ajustarla” en la jerarquía de IPorPagar (es decir, cambiaremos el nombre del método Ingresos de EmpleadoAsalariado por el de ObtenerMontoPago).

Buena práctica de programación 11.1 Por convención, el nombre de una interfaz empieza con "I". Esto ayuda a diferenciar las interfaces de las clases, con lo cual se mejora la legibilidad del código.

Buena práctica de programación 11.2 Al declarar un método en una interfaz, seleccione un nombre para el método que describa su propósito en forma general, ya que el método podría implementarse por un amplio rango de clases no relacionadas.

Las clases Factura y Empleado representan cosas para las cuales la compañía debe calcular un monto a pagar. Ambas clases implementan a IPorPagar, por lo que una aplicación puede invocar al método ObtenerMontoPago en objetos Factura y Empleado por igual. Esto permite el procesamiento polimórfico de objetos Factura y Empleado requerido para la aplicación de cuentas por pagar de nuestra compañía. El diagrama de clases de UML en la figura 11.10 muestra la jerarquía de interfaz y clases utilizada en nuestra aplicación de cuentas por pagar. La jerarquía comienza con la interfaz IPorPagar. UML diferencia a una interfaz de una clase colocando la palabra “interface” entre los signos « y », por encima del nombre de la interfaz. UML

11.7 Caso de estudio: creación y uso de interfaces

365

«interface» IPorPagar

Factura

Empleado

EmpleadoAsalariado

Figura 11.10 | Diagrama de clases de UML de la jerarquía de clases y la interfaz IPorPagar.

1 2 3 4 5 6

// Fig. 11.11: IPorPagar.cs // Declaración de la interfaz IPorPagar. public interface IPorPagar { decimal ObtenerMontoPago(); // calcula el pago; no hay implementación } // fin de la interfaz IPorPagar

Figura 11.11 | Declaración de la interfaz IPorPagar. expresa la relación entre una clase y una interfaz a través de una realización. Se dice que una clase “realiza”, o implementa, a una interfaz. Un diagrama de clases modela una realización como una flecha punteada que parte de la clase que va a realizar la implementación, hasta la interfaz. El diagrama en la figura 11.10 indica que cada una de las clases Factura y Empleado realizan (es decir, implementan) la interfaz IPorPagar. Observe que, al igual que en el diagrama de clases de la figura 11.2, la clase Empleado aparece en cursivas, lo cual indica que es una clase abstracta. La clase concreta EmpleadoAsalariado extiende a Empleado y hereda la relación de realización de la clase base con la interfaz IPorPagar.

11.7.2 Declaración de la interfaz IPorPagar La declaración de la interfaz IPorPagar empieza en la figura 11.11, línea 3. La interfaz IPorPagar contiene el método public abstract ObtenerMontoPago (línea 5). Recuerde que este método no puede declararse en forma explícita como public o abstract. La interfaz IPorPagar sólo tiene un método, pero las interfaces pueden tener cualquier número de miembros. Además, el método ObtenerMontoPago no tiene parámetros, pero los métodos de las interfaces pueden tener parámetros.

11.7.3 Creación de la clase Factura Ahora vamos a crear la clase Factura (figura 11.12) para representar una factura simple que contiene información de facturación para cierto tipo de pieza. La clase declara las variables de instancia private numeroPieza, descripcionPieza, cantidad y precioPorArticulo (líneas 5-8), las cuales indican el número de pieza, su descripción, la cantidad de piezas ordenadas y el precio por artículo. La clase Factura también contiene un constructor (líneas 11-18), propiedades (líneas 21-70) que manipulan las variables de instancia de la clase y un método ToString (líneas 73-79) que devuelve una representación string de un objeto Factura. Observe que los descriptores de acceso set de las propiedades Cantidad (líneas 53-56) y PrecioPorArticulo (líneas 66-69) aseguran que a cantidad y a precioPorArticulo se les asignen sólo valores no negativos. La línea 3 de la figura 11.12 indica que la clase Factura implementa a la interfaz IPorPagar. Al igual que todas las clases, la clase Factura también extiende a object de manera implícita. C# no permite que las clases derivadas hereden de más de una clase base, pero sí permite que una clase herede de una clase base e implemente cualquier número de interfaces. Todos los objetos de una clase que implementan varias interfaces tienen la

366

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

relación es un con cada tipo de interfaz implementada. Para implementar más de una interfaz, utilice una lista separada por comas de nombres de interfaz después del signo de dos puntos (:) en la declaración de la clase, como se muestra a continuación: public class NombreClase : NombreClaseBase, PrimeraInterfaz, SegundaInterfaz, …

Cuando una clase hereda de una clase base e implementa a una o más interfaces, la declaración de la clase debe listar el nombre de la clase base antes de cualquier nombre de interfaz.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

// Fig. 11.12: Factura.cs // La clase Factura implementa a IPorPagar. public class Factura : IPorPagar { private string numeroPieza; private string descripcionPieza; private int cantidad; private decimal precioPorArticulo; // constructor con cuatro parámetros public Factura( string pieza, string descripcion, int cuenta, decimal precio ) { NumeroPieza = pieza; DescripcionPieza = descripcion; Cantidad = cuenta; // valida la cantidad a través de una propiedad PrecioPorArticulo = precio; // valida el precio por artículo a través de una propiedad } // fin del constructor de Factura con cuatro parámetros // propiedad que obtiene y establece el número de pieza en la factura public string NumeroPieza { get { return numeroPieza; } // fin de get set { numeroPieza = value; // debería validarse } // fin de set } // fin de propiedad NumeroPieza // propiedad que obtiene y establece la descripción de la pieza en la factura public string DescripcionPieza { get { return descripcionPieza; } // fin de get set { descripcionPieza = value; // debería validarse } // fin de set } // fin de la propiedad DescripcionPieza // propiedad que obtiene y establece la cantidad en la factura public int Cantidad

Figura 11.12 | La clase Factura implementa a IPorPagar. (Parte 1 de 2).

11.7 Caso de estudio: creación y uso de interfaces

48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86

367

{ get { return cantidad; } // fin de get set { cantidad = ( value < 0 ) ? 0 : value; // valida la cantidad } // fin de set } // fin de la propiedad Cantidad // propiedad que obtiene y establece el precio por artículo public decimal PrecioPorArticulo { get { return precioPorArticulo; } // fin de get set { precioPorArticulo = ( value < 0 ) ? 0 : value; // valida el precio } // fin de set } // fin de la propiedad PrecioPorArticulo // devuelve representación string de objeto Factura public override string ToString() { return string.Format( "{0}: \n{1}: {2} ({3}) \n{4}: {5} \n{6}: {7:C}", "factura", "número de pieza", NumeroPieza, DescripcionPieza, "cantidad", Cantidad, "precio por artículo", PrecioPorArticulo ); } // fin del método ToString // método requerido para llevar a cabo el contrato con la interfaz IPorPagar public decimal ObtenerMontoPago() { return Cantidad * PrecioPorArticulo; // calcula el costo total } // fin del método ObtenerMontoPago } // fin de la clase Factura

Figura 11.12 | La clase Factura implementa a IPorPagar. (Parte 2 de 2). La clase Factura implementa el único método de la interfaz IPorPagar; el método ObtenerMontoPago se declara en las líneas 82-85. Este método calcula el monto requerido para pagar la factura. El método multiplica los valores de cantidad y precioPorArticulo (que se obtienen a través de las propiedades apropiadas) y devuelve el resultado (línea 84). Este método cumple con el requerimiento de implementación para el método en la interfaz IPorPagar; hemos cumplido el contrato de interfaz con el compilador.

11.7.4 Modificación de la clase Empleado para implementar la interfaz IPorPagar Ahora vamos a modificar la clase Empleado para que implemente la interfaz IPorPagar. La figura 11.13 contiene la clase Empleado modificada. Esta declaración de la clase es idéntica a la de la figura 11.4, con dos excepciones. En primer lugar, la línea 3 de la figura 11.13 indica que la clase Empleado ahora implementa a la interfaz IPorPagar. En segundo lugar, como Empleado ahora implementa a la interfaz IPorPagar, debemos cambiar el nombre de Ingresos por el de ObtenerMontoPago en toda la jerarquía de Empleado. Sin embargo, al igual que con el método Ingresos en la versión de la clase Empleado de la figura 11.4, no tiene sentido implementar el método ObtenerMontoPago en la clase Empleado, ya que no podemos calcular el pago de los ingresos para un Empleado general; primero debemos conocer el tipo específico de Empleado. En la figura 11.4 declaramos el método

368

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

Ingresos como abstract por esta razón y, como resultado, la clase Empleado tuvo que declararse como abstract. Esto obliga a cada clase derivada de Empleado a redefinir Ingresos con una implementación concreta.

En la figura 11.13, manejamos esta situación de la misma forma. Recuerde que cuando una clase implementa a una interfaz, la clase hace un contrato con el compilador, en el que se establece que la clase va a implementar cada uno de los métodos en la interfaz, o los va a declarar como abstract. Si se elige la última opción, también debemos declarar la clase como abstract. Como vimos en la sección 11.4, cualquier clase derivada concreta de la clase abstracta debe implementar a los métodos abstract de la clase base. Si la clase derivada no lo hace, también debe declararse como abstract. Como lo indican los comentarios en las líneas 51-52, la clase Empleado de la figura 11.13 no implementa al método ObtenerMontoPago, por lo que la clase se declara como abstract.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

// Fig. 11.13: Empleado.cs // La clase base abstracta Empleado. public abstract class Empleado : IPorPagar { private string primerNombre; private string apellidoPaterno; private string numeroSeguroSocial; // constructor con tres parámetros public Empleado( string nombre, string apellido, string nss ) { primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; } // fin del constructor de Empleado con tres parámetros // propiedad de sólo lectura que obtiene el primer nombre del empleado public string PrimerNombre { get { return primerNombre; } // fin de get } // fin de la propiedad PrimerNombre // propiedad de sólo lectura que obtiene el apellido paterno del empleado public string ApellidoPaterno { get { return apellidoPaterno; } // fin de get } // fin de la propiedad ApellidoPaterno // propiedad de sólo lectura que obtiene el número de seguro social del empleado public string NumeroSeguroSocial { get { return numeroSeguroSocial; } // fin de get } // fin de la propiedad NumeroSeguroSocial // devuelve representación string del objeto Empleado public override string ToString()

Figura 11.13 | La clase base abstracta Empleado. (Parte 1 de 2).

11.7 Caso de estudio: creación y uso de interfaces

46 47 48 49 50 51 52 53 54

369

{ return string.Format( "{0} {1}\nnúmero de seguro social: {2}", PrimerNombre, ApellidoPaterno, NumeroSeguroSocial ); } // fin del método ToString // Nota: No implementamos aquí el método ObtenerMontoPago de IPorPagar, por lo // que esta clase debe declararse como abstracta para evitar un error de compilación. public abstract decimal ObtenerMontoPago(); } // fin de la clase abstracta Empleado

Figura 11.13 | La clase base abstracta Empleado. (Parte 2 de 2).

11.7.5 Modificación de la clase EmpleadoAsalariado para usarla en la jerarquía IPorPagar La figura 11.14 contiene una versión modificada de la clase EmpleadoAsalariado que extiende a Empleado e implementa el método ObtenerMontoPago. Esta versión de EmpleadoAsalariado es idéntica a la de la figura 11.5, con la excepción de que esta versión implementa al método ObtenerMontoPago (líneas 29-32) en vez del método Ingresos. Los dos métodos contienen la misma funcionalidad, pero tienen distintos nombres. Recuerde que la versión de IPorPagar del método tiene un nombre más general para que pueda aplicarse a clases que sean posiblemente dispares. El resto de las clases derivadas de Empleado (EmpleadoPorHoras, EmpleadoPorComision y EmpleadoBaseMasComision) también deben modificarse para que contengan el método ObtenerMontoPago en vez de Ingresos, y así reflejar el hecho de que ahora Empleado implementa a IPorPagar. Dejaremos estas modificaciones como un ejercicio y sólo utilizaremos a EmpleadoAsalariado en nuestra aplicación de prueba en esta sección.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

// Fig. 11.14: EmpleadoAsalariado.cs // La clase EmpleadoAsalariado que extiende a Empleado. public class EmpleadoAsalariado : Empleado { private decimal salarioSemanal; // constructor con cuatro parámetros public EmpleadoAsalariado( string nombre, string apellido, string nss, decimal salario ) : base( nombre, apellido, nss ) { SalarioSemanal = salario; // valida el salario a través de una propiedad } // fin del constructor de EmpleadoAsalariado con cuatro parámetros // propiedad que obtiene y establece el salario del empleado asalariado public decimal SalarioSemanal { get { return salarioSemanal; } // fin de get set { salarioSemanal = value < 0 ? 0 : value; // validación } // fin de set } // fin de la propiedad SalarioSemanal // calcula los ingresos; implementa el método de la interfaz // IPorPagar que es abstracto en la clase base Empleado

Figura 11.14 | La clase EmpleadoAsalariado que extiende a Empleado. (Parte 1 de 2).

370

29 30 31 32 33 34 35 36 37 38 39 40

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

public override decimal ObtenerMontoPago() { return SalarioSemanal; } // fin del método ObtenerMontoPago // devuelve representación string del objeto EmpleadoAsalariado public override string ToString() { return string.Format( "empleado asalariado: {0}\n{1}: {2:C}", base.ToString(), "salario semanal", SalarioSemanal ); } // fin del método ToString } // fin de la clase EmpleadoAsalariado

Figura 11.14 | La clase EmpleadoAsalariado que extiende a Empleado. (Parte 2 de 2). Cuando una clase implementa a una interfaz, se aplica la misma relación es un que proporciona la herencia. Por ejemplo, la clase Empleado implementa a IPorPagar, por lo que podemos decir que un Empleado es un IPorPagar, al igual que cualquier clase que extienda a Empleado. Por ejemplo, los objetos EmpleadoAsalariado son objetos IPorPagar. Al igual que con las relaciones de herencia, un objeto de una clase que implemente a una interfaz puede considerarse como un objeto del tipo de la interfaz. Los objetos de cualquier clase derivada de la clase que implementa a la interfaz también pueden considerarse como objetos del tipo de la interfaz. Por ende, así como podemos asignar la referencia de un objeto EmpleadoAsalariado a una variable de la clase base Empleado, también podemos asignar la referencia de un objeto EmpleadoAsalariado a una interfaz IPorPagar. Factura implementa a IPorPagar, por lo que un objeto Factura también es un objeto IPorPagar, y podemos asignar la referencia de un objeto Factura a una variable IPorPagar.

Observación de ingeniería de software 11.6 La herencia y las interfaces son similares en cuanto a su implementación de la relación es un. Un objeto de una clase que implementa a una interfaz puede considerarse como un objeto del tipo de esa interfaz. Un objeto de cualquier clase derivada de una clase que implementa a una interfaz también puede considerarse como un objeto del tipo de la interfaz.

Observación de ingeniería de software 11.7 La relación es un que existe entre las clases base y las clases derivadas, y entre las interfaces y las clases que las implementan, se mantiene cuando se pasa un objeto a un método. Cuando el parámetro de un método recibe una variable de una clase base o de un tipo de interfaz, el método procesa en forma polimórfica al objeto que recibe como argumento.

11.7.6 Uso de la interfaz IPorPagar para procesar objetos Factura y Empleado mediante el polimorfismo PruebaInterfazPorPagar (figura 11.15) ilustra que la interfaz IPorPagar puede usarse para procesar un conjunto de objetos Factura y Empleado en forma polimórfica en una sola aplicación. La línea 10 declara a objetosPorPagar y le asigna un arreglo de cuatro variables IPorPagar. Las líneas 13-14 asignan las referencias de objetos EmpleadoAsalariado a los dos elementos restantes de objetosPorPagar. Las líneas 15-18 asignan las referencias a los objetos EmpleadoSalario a los dos elementos restantes de objetosPorPagar. Estas asignaciones se permiten debido a que un objeto Factura es un objeto IPorPagar. Las líneas 24-29 utilizan una instrucción foreach para procesar cada objeto IPorPagar en objetosPorPagar de manera polimórfica, imprimiendo en pantalla el objeto como un string, junto con el pago vencido. Observe que la línea 27 invoca en forma implícita al método ToString de una referencia de la interfaz IPorPagar, aun cuando ToString no se declara en la interfaz IPorPagar; todas las referencias (incluyendo las de los tipos de interfaces) se refieren a objetos que extienden a object y, por lo tanto, tienen un método ToString. La línea 28 invoca al método ObtenerMontoPago de IPorPagar para obtener

11.7 Caso de estudio: creación y uso de interfaces

371

el monto a pagar para cada objeto en objetosPorPagar, sin importar el tipo del objeto. Los resultados revelan que las llamadas a los métodos en las líneas 27-28 invocan a la implementación de la clase apropiada de los métodos ToString y ObtenerMontoPago. Por ejemplo, cuando empleadoActual se refiere a un objeto Factura durante la primera iteración del ciclo foreach, se ejecutan los métodos ToString y ObtenerMontoPago de la clase Factura.

Observación de ingeniería de software 11.8 Todos los métodos de la clase object pueden llamarse mediante el uso de una referencia de un tipo de interfaz; la referencia se refiere a un objeto, y todos los objetos heredan los métodos de la clase object.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

// Fig. 11.15: PruebaInterfazPorPagar.cs // Prueba la interfaz IPorPagar con clases dispares. using System; public class PruebaInterfazPorPagar { public static void Main( string[] args ) { // crea arreglo IPorPagar de cuatro elementos IPorPagar[] objetosPorPagar = new IPorPagar[ 4 ]; // llena el arreglo con objetos que implementan a IPorPagar objetosPorPagar[ 0 ] = new Factura( "01234", "asiento", 2, 375.00M ); objetosPorPagar[ 1 ] = new Factura( "56789", "llanta", 4, 79.95M ); objetosPorPagar[ 2 ] = new EmpleadoAsalariado( "John", "Smith", "111-11-1111", 800.00M ); objetosPorPagar[ 3 ] = new EmpleadoAsalariado( "Lisa", "Barnes", "888-88-8888", 1200.00M ); Console.WriteLine( "Facturas y Empleados procesados en forma polimórfica:\n" ); // procesa en forma genérica cada elemento en el arreglo objetosPorPagar foreach ( IPorPagar porPagarActual in objetosPorPagar ) { // imprime en pantalla el valor de porPagarActual y el monto a pagar apropiado Console.WriteLine( "{0} \n{1}: {2:C}\n", porPagarActual, "pago vencido", porPagarActual.ObtenerMontoPago() ); } // fin de foreach } // fin de Main } // fin de la clase PruebaInterfazPorPagar

Facturas y Empleados procesados en forma polimórfica: factura: número de pieza: 01234 (asiento) cantidad: 2 precio por artículo: $375.00 pago vencido: $750.00 factura: número de pieza: 56789 (llanta) cantidad: 4 precio por artículo: $79.95 pago vencido: $319.80

Figura 11.15 | Prueba de la interfaz IPorPagar con clases dispares. (Parte 1 de 2).

372

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

empleado asalariado: John Smith número de seguro social: 111-11-1111 salario semanal: $800.00 pago vencido: $800.00 empleado asalariado: Lisa Barnes número de seguro social: 888-88-8888 salario semanal: $1,200.00 pago vencido: $1,200.00

Figura 11.15 | Prueba de la interfaz IPorPagar con clases dispares. (Parte 2 de 2).

11.7.7 Interfaces comunes de la Biblioteca de clases del .NET Framework En esta sección veremos las generalidades acerca de varias interfaces comunes en la Biblioteca de clases del .NET Framework. Estas interfaces se implementan y se utilizan de la misma forma que la interfaz que creamos en secciones anteriores (la interfaz IPorPagar en la sección 11.7.2). Las interfaces de la FCL le permiten extender muchos aspectos importantes de C# con sus propias clases. La figura 11.16 muestra los aspectos generales de varias interfaces de uso común de la FCL.

11.8 Sobrecarga de operadores Las manipulaciones en los objetos de las clases se realizan mediante el envío de mensajes (en forma de llamadas a métodos) a los objetos. Esta notación de llamadas a métodos es incómoda para ciertos tipos de clases, en especial las clases matemáticas. Para estas clases, sería conveniente utilizar el robusto conjunto de operadores integrados de C# para especificar las manipulaciones de objetos. En esta sección le mostraremos cómo permitir que estos operadores trabajen con objetos de clases, a través de un proceso conocido como sobrecarga de operadores.

Interface

Descripción

IComparable

Como vio en el capítulo 3, C# contiene varios operadores de comparación (por ejemplo, =, ==, !=)

IComponent

Cualquier clase que represente un componente puede implementar a esta interfaz, incluyendo los controles (tales como botones o etiquetas) de la Interfaz Gráfica de Usuario (GUI). La interfaz IComponent define los comportamientos que deben implementar los componentes. En el capítulo 13, Conceptos de interfaz gráfica de usuario: parte I, y en el capítulo 14, Conceptos de interfaz gráfica de usuario: parte 2, hablaremos sobre IComponent y muchos controles de GUI que implementan a esta interfaz.

Figura 11.16 | Interfaces comunes de la Biblioteca de clases del .NET Framework. (Parte 1 de 2).

11.8 Sobrecarga de operadores

373

Interface

Descripción

IDisposable

Las clases que deben proporcionar un mecanismo explícito para liberar recursos pueden implementar a esta interfaz. Algunos recursos pueden ser utilizados sólo por un programa a la vez. Además, algunos recursos tales como los archivos en disco son recursos no administrados que, a diferencia de la memoria, no pueden ser liberados por el recolector de basura. Las clases que implementan a la interfaz IDisposable proporcionan un método Dispose que puede llamarse para liberar recursos en forma explícita. En el capítulo 12, Manejo de excepciones, hablaremos un poco acerca de IDisposable. Puede aprender más acerca de esta interfaz en msdn2.microsoft.com/en-us/library/aax125c9 (inglés), o en msdn2.microsoft.com/es-es/ library/system.idisposable(VS.80).aspx (español). El artículo de MSDN Implementar un método Dispose en msdn2.microsoft.com/es-es/library/fs2xkftw(VS.80).aspx habla sobre cómo implementar en forma apropiada esta interfaz en sus clases.

IEnumerator

Se utiliza para iterar a través de los elementos de una colección (como un arreglo), un elemento a la vez. La interfaz IEnumerator contiene el método MoveNext para desplazarse al siguiente elemento en una colección, el método Reset para desplazarse a la posición anterior al primer elemento, y la propiedad Current para devolver el objeto en la ubicación actual. En el capítulo 26, Colecciones, utilizaremos a IEnumerator.

Figura 11.16 | Interfaces comunes de la Biblioteca de clases del .NET Framework. (Parte 2 de 2).

Observación de ingeniería de software 11.9 Use la sobrecarga de operadores cuando ésta haga que la aplicación sea más clara que lograr las mismas operaciones con llamadas explícitas a métodos.

C# le permite sobrecargar la mayoría de los operadores para hacerlos sensibles al contexto en el que se utilicen. Algunos operadores se sobrecargan con frecuencia, en especial varios operadores aritméticos tales como + y –. El trabajo realizado por los operadores sobrecargados también puede realizarse mediante llamadas explícitas a métodos, pero la notación de los operadores es por lo común más natural. Las figuras 11.17 y 11.18 muestran un ejemplo del uso de la sobrecarga de operadores, con una clase NumeroComplejo. La clase NumeroComplejo (figura 11.17) sobrecarga los operadores de suma (+), resta (-) y multiplicación (*) para permitir a los programas sumar, restar y multiplicar instancias de la clase NumeroComplejo mediante el uso de la notación matemática común. Las líneas 8-9 declaran variables de instancia para las partes real e imaginaria del número complejo.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

// Fig. 11.17: NumeroComplejo.cs // Clase que sobrecarga operadores para sumar, restar // y multiplicar números complejos. using System; public class NumeroComplejo { private double real; // componente real del número complejo private double imaginario; // componente imaginario del número complejo // constructor public NumeroComplejo( double a, double b ) { real = a; imaginario = b;

Figura 11.17 | Clase que sobrecarga operadores para sumar, restar y multiplicar números complejos. (Parte 1 de 2).

374

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

} // fin del constructor // devuelve representación string de NumeroComplejo public override string ToString() { return string.Format( "({0} {1} {2}i)", Real, ( Imaginario < 0 ? "-" : "+" ), Math.Abs( Imaginario ) ); } // fin del método ToString // propiedad de sólo lectura que obtiene el componente real public double Real { get { return real; } // fin de get } // fin de la propiedad Real // propiedad de sólo lectura que obtiene el componente imaginario public double Imaginario { get { return imaginario; } // fin de get } // fin de la propiedad Imaginario // sobrecarga el operador de suma public static NumeroComplejo operator+( NumeroComplejo x, NumeroComplejo y ) { return new NumeroComplejo( x.Real + y.Real, x.Imaginario + y.Imaginario ); } // fin del operator + // sobrecarga el operador de resta public static NumeroComplejo operator-( NumeroComplejo x, NumeroComplejo y ) { return new NumeroComplejo( x.Real - y.Real, x.Imaginario - y.Imaginario ); } // fin del operador // sobrecarga el operador de multiplicación public static NumeroComplejo operator*( NumeroComplejo x, NumeroComplejo y ) { return new NumeroComplejo( x.Real * y.Real - x.Imaginario * y.Imaginario, x.Real * y.Imaginario + y.Real * x.Imaginario ); } // fin del operador * } // fin de la clase NumeroComplejo

Figura 11.17 | Clase que sobrecarga operadores para sumar, restar y multiplicar números complejos. (Parte 2 de 2).

Las líneas 44-49 sobrecargan el operador de suma (+) para realizar la suma de objetos NumeroComplejo. La palabra clave operator, seguida de un símbolo de operador, indica que un método sobrecarga el operador especificado. Los métodos que sobrecargan operadores binarios deben recibir dos argumentos. El primer argumento

11.8 Sobrecarga de operadores

375

es el operando izquierdo, y el segundo argumento es el operando derecho. El operador de suma sobrecargado de la clase NumeroComplejo recibe dos referencias NumeroComplejo como argumentos y devuelve un NumeroComplejo que representa la suma de los argumentos. Observe que este método se marca como public y static, obligatorio para los operadores sobrecargados. El cuerpo del método (líneas 47-48) realiza la suma y devuelve el resultado como un nuevo NumeroComplejo. Observe que no modificamos el contenido de ninguno de los operandos originales que se pasan como los argumentos x y y. Esto concuerda con nuestro sentido intuitivo de cómo debe comportarse este operador: al sumar dos números no se modifica ninguno de los números originales. Las líneas 52-66 proporcionan operadores sobrecargados similares para restar y multiplicar objetos NumeroComplejo.

Observación de ingeniería de software 11.10 Sobrecargue operadores para realizar la misma función o funciones similares en los objetos de clases que los operadores realizan en objetos de tipos simples. Evite el uso no intuitivo de los operadores.

Observación de ingeniería de software 11.11 Al menos un argumento del método de un operador sobrecargado debe ser una referencia a un objeto de la clase en la que se sobrecarga el operador. Esto evita que los programadores cambien la forma en que trabajan los operadores con los tipos simples.

La clase PruebaComplejo (figura 11.18) demuestra el uso de los operadores sobrecargados para sumar, restar y multiplicar objetos NumeroComplejo. Las líneas 14-27 piden al usuario que introduzca dos números complejos, y después utilizan esta entrada para crear dos objetos NumeroComplejo y asignarlos a las variables x y y.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

// Fig 11.18: PruebaComplejo.cs // Sobrecarga de operadores para números complejos. using System; public class PruebaComplejo { public static void Main( string[] args ) { // declara dos variables para almacenar números complejos // que introduce el usuario NumeroComplejo x, y; // pide al usuario que introduzca el primer número complejo Console.Write( "Escriba la parte real del número complejo x: " ); double parteReal = Convert.ToDouble( Console.ReadLine() ); Console.Write( "Escriba la parte imaginaria del número complejo x: " ); double parteImaginaria = Convert.ToDouble( Console.ReadLine() ); x = new NumeroComplejo( parteReal, parteImaginaria ); // pide al usuario que introduzca el segundo número complejo Console.Write( "\nEscriba la parte real del número complejo y: " ); parteReal = Convert.ToDouble( Console.ReadLine() ); Console.Write( "Escriba la parte imaginaria del número complejo y: " ); parteImaginaria = Convert.ToDouble( Console.ReadLine() ); y = new NumeroComplejo( parteReal, parteImaginaria ); // muestra en pantalla los resultados de los cálculos con x y y Console.WriteLine(); Console.WriteLine( "{0} + {1} = {2}", x, y, (x + y ).ToString() );

Figura 11.18 | Sobrecarga de operadores para números complejos. (Parte 1 de 2).

376

32 33 34 35

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

Console.WriteLine( "{0} - {1} = {2}", x, y, x - y ); Console.WriteLine( "{0} * {1} = {2}", x, y, x * y ); } // fin del método Main } // fin de la clase PruebaComplejo

Escriba la parte real del número complejo x: 2 Escriba la parte imaginaria del número complejo x: 4 Escriba la parte real del número complejo y: 4 Escriba la parte imaginaria del número complejo y: -2 (2 + 4i) + (4 - 2i) = (6 + 2i) (2 + 4i) - (4 - 2i) = (-2 + 6i) (2 + 4i) * (4 - 2i) = (16 + 12i)

Figura 11.18 | Sobrecarga de operadores para números complejos. (Parte 2 de 2).

Las líneas 31-33 suman, restan y multiplican x y y con los operadores sobrecargados, y después imprimen los resultados en pantalla. En la línea 31, para realizar la suma utilizamos el operador de suma con los operandos NumeroComplejo x y y. Sin la sobrecarga de operadores, la expresión x + y no tendría sentido; el compilador no sabría cómo deberían sumarse los dos objetos. Esta expresión tiene sentido aquí debido a que hemos definido el operador de suma para dos objetos NumeroComplejo en las líneas 44-49 de la figura 11.17. Cuando se “suman” los dos objetos NumeroComplejo en la línea 31 de la figura 11.18, se invoca la declaración de operador+, se pasa el operando izquierdo como el primer argumento y el operando derecho como el segundo argumento. Cuando utilizamos los operadores de resta y multiplicación en las líneas 32-33, se invocan de manera similar sus respectivas declaraciones de operadores sobrecargados. Observe que el resultado de cada cálculo es una referencia a un nuevo objeto NumeroComplejo. Cuando se pasa este nuevo objeto al método WriteLine de la clase Console, se invoca en forma implícita a su método ToString (líneas 19-23 de la figura 11.17). No necesitamos asignar un objeto a una variable de tipo de referencia para invocar a su método ToString. La línea 31 de la figura 11.18 podría reescribirse para invocar en forma explícita al método ToString del objeto creado por el operador de suma sobrecargado, como en: Console.WriteLine( "{0} + {1} = {2}", x, y, ( x + y ).ToString() );

11.9 (Opcional) Caso de estudio de ingeniería de software: incorporación de la herencia y el polimorfismo en el sistema ATM Ahora regresaremos a nuestro diseño del sistema ATM para ver cómo podría beneficiarse de la herencia y el polimorfismo. Para aplicar la herencia, primero buscamos características comunes entre las clases del sistema. Creamos una jerarquía de herencia para modelar las clases similares en una forma elegante y eficiente que nos permita procesar objetos de estas clases mediante el polimorfismo. Después modificamos nuestro diagrama de clases para incorporar las nuevas relaciones de herencia. Por último, demostramos cómo traducir los aspectos relacionados con la herencia de nuestro diseño actualizado, a código de C#. En la sección 4.11 nos topamos con el problema de representar una transacción financiera en el sistema. En vez de crear una clase para representar a todos los tipos de transacciones, creamos tres clases distintas de transacciones (SolicitudSaldo, Retiro y Deposito) para representar las transacciones que puede realizar el sistema ATM. El diagrama de clases de la figura 11.19 muestra los atributos y operaciones de estas clases. Observe que tienen un atributo privado (numeroCuenta) y una operación pública (Ejecutar) en común. Cada clase requiere que el atributo numeroCuenta especifique la cuenta a la que se aplica la transacción. Cada clase contiene la operación Ejecutar(), que el ATM invoca para realizar la transacción. Es evidente que SolicitudSaldo, Retiro y Deposito representan tipos de transacciones. La figura 11.19 revela las características comunes entre las clases de transacciones, por lo que el uso de la herencia para factorizar las características comunes parece apropiado para diseñar estas clases. Colocamos la funcionalidad común en la clase base Transaccion y derivamos las clases SolicitudSaldo, Retiro y Deposito de Transaccion (figura 11.20).

11.9 Incorporación de la herencia y el polimorfismo en el sistema ATM

377

SolicitudSaldo – numeroCuenta : int + Ejecutar() Retiro

Deposito

– numeroCuenta : int – monto : decimal

– numeroCuenta : int – monto : decimal

+ Ejecutar()

+ Ejecutar()

Figura 11.19 | Atributos y operaciones de las clases SolicitudSaldo, Retiro y Deposito. UML especifica una relación conocida como generalización para modelar la herencia. La figura 11.20 es el diagrama de clases que modela la relación de herencia entre la clase base Transaccion y sus tres clases derivadas. Las flechas con puntas triangulares huecas indican que las clases SolicitudSaldo, Retiro y Deposito se derivan de la clase Transaccion por herencia. Se dice que la clase Transaccion es una generalización de sus clases derivadas. Se dice que las clases derivadas son especializaciones de la clase Transaccion. Como se muestra en la figura 11.19, las clases SolicitudSaldo, Retiro y Deposito comparten el atributo privado int numeroCuenta. Nos gustaría factorizar este atributo común y colocarlo en la clase base Transaccion. Sin embargo, recuerde que los atributos privados de una clase base no son accesibles en las clases derivadas. Las clases derivadas de Transaccion requieren acceso al atributo numeroCuenta, para poder especificar cuál Cuenta se va a procesar en la BaseDatosBanco. Como vimos en el capítulo 10, una clase derivada sólo puede tener acceso a los miembros public, protected internal de su clase base. No obstante, las clases derivadas en este caso no necesitan modificar el atributo numeroCuenta; sólo necesitan acceder a su valor. Por esta razón hemos optado por sustituir el atributo privado numeroCuenta en nuestro modelo con la propiedad pública de sólo lectura NumeroCuenta. Como ésta es una propiedad de sólo lectura, sólo proporciona un descriptor de acceso get para acceder al número de cuenta. Cada clase derivada hereda esta propiedad, lo cual le permite acceder a su número de cuenta según lo necesite para ejecutar una transacción. Ya no listamos numeroCuenta en el segundo compartimiento de cada clase derivada, ya que las tres clases derivadas heredan la propiedad NumeroCuenta de Transaccion. De acuerdo con la figura 11.19, las clases SolicitudSaldo, Retiro y Deposito también comparten la operación Ejecutar, por lo que la clase base Transaccion debería contener la operación public Ejecutar. Sin embargo, no tiene sentido implementar a Ejecutar en la clase Transaccion, ya que la funcionalidad que proporciona esta operación depende del tipo específico de la transacción actual. Por lo tanto, declaramos a

Transaccion + «property» NumeroCuenta : int {readOnly} + Ejecutar()

SolicitudSaldo + Ejecutar()

Retiro

Deposito

– monto : decimal

– monto : decimal

+ Ejecutar()

+ Ejecutar()

Figura 11.20 | Diagrama de clases que modela la relación de generalización (es decir, herencia) entre la clase base Transaccion y sus clases derivadas SolicitudSaldo, Retiro y Deposito.

378

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

Ejecutar como una operación abstracta en la clase base Transaccion; se convertirá en un método abstract en la implementación en C#. Esto hace a Transaccion una clase abstracta y obliga a cualquier clase derivada de Transaccion que deba ser una clase concreta (es decir, SolicitudSaldo, Retiro y Deposito) a implementar la operación Ejecutar, para hacer que la clase derivada sea concreta. UML requiere que coloquemos los nombres de clase abstractos y las operaciones abstractas en cursivas. Por ende, en la figura 11.20 Transaccion y Ejecutar aparecen en cursivas para la clase Transaccion; Ejecutar no se coloca en cursivas en las clases derivadas SolicitudSaldo, Retiro y Deposito. Cada clase derivada redefine a la operación Ejecutar de Transaccion con una implementación concreta apropiada. Observe que la figura 11.20 incluye la operación Ejecutar en el tercer compartimiento de las clases SolicitudSaldo, Retiro y Deposito, ya que cada clase tiene una implementación concreta distinta de la operación redefinida. Como aprendió en este capítulo, una clase derivada puede heredar la interfaz y la implementación de una clase base. En comparación con una jerarquía diseñada para la herencia de implementación, una diseñada para la herencia de interfaz tiende a tener su funcionalidad a un nivel más bajo en la jerarquía; una clase base significa una o más operaciones que debe definir cada clase en la jerarquía, pero las clases derivadas individuales proporcionan sus propias implementaciones de la(s) operación(es). La jerarquía de herencia diseñada para el sistema ATM aprovecha este tipo de herencia, la cual proporciona a la clase ATM una manera elegante de ejecutar todas las transacciones “en general” (es decir, en forma polimórfica). Cada clase derivada de Transaccion hereda ciertos detalles de implementación (por ejemplo, la propiedad NumeroCuenta), pero el principal beneficio de incorporar la herencia en nuestro sistema es que las clases derivadas comparten una interfaz común (por ejemplo, la operación abstracta Ejecutar). La clase ATM puede dirigir una referencia Transaccion a cualquier transacción, y cuando ATM invoca a la operación Ejecutar a través de esta referencia, se ejecuta la versión de Ejecutar específica para esa transacción (en forma polimórfica) de manera automática (debido al polimorfismo). Por ejemplo, suponga que un usuario selecciona la opción para solicitar su saldo. La clase ATM dirige una referencia Transaccion a un nuevo objeto de la clase SolicitudSaldo, que el compilador de C# permite debido a que un objeto SolicitudSaldo es un objeto Transaccion. Cuando la clase ATM utiliza esta referencia para invocar a Ejecutar, se llama a la versión de Ejecutar de SolicitudSaldo (en forma polimórfica). Este enfoque polimórfico también facilita la extensibilidad del sistema. Si deseamos crear un nuevo tipo de transacción (por ejemplo, una transferencia de fondos o el pago de un recibo), tan sólo tenemos que crear una clase derivada de Transaccion adicional que redefina la operación Ejecutar con una versión apropiada para el nuevo tipo de transacción. Sólo tendríamos que realizar pequeñas modificaciones al código del sistema, para permitir que los usuarios seleccionaran el nuevo tipo de transacción del menú principal y para que la clase ATM creara instancias y ejecutara objetos de la nueva clase derivada. La clase ATM podría ejecutar transacciones del nuevo tipo utilizando el código actual, ya que éste ejecuta todas las transacciones de manera idéntica (a través del polimorfismo). Como aprendió antes en este capítulo, una clase abstracta tal como Transaccion es una para la cual el programador nunca tendrá la intención de (y de hecho, no podrá) crear objetos. Una clase abstracta sólo declara los atributos y comportamientos comunes para sus clases derivadas en una jerarquía de herencia. La clase Transaccion define el concepto de lo que significa ser una transacción que tiene un número de cuenta y puede ejecutarse. Tal vez usted se pregunte por qué nos tomamos la molestia de incluir la operación abstracta Ejecutar en la clase Transaccion, si Ejecutar carece de una implementación completa. En concepto, incluimos esta operación porque es el comportamiento que define a todas las transacciones: ejecutarse. Técnicamente, debemos incluir la operación Ejecutar en la clase base Transaccion, de manera que la clase ATM (o cualquier otra clase) pueda invocar a la versión redefinida de esta operación de cada clase derivada en forma polimórfica, a través de una referencia Transaccion. Las clases derivadas SolicitudSaldo, Retiro y Deposito heredan la propiedad NumeroCuenta de la clase base Transaccion, pero las clases Retiro y Deposito contienen el atributo adicional monto que las diferencia de la clase SolicitudSaldo. Las clases Retiro y Deposito requieren este atributo adicional para almacenar el monto de dinero que el usuario desea retirar o depositar. La clase SolicitudSaldo no necesita dicho atributo puesto que sólo requiere un número de cuenta para ejecutarse. Aun cuando dos de las tres clases derivadas de Transaccion comparten el atributo monto, no lo colocamos en la clase base Transaccion; en la clase base sólo colocamos las características comunes para todas las clases derivadas, para que éstas no hereden atributos (y operaciones) innecesarios.

11.9 Incorporación de la herencia y el polimorfismo en el sistema ATM

1

379

1 Teclado

1

1

DispensadorEfectivo

1

1 RanuraDeposito

Pantalla

1

0..1

1

Retiro

1 1

1

1

1

ATM

0..1

0..1 Ejecuta 1

0..1

Transaccion 0..1

0..1

Deposito

0..1

1 Autentica al usuario contra 1

SolicitudSaldo

1 BaseDatosBanco

Contiene

Accede a/modifica el saldo de una cuenta a través de

1 0..1

Cuenta

Figura 11.21 | Diagrama de clases del sistema ATM (en el que se incorpora la herencia). Observe que el nombre de la clase abstracta Transaccion aparece en cursivas. La figura 11.21 presenta un diagrama de clases actualizado de nuestro modelo, en el cual se incorpora la herencia y se introduce la clase base abstracta Transaccion. Modelamos una asociación entre la clase ATM y la clase Transaccion para mostrar que la clase ATM, en cualquier momento dado, está ejecutando una transacción o no lo está (es decir, existen cero o un objetos de tipo Transaccion en el sistema, en cualquier momento dado). Como un Retiro es un tipo de Transaccion, ya no dibujamos una línea de asociación directamente entre la clase ATM y la clase Retiro; la clase derivada Retiro hereda la asociación de la clase Transaccion con la clase ATM. Las clases derivadas SolicitudSaldo y Deposito también heredan esta asociación, la cual sustituye a las asociaciones que omitimos antes entre las clases SolicitudSaldo y Deposito, y la clase ATM. Observe de nuevo el uso de las puntas de flecha triangulares sin relleno para indicar las especializaciones (es decir, clases derivadas) de la clase Transaccion, como se indica en la figura 11.20. También agregamos una asociación entre la clase Transaccion y la clase BaseDatosBanco (figura 11.21). Todos los objetos Transaccion requieren una referencia a BaseDatosBanco, de manera que puedan acceder y modificar la información de las cuentas. Cada clase derivada de Transaccion hereda esta referencia, por lo que ya no tenemos que modelar la asociación entre la clase Retiro y BaseDatosBanco. Observe que la asociación entre la clase Transaccion y BaseDatosBanco sustituye a las asociaciones que omitimos antes entre las clases SolicitudSaldo y Deposito, y BaseDatosBanco. Incluimos una asociación entre la clase Transaccion y la clase Pantalla, debido a que todos los objetos Transaccion muestran los resultados al usuario a través de la Pantalla. Cada clase derivada hereda esta asociación. Por lo tanto, ya no incluimos la asociación que modelamos antes entre Retiro y Pantalla. No obstante, la clase Retiro aún participa en las asociaciones con DispensadorEfectivo y Teclado; estas asociaciones se aplican a la clase derivada Retiro, pero no a las clases derivadas SolicitudSaldo y Deposito, por lo que no movemos estas asociaciones a la clase base Transaccion.

380

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

Nuestro diagrama de clases que incorpora la herencia (figura 11.21) también modela las clases Deposito y SolicitudSaldo. Mostramos las asociaciones entre Deposito y las clases RanuraDeposito y Teclado. Observe que la clase SolicitudSaldo participa sólo en las asociaciones heredadas de la clase Transaccion; una SolicitudSaldo interactúa sólo con la BaseDatosBanco y la Pantalla. El diagrama de clases de la figura 9.23 muestra los atributos, las propiedades y las operaciones con marcadores de visibilidad. Ahora presentamos un diagrama de clases modificado en la figura 11.22, el cual incluye la clase base abstracta Transaccion. Este diagrama abreviado no muestra las relaciones de herencia (éstas aparecen en la figura 11.21), sino los atributos y las operaciones después de haber empleado la herencia en nuestro sistema. Observe que el nombre de la clase base abstracta Transaccion y el nombre de la operación abstracta Ejecutar aparecen en cursivas. Para ahorrar espacio, como hicimos en la figura 5.16, no incluimos los atributos mostrados por las asociaciones en la figura 11.22; sin embargo, los incluimos en la implementación en C# del apéndice J. También omitimos todos los parámetros de las operaciones, como hicimos en la figura 9.23; al incorporar la herencia no se afectan los parámetros que ya estaban modelados en las figuras 7.22-7.24.

ATM – usuarioAutenticado : bool = false

Transaccion + «property» NumeroCuenta : int {readOnly} + Ejecutar() SolicitudSaldo + Ejecutar()

Cuenta – numeroCuenta : int – nip : int + «property» SaldoDisponible decimal {readOnly} + «property» SaldoTotal : decimal {readOnly} + ValidarNIP : bool + Abonar() + Cargar()

Pantalla + MostrarMensaje()

Retiro

Teclado

– monto : decimal + Ejecutar()

+ ObtenerEntrada() : int Deposito

– monto : decimal + Ejecutar()

DispensadorEfectivo – cuentaBilletes : int = 500 + DispensarEfectivo() + HaySuficienteEfectivoDisponible() : bool

BaseDatosBanco RanuraDeposito + AutenticarUsuario() : bool + ObtenerSaldoDisponible() : decimal + ObtenerSaldoTotal() : decimal + Abonar() + Cargar()

+ SeRecibioSobre() : bool

Figura 11.22 | Diagrama de clases después de incorporar la herencia en el sistema.

11.9 Incorporación de la herencia y el polimorfismo en el sistema ATM

381

Observación de ingeniería de software 11.12 Un diagrama de clases completo muestra todas las asociaciones entre clases, junto con todos los atributos y operaciones para cada clase. Cuando el número de atributos, operaciones y asociaciones de las clases es substancial (como en las figuras 11.21 y 11.22), una buena práctica que promueve la legibilidad es dividir esta información entre dos diagramas de clases: uno que se enfoque en las asociaciones y el otro en los atributos y operaciones. Al examinar las clases modeladas en esta forma, es imprescindible considerar ambos diagramas de clases para obtener una idea completa acerca de las clases. Por ejemplo, uno debe referirse a la figura 11.21 para observar la relación de herencia entre Transaccion y sus clases derivadas; esa relación se omite en la figura 11.22.

Implementación del diseño del sistema ATM en el que se incorpora la herencia En la sección 9.17 empezamos a implementar el diseño del sistema ATM en código de C#. Ahora modificaremos nuestra implementación para incorporar la herencia, usando la clase Retiro como ejemplo. 1. Si la clase A es una generalización de la clase B, entonces la clase B se deriva (y es una especialización) de la clase A. Por ejemplo, la clase base abstracta Transaccion es una generalización de la clase Retiro. Por ende, la clase Retiro se deriva (y es una especialización) de la clase Transaccion. La figura 11.23 contiene la estructura de la clase Retiro, en la que la definición de la clase indica la relación de herencia entre Retiro y Transacción (línea 3). 2. Si la clase A es una clase abstracta y la clase B se deriva de la clase A, entonces la clase B debe implementar las operaciones abstractas de la clase A, si la clase B va a ser una clase concreta. Por ejemplo, la clase Transaccion contiene la operación abstracta Ejecutar, por lo que la clase Retiro debe implementar esta operación si queremos crear instancias de objetos Retiro. La figura 11.24 contiene las porciones del código en C# para la clase Retiro que pueden inferirse de las figuras 11.21 y 11.22. La clase Retiro hereda la propiedad NumeroCuenta de la clase base Transaccion, por lo que Retiro no declara esta propiedad. La clase Retiro también hereda referencias a las clases Pantalla y BaseDatosBanco de la clase Transaccion, por lo que no incluimos estas referencias en nuestro código. La figura 11.22 especifica el atributo monto y la operación Ejecutar para la clase Retiro. La línea 6 de la figura 11.24 declara una variable de instancia para el atributo monto. Las líneas 17-20 declaran la estructura de un método para la operación Ejecutar. Recuerde que la clase derivada Retiro debe proporcionar una implementación concreta del método abstract Ejecutar de la clase base Transaccion. Las referencias teclado y dispensadorEfectivo (líneas 7-8) son variables de instancia, cuya necesidad es aparente debido a las asociaciones de la clase Retiro en la figura 11.21; en la implementación en C# de esta clase en el apéndice J, un constructor inicializa estas referencias a objetos reales.

1 2 3 4 5 6

// Fig 11.23: Retiro.cs // La clase Retiro representa una transacción de retiro del ATM. public class Retiro : Transaccion { // código para los miembros de la clase Retiro } // fin de la clase Retiro

Figura 11.23 | Código en C# para la estructura de la clase Retiro.

1 2 3 4 5 6 7

// Fig 11.24: Retiro.cs // La clase Retiro representa una transacción de retiro del ATM. public class Retiro : Transaccion { // atributos private decimal monto; // monto a retirar private Teclado teclado; // referencia al teclado

Figura 11.24 | Código en C# para la clase Retiro, con base en las figuras 11.21 y 11.22. (Parte 1 de 2).

382

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

8

private DispensadorEfectivo dispensadorEfectivo; // referencia al dispensador de efectivo

9 10 11 12 13 14 15 16 17 18 19 20 21

// constructor sin parámetros public Retiro() { // código del cuerpo del constructor } // fin del constructor // método que redefine a Ejecutar public override void Ejecutar() { // código del cuerpo del método Ejecutar } // fin del método Ejecutar } // fin de la clase Retiro

Figura 11.24 | Código en C# para la clase Retiro, con base en las figuras 11.21 y 11.22. (Parte 2 de 2). En la sección J.2 de la implementación del ATM hablaremos sobre el procesamiento polimórfico de objetos La clase ATM realiza la llamada polimórfica al método Ejecutar en la línea 99 de la figura J.1.

Transaccion.

Conclusión del caso de estudio sobre el ATM Esto concluye nuestro diseño orientado a objetos del sistema ATM. En el apéndice J aparece una implementación en C# completa del sistema ATM, en 655 líneas de código. Esta implementación funcional utiliza la mayor parte de los conceptos de programación orientada a objetos que hemos cubierto hasta este punto en el libro, incluyendo: clases, objetos, encapsulamiento, visibilidad, composición, herencia y polimorfismo. El código contiene abundantes comentarios y se conforma a las prácticas de codificación que usted ha aprendido hasta ahora. Dominar este código será una maravillosa experiencia culminante para usted, después de haber estudiado las nueve secciones del Caso de estudio de ingeniería de software en los capítulos 1, 3 al 9 y 11.

Ejercicios de autoevaluación del Caso de estudio de ingeniería de software 11.1

UML utiliza una flecha con una ________ para indicar una relación de generalización. a) punta con relleno sólido b) punta triangular sin relleno c) punta hueca en forma de diamante d) punta lineal

11.2 Indique si el siguiente enunciado es verdadero o falso y, si es falso, explique por qué: UML requiere que subrayemos los nombres de las clases abstractas y los nombres de las operaciones abstractas. 11.3 Escriba código en C# para empezar a implementar el diseño para la clase Transaccion que se especifica en las figuras 11.21 y 11.22. Asegúrese de incluir las referencias private basadas en las asociaciones de la clase Transaccion. Asegúrese también de incluir las propiedades con los descriptores de acceso public get para cualquiera de las variables de instancia private a las que deban acceder las clases derivadas, para realizar sus tareas.

Respuestas a los ejercicios de autoevaluación del Caso de estudio de ingeniería de software 11.1

b.

11.2

Falso. UML requiere que se escriban los nombres de las clases y de las operaciones en cursivas.

11.3 El diseño para la clase Transaccion produce el código de la figura 11.25. En la implementación en el apéndice J, un constructor inicializa las variables de instancia private pantallaUsuario y baseDatos para objetos reales, y las propiedades de sólo lectura PantallaUsuario y BaseDatos acceden a estas variables de instancia. Estas propiedades permiten que las clases derivadas de Transaccion accedan a la pantalla del ATM e interactúen con la base de datos del banco. Observe que elegimos los nombres de las propiedades PantallaUsuario y BaseDatos por cuestión de claridad; queremos evitar nombres de propiedades que sean iguales que los nombres de las clases Pantalla y BaseDatosBanco, lo cual puede ser confuso.

11.10 Conclusión

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

383

// Fig 11.25: Transaccion.cs // La clase base abstracta Transaccion representa una transacción del ATM. public abstract class Transaccion { private int numeroCuenta; // indica la cuenta involucrada private Pantalla pantallaUsuario; // pantalla del ATM private BaseDatosBanco baseDatos; // base de datos de información de las cuentas // constructor sin parámetros public Transaccion() { // código del cuerpo del constructor } // fin del constructor // propiedad de sólo lectura que obtiene el número de cuenta public int NumeroCuenta { get { return numeroCuenta; } // fin de get } // fin de la propiedad NumeroCuenta // propiedad de sólo lectura que obtiene la referencia a la pantalla public Pantalla pantallaUsuario { get { return pantallaUsuario; } // fin de get } // fin de la propiedad PantallaUsuario // propiedad de sólo lectura que obtiene la referencia a la base de datos del banco public BaseDatosBanco BaseDatos { get { return baseDatos; } // fin de get } // fin de la propiedad BaseDatos // realiza la transacción (cada clase derivada redefine este método) public abstract void Ejecutar(); } // fin de la clase Transaccion

Figura 11.25 | Código en C# para la clase Transaccion, con base en las figuras 11.21 y 11.22.

11.10 Conclusión En este capítulo se introdujo el polimorfismo: la habilidad de procesar objetos que comparten la misma clase base en una jerarquía de clases, como si todos fueran objetos de la clase base. En este capítulo hablamos sobre cómo el polimorfismo facilita la extensibilidad y manejabilidad de los sistemas, y después demostramos cómo utilizar métodos redefinidos para llevar a cabo el comportamiento polimórfico. Presentamos la noción de una clase abstracta, la cual nos proporciona una clase base apropiada, a partir de la cual otras clases pueden heredar. Aprendió que una clase abstracta puede declarar métodos abstractos que debe implementar cada clase derivada para convertirse en clase concreta, y que una aplicación puede utilizar variables de una clase abstracta para invocar implementaciones de clases derivadas de los métodos abstractos en forma polimórfica. También aprendió a determinar el tipo de un objeto en tiempo de ejecución. Le mostramos cómo crear métodos y clases sealed. Hablamos

384

Capítulo 11 Polimorfismo, interfaces y sobrecarga de operadores

también sobre la declaración e implementación de una interfaz, como otra manera de lograr el comportamiento polimórfico, a menudo entre objetos de distintas clases. Por último, aprendió a definir el comportamiento de los operadores integrados en objetos de sus propias clases, mediante la sobrecarga de operadores. Ahora deberá estar familiarizado con las clases, los objetos, el encapsulamiento, la herencia, las interfaces y el polimorfismo: los aspectos más esenciales de la programación orientada a objetos. En el siguiente capítulo analizaremos con más detalle cómo utilizar el manejo de excepciones para lidiar con los errores en tiempo de ejecución.

12

Manejo de excepciones

OBJETIVOS En este capítulo aprenderá lo siguiente:

Es cuestión de sentido común tomar un método y probarlo. Si falla, admítalo francamente y pruebe otro. Pero sobre todo, inténtelo. —Franklin Delano Roosevelt

¡Oh! Arroja la peor parte de ello, y vive en forma más pura con la otra mitad.

Q

Qué son las excepciones y cómo se manejan.

Q

Cuándo utilizar el manejo de excepciones.

Q

Utilizar bloques try para delimitar código en el que podrían ocurrir las excepciones.

Q

Lanzar (throw) excepciones para indicar un problema.

Q

Utilizar bloques catch para especificar manejadores de excepciones.

Q

Utilizar el bloque finally para liberar recursos.

—J. D. Salinger

Q

La jerarquía de clases de excepciones de .NET.

Q

Las propiedades de Exception.

Q

Crear excepciones definidas por el usuario.

A menudo, la excusa de una falta hace que ésta sea aún peor debido a la excusa.

—William Shakespeare

Si están corriendo y no saben hacia dónde se dirigen tengo que salir de alguna parte y atraparlos.

—William Shakespeare

¡Oh, infinita virtud! ¿Vienes sonriendo de la gran trampa del mundo sin ser atrapada? —William Shakespeare

Pla n g e ne r a l

386

Capítulo 12 Manejo de excepciones

12.1 12.2 12.3 12.4

12.5

12.6 12.7 12.8 12.9

Introducción Generalidades acerca del manejo de excepciones Ejemplo: división entre cero sin manejo de excepciones Ejemplo: manejo de las excepciones DivideByZeroException y FormatException 12.4.1 Encerrar código en un bloque try 12.4.2 Atrapar excepciones 12.4.3 Excepciones no atrapadas 12.4.4 Modelo de terminación del manejo de excepciones 12.4.5 Flujo de control cuando ocurren las excepciones Jerarquía Exception de .NET 12.5.1 Las clases ApplicationException y SystemException 12.5.2 Determinar cuáles excepciones debe lanzar un método El bloque finally Propiedades de Exception Clases de excepciones definidas por el usuario Conclusión

12.1 Introducción

En este capítulo presentaremos el manejo de excepciones. Una excepción es la indicación de un problema que ocurre durante la ejecución de un programa. El nombre “excepción” viene del hecho de que, aunque puede ocurrir un problema, éste ocurre con poca frecuencia. Si la “regla” es que una instrucción generalmente se ejecuta en forma correcta, entonces la ocurrencia de un problema representa la “excepción a la regla”. El manejo de excepciones permite a los programadores crear aplicaciones que puedan resolver (o manejar) las excepciones. En muchos casos, el manejo de una excepción permite que el programa continúe su ejecución como si no se hubieran encontrado problemas. No obstante, los problemas más graves podrían evitar que un programa continuara su ejecución normal, en vez de requerir al programa que notifique al usuario sobre el problema y después terminar de una manera controlada. Las características que presentamos en este capítulo permiten a los programadores escribir programas más tolerantes a fallas y robustos (es decir, programas que pueden lidiar con problemas que pueden surgir y continúan ejecutándose). El estilo y los detalles del manejo de excepciones de C# se basan, en parte, en el trabajo de Andrew Koenig y Bjarne Stroustrup. Las “mejores prácticas” para el manejo de excepciones en Visual C# 2005 se especifican en la documentación de Visual Studio.1

Tip de prevención de errores 12.1 El manejo de excepciones ayuda a mejorar la tolerancia a fallas de un programa.

Este capítulo empieza con una descripción general de los conceptos y demostraciones de las técnicas básicas del manejo de excepciones. El capítulo también presenta una descripción general de la jerarquía de clases de manejo de excepciones de .NET. Por lo general, los programas solicitan y liberan recursos (como archivos en el disco) durante la ejecución de un programa. A menudo, el suministro de estos recursos está limitado, o sólo un programa puede utilizar los recursos a la vez. Demostraremos una parte del mecanismo de manejo de excepciones que permite que un programa utilice un recurso y después garantice que será liberado para que lo utilicen otros programas, aun cuando ocurra una excepción. El capítulo demuestra varias propiedades de la clase System. Exception (la clase base de todas las clases de excepciones) y habla acerca de cómo usted puede crear y utilizar sus propias clases de excepciones. 1.

“Best Practices for Handling Exceptions [C#]”, Guía del desarrollador de .NET Framework, Ayuda en línea de Visual Studio .NET. Disponible en msdn2.microsoft.com/en-us/library/seyhszts.aspx.

12.3 Ejemplo: división entre cero sin manejo de excepciones

387

12.2 Generalidades acerca del manejo de excepciones Con frecuencia, los programas evalúan condiciones para determinar cómo debe proceder la ejecución de un programa. Considere el siguiente seudocódigo: Realizar una tarea Si la tarea anterior no se ejecutó en forma correcta Realizar el procesamiento de los errores Realizar la siguiente tarea Si la tarea anterior no se ejecutó en forma correcta Realizar el procesamiento de los errores …

En este seudocódigo, empezaremos por realizar una tarea; después evaluaremos si se ejecutó en forma correcta. Si no lo hizo, realizamos el procesamiento de los errores. De otra manera, continuamos con la siguiente tarea. Aunque esta forma de manejo de excepciones funciona, al entremezclar la lógica del programa con la lógica del manejo de errores el programa podría ser difícil de leer, modificar, mantener y depurar; especialmente en aplicaciones extensas. El manejo de excepciones permite a los programadores remover el código para manejo de errores de la “línea principal” de ejecución del programa, lo cual mejora su claridad y aumenta la capacidad de modificación del mismo. Los programadores pueden optar por manejar todas las excepciones que elijan: todas las excepciones, todas las excepciones de cierto tipo o todas las excepciones de un grupo de tipos relacionados (por ejemplo, los tipos de excepciones que están relacionados a través de una jerarquía de herencia). Esta flexibilidad reduce la probabilidad de que los errores se pasen por alto y, por consecuencia, hace que un programa sea más robusto. Con lenguajes de programación que no soportan el manejo de excepciones, los programadores a menudo retrasan la escritura de código de procesamiento de errores y en ocasiones olvidan incluirlo. Esto hace que los productos de software sean menos robustos. C# permite a los programadores tratar con el manejo de excepciones fácilmente, desde el comienzo de un proyecto.

12.3 Ejemplo: división entre cero sin manejo de excepciones Primero demostraremos lo que ocurre cuando surgen los errores en una aplicación de consola que no utiliza el manejo de excepciones. La figura 12.1 recibe como entrada dos enteros del usuario, y después divide el primer entero entre el segundo entero, utilizando la división entera para obtener un resultado int. En este ejemplo veremos que se lanza una excepción (es decir, ocurre una excepción) cuando un método detecta un problema y no puede manejarlo.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

// Fig. 12.1: DivisionEntreCeroSinManejoExcepciones.cs // Una aplicación que trata de dividir entre cero. using System; class DivisionEntreCeroSinManejoExcepciones { static void Main() { // obtiene el numerador y el denominador Console.Write( "Escriba un numerador entero: " ); int numerador = Convert.ToInt32( Console.ReadLine() ); Console.Write( "Escriba un denominador entero: " ); int denominador = Convert.ToInt32( Console.ReadLine() ); // divide los dos enteros, después muestra en pantalla el resultado

Figura 12.1 | División de enteros sin el manejo de excepciones. (Parte 1 de 2).

388

16 17 18 19 20

Capítulo 12 Manejo de excepciones

int resultado = numerador / denominador; Console.WriteLine( "\nResultado: {0:D} / {1:D} = {2:D}", numerador, denominador, resultado ); } // fin de Main } // fin de la clase DivisionEntreCeroSinManejoExcepciones

Escriba un numerador entero: 100 Escriba un denominador entero: 7 Resultado: 100 / 7 = 14

Escriba un numerador entero: 100 Escriba un denominador entero: 0 Excepción no controlada: System.DivideByZeroException: Intento de dividir por cero. en DivisionEntreCeroSinManejoExcepciones.Main() en C:\MisProyectos\fig12_01\fig12_01\ DivisionEntreCeroSinManejoExcepciones.cs:línea 16

Escriba un numerador entero: 100 Escriba un denominador entero: hola Excepción no controlada: System.FormatException: La cadena de entrada no tiene el formato correcto. en System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) en System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) en System.Convert.ToString32(String value) en DivisionEntreCeroSinManejoExcepciones.Main() en C:\MisProyectos\fig12_01\fig12_01\ DivisionEntreCeroSinManejoExcepciones.cs:línea 13

Figura 12.1 | División de enteros sin el manejo de excepciones. (Parte 2 de 2).

Ejecución de la aplicación En la mayoría de los ejemplos que hemos creado hasta ahora, la aplicación parece ejecutarse igual con o sin la depuración. Como veremos en breve, el ejemplo en la figura 12.1 podría ocasionar errores, dependiendo de la entrada del usuario. Si ejecuta esta aplicación usando la opción Depurar > Iniciar depuración, el programa se detiene en la línea en la que ocurre una excepción y muestra el Ayudante de excepciones, que le permite analizar el estado actual del programa y depurarlo. En la sección 12.4.3 hablaremos sobre el Ayudante de excepciones. En el apéndice C hablaremos con detalle sobre la depuración. En este ejemplo no deseamos depurar la aplicación: sólo queremos ver qué ocurre cuando surgen los errores. Por esta razón, ejecutaremos la aplicación desde una ventana de Símbolo del sistema. Seleccione Inicio > Todos los programas > Accesorios > Símbolo del sistema para abrir una ventana Símbolo del sistema y después utilice el comando cd para cambiar al directorio debug de la aplicación. Por ejemplo, si esta aplicación reside en el directorio C:\ejemplos\cap12\Fig12_01\DivisionEntreCeroSinManejoExcepciones en su sistema, debería escribir: cd /d C:\ejemplos\cap12\Fig12_01\DivisionEntreCeroSinManejoExcepciones\bin\Debug

en el Símbolo del sistema, ahora oprima Intro para cambiar al directorio debug de la aplicación. Para ejecutar la aplicación, escriba: DivisionEntreCeroSinManejoExcepciones.exe

12.3 Ejemplo: división entre cero sin manejo de excepciones

389

en el Símbolo del sistema, y después oprima Intro. Si surge un error durante la ejecución, se muestra un cuadro de diálogo indicando que la aplicación encontró un problema y necesita cerrarse. El cuadro de diálogo también le pregunta si desea enviar información acerca de este error a Microsoft. Como crearemos este error para fines de demostración, haga clic en No enviar. [Nota: en algunos sistemas se muestra un cuadro de diálogo Depuración Just-In-Time. Si esto ocurre, simplemente haga clic en el botón No para cerrar el cuadro de diálogo.] En este punto aparecerá un mensaje en el Símbolo del sistema, en el que se describe el problema. En la figura 12.1 aplicamos formato a los mensajes de error para mejorar la legibilidad. [Nota: al seleccionar Depurar > Iniciar sin depurar (o F5) para ejecutar la aplicación desde Visual Studio, se ejecuta la denominada versión de liberación de la aplicación. Los mensajes de error producidos por esta versión de la aplicación pueden ser distintos a los que se muestran en la figura 12.1, debido a las optimizaciones que realiza el compilador para crear la versión de liberación de una aplicación.]

Análisis de los resultados La primera ejecución de ejemplo en la figura 12.1 muestra una división exitosa. En la segunda ejecución de ejemplo, el usuario introduce 0 como el denominador. Observe que se muestran varias líneas de información, en respuesta a la entrada inválida. Esta información (conocida como rastreo de pila) incluye el nombre de la excepción (System.DivideByZeroException) en un mensaje descriptivo que indica el problema que ocurrió, y la ruta de ejecución que condujo a la excepción, método por método. Esta información le ayuda a depurar un programa. La primera línea del mensaje de error especifica que ha ocurrido una excepción DivideByZeroException. Cuando ocurre la división entre cero en la aritmética de enteros, el CLR lanza una excepción DivideByZeroException (espacio de nombres System). El texto después de la excepción, “Intento de dividir por cero”, indica que esta excepción ocurrió como resultado de un intento de dividir entre cero. La división entre cero no se permite en la aritmética de enteros. [Nota: se permite la división entre cero con valores de punto flotante. Dicho cálculo produce el valor de infinito, el cual se representa mediante la constante Double.PositiveInfinity o la constante Double.NegativeInfinity, dependiendo de si el numerador es positivo o negativo. Estos valores se muestran como Infinity o –Infinity. Si tanto el numerador como el denominador son cero, el resultado del cálculo es la constante Double.NaN (“no es un número”), el cual se devuelve cuando el resultado de un cálculo es indefinido.] Cada línea “en” en el rastreo de pila indica una línea de código en el método específico que se estaba ejecutando cuando ocurrió la excepción. La línea “en” contiene el espacio de nombres, el nombre de la clase y el nombre del método en el cual ocurrió la excepción (DivisionEntreCeroSinManejoExcepciones.Main), la ubicación y el nombre del archivo en el que reside el código (C:\MisProyectos\Cap12\fig12_01\DivisionEntreCeroSinManejoExcepciones.cs:línea 16) y la línea de código en donde ocurrió la excepción. En este caso, el rastreo de pila indica que la excepción DivideByZeroException ocurrió cuando el programa estaba ejecutando la línea 16 del método Main. La primera línea "en" en el rastreo de pila indica el punto de lanzamiento de la excepción: el punto inicial en que ocurrió la excepción (es decir, la línea 16 en Main). Esta información facilita al programador la tarea de ver en dónde se originó la excepción, y qué llamadas a métodos se hicieron para llegar hasta ese punto en el programa. Ahora veamos un rastreo de pila más detallado. En la tercera ejecución de ejemplo, el usuario introduce la cadena "hola" como el denominador. Esto produce una excepción FormatException y se muestra otro rastreo de pila. En nuestros primeros ejemplos que leían valores numéricos del usuario, se asumía que éste introduciría un valor entero. No obstante, un usuario podría introducir por error un valor no entero. Por ejemplo, una excepción FormatException (espacio de nombres System) ocurre cuando el método ToInt32 de Convert recibe una cadena que no representa un entero válido. Empezando desde la última línea "en" en el rastreo de pila, podemos ver que la excepción se detectó en la línea 13 el método Main. El rastreo de pila también muestra los demás métodos que condujeron a la excepción que se lanzó: Convert.ToInt32, Number.ParseInt32 y Number.StringToNumber. Para realizar su tarea, Convert.ToInt32 llama al método Number.ParseInt32, quien a su vez llama a Number.StringToNumber. El punto de lanzamiento ocurre en Number.StringToNumber, como se indica mediante la primera línea "en" en el rastreo de pila. Observe que en las ejecuciones de ejemplo de la figura 12.1, el programa también termina cuando ocurren las excepciones y se muestran los rastreos de pila. Esto no siempre ocurre; en ocasiones un programa puede seguir ejecutándose aun cuando se haya producido una excepción, y se haya impreso un rastreo de pila. En tales casos, la aplicación puede producir resultados erróneos. La siguiente sección demuestra cómo manejar excepciones para permitir que el programa se ejecute hasta terminar en forma normal.

390

Capítulo 12 Manejo de excepciones

12.4 Ejemplo: manejo de las excepciones DivideByZeroException y FormatException Consideraremos un ejemplo simple de manejo de excepciones. La aplicación en la figura 12.2 utiliza el manejo de excepciones para procesar cualquier excepción DivideByZeroException y FormatException que pueda surgir. La aplicación muestra dos objetos TextBox en los que el usuario puede escribir enteros. Cuando el usuario oprime Haga clic para dividir, el programa invoca al manejador de eventos DividirButton_Click (líneas 17-48), el cual obtiene la entrada del usuario, convierte los valores de entrada al tipo int y divide el primer número (numerador) entre el segundo número (denominador). Suponiendo que el usuario proporcione enteros como entrada y no especifique 0 como el denominador para la división, DividirButton_Click muestra el resultado de la división en SalidaLabel. No obstante, si el usuario introduce un valor no entero o 0 como el denominador, ocurre una excepción. Este programa demuestra cómo atrapar y manejar (es decir, tratar con) dichas excepciones; en este caso, muestra un mensaje de error y permite al usuario que introduzca otro conjunto de valores.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

// Fig. 12.2: PruebaDivisionEntreCero.cs // Manejadores de excepciones para FormatException y DivideByZeroException. using System; using System.Windows.Forms; namespace PruebaDivisionEntreCero { public partial class DivisionEntreCeroForm : Form { public DivisionEntreCeroForm() { InitializeComponent(); } // fin del constructor // obtiene 2 enteros del usuario // y divide el numerador entre el denominador private void DividirButton_Click( object sender, EventArgs e ) { SalidaLabel.Text = ""; // borra control Label EtiquetaSalida // extrae entrada del usuario y calcula el cociente try { // Convert.ToInt32 genera excepción FormatException // si el argumento no es un entero int numerador = Convert.ToInt32( NumeradorTextBox.Text ); int denominador = Convert.ToInt32( DenominadorTextBox.Text ); // la división genera una excepción DivideByZeroException // si el denominador es 0 int resultado = numerador / denominador; // muestra el resultado en SalidaLabel SalidaLabel.Text = resultado.ToString(); } // fin de try catch ( FormatException ) { MessageBox.Show( "Debe escribir dos enteros.", "Formato de número inválido", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch catch ( DivideByZeroException divideByZeroExceptionParameter )

Figura 12.2 | Manejadores de excepciones para FormatException y DivideByZeroException. (Parte 1 de 2).

12.4 Ejemplo: manejo de las excepciones DivideByZeroException y FormatException

43 44 45 46 47 48 49 50

391

{ MessageBox.Show( divideByZeroExceptionParameter.Message, "Intento de división por cero", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin del método DividirButton_Click } // fin de la clase FormDivisionEntreCero } // fin del espacio de nombres PruebaDivisionEntreCero a)

b)

c)

d)

e)

Figura 12.2 | Manejadores de excepciones para FormatException y DivideByZeroException. (Parte 2 de 2). Antes de hablar sobre los detalles del programa, consideraremos las ventanas de resultados en la figura 12.2. La ventana en la figura 12.2(a) muestra un cálculo exitoso, en el que el usuario introduce el numerador 100 y el denominador 7. Observe que el resultado (14) es un int, ya que la división de enteros siempre produce un resultado int. Las siguientes dos ventanas, las figuras 12.2(b) y 12.2(c), demuestran el resultado de un intento de dividir entre cero. En la aritmética de enteros, el CLR evalúa la condición de división entre cero y genera una excepción DivideByZeroException si el denominador es cero. El programa detecta la excepción y muestra el cuadro de diálogo con el mensaje de error en la figura 12.2(c), indicando el intento de dividir entre cero. Las últimas dos ventanas de resultados, las figuras 12.2(d) y 12.2(e), ilustran el resultado de introducir un valor no int; en este caso, el usuario introduce “hola” en el segundo objeto TextBox, como se muestra en la figura 12.2(d). Cuando el usuario hace clic en Haga clic para dividir, el programa intenta convertir los objetos string de entrada en valores int mediante el uso del método Convert.ToInt32 (líneas 26-27). Si un argumento que se pasa a Convert.ToInt32 no puede convertirse en valor int, el método lanza una excepción FormatException. El programa atrapa la excepción y muestra el cuadro de diálogo con el mensaje de error en la figura 12.2(e), indicando que el usuario debe introducir dos valores int. Observe que no incluimos el nombre de un parámetro para la instrucción catch en la línea 36. En el bloque del catch no utilizamos información del objeto FormatException. Al omitir el nombre del parámetro evitamos que el compilador genere una advertencia en la que se indique que declaramos una variable, pero no la utilizamos en el bloque catch.

392

Capítulo 12 Manejo de excepciones

12.4.1 Encerrar código en un bloque try Ahora consideraremos las interacciones del usuario y el flujo de control que producen los resultados que se muestran en las ventanas de resultados de ejemplo. El usuario introduce valores en los objetos TextBox que representan el numerador y el denominador, y después oprime el botón Haga clic para dividir. En este punto, el programa invoca al método DividirButton_Click. La línea 19 asigna la cadena vacía a SalidaLabel para borrar cualquier resultado anterior y prepararse para un nuevo cálculo. Las líneas 22-35 definen un bloque try, el cual encierra el código que podría lanzar excepciones, así como el código que se omite cuando ocurre una excepción. Por ejemplo, el programa no debería mostrar un nuevo resultado en SalidaLabel (línea 34), a menos que el cálculo en la línea 31 se complete con éxito. Las dos instrucciones que leen los valores int de los objetos TextBox (líneas 26-27) llaman al método Convert.ToInt32 para convertir objetos string en valores int. Este método lanza una excepción FormatException si no puede convertir su argumento string en un int. Si las líneas 26-27 convierten los valores en forma apropiada (es decir, que no ocurran excepciones), entonces la línea 31 divide el numerador entre el denominador y asigna el resultado a la variable resultado. Si denominador es 0, la línea 31 hace que el CLR lance una excepción DivideByZeroException. Si la línea 31 no hace que se lance una excepción, entonces la línea 34 muestra el resultado de la división.

12.4.2 Atrapar excepciones

El código para el manejo de excepciones aparece en un bloque catch. En general, cuando ocurre una excepción en un bloque try, un bloque catch correspondiente atrapa la excepción y la maneja. En este ejemplo, el bloque try va seguido de dos bloques catch: uno que maneja una excepción FormatException (líneas 36-41) y otro que maneja una excepción DivideByZeroException (líneas 42-47). Un bloque catch especifica un parámetro de excepción que representa la excepción que puede manejar ese bloque catch. El bloque catch puede utilizar el identificador del parámetro (que el programador elije) para interactuar con un objeto de excepción atrapada. Si no hay necesidad de usar el objeto de excepción en el bloque catch, puede omitirse el identificador del parámetro de la excepción. El tipo del parámetro del catch es el tipo de la excepción que maneja ese bloque catch. De manera opcional, los programadores pueden incluir un bloque catch que no especifique un tipo de excepción o un identificador; dicho bloque catch (conocido como cláusula catch general ) atrapa todos los tipos de excepciones. Debe haber por lo menos un bloque catch y/o un bloque finally (que veremos en la sección 12.6) inmediatamente después de un bloque try. En la figura 12.2, el primer bloque catch atrapa excepciones FormatException (lanzadas por el método Convert.ToInt32), y el segundo bloque catch atrapa excepciones DivideByZeroException (lanzadas por el CLR). Si ocurre una excepción, el programa ejecuta sólo el primer bloque catch que concuerde. Ambos manejadores de excepciones en este ejemplo muestran un cuadro de diálogo con un mensaje de error. Una vez que termina cualquiera de los bloques catch, el control del programa continúa con la primera instrucción después del último bloque catch (en este ejemplo, el final del método). Pronto veremos con más detalle la forma en que funciona este flujo de control en el manejo de excepciones.

12.4.3 Excepciones no atrapadas

Una excepción no atrapada es una excepción para la que no hay un bloque catch relacionado. Usted vio los resultados de las excepciones no atrapadas en el segundo y tercer conjunto de resultados de la figura 12.1. Recuerde que cuando ocurren excepciones en ese ejemplo, la aplicación termina en forma anticipada (después de mostrar el rastreo de pila de la excepción). El resultado de una excepción no atrapada depende de la forma en que se ejecuta el programa; la figura 12.1 demostró los resultados de una excepción no atrapada cuando se ejecuta una aplicación en un Símbolo del sistema. Si usted ejecuta la aplicación desde Visual Studio con depuración y el entorno en tiempo de ejecución detecta una excepción no atrapada, la aplicación se pausa y aparece una ventana llamada Ayudante de excepciones, la cual indica en dónde ocurrió la excepción, el tipo de la excepción y vínculos a información útil acerca de cómo manejar la excepción. La figura 12.3 muestra el Ayudante de excepciones que aparece si el usuario intenta dividir entre cero, en la aplicación de la figura 12.1.

12.4.4 Modelo de terminación del manejo de excepciones Cuando un método que se llama en un programa o el CLR detectan un problema, el método o el CLR lanzan una excepción. Recuerde que el punto en el programa en el cual ocurre una excepción se conoce como el punto

12.4 Ejemplo: manejo de las excepciones DivideByZeroException y FormatException

Punto de lanzamiento

393

Asistente de excepciones

Figura 12.3 | Ayudante de excepciones. de lanzamiento; ésta es una ubicación importante para fines de depuración (como veremos en la sección 12.7). Si ocurre una excepción en un bloque try (por ejemplo, cuando se lanza una FormatException como resultado del código en la línea 27 de la figura 12.2), el bloque try termina de inmediato y el control del programa se transfiere al primero de los siguientes bloques catch en los que el tipo del parámetro de la excepción concuerde con el tipo de la excepción que se lanzó. En la figura 12.2, el primer bloque catch atrapa excepciones FormatException (que ocurren cuando se introduce un tipo inválido); el segundo bloque catch atrapa excepciones DivideByZero (que ocurren si hay un intento de dividir entre cero). Una vez que se maneja la excepción, el control del programa no regresa al punto de lanzamiento, ya que el bloque try ha expirado (lo cual también hace que cualquiera de sus variables locales quede fuera de alcance). En vez de ello, el control continúa después del último bloque catch. Esto se conoce como el modelo de terminación del manejo de excepciones. [Nota: algunos lenguajes utilizan el modelo de reanudación del manejo de excepciones, en el cual después de manejar una excepción, el control se reanuda justo después del punto de lanzamiento.]

Error común de programación 12.1 Pueden ocurrir errores lógicos si asumimos que, después de manejar una excepción, el control regresará a la primera instrucción después del punto de lanzamiento.

Si no ocurre una excepción en el bloque try, el programa de la figura 12.2 completa con éxito el bloque try, ignorando los bloques catch en las líneas 36-41 y 42-47, y pasa a la línea 47. Después, el programa ejecuta la primera instrucción después de los bloques try y catch. En este ejemplo, el programa llega al final del manejador de eventos DividirButton_Click (línea 48), por lo que el método termina y el programa espera la siguiente interacción del usuario. En conjunto, el bloque try y sus correspondientes bloques catch y finally forman una instrucción try. Es importante no confundir los términos “bloque try” e “instrucción try”; el término “bloque try” se refiere al bloque de código que va después de la palabra clave try (pero antes de cualquier bloque catch o finally), mientras que el término “instrucción try” incluye todo el código desde la palabra clave de apertura try, hasta el final del último bloque catch o finally. Esto incluye el bloque try, así como cualquier bloque catch y finally asociado. Al igual que con cualquier otro bloque de código, cuando termina un bloque try, las variables locales definidas en el bloque quedan fuera de alcance. Si un bloque try termina debido a una excepción, el CLR busca el primer bloque catch que pueda procesar el tipo de excepción que ocurrió. Para localizar el catch que concuerde, el CLR compara el tipo de la excepción lanzada con el tipo de parámetro de catch. Se produce una concordancia si los tipos son idénticos, o si el tipo de la excepción lanzada es una clase derivada del tipo del parámetro catch. Una vez que se relaciona una excepción con un bloque catch, se ejecuta el código en ese bloque y se ignoran los demás bloques catch en la instrucción try.

394

Capítulo 12 Manejo de excepciones

12.4.5 Flujo de control cuando ocurren las excepciones En los resultados de ejemplo de la figura 12.2(b), el usuario introduce hola como el denominador. Cuando se ejecuta la línea 27, Convert.ToInt32 no puede convertir este string en un int, por lo que lanza un objeto FormatException para indicar que el método no pudo convertir el string en int. Cuando ocurre la excepción, el bloque try expira (termina). Después, el CLR trata de localizar un bloque catch que concuerde. Se produce una concordancia con el bloque catch en la línea 36, por lo que se ejecuta el manejador de excepciones y el programa ignora todos los demás manejadores de excepciones que van después del bloque try.

Error común de programación 12.2 Especificar una lista de parámetros separada por comas en un bloque catch es un error de sintaxis. Un bloque catch puede tener a lo más un parámetro.

En los resultados de ejemplo de la figura 12.2(d), el usuario introduce un 0 como el denominador. Cuando se ejecuta la división en la línea 31, se produce una excepción DivideByZeroException. Una vez más el bloque try termina y el programa trata de localizar un bloque catch que concuerde. En este caso, el primer bloque catch no concuerda; el tipo de excepción en la declaración del manejador del catch no es el mismo que el tipo de la excepción lanzada, y FormatException no es una clase base de DivideByZeroException. Por lo tanto, el programa continúa buscando un bloque catch que concuerde, y lo encuentra en la línea 42. La línea 44 muestra el valor de la propiedad Message de la clase Exception, la cual contiene el mensaje de error. Observe que nuestro programa nunca “establece” este atributo del mensaje de error. Esto lo hace el CLR cuando crea el objeto excepción.

12.5 Jerarquía Exception de .NET En C#, el mecanismo de manejo de excepciones sólo permite lanzar y atrapar objetos de la clase Exception (espacio de nombres System) y sus clases derivadas. No obstante, hay que tener en cuenta que los programas en C# pueden interactuar con componentes de software escritos en otros lenguajes .NET (como C++) que no restringen los tipos de las excepciones. En estos casos, es posible utilizar la cláusula catch general para atrapar dichas excepciones. En esta sección veremos las generalidades acerca de varias de las clases de excepciones del .NET Framework, y nos enfocaremos exclusivamente en las que se derivan de la clase Exception. Además hablaremos acerca de cómo determinar si un método específico va a lanzar excepciones.

12.5.1 Las clases ApplicationException y SystemException La clase Exception del espacio de nombres System es la clase base de la jerarquía de clases de excepciones del .NET Framework. Dos de las clases más importantes que se derivan de Exception son ApplicationException y SystemException. ApplicationException es una clase base que los programadores pueden extender para crear clases de excepciones específicas para sus aplicaciones. En la sección 12.8 le mostraremos cómo crear clases de excepciones definidas por el usuario. Los programas pueden recuperarse de la mayoría de las excepciones ApplicationException y continuar su ejecución. El CLR genera excepciones SystemException, que pueden ocurrir en cualquier punto durante la ejecución de un programa. Podemos evitar muchas de estas excepciones si codificamos las aplicaciones en forma apropiada. Por ejemplo, si un programa trata de acceder a un subíndice de arreglo fuera de rango, el CLR lanza una excepción de tipo IndexOutOfRangeException (una clase derivada de SystemException). De manera similar, una excepción ocurre cuando un programa utiliza la referencia a un objeto para manipular un objeto que aún no existe (es decir, la referencia tiene un valor de null). Si tratamos de utilizar una referencia null se produce una excepción NullReferenceException (otra clase derivada de SystemException). Anteriormente en este capítulo vimos que una excepción DivideByZeroException ocurre en la división de enteros, cuando un programa trata de dividir entre cero. Otros tipos de excepciones SystemException que lanza el CLR son: OutOfMemoryException, StackOverFlowException y ExecutionEngineException. Éstas se lanzan cuando algo sale mal y el CLR se vuelve inestable. En ciertos casos, incluso dichas excepciones no pueden atraparse. En general, es mejor anotar en el registro esas excepciones y después terminar la aplicación. Un beneficio de la jerarquía de clases de excepciones es que un bloque catch puede atrapar excepciones de un tipo específico o (debido a la relación es un de la herencia) puede utilizar un tipo de clase base para atrapar

12.6 El bloque finally

395

excepciones, en una jerarquía de tipos de excepciones relacionadas. Por ejemplo, en la sección 12.4.2 vimos el bloque catch sin parámetro, que atrapa excepciones de todos los tipos (incluyendo los que no se derivan de Exception). Un bloque catch que especifica un parámetro de tipo Exception puede atrapar todas las excepciones que se derivan de Exception, ya que ésta es la clase base de todas las clases de excepciones. La ventaja de este enfoque es que el manejador de excepciones puede acceder a la información de la excepción atrapada mediante el parámetro en el catch. En la línea 44 de la figura 12.2 demostramos cómo acceder a la información en una excepción. En la sección 12.7 veremos más acerca del acceso a la información de una excepción. El uso de la herencia con las excepciones permite a un bloque catch atrapar excepciones relacionadas, mediante el uso de una notación concisa. Un conjunto de manejadores de excepciones podría atrapar a cada tipo de excepción de clase derivada en forma individual, pero es más conciso atrapar el tipo de excepción de clase base. No obstante, esta técnica tiene sentido sólo si el comportamiento del manejo es el mismo para una clase base que para todas las clases derivadas. De no ser así, hay que atrapar cada excepción de clase derivada en forma individual.

Error común de programación 12.3 Si un bloque catch que atrapa una excepción de clase base se coloca antes de un bloque catch para cualquiera de los tipos de clases derivadas de esa clase, se produce un error de compilación. Si se permitiera esto, el bloque catch de la clase base atraparía todas las excepciones de la clase base y las clases derivadas, y por lo tanto el manejador de excepciones de las clases derivadas nunca se ejecutaría.

12.5.2 Determinar cuáles excepciones debe lanzar un método ¿Cómo determinamos que podría ocurrir una excepción en un programa? Para los métodos contenidos en las clases del .NET Framework, lea las descripciones detalladas de los métodos en la documentación en línea. Si un método lanza una excepción, su descripción contiene una sección llamada Exceptions, la cual especifica los tipos de excepciones que lanza ese método y describe en forma breve las posibles causas de las excepciones. Por ejemplo, busque “método Convert.ToInt32” en el Índice de la documentación en línea de Visual Studio (use el filtro .NET Framework). Seleccione el documento titulado Convert.ToInt32 (Método) (System). En el documento que describe al método, haga clic en el vínculo Convert.ToInt32(String). En el documento que aparezca, la sección Exceptions (cerca de la parte inferior del documento) indica que el método Convert.ToInt32 lanza dos tipos de excepciones (FormatException y OverflowException) y describe la razón por la cual se podría producir cada una de ellas.

Observación de ingeniería de software 12.1 Si un método lanza excepciones, las instrucciones que invocan al método en forma directa o indirecta deberían colocarse en bloques try, y esas excepciones deberían atraparse y manejarse.

Es más difícil determinar cuando el CLR lanza excepciones. Dicha información aparece en la Especificación del lenguaje C#. Este documento define la sintaxis de C# y especifica los casos en los que se lanzan excepciones. La figura 12.2 demostró que el CLR lanza una excepción DivideByZeroException en la aritmética de enteros, cuando un programa trata de dividir entre cero. La sección 7.7.2 de la especificación del lenguaje (14.7.2 en la versión ECMA) habla sobre el operador de división y cuándo ocurren las excepciones DivideByZeroException.

12.6 El bloque finally Con frecuencia, los programas solicitan y liberan recursos en forma dinámica (es decir, en tiempo de ejecución). Por ejemplo, un programa que lee un archivo del disco primero hace una solicitud para abrir el archivo (como veremos en el capítulo 18, Archivos y flujos). Si esa solicitud tiene éxito, el programa lee el contenido del archivo. Por lo general, los sistemas operativos evitan que más de un programa manipule un archivo al mismo tiempo. Por lo tanto, cuando un programa termina de procesar un archivo, éste debe cerrarlo (es decir, liberar el recurso) para que otros programas puedan utilizar el archivo. Si no se cierra, ocurre una fuga de recursos. En tal caso, el recurso del archivo no está disponible para otros programas, posiblemente debido a que un programa que utiliza el archivo no lo ha cerrado. En lenguajes de programación como C y C++, en los que el programador (y no el lenguaje) es responsable de la administración de la memoria dinámica, el tipo más común de fuga de recursos es una fuga de memoria.

396

Capítulo 12 Manejo de excepciones

Este tipo de fuga ocurre cuando un programa asigna memoria (como hacen los programadores en C# mediante la palabra clave new), pero no la desasigna cuando ya no se necesita. Por lo general esto no representa un problema en C#, ya que el CLR se encarga de la recolección de basura para la memoria que ya no necesita un programa en ejecución (sección 9.10). No obstante, pueden ocurrir otros tipos de fugas de recursos (como los archivos sin cerrar).

Tip de prevención de errores 12.2 El CLR no elimina por completo las fugas de memoria. El CLR no hará la recolección de basura sobre un objeto sino hasta que el programa no contenga más referencias a ese objeto. Por ende, pueden ocurrir fugas de memoria si los programadores mantienen referencias a objetos indeseados, sin darse cuenta.

Desplazar el código de liberación de recursos a un bloque finally Por lo general, las excepciones ocurren cuando se procesan recursos que requieren una liberación explícita. Por ejemplo, un programa que procesa un archivo podría recibir excepciones IOException durante el procesamiento. Por esta razón, el código para procesar archivos aparece normalmente en un bloque try. Sin importar si un programa experimenta excepciones mientras procesa un archivo, debe cerrarlo cuando ya no lo necesita. Suponga que un programa coloca todo el código de solicitud y liberación de recursos en un bloque try. Si no ocurren excepciones, el bloque try se ejecuta normalmente y libera los recursos después de utilizarlos. No obstante, si ocurre una excepción, el bloque try tal vez expire antes de que el código de liberación de recursos pueda ejecutarse. Podríamos duplicar todo el código de liberación de recursos en cada uno de los bloques catch, pero esto haría que el código fuera más difícil de modificar y mantener. También podríamos colocar el código de liberación de recursos después de la instrucción try; pero si el bloque try termina debido a una instrucción de retorno, el código después de la instrucción try nunca se ejecutaría. Para lidiar con estos problemas, el mecanismo de manejo de excepciones de C# cuenta con el bloque finally, con el cual se garantiza que se ejecutará sin importar que el bloque try se ejecute con éxito o que ocurra una excepción. Esto convierte al bloque finally en una ubicación ideal en la cual colocar el código de liberación de recursos, para los recursos que se adquieren y se manipulan en el bloque try correspondiente. Si el bloque try se ejecuta con éxito, el bloque finally se ejecuta inmediatamente después de que termina el bloque try. Si ocurre una excepción en el bloque try, el bloque finally se ejecuta inmediatamente después de que termina un bloque catch. Si la excepción no se atrapa mediante un bloque catch asociado con el bloque try, o si un bloque catch asociado con el bloque try lanza una excepción por sí mismo, el bloque finally se ejecuta antes que se procese la excepción por el siguiente bloque try circundante (si hay uno). Al colocar el código de liberación de recursos en un bloque finally, aseguramos que, aun si el programa termina debido a una excepción no atrapada, el recurso se desasignará. Observe que no se puede acceder a las variables locales de un bloque try en el bloque finally correspondiente. Por esta razón, las variables a las que se debe acceder tanto en un bloque try como en su bloque finally correspondiente deben declararse antes del bloque try.

Tip de prevención de errores 12.3 Por lo general, un bloque finally contiene código para liberar los recursos adquiridos en el bloque try correspondiente, lo cual hace del bloque finally un mecanismo efectivo para eliminar fugas de recursos.

Tip de rendimiento 12.1 Como regla, los recursos deben liberarse tan pronto como ya no sean necesarios en un programa. Esto los hace disponibles para su reutilización en el menor tiempo posible.

Si uno o más bloques catch siguen después de un bloque try, el bloque finally es opcional. No obstante, si ningún bloque catch sigue después de un bloque try, debe aparecer un bloque finally justo después del bloque try. Si uno o más bloques catch siguen después de un bloque try, el bloque finally (si hay uno) aparece después del último bloque catch. Sólo puede haber espacio en blanco y comentarios para separar los bloques en una instrucción try.

12.6 El bloque finally

397

Error común de programación 12.4 Colocar el bloque finally antes de un bloque catch es un error de sintaxis.

Demostración del bloque finally La aplicación en la figura 12.4 demuestra que el bloque finally siempre se ejecuta, sin importar que ocurra o no una excepción en el bloque try correspondiente. El programa consiste en el método Main (líneas 8-47) y otros cuatro métodos que invoca Main para demostrar el bloque finally. Estos métodos son NoLanzaExcepcion (líneas 50-67), LanzaExcepcionConCatch (líneas 70-89), LanzaExcepcionSinCatch (líneas 92-108) y LanzaExcepcionCatchLanzaDeNuevo (líneas 111-136). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

// Fig. 12.4: UsoDeExcepciones.cs // Uso de los bloques finally. // Demuestra que un bloque finally siempre se ejecuta. using System; class UsoDeExcepciones { static void Main() { // Caso 1: No ocurren excepciones en el método que se llamó Console.WriteLine( "Llamando a NoLanzaExcepcion" ); NoLanzaExcepcion(); // Case 2: Ocurre una excepción y se atrapa en el método que se llamó Console.WriteLine( "\nLLamando a LanzaExcepcionConCatch" ); LanzaExcepcionConCatch(); // Caso 3: Ocurre una excepción, pero no se atrapa en el método que se llamó, // ya que no hay bloque catch. Console.WriteLine( "\nLlamando a LanzaExcepcionSinCatch" ); // llama a LanzaExcepcionSinCatch try { LanzaExcepcionSinCatch(); } // fin de try catch { Console.WriteLine( "Atrapó la excepción de " + "LanzaExcepcionSinCatch en Main" ); } // fin de catch // Caso 4: Ocurre una excepción y se atrapa en el método que se llamó, // después se vuelve a lanzar al que hizo la llamada. Console.WriteLine( "\nLlamando a LanzaExcepcionCatchLanzaDeNuevo" ); // llama a LanzaExcepcionCatchLanzaDeNuevo try { LanzaExcepcionCatchLanzaDeNuevo(); } // fin de try catch { Console.WriteLine( "Atrapó la excepción de " + "LanzaExcepcionCatchLanzaDeNuevo en Main" ); } // fin de catch

Figura 12.4 | Los bloques finally siempre se ejecutan, sin importar que ocurra o no una excepción. (Parte 1 de 3).

398

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

Capítulo 12 Manejo de excepciones

} // fin del método Main // no se lanzan excepciones static void NoLanzaExcepcion() { // el bloque try no lanza excepciones try { Console.WriteLine( "En NoLanzaExcepcion" ); } // fin de try catch { Console.WriteLine( "Este bloque catch nunca se ejecuta" ); } // fin de catch finally { Console.WriteLine( "se ejecutó finally en NoLanzaExcepcion" ); } // fin de finally Console.WriteLine( "Fin de NoLanzaExcepcion" ); } // fin del método NoLanzaExcepcion // lanza la excepción y la atrapa en forma local static void LanzaExcepcionConCatch() { // el bloque try lanza la excepción try { Console.WriteLine( "En LanzaExcepcionConCatch" ); throw new Exception( "Excepción en LanzaExcepcionConCatch" ); } // fin de try catch (Exception parametroExcepcion) { Console.WriteLine( "Mensaje: " + parametroExcepcion.Message ); } // fin de catch finally { Console.WriteLine( "se ejecutó finally en LanzaExcepcionConCatch" ); } // fin de finally Console.WriteLine( "Fin de LanzaExcepcionConCatch" ); } // fin del método LanzaExcepcionConCatch // lanza la excepción y no la atrapa en forma local static void LanzaExcepcionSinCatch() { // lanza la excepción, pero no la atrapa try { Console.WriteLine( "En LanzaExcepcionSinCatch" ); throw new Exception( "Excepción en LanzaExcepcionSinCatch" ); } // fin de try finally { Console.WriteLine( "se ejecutó finally en " + "LanzaExcepcionSinCatch" ); } // fin de finally

Figura 12.4 | Los bloques finally siempre se ejecutan, sin importar que ocurra o no una excepción. (Parte 2 de 3).

12.6 El bloque finally

399

105 106 // código inalcanzable; error lógico 107 Console.WriteLine( "Fin de LanzaExcepcionSinCatch" ); 108 } // fin del método LanzaExcepcionSinCatch 109 110 // lanza la excepción, la atrapa y la vuelve a lanzar 111 static void LanzaExcepcionCatchLanzaDeNuevo() 112 { 113 // el bloque try lanza la excepción 114 try 115 { 116 Console.WriteLine( "En LanzaExcepcionCatchLanzaDeNuevo" ); 117 throw new Exception( "Excepción en LanzaExcepcionCatchLanzaDeNuevo" ); 118 } // fin de try 119 catch ( Exception parametroExcepcion ) 120 { 121 Console.WriteLine( "Mensaje: " + parametroExcepcion.Message ); 122 123 // vuelve a lanzar la excepción para procesarla después 124 throw; 125 126 // código inalcanzable; error lógico 127 } // fin de catch 128 finally 129 { 130 Console.WriteLine( "se ejecutó finally en " + 131 "LanzaExcepcionCatchLanzaDeNuevo" ); 132 } // fin de finally 133 134 // cualquier código que se coloque aquí nunca se ejecutará 135 Console.WriteLine( "Fin de LanzaExcepcionCatchLanzaDeNuevo" ); 136 } // fin del método LanzaExcepcionCatchLanzaDeNuevo 137 } // fin de la clase UsoDeExcepciones Llamando a NoLanzaExcepcion En NoLanzaExcepcion se ejecutó finally en NoLanzaExcepcion Fin de NoLanzaExcepcion LLamando a LanzaExcepcionConCatch En LanzaExcepcionConCatch Mensaje: Excepción en LanzaExcepcionConCatch se ejecutó finally en LanzaExcepcionConCatch Fin de LanzaExcepcionConCatch Llamando a LanzaExcepcionSinCatch En LanzaExcepcionSinCatch se ejecutó finally en LanzaExcepcionSinCatch Atrapó la excepción de LanzaExcepcionSinCatch en Main Llamando a LanzaExcepcionCatchLanzaDeNuevo En LanzaExcepcionCatchLanzaDeNuevo Mensaje: Excepción en LanzaExcepcionCatchLanzaDeNuevo se ejecutó finally en LanzaExcepcionCatchLanzaDeNuevo Atrapó la excepción de LanzaExcepcionCatchLanzaDeNuevo en Main

Figura 12.4 | Los bloques finally siempre se ejecutan, sin importar que ocurra o no una excepción. (Parte 3 de 3). La línea 12 de Main invoca al método NoLanzaExcepcion. El bloque try para este método imprime un mensaje en pantalla (línea 55). Como el bloque try no lanza excepciones, el control del programa ignora el bloque catch (líneas 57-60) y ejecuta el bloque finally (líneas 61-64), el cual imprime un mensaje en pantalla. En

400

Capítulo 12 Manejo de excepciones

este punto, el control del programa continúa con la primera instrucción que sigue después del cierre del bloque finally (línea 66), la cual imprime un mensaje en pantalla indicando que se ha llegado al final del método. Después, el control del programa regresa a Main.

Lanzar excepciones mediante el uso de la instrucción throw La línea 16 de Main invoca al método LanzarExcepcionConCatch (líneas 70-89), el cual empieza en su bloque try (líneas 73-77) imprimiendo un mensaje. A continuación, el bloque try crea un objeto Exception y utiliza una instrucción throw para lanzar el objeto excepción (línea 76). Al ejecutar la instrucción throw se indica que ocurrió una excepción. Hasta ahora sólo hemos atrapado excepciones que lanzan los métodos a los que se llama. Con la instrucción throw puede lanzar excepciones. Al igual que con las excepciones que lanzan los métodos de la FCL y el CLR, esto indica a las aplicaciones cliente que ocurrió un error. Una instrucción throw especifica un objeto a lanzar. El operando de una instrucción throw puede ser de tipo Exception, o de cualquier tipo derivado de la clase Exception.

Error común de programación 12.5 Si el argumento de una instrucción throw (un objeto excepción) no es de la clase Exception o de una de sus clases derivadas, se produce un error de compilación.

El objeto string que se pasa al constructor se convierte en el mensaje de error del objeto excepción. Cuando se ejecuta una instrucción throw en un bloque try, el bloque try expira de inmediato y el control del programa continúa con el primer bloque catch que concuerde (líneas 78-81), que siga después del bloque try. En este ejemplo, el tipo lanzado (Exception) concuerda con el tipo especificado en el bloque catch, por lo que la línea 80 imprime un mensaje en pantalla indicando que ocurrió la excepción. Después, el bloque finally (líneas 82-86) se ejecuta e imprime un mensaje en pantalla. En este punto, el control del programa continúa con la primera instrucción que va después del cierre del bloque finally (línea 88), el cual imprime un mensaje en pantalla que indica que se ha llegado al final del archivo. Después, el control del programa regresa a Main. En la línea 80, observe que utilizamos la propiedad Message del objeto excepción para extraer el mensaje de error asociado con la excepción (es decir, el mensaje que se pasa al constructor de Exception). La sección 12.7 habla sobre varias propiedades de la clase Exception. Las líneas 23-31 de Main definen una instrucción try, en la que Main invoca al método LanzaExcepcionSinCatch (líneas 92-108). El bloque try permite que Main atrape cualquier excepción lanzada por LanzaExcepcionSinCatch. El bloque try en las líneas 95-99 de LanzaExcepcionSinCatch empieza imprimiendo un mensaje en pantalla. Después, el bloque try lanza una excepción tipo Exception (línea 98) y expira de inmediato. Por lo general, el control del programa continuaría en el primer bloque catch después de este bloque try. Sin embargo, este bloque try no tiene bloques catch. Por lo tanto, la excepción no se atrapa en el método LanzaExcepcionSinCatch. El control del programa se reanuda en el bloque finally (líneas 100-104), el cual imprime un mensaje en pantalla. En este punto, el control del programa regresa a Main; cualquier instrucción que aparezca después del bloque finally (por ejemplo, la línea 107) no se ejecuta. En este ejemplo, dichas instrucciones podrían ocasionar errores lógicos, ya que la excepción que se lanza en la línea 98 no se atrapa. En Main, el bloque catch en las líneas 27-31 atrapa la excepción y muestra un mensaje en pantalla, indicando que la excepción se atrapó en Main.

Volver a lanzar excepciones Las líneas 38-46 de Main definen una instrucción try en la que Main invoca al método LanzaExcepcionCatchLanzaDeNuevo (líneas 111-136). La instrucción try permite que Main atrape cualquier excepción que lance LanzaExcepcionCatchLanzaDeNuevo. La instrucción try en las líneas 114-132 de LanzaExcepcionCatchLanzaDeNuevo empieza imprimiendo un mensaje en pantalla. A continuación, el bloque try lanza una excepción tipo Exception (línea 117). El bloque try expira de inmediato y el control del programa continúa en el primer bloque catch (líneas 119-127) que va después del bloque try. En este ejemplo, el tipo que se lanza (Exception) concuerda con el tipo especificado en el bloque catch, por lo que la línea 121 imprime un mensaje de salida que indica en dónde ocurrió la excepción. La línea 124 utiliza la instrucción throw para volver a lanzar la excepción. Esto indica que el bloque catch realizó un procesamiento parcial de la excepción y ahora la está pasando de vuelta al método que hizo la llamada (en este caso, Main) para que la siga procesando.

12.6 El bloque finally

401

También se puede volver a lanzar una excepción con una versión de la instrucción throw que recibe un operando, el cual es la referencia a la excepción que se atrapó. No obstante, es importante tener en cuenta que esta forma de instrucción throw restablece el punto de lanzamiento, por lo que se pierde la información original del rastreo de pila del punto de lanzamiento. La sección 12.7 demuestra el uso de una instrucción throw con un operando proveniente de un bloque catch. En esa sección veremos que, después de atrapar una excepción, se puede crear y lanzar un tipo distinto de objeto excepción del bloque catch y se puede incluir la excepción original como parte del nuevo objeto excepción. Los diseñadores de biblioteca de clases hacen esto con frecuencia, para personalizar los tipos de excepciones que se lanzan de los métodos en sus bibliotecas de clases, o para proporcionar información de depuración adicional. El manejo de excepciones en el método LanzaExcepcionCatchLanzaDeNuevo no se completa, ya que el programa no puede ejecutar el código en el bloque catch que se coloca después de la invocación de la instrucción throw en la línea 124. Por lo tanto, el método LanzaExcepcionCatchLanzaDeNuevo termina y devuelve el control a Main. Una vez más, el bloque finally (líneas 128-132) se ejecuta e imprime un mensaje en pantalla, antes de que regrese el control a Main. Cuando el control regresa a Main, el bloque catch en las líneas 42-46 atrapa la excepción y muestra un mensaje que indica que se atrapó la excepción. Después, el programa termina.

Regresar después de un bloque finally Observe que la siguiente instrucción a ejecutar después de que termina un bloque finally depende del estado del manejo de la excepción. Si el bloque try se completa con éxito, o si un bloque catch atrapa y maneja una excepción, el programa continúa su ejecución con la siguiente instrucción después del bloque finally. No obstante, si no se atrapa una excepción, o si un bloque catch vuelve a lanzar una excepción, el control del programa continúa en el siguiente bloque try circundante. El bloque try circundante podría estar en el método que hizo la llamada o en uno de los que lo llamaron. También es posible anidar una instrucción try en un bloque try; en tal caso, los bloques catch de la instrucción try externa procesarían las excepciones que no se atraparan en la instrucción try interna. Si se ejecuta un bloque try y tiene un bloque finally correspondiente, el bloque finally se ejecuta incluso aunque termine el bloque try, debido a una instrucción return. El retorno ocurre después de la ejecución del bloque finally.

Error común de programación 12.6 Lanzar una excepción desde un bloque finally puede ser peligroso. Si una excepción sin atrapar está esperando ser procesada cuando se ejecute el bloque finally, y el bloque finally lanza una nueva excepción que no se atrapa en el bloque finally, se pierde la primera excepción y la nueva excepción se pasa al siguiente bloque try circundante.

Tip de prevención de errores 12.4 Cuando coloque código que puede lanzar una excepción en un bloque finally, siempre encierre el código en la instrucción try que atrapa los tipos de excepciones apropiados. Esto evita la pérdida de cualquier excepción no atrapada y vuelta a lanzar, que ocurra antes de que se ejecute el bloque finally.

Observación de ingeniería de software 12.2 No coloque bloques try alrededor de cada instrucción que pueda lanzar una excepción, ya que esto puede dificultar la legibilidad de sus programas. Es mejor colocar un bloque try alrededor de una porción considerable de código, y colocar después este bloque try con bloques catch que manejen cada una de las posibles excepciones. Luego coloque un solo bloque finally después de los bloques catch. Se deben utilizar bloques try separados cuando es importante diferenciar entre varias instrucciones que puedan lanzar el mismo tipo de excepción. using

Anteriormente en esta sección vimos que el código de liberación de recursos debe colocarse en un bloque finally para asegurar que se libere un recurso, sin importar si hubo excepciones cuando se utilizó el recurso en el bloque try correspondiente. La instrucción using, una instrucción alternativa (que no debe confundirse con la directiva using para usar espacios de nombres), simplifica la escritura del código mediante el cual se obtiene un recurso, se utiliza el recurso en un bloque try y se libera el recurso en un bloque finally correspondiente. Por ejemplo, una aplicación de procesamiento de archivos (capítulo 18) podría procesar un archivo con una instrucción using para asegurar que el archivo se cierre en forma apropiada cuando ya no se necesite. El recurso debe ser un objeto

402

Capítulo 12 Manejo de excepciones

que implementa la interfaz instrucción using sería:

IDisposable

y, por lo tanto, tiene un método

Dispose.

La forma general de una

using ( ObjetoEjemplo objetoEjemplo = new ObjetoEjemplo() ) { objetoEjemplo.UnMetodo(); }

en donde ObjetoEjemplo es una clase que implementa a la interfaz IDisposable. Este código crea un objeto de tipo ObjetoEjemplo y lo utiliza en una instrucción; después llama a su método Dispose para liberar cualquier recurso utilizado por el objeto. La instrucción using coloca en forma implícita el código de su cuerpo en un bloque try con su correspondiente bloque finally, el cual llama al método Dispose del objeto. Por ejemplo, el código anterior es equivalente a: { ObjetoEjemplo objetoEjemplo = new ObjetoEjemplo(); try { objetoEjemplo.UnMetodo(); } finally { if (objetoEjemplo != null ) ( ( IDisposable ) objetoEjemplo ).Dispose(); } }

Observe que la instrucción if asegura que objetoEjemplo sigue referenciando a un objeto; de no ser así, podría ocurrir una excepción NullReferenceException. En la sección 8.13 de la Especificación del lenguaje C# (sección 15.13 en la versión ECMA) podrá leer más acerca del uso de la instrucción using.

12.7 Propiedades de Exception Como vimos en la sección 12.5, los tipos de excepciones se derivan de la clase Exception, la cual tiene varias propiedades. Éstas se utilizan con frecuencia para formular mensajes de error que indican una excepción atrapada. Dos propiedades importantes son Message y StackTrace. La propiedad Message almacena el mensaje de error asociado con un objeto Exception. Este mensaje puede ser un mensaje predeterminado asociado con el tipo de excepción, o puede ser un mensaje personalizado que se pasa al constructor de un objeto Exception cuando éste se lanza. La propiedad StackTrace contiene un objeto string que representa la pila de llamadas a los métodos. Recuerde que el entorno en tiempo de ejecución mantiene en todo momento una lista de llamadas abiertas a métodos, que se han realizado pero que no han regresado aún. La propiedad StackTrace representa la serie de métodos que no han terminado de procesarse al momento en que ocurre la excepción.

Tip de prevención de errores 12.5 Un rastreo de pila muestra la pila de llamadas a métodos completa, al momento en que ocurrió una excepción. Esto permite al programador ver la serie de llamadas a métodos que condujeron a la excepción. La información en el rastreo de pila incluye los nombres de los métodos en la pila de llamadas al momento de la excepción, los nombres de las clases en las que están definidos los métodos y los nombres de los espacios de nombres en los que están definidas las clases. Si el archivo de base de datos del programa (PDB) que contiene la información de depuración para el método está disponible, el rastreo de pila también incluye los números de línea; el primer número de línea indica el punto de lanzamiento y los subsiguientes números de línea indican las ubicaciones desde las que se llamaron los métodos en el rastreo de pila. El IDE crea los archivos PDB para mantener la información de depuración para los proyectos que cree.

Propiedad InnerException Otra propiedad que utilizan con frecuencia los programadores de bibliotecas de clases es InnerException. Por lo general, los programadores de bibliotecas de clases “envuelven” los objetos de excepciones que se atrapan en su código, para que entonces puedan lanzar nuevos tipos de excepciones que sean específicos para sus bibliotecas.

12.7 Propiedades de Exception

403

Por ejemplo, un programador que implemente un sistema de contabilidad podría tener cierto código de procesamiento de números de cuenta, en el cual los números de cuenta se introduzcan como objetos string, pero se representen como objetos int en el código. Recuerde que un programa puede convertir valores string en valores int mediante Convert.ToInt32, el cual lanza una excepción FormatException cuando encuentra un formato de número inválido. Cuando se produce un formato de número de cuenta inválido, el programador del sistema de contabilidad tal vez desee emplear un mensaje de error distinto al mensaje predeterminado suministrado por FormatException, o tal vez desee indicar un nuevo tipo de excepción, como NumeroCuentaInvalidoFormatException. En tales casos, el programador proporcionaría código para atrapar la excepción FormatException y después crearía un tipo apropiado de objeto Exception en el bloque catch, y pasaría la excepción original como uno de los argumentos para el constructor. El objeto excepción original se convierte en la excepción interior (InnerException) del nuevo objeto excepción. Cuando ocurre una excepción NumeroCuentaInvalidoFormatException en el código que utiliza la biblioteca del sistema de contabilidad, el bloque catch que atrapa la excepción puede obtener una referencia a la excepción original, a través de la propiedad InnerException. Por ende, la excepción indica que el usuario específico un número de cuenta inválido y que el problema era un formato de número inválido. Si la propiedad InnerException es null, esto indica que la excepción no se produjo por otra excepción.

Otras propiedades de Exception La clase Exception tiene otras propiedades, incluyendo HelpLink, Source y TargetSite. La propiedad HelpLink especifica la ubicación del archivo de ayuda que describe el problema que ocurrió. Esta propiedad es null si no existe dicho archivo. La propiedad Source especifica el nombre de la aplicación en la que ocurrió la excepción. La propiedad TargetSite especifica el método en donde se originó la excepción.

Demostración de las propiedades de Exception y la limpieza de la pila Nuestro siguiente ejemplo (figura 12.5) demuestra las propiedades Message, StackTrace e InnerException, y el método ToString de la clase Exception. Además, el ejemplo introduce el concepto de limpieza de la pila: cuando se lanza una excepción pero no se atrapa en un alcance específico, la pila de llamadas a métodos se “limpia” y se hace un intento de atrapar (catch) la excepción en el siguiente bloque try exterior. Llevaremos el registro de los métodos en la pila de llamadas a medida que hablemos sobre la propiedad StackTrace y el mecanismo de limpieza de la pila. Para ver el rastreo de pila apropiado, debe ejecutar este programa utilizando pasos similares a los que presentamos en la sección 12.3.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

// Fig. 12.5: Propiedades.cs // Limpieza de la pila y las propiedades de la calse Exception. // Demuestra el uso de las propiedades Message, StackTrace e InnerException. using System; class Propiedades { static void Main() { // llama a Metodo1; cualquier Exception que se genere // se atrapa en el bloque catch que le sigue try { Metodo1(); } // fin de try catch ( Exception parametroExcepcion ) { // imprime en pantalla la representación string de Exception, después imprime // en pantalla las propiedades InnerException, Message y StackTrace Console.WriteLine( "parametroExcepcion.ToString: \n{0}\n", parametroExcepcion.ToString() );

Figura 12.5 | Las propiedades de Exception y la limpieza de la pila. (Parte 1 de 3).

404

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

Capítulo 12 Manejo de excepciones

Console.WriteLine( "parametroExcepcion.Message: \n{0}\n", parametroExcepcion.Message ); Console.WriteLine( "parametroExcepcion.StackTrace: \n{0}\n", parametroExcepcion.StackTrace ); Console.WriteLine( "parametroExcepcion.InnerException: \n{0}\n", parametroExcepcion.InnerException.ToString() ); } // fin de catch } // fin del método Main // llama a Metodo2 static void Metodo1() { Metodo2(); } // fin del método Metodo1 // llama a Metodo3 static void Metodo2() { Metodo3(); } // fin del método Metodo2 // lanza una Exception que contiene una InnerException static void Metodo3() { // trata de convertir string en int try { Convert.ToInt32( "No es un entero" ); } // fin de try catch ( FormatException parametroFormatException ) { // envuelve a FormatException en nueva Exception throw new Exception( "Ocurrió una excepción en Metodo3", parametroFormatException ); } // fin de catch } // fin del método Metodo3 } // fin de la clase Propiedades

parametroExcepcion.ToString: System.Exception: Ocurrió una excepción en Metodo3 ---> System.FormatException: La cadena de entrada no tiene el formato correcto. en System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) en System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) en System.Convert.ToInt32(String value) en Propiedades.Metodo3() en C:\ejemplos\cap12\Fig12_04\Propiedades\ Propiedades.cs:línea 49 --- Fin del seguimiento de la pila de la excepción interna --en Propiedades.Metodo3() en C:\ejemplos\cap12\Fig12_04\Propiedades\ Propiedades.cs:línea 54 en Propiedades.Metodo2() en C:\ejemplos\cap12\Fig12_04\Propiedades\ Propiedades.cs:línea 40 en Propiedades.Metodo1() en C:\ejemplos\cap12\Fig12_04\Propiedades\ Propiedades.cs:línea 34 en Propiedades.Main() en C:\ejemplos\cap12\Fig12_04\Propiedades\ Propiedades.cs:línea 14

Figura 12.5 | Las propiedades de Exception y la limpieza de la pila. (Parte 2 de 3).

12.7 Propiedades de Exception

405

parametroExcepcion.Message: Ocurrió una excepción en Metodo3 parametroExcepcion.StackTrace: en Propiedades.Metodo3() en C:\ejemplos\cap12\Fig12_04\Propiedades\ Propiedades.cs:línea 54 en Propiedades.Metodo2() en C:\ejemplos\cap12\Fig12_04\Propiedades\ Propiedades.cs:línea 40 en Propiedades.Metodo1() en C:\ejemplos\cap12\Fig12_04\Propiedades\ Propiedades.cs:línea 34 en Propiedades.Main() en C:\ejemplos\cap12\Fig12_04\Propiedades\ Propiedades.cs:línea 14 parametroExcepcion.InnerException: System.FormatException: La cadena de entrada no tiene el formato correcto. en System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) en System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) en System.Convert.ToInt32(String value) en Propiedades.Metodo3() en C:\ejemplos\cap12\Fig12_04\Propiedades\ Propiedades.cs:línea 49

Figura 12.5 | Las propiedades de Exception y la limpieza de la pila. (Parte 3 de 3). La ejecución del programa empieza con Main, que se convierte en el primer método en la pila de llamadas a métodos. La línea 14 del bloque try en Main invoca al Metodo1 (declarado en las líneas 32-35), el cual se convierte en el segundo método en la pila. Si Metodo1 lanza una excepción, el bloque catch en las líneas 16-28 maneja la excepción e imprime en pantalla información acerca de la excepción que ocurrió. La línea 34 de Metodo1 invoca a Metodo2 (líneas 38-41), el cual se convierte en el tercer método en la pila. Después la línea 40 de Metodo2 invoca a Metodo3 (líneas 44-57), el cual se convierte en el cuarto método en la pila. En este punto, la pila de llamadas a métodos (de arriba-abajo) para el programa es: Metodo3 Metodo2 Metodo1 Main

El método que se llamó al último (Metodo3) aparece en la parte superior de la pila; el primer método que se llamó (Main) aparece en la parte inferior. La instrucción try (líneas 47-56) en Metodo3 invoca al método Convert. ToInt32 (línea 49), el cual trata de convertir un valor string en un valor int. En este punto, Convert.ToInt32 se convierte en el método 50, y final, de la pila.

Lanzar una Exception con una InnerException Debido a que el argumento para Convert.ToInt32 no está en formato int, la línea 49 lanza una excepción FormatException que se atrapa en la línea 51 de Metodo3. La excepción termina la llamada a Convert.ToInt32, por lo que el método se elimina (o limpia) de la pila de llamadas a métodos. Después, el bloque catch en Metodo3 crea y lanza un objeto Exception. El primer argumento para el constructor de Exception es el mensaje de error personalizado para nuestro ejemplo, “Ocurrió una excepción en Metodo3”. El segundo argumento es InnerException: la excepción FormatException que se atrapó. El valor de StackTrace para este nuevo objeto excepción refleja el punto en el cual se lanzó la excepción (líneas 54-55). Ahora Metodo3 termina, ya que la excepción que se lanzó en el bloque catch no se atrapó en el cuerpo del método. Por ende, el control regresa a la instrucción que invocó a Metodo3 en el método anterior en la pila de llamadas (Metodo2). Esto elimina, o limpia, a Metodo3 de la pila de llamadas a métodos. Cuando el control regresa a la línea 40 en Metodo2, el CLR determina que la línea 40 no está dentro de un bloque try. Por lo tanto, la excepción no se puede atrapar en Metodo2 y éste termina. Esto limpia a Metodo2 de la pila de llamadas y se regresa el control a la línea 28 en Metodo1.

406

Capítulo 12 Manejo de excepciones

Aquí podemos ver otra vez que la línea 34 no está dentro de un bloque try, por lo que Metodo1 no puede atrapar la excepción. El método termina y se limpia de la pila de llamadas, regresando el control a la línea 14 en Main, que está ubicado dentro de un bloque try. El bloque try en Main expira y el bloque catch (líneas 16-28) atrapa la excepción. El bloque catch utiliza el método ToString y las propiedades Message, StackTrace e InnerException para crear la salida. Observe que la limpieza de la pila continúa hasta que un bloque catch atrape la excepción, o cuando termine el programa.

Mostrar información acerca de la excepción Exception El primer bloque de salida (le dimos formato otra vez, para mejorar la legibilidad) en la figura 12.5 contiene la representación string de la excepción, que devuelve el método ToString. El objeto string empieza con el nombre de la clase de la excepción, seguido del valor de la propiedad Message. Los siguientes cuatro elementos presentan el rastreo de pila del objeto InnerException. El resto del bloque de resultado muestra la propiedad StackTrace para la excepción que se lanza en Metodo3. Observe que StackTrace representa el estado de la pila de llamadas a métodos en el punto de lanzamiento de la excepción, en vez de que sea el punto en donde se atrapará la excepción, en un momento dado. Cada línea de StackTrace que empieza con “en” representa un método en la pila de llamadas. Estas líneas indican el método en el que ocurrió la excepción, el archivo en el cual reside el método y el número de línea del punto de lanzamiento en el archivo. Observe que la información de la excepción interior incluye el rastreo de pila de esta excepción.

Tip de prevención de errores 12.6 Al atrapar y volver a lanzar una excepción, debe proporcionar información de depuración adicional en la excepción que se volvió a lanzar. Para ello, cree un objeto Exception que contenga información de depuración más específica, y después pase la excepción original que se atrapó al constructor del nuevo objeto excepción, para inicializar la propiedad InnerException.

El siguiente bloque de resultados (dos líneas) sólo muestra en pantalla el valor de la propiedad Message (Ocurrió una excepción en Metodo3) de la excepción que se lanzó en Metodo3. El tercer bloque de resultados muestra en pantalla la propiedad StackTrace de la excepción que se lanzó en Metodo3. Observe que esta propiedad StackTrace contiene el rastreo de pila, empezando desde la línea 54 en Metodo3, ya que es el punto en el que se creó y se lanzó el objeto Exception. El rastreo de pila siempre empieza desde el punto de lanzamiento de la excepción. Para terminar, el último bloque de resultados muestra en pantalla la representación string de la propiedad InnerException, la cual incluye el espacio de nombres y el nombre de clase del objeto excepción, así como las propiedades Message y StackTrace.

12.8 Clases de excepciones definidas por el usuario En muchos casos, puede utilizar las clases de excepciones existentes de la Biblioteca de clases del .NET Framework para indicar las excepciones que ocurren en sus programas. No obstante, en algunos casos tal vez sea conveniente crear nuevas clases de excepciones que sean específicas para los problemas que ocurran en sus programas. Las clases de excepciones definidas por el usuario deben derivarse de manera directa o indirecta de la clase ApplicationException, del espacio de nombres System.

Buena práctica de programación 12.1 Asociar cada tipo de falla con una clase de excepción con un nombre apropiado mejora la legibilidad del programa.

Observación de ingeniería de software 12.3 Antes de crear una clase de excepción definida por el usuario, investigue las excepciones existentes en la Biblioteca de clases del .NET Framework para determinar si ya existe un tipo apropiado de excepción.

Las figuras 12.6 y 12.7 demuestran el uso de una clase de excepción definida por el usuario. La clase Numero(figura 12.6) es una clase de excepción definida por el usuario, la cual representa excepciones que ocurren cuando un programa realiza una operación ilegal con un número negativo, como tratar de calcular su raíz cuadrada.

NegativoException

12.8 Clases de excepciones definidas por el usuario

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

407

// Fig. 12.6: NumeroNegativoException.cs // NumeroNegativoException representa las excepciones producidas por // operaciones ilegales con números negativos. using System; namespace PruebaRaizCuadrada { class NumeroNegativoException : ApplicationException { // constructor predeterminado public NumeroNegativoException() : base( "Operación ilegal para un número negativo" ) { // cuerpo vacío } // fin del constructor predeterminado // constructor para personalizar el mensaje de error public NumeroNegativoException( string valorMensaje ) : base( valorMensaje ) { // cuerpo vacío } // fin del constructor con un argumento // constructor para personalizar el mensaje de error // de la excepción y especificar el objeto InnerException public NumeroNegativoException( string valorMensaje, Exception exterior ) : base( valorMensaje, exterior ) { // cuerpo vacío } // fin del constructor con dos argumentos } // fin de la clase NumeroNegativoException } // fin del espacio de nombres PruebaRaizCuadrada

Figura 12.6 | La clase derivada de ApplicationException, que se lanza cuando un programa realiza una operación ilegal con un número negativo.

De acuerdo con las “Mejores prácticas para el manejo de excepciones [C#]”, las excepciones definidas por el usuario deben extender la clase ApplicationException, deben tener un nombre de clase que termine con “Exception” y deben definir tres constructores: un constructor sin parámetros; un constructor que reciba un argumento string (el mensaje de error); y un constructor que reciba un argumento string y un argumento Exception (el mensaje de error y el objeto excepción interior). Si define estos tres constructores, su clase de excepción será más flexible, lo que permitirá que otros programadores puedan usarla y extenderla con facilidad. Las excepciones del tipo NumeroNegativoException ocurren con más frecuencia durante las operaciones aritméticas, por lo que parece lógico derivar la clase NumeroNegativoException de la clase ArithmeticException. No obstante, la clase ArithmeticException se deriva de la clase SystemException: la categoría de excepciones que lanza el CLR. Recuerde que las clases de excepciones definidas por el usuario deben heredar de ApplicationException, en vez de SystemException. La clase RaizCuadradaForm (figura 12.7) demuestra nuestra clase de excepción definida por el usuario. La aplicación permite al usuario introducir un valor numérico y después invoca al método RaizCuadrada (líneas 17-25) para calcular la raíz cuadrada de ese valor. Para realizar este cálculo, RaizCuadrada invoca al método Sqrt de la clase Math, el cual recibe un valor double como argumento. Por lo general, si el argumento es negativo, el método Sqrt devuelve NaN. En este programa nos gustaría evitar que el usuario calculara la raíz cuadrada de un número negativo. Si el valor numérico que introduce el usuario es negativo, el método RaizCuadrada lanza una excepción NumeroNegativoException (líneas 21-22). De no ser así, RaizCuadrada invoca al método Sqrt de la clase Math para calcular la raíz cuadrada (línea 24).

408

Capítulo 12 Manejo de excepciones

Cuando el usuario introduce un valor y hace clic en el botón Raiz cuadrada, el programa invoca al manejador de eventos RaizCuadradaButton_Click (líneas 28-53). La instrucción try (líneas 33-52) intenta invocar a RaizCuadrada usando el valor introducido por el usuario. Si la entrada del usuario no es un número válido, se produce una excepción FormatException y el bloque catch en las líneas 40-45 procesa la excepción. Si el usuario introduce un número negativo, el método RaizCuadrada lanza una excepción NumeroNegativoException (líneas 21-22); el bloque catch en las líneas 46-52 atrapa y maneja este tipo de excepción. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

// Fig. 12.7: PruebaRaizCuadrada.cs // Demostración de una clase de excepción definida por el usuario. using System; using System.Windows.Forms; namespace PruebaRaizCuadrada { public partial class RaizCuadradaForm : Form { public RaizCuadradaForm() { InitializeComponent(); } // fin del constructor // calcula la raíz cuadrada del parámetro; lanza excepción // NumeroNegativoException si el parámetro es negativo public double RaizCuadrada( double valor ) { // si el operando es negativo, lanza NumeroNegativoException if ( valor < 0 ) throw new NumeroNegativoException( "No se permite la raíz cuadrada de un número negativo" ); else return Math.Sqrt( valor ); // calcula raíz cuadrada } // fin del método RaizCuadrada // obtiene la entrada del usuario, convierte en double, calcula la raíz cuadrada private void RaizCuadradaButton_Click( object sender, EventArgs e ) { SalidaLabel.Text = ""; // borra SalidaLabel // atrapa cualquier excepción NumeroNegativoException que se lance try { double resultado = RaizCuadrada( Convert.ToDouble( EntradaTextBox.Text ) ); SalidaLabel.Text = resultado.ToString(); } // fin de try catch ( FormatException parametroFormatException ) { MessageBox.Show( parametroFormatException.Message, "Formato de número inválido", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch catch ( NumeroNegativoException parametroNumeroNegativoException ) { MessageBox.Show( parametroNumeroNegativoException.Message,

Figura 12.7 | La clase RaizCuadradaForm lanza una excepción si ocurre un error al calcular la raíz cuadrada. (Parte 1 de 2).

12.9 Conclusión

50 51 52 53 54 55

409

"Operación inválida", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin del método RaizCuadradaButton_Click } // fin de la clase RaizCuadradaForm } // fin del espacio de nombres PruebaRaizCuadrada a)

b)

c)

d)

Figura 12.7 | La clase RaizCuadradaForm lanza una excepción si ocurre un error al calcular la raíz cuadrada. (Parte 2 de 2).

12.9 Conclusión En este capítulo aprendió a utilizar el manejo de excepciones para lidiar con los errores en una aplicación. Demostramos que el manejo de las excepciones le permite eliminar el código para manejar errores de la “línea principal” de ejecución del programa. Vio el manejo de excepciones en el contexto de un ejemplo de división entre cero. Aprendió a utilizar bloques try para encerrar el código que podría lanzar una excepción, y cómo utilizar bloques catch para lidiar con las excepciones que puedan surgir. Hablamos sobre el modelo de terminación del manejo de excepciones, en el cual después de manejar una excepción, el control del programa no regresa al punto de lanzamiento. También vimos varias clases importantes de la jerarquía Exception de .NET, incluyendo ApplicationException (a partir de la cual se derivan las clases de excepciones definidas por el usuario) y SystemException. Después aprendió a utilizar el bloque finally para liberar recursos en caso de que ocurra o no una excepción, y cómo lanzar y volver a lanzar excepciones mediante la instrucción throw. Hablamos además sobre cómo puede usarse la instrucción using para automatizar el proceso de liberar un recurso. Más adelante aprendió a obtener información acerca de una excepción, mediante el uso de las propiedades Message, StackTrace e InnerException de la clase Exception, junto con el método ToString. Aprendió a crear sus propias clases de excepciones. En los siguientes dos capítulos, presentaremos un tratamiento detallado de las interfaces gráficas de usuario. En estos capítulos y durante el resto de este libro, usaremos el manejo de excepciones para que nuestros ejemplos sean más robustos, al mismo tiempo que se demuestran las nuevas características del lenguaje.

13

Conceptos de interfaz gráfica de usuario: parte I …los profetas más sabios se aseguran primero del evento. —Horace Walpole

OBJETIVOS En este capítulo aprenderá lo siguiente: Q

Los principios de diseño de las interfaces gráficas de usuario (GUIs).

Q

Crear interfaces gráficas de usuario.

Q

Procesar eventos que se generen mediante las interacciones del usuario con los controles de la GUI.

Q

Acerca de los espacios de nombres que contienen las clases para los controles de la interfaz gráfica de usuario y el manejo de los eventos.

Q

Crear y manipular controles Button, Label, RadioButton, CheckBox, TextBox, Panel y NumericUpDown.

Q

Agregar cuadros de información de la herramienta (ToolTip) descriptivos a los controles de la GUI.

Q

Procesar eventos del ratón y del teclado.

… El usuario debe sentirse en control de la computadora; no al revés. Esto se logra en aplicaciones que abarcan tres cualidades: sensibilidad, permisividad y consistencia. —Apple Computer, Inc. 1985

Para verte mejor, querida. —El Gran Lobo Malo a Caperucita Roja

Pla n g e ne r a l

412

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

13.1 Introducción 13.2 Formularios Windows Forms 13.3 Manejo de eventos 13.3.1 Una GUI simple controlada por eventos 13.3.2 Otro vistazo al código generado por Visual Studio 13.3.3 Delegados y el mecanismo de manejo de eventos 13.3.4 Otras formas de crear manejadores de eventos 13.3.5 Localización de la información de los eventos 13.4 Propiedades y distribución de los controles 13.5 Controles Label, TextBox y Button 13.6 Controles GroupBox y Panel 13.7 Controles CheckBox y RadioButton 13.8 Controles PictureBox 13.9 Controles ToolTip 13.10 Control NumericUpDown 13.11 Manejo de los eventos del ratón 13.12 Manejo de los eventos del teclado 13.13 Conclusión

13.1 Introducción Una interfaz gráfica de usuario (GUI) permite a un usuario interactuar con un programa en forma visual; además, proporciona a un programa una “apariencia visual” única. Al proporcionar distintas aplicaciones en las que los componentes de la interfaz de usuario sean consistentes e intuitivos, los usuarios pueden conocer con mayor rapidez las aplicaciones y así volverse más productivos. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 13.1 Las interfaces de usuario consistentes permiten aprender nuevas aplicaciones con más rapidez, ya que éstas tienen la misma “apariencia visual”.

Como ejemplo de GUI, considere la figura 13.1, que muestra una ventana del navegador Web Internet Explorer que contiene varios controles. Cerca de la parte superior de la ventana hay una barra de menús que muestra los menús Archivo, Edición, Ver, Favoritos, Herramientas y Ayuda. Debajo del menú hay un conjunto de botones, cada uno de los cuales tiene una tarea definida, como regresar a la página Web anterior, imprimir la página actual o actualizarla. Debajo de estos botones hay un cuadro combinado, en el cual, los usuarios pueden escribir las ubicaciones de los sitios Web que desean visitar. A la izquierda del cuadro combinado hay una etiqueta (Dirección) que indica el propósito del cuadro combinado (en este caso, escribir la ubicación de un sitio Web). En la parte derecha e inferior de la ventana hay barras de desplazamiento. Por lo general, estas barras aparecen cuando una ventana contiene más información de la que puede desplegar en su área visible. Las barras de desplazamiento permiten que el usuario vea distintas porciones del contenido de la ventana. Estos controles forman una interfaz amigable al usuario, a través de la cual éste interactúa con el navegador Web Internet Explorer. Las GUIs se crean a partir de controles de la GUI (conocidos como componentes o widgets [accesorios de ventana]). Los controles de la GUI son objetos que pueden mostrar información en la pantalla o que permiten a los usuarios interactuar con una aplicación a través del ratón, del teclado o de alguna otra forma de entrada (como los comandos de voz). En la figura 13.2 se listan varios controles comunes de la GUI; en las subsiguientes secciones y en el capítulo 14 hablaremos detalladamente sobre cada uno de estos controles. En el capítulo 14 también se exploran las características y propiedades de controles adicionales de la GUI.

13.2 Formularios Windows Forms

Etiqueta

Botón

Menú

Barra de título

Barra de menús

Cuadro combinado

413

Barras de desplazamiento

Figura 13.1 | Controles de la GUI en una ventana de Internet Explorer.

Control

Descripción

Label

Muestra imágenes o texto que no puede editarse. Permite al usuario introducir datos mediante el teclado. También puede usarse para mostrar texto que puede o no editarse. Activa un evento cuando se hace clic con el ratón. Especifica una opción que puede seleccionarse (activada) o deseleccionarse (desactivada). Proporciona una lista desplegable de elementos, de los cuales el usuario puede seleccionar uno, haciendo clic en un elemento de la lista o escribiendo dentro de un cuadro. Proporciona una lista de elementos, de los cuales el usuario puede seleccionar uno, haciendo clic en un elemento de la lista. Pueden seleccionarse varios elementos a la vez. Un contenedor en el que pueden colocarse y organizarse controles. Permite al usuario seleccionar de un rango de valores de entrada.

TextBox Button CheckBox ComboBox ListBox Panel NumericUpDown

Figura 13.2 | Algunos controles básicos de la GUI.

13.2 Formularios Windows Forms

Los formularios Windows Forms se utilizan para crear las GUIs para los programas. Un formulario (Form) es un elemento gráfico que aparece en el escritorio de su computadora; puede ser un cuadro de diálogo, una ventana o una ventana MDI (ventana de interfaz de múltiples documentos), que veremos en el capítulo 14, Conceptos de interfaz gráfica de usuario: parte 2. Un componente es una instancia de una clase que implementa a la interfaz IComponent, la cual define los comportamientos que deben implementar los componentes, como la forma en que se debe cargar el componente. Un control como Button o Label tiene una representación gráfica en

414

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

tiempo de ejecución. Algunos componentes carecen de representaciones gráficas. (Por ejemplo, la clase Timer del espacio de nombres System.Windows.Forms; vea el capítulo 14). Dichos componentes no son visibles en tiempo de ejecución. La figura 13.3 muestra los controles y componentes Windows Forms del Cuadro de herramientas de C#. Los controles y componentes se organizan en categorías, con base en su funcionalidad. Si selecciona la categoría Todos los formularios Windows Forms en la parte superior del Cuadro de herramientas, podrá ver todos los controles y componentes de las demás fichas en una sola lista (como se muestra en la figura 13.3). En este capítulo y en el siguiente, hablaremos sobre muchos de estos controles y componentes. Para agregar un control o componente a un formulario, seleccione ese control o componente del Cuadro de herramientas y arrástrelo en el formulario. Para deseleccionar un control o componente, seleccione el elemento Puntero en el Cuadro de herramientas (el icono en la parte superior de la lista). Cuando se selecciona el elemento Puntero, no se puede agregar de manera accidental un nuevo control al formulario. Cuando hay varias ventanas en la pantalla, la ventana activa es la que está al frente y tiene su barra de título resaltada; por lo general de un color azul más oscuro que el de las otras ventanas en la pantalla. Una ventana se convierte en la ventana activa cuando el usuario hace clic en alguna parte dentro de ella. Se dice que la ventana activa “tiene el foco”. Por ejemplo, en Visual Studio la ventana activa es el Cuadro de herramientas cuando se selecciona un elemento de ella, o la ventana Propiedades cuando estamos editando las propiedades de un control. Un formulario es un contenedor de controles y componentes. Cuando usted arrastra un control o componente del Cuadro de herramientas hacia el formulario, Visual Studio genera código que crea una instancia del objeto y establece sus propiedades básicas. Este código se actualiza cuando se modifican las propiedades del control o componente en el IDE. Si se elimina un control o componente del formulario, se elimina el código generado para ese control. El IDE coloca el código generado en un archivo separado, usando clases parciales. Aunque podríamos escribir este código por nuestra cuenta, es mucho más fácil crear y modificar controles y componentes utilizando las ventanas Cuadro de herramientas y Propiedades, y permitir que Visual Studio se encargue de los detalles. En el

Muestra todos los controles y componentes

Categorías que organizan los controles y componentes con base en su funcionalidad

Figura 13.3 | Componentes y controles para formularios Windows Forms.

13.3 Manejo de eventos

Propiedades, métodos y eventos de Form

415

Descripción

Propiedades comunes AcceptButton AutoScroll CancelButton FormBorderStyle Font Text

Control Button en el que se hace clic al oprimir Intro. Valor boolean que activa o desactiva las barras de desplazamiento, según sea necesario. Control Button en el que se hace clic al oprimir Esc. Estilo de borde para el formulario. (Por ejemplo: ninguno, simple o tridimensional). La fuente de texto que se muestra en el formulario, y la fuente predeterminada para los controles que se agregan al formulario. El texto en la barra de título del formulario.

Métodos comunes Close

Hide Show

Cierra un formulario y libera todos los recursos, como la memoria utilizada para los controles y componentes del formulario. Un formulario cerrado no puede volver a abrirse. Oculta un formulario, pero no lo destruye ni libera sus recursos. Muestra un formulario oculto.

Eventos comunes Load

Ocurre antes de que se muestre un formulario al usuario. El manejador para este evento se muestra en el editor de Visual Studio al hacer doble clic en el formulario, en el diseñador de Visual Studio.

Figura 13.4 | Propiedades, métodos y eventos comunes del control Form.

capítulo 2 presentamos los conceptos de la programación visual. En este capítulo y en el siguiente, utilizaremos la programación visual para crear GUIs más robustas. Cada control o componente que presentamos en este capítulo se encuentra en el espacio de nombres System.Windows.Forms. Para crear una aplicación de Windows, por lo general se crea un formulario, se establecen sus propiedades, se agregan controles al formulario, se establecen las propiedades de estos controles y se implementan manejadores de eventos (métodos) que respondan a los eventos generados por los controles. La figura 13.4 lista las propiedades, métodos y eventos comunes del control Form. Cuando creamos controles y manejadores de eventos, Visual Studio genera la mayor parte del código relacionado con la GUI. En la programación visual, el IDE se encarga de mantener el código relacionado con la GUI y usted escribe los cuerpos de los manejadores de eventos, para indicar qué acciones debe realizar el programa cuando ocurren ciertos eventos.

13.3 Manejo de eventos Por lo general, un usuario interactúa con la GUI de una aplicación para indicar las tareas que ésta debe realizar. Por ejemplo, cuando usted escribe un correo electrónico en una aplicación para correo electrónico, al hacer clic en el botón Enviar indica a la aplicación que debe enviar el correo electrónico a las direcciones de correo electrónico especificadas. Las GUIs son controladas por eventos. Cuando el usuario interactúa con un componente de la GUI, la interacción (conocida como evento) controla el programa para que realice una tarea. Los eventos comunes (interacciones del usuario) que podrían hacer que una aplicación realice una tarea incluyen el hacer clic en un control Button, escribir en un control TextBox, seleccionar un elemento de un menú, cerrar una ventana y mover el ratón. A un método que realiza una tarea en respuesta a un evento se le conoce como manejador de eventos, y al proceso general de responder a los eventos se le conoce como manejo de eventos.

416

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

13.3.1 Una GUI simple controlada por eventos El formulario (Form) en la aplicación de la figura 13.5 contiene un control Button en el que puede hacer clic el usuario para mostrar un control MessageBox. Ya hemos creado varios ejemplos de la GUI que ejecutan un manejador de eventos, en respuesta al clic de un control Button. En este ejemplo veremos el código generado en forma automática por Visual Studio con más detalle. Usando las técnicas que presentamos en capítulos anteriores de este libro, cree un formulario que contenga un botón (Button). Primero, cree una aplicación nueva de Windows y agregue un botón al formulario. En la ventana Propiedades para el control Button, escriba en la propiedad (Name) el texto clicButton y en la propiedad Text el texto Haga clic. Como podrá observar, utilizamos una convención en la que el nombre de cada variable que creamos para un control, termina con el tipo del control. Por ejemplo, en la variable llamada clicButton, “Button” es el tipo del control. Cuando el usuario hace clic en el control Button en este ejemplo, queremos que la aplicación responda mostrando un cuadro de diálogo MessageBox. Para ello, debemos crear un manejador de eventos para el evento Click del control Button. Para crear este manejador de eventos podemos hacer doble clic en el control Button dentro del formulario, el cual declara el siguiente manejador de eventos vacío en el código del programa: private void clicButton_Click( object sender, EventArgs e ) { }

// fin del método clicButton_Click

Por convención, C# nombra al método manejador de eventos así: nombreControl_nombreEvento. (Por ejemplo, clicButton_Click). El manejador de eventos clicButton_Click se ejecuta cuando el usuario hace clic en el control clicButton. Cuando se hace una llamada a un manejador de eventos, éste recibe dos parámetros. El primero (una referencia object llamada sender) es una referencia al objeto que generó el evento. El segundo es una referencia a un objeto de argumentos de evento de tipo EventArgs (o una de sus clases derivadas), que por lo general se llama e.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

// Fig. 13.5: EjemploSimpleEventoForm.cs // Uso de Visual Studio para crear manejadores de eventos. using System; using System.Windows.Forms; // Formulario que muestra un manejador simple de eventos public partial class EjemploSimpleEventoForm : Form { // constructor predeterminado public EjemploSimpleEventoForm() { InitializeComponent(); } // fin del constructor // maneja evento de clic del control Button clickButton private void clicButton_Click( object sender, EventArgs e ) { MessageBox.Show( "Se hizo clic en el botón." ); } // fin del método clicButton_Click } // fin de la clase EjemploSimpleEventoForm

Figura 13.5 | Ejemplo simple de manejo de eventos, usando programación visual.

13.3 Manejo de eventos

417

Este objeto contiene información adicional acerca del evento que ocurrió. EventArgs es la clase base de todas las clases que representan la información de un evento.

Observación de ingeniería de software 13.1 No debe esperar valores de retorno de los manejadores de eventos; éstos están diseñados para ejecutar código con base en una acción, y devuelven el control al programa principal.

Buena práctica de programación 13.1 Use la convención de nomenclatura de manejadores de eventos nombreControl_nombreEvento, de manera que los nombres de los eventos sean significativos. Dichos nombres indican a los usuarios cuál es el evento que maneja un método y para qué control. Esta convención no es obligatoria, pero mejora la legibilidad de su código y facilita su comprensión, modificación y mantenimiento.

Para mostrar un cuadro de diálogo MessageBox en respuesta al evento, inserte la instrucción MessageBox.Show( "Se hizo clic en el botón." );

en el cuerpo del manejador del evento. El manejador de evento resultante aparece en las líneas 16-19 de la figura 13.5. Cuando ejecute la aplicación y haga clic en el botón (Button), aparecerá un cuadro de diálogo MessageBox en el que se mostrará el texto "Se hizo clic en el botón".

13.3.2 Otro vistazo al código generado por Visual Studio Visual Studio genera el código para crear e inicializar la GUI que usted creará en la ventana de diseño de la GUI. Este código generado en forma automática se coloca en el archivo Designer.cs del control Form (EjemploSimpleEventoForm.Designer.cs en este ejemplo). Puede abrir este archivo expandiendo el nodo del archivo con el que se encuentra trabajando en estos momentos (EjemploSimpleEventoForm.cs) y haciendo doble clic en el nombre del archivo que termina con Designer.cs. Las figuras 13.6-13.7 muestran el contenido de este archivo. El IDE contrae el código en las líneas 21-53 de la figura 13.7 de manera predeterminada. Ahora que hemos estudiado las clases y los objetos con detalle, este código le será más fácil de entender. Como Visual Studio crea y da mantenimiento a este código, por lo general no necesitamos analizarlo. De hecho, no nece-

Figura 13.6 | Primera mitad del archivo de código generado por Visual Studio.

418

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

Figura 13.7 | Segunda mitad del archivo de código generado por Visual Studio. sita comprender la mayor parte del código que se muestra aquí para crear aplicaciones con la GUI. No obstante, ahora lo analizaremos más de cerca para ayudarle a comprender cómo funcionan las aplicaciones con GUI. El código generado en forma automática que define la GUI en realidad forma parte de la clase de Form; en este caso, EjemploSimpleEventoForm. La línea 1 de la figura 13.6 utiliza el modificador partial, el cual permite dividir esta clase entre varios archivos. La línea 55 contiene la declaración del control Button llamado clicButton que creamos en el modo de Diseño. Observe que el control se declara como una variable de instancia de la clase EjemploSimpleEventoForm. De manera predeterminada, todas las declaraciones de las variables para los controles que se crean mediante la ventana de diseño de C# tienen un modificador de acceso private. El código también incluye el método Dispose para liberar recursos (líneas 12-19) y el método InitializeComponent (líneas 27-51), que contiene el código para crear el control Button, y después establece algunas de las propiedades del control Button y del control Form. Los valores de las propiedades corresponden a los valores establecidos en la ventana Propiedades para cada control. Observe que Visual Studio agrega comentarios al código que genera, como en las líneas 31-33. La línea 39 se generó cuando creamos el manejador de eventos para el evento Click de Button. El método InitializeComponent se llama cuando se crea el formulario, y establece propiedades tales como el título del formulario, su tamaño, los tamaños de los controles y el texto. Visual Studio también utiliza el código en este método para crear la GUI que vemos en la vista de diseño. Si cambia el código en InitializeComponent, podría evitar que Visual Studio desplegara la GUI en forma apropiada.

Tip de prevención de errores 13.1 El código generado al crear una GUI en el modo de Diseño no debe modificarse en forma directa; si lo hace la aplicación podría funcionar en forma incorrecta. Debe modificar las propiedades de los controles a través de la ventana Propiedades.

13.3 Manejo de eventos

419

13.3.3 Delegados y el mecanismo de manejo de eventos

El control que genera un evento se conoce como el emisor del evento. Un método manejador de eventos (conocido como el receptor del evento) responde a un evento específico que genera un control. Cuando ocurre un evento, el emisor del evento llama al receptor de su evento para realizar una tarea (es decir, para “manejar el evento”). El mecanismo para manejar eventos de .NET le permite elegir sus propios nombres para los métodos manejadores de eventos. No obstante, cada método manejador de eventos debe declarar los parámetros apropiados para recibir información acerca del evento que maneja. Como usted puede elegir sus propios nombres para los métodos, un emisor de eventos tal como un control Button no puede saber de antemano cuál método responderá a sus eventos. Por lo tanto, necesitamos un mecanismo para indicar cuál método será el receptor de eventos para un evento dado.

Delegados Los manejadores de eventos se conectan a los eventos de un control a través de objetos especiales, conocidos como delegados. Un objeto delegado guarda una referencia a un método con una firma que se especifica mediante la declaración del tipo del delegado. Los controles de la GUI tienen delegados predefinidos que corresponden a cada uno de los eventos que pueden generar. Por ejemplo, el delegado para el evento Click de un control Button es del tipo EventHandler (espacio de nombres System). Si busca este tipo en la documentación de la ayuda en línea, verá que se declara de la siguiente manera: public delegate void EventHandler( object sender, EventArgs e );

Aquí se utiliza la palabra clave delegate para declarar un tipo de delegado llamado EventHandler, el cual puede guardar referencias a los métodos que devuelven void y reciben dos parámetros: uno de tipo object (el emisor del evento) y uno de tipo EventArgs. Si compara la declaración del delegado con el encabezado de clicButton_ Click (figura 13.5, línea 16), podrá ver que este manejador de evento sin duda cumple con los requerimientos del delegado EventHandler. Observe que la anterior declaración crea toda una clase completa para usted. El compilador se encarga de los detalles relacionados con la declaración de esta clase especial.

Indicar el método que debe llamar un delegado Un emisor de eventos llama a un objeto delegado de la misma forma que a un método. Como cada manejador de eventos se declara como un delegado, el emisor de eventos sólo necesita llamar al delegado apropiado cuando ocurra un evento; un control Button llama a su delegado EventHandler en respuesta a un clic. El trabajo del delegado es invocar al método apropiado. Para que se pueda hacer la llamada al método clicButton_Click, Visual Studio asigna clicButton_Click al delegado, como se muestra en la línea 39 de la figura 13.7. Visual Studio agrega este código cuando usted hace doble clic en el control Button, en el modo de Diseño. La expresión new System.EventHandler(this.clicButton_Click);

crea un objeto delegado EventHandler y lo inicializa con el método clicButton_Click. La línea 39 utiliza el operador += para agregar el delegado al evento Click del objeto Button. Esto indica que clicButton_Click responderá cuando un usuario haga clic en el botón. Observe que la clase del delegado sobrecarga el operador += cuando el compilador la crea. Para especificar que se deben invocar varios métodos distintos en respuesta a un evento, podemos agregar otros delegados al evento Click del control Button con instrucciones similares a la línea 39 de la figura 13.7. Los delegados de eventos son multidifusión; representan un conjunto de objetos delegados, en donde todos tienen la misma firma. Los delegados multidifusión permiten que varios métodos se llamen en respuesta a un solo evento. Cuando ocurre un evento, el emisor de eventos llama a todos los métodos referenciados por el delegado multidifusión. A esto se le conoce como multidifusión de eventos. Los delegados de eventos se derivan de la clase MulticastDelegate, la cual se deriva de la clase Delegate (ambas del espacio de nombres System).

13.3.4 Otras formas de crear manejadores de eventos En todas las aplicaciones con GUI que hemos creado hasta ahora, hacemos doble clic en un control en el formulario para crear un manejador de eventos para ese control. Esta técnica crea un manejador de eventos para el evento predeterminado de un control: el evento que se utiliza con más frecuencia con ese control. Por lo general, los controles pueden generar muchos tipos distintos de eventos, y cada tipo puede tener su propio manejador de eventos. Por ejemplo, ya ha creado manejadores de eventos Click para controles Button, haciendo doble clic en

420

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

un control Button en vista de diseño (Click es el evento predeterminado para un control Button). No obstante, su aplicación también puede proporcionar un manejador de eventos para un evento MouseHover del control Button, el cual ocurre cuando el puntero del ratón permanece posicionado sobre el control Button. Ahora veremos cómo crear un manejador de eventos para un evento que no es el evento predeterminado de un control.

Uso de la ventana Propiedades para crear manejadores de eventos Puede crear manejadores de eventos adicionales, a través de la ventana Propiedades. Si selecciona un control en el formulario, haga clic en el icono Eventos (el icono del rayo en la figura 13.8) de la ventana Propiedades para que se listen todos los eventos para ese control. Puede hacer doble clic en el nombre de un evento para mostrar el manejador de eventos en el editor, si el manejador de eventos ya existe, o para crearlo. También puede seleccionar un evento y después usar la lista desplegable a su derecha para seleccionar un método existente que deba usarse como el manejador de eventos para ese evento. Los métodos que aparecen en esta lista desplegable son los métodos de la clase que tienen la firma apropiada para ser manejadores de eventos para el evento seleccionado. Para regresar a ver las propiedades de un control, seleccione el icono Propiedades (figura 13.8).

13.3.5 Localización de la información de los eventos En la documentación de Visual Studio puede aprender acerca de los distintos eventos que produce un control. Para ello, seleccione Ayuda > Índice. En la ventana que aparezca, seleccione .Net Framework en la lista desplegable Filtrado por y escriba el nombre de la clase del control en la ventana Índice. Para asegurar que seleccione la clase apropiada, escribe el nombre completamente calificado de la clase, como se muestra en la figura 13.9 para la clase System. Windows.Forms.Button. Una vez que selecciona la clase de un control en la documentación, aparece una lista de todos los miembros de esa clase. Esta lista incluye los eventos que puede generar la clase. En la figura 13.9 nos desplazamos hasta los eventos de la clase Button. Haga clic en el nombre de un evento para ver su descripción y ejemplos de su uso (figura 13.10). Observe que el evento Click se lista como miembro de la clase Control, ya que el evento Click de la clase Button se hereda de la clase Control.

13.4 Propiedades y distribución de los controles En esta sección veremos las generalidades sobre las propiedades comunes para muchos controles. Los controles se derivan de la clase Control (espacio de nombres System.Windows.Forms). La figura 13.11 lista algunas de las propiedades y métodos de la clase Control. Las propiedades que se muestran aquí pueden establecerse para muchos controles. Por ejemplo, la propiedad Text especifica el texto que aparece en un control. La posición de

Icono Propiedades

Icono Eventos Evento seleccionado

Figura 13.8 | En la ventana Propiedades se pueden ver los eventos para un control Button.

13.4 Propiedades y distribución de los controles

Nombre de la clase

421

Lista de eventos

Figura 13.9 | Lista de eventos de Button.

Nombre del evento Tipo del evento Clase del argumento del evento

Figura 13.10 | Detalles del evento Click.

Propiedades y métodos de la clase Control

Descripción

Propiedades comunes BackColor BackgroundImage Enabled

Focused Font

El color de fondo del control. La imagen de fondo del control. Especifica si el control está habilitado (es decir, si el usuario puede interactuar con él). Por lo general, ciertas partes de un control deshabilitado aparecen en color gris como indicación visual para el usuario de que el control está deshabilitado. Indica si el control tiene el foco. La fuente utilizada para mostrar el texto del control.

Figura 13.11 | Propiedades y métodos de la clase Control. (Parte 1 de 2).

422

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

Propiedades y métodos de la clase Control ForeColor TabIndex TabStop Text Visible

Descripción El color de primer plano del control. Por lo general, esta propiedad determina el color del texto en la propiedad Text. El orden de tabulación del control. Cuando se oprime Tab, el foco se transfiere entre los controles, con base en el orden de tabulación. Usted puede establecer este orden. Si es true, entonces un usuario puede dar el foco a este control al oprimir Tab. El texto asociado con el control. La ubicación y apariencia del texto varían, dependiendo del tipo de control. Indica si el control es visible.

Métodos comunes Focus Hide Show

Adquiere el foco. Oculta el control (establece la propiedad Visible a false). Muestra el control (establece la propiedad Visible a true).

Figura 13.11 | Propiedades y métodos de la clase Control. (Parte 2 de 2).

este texto varía, dependiendo del control. En un formulario de Windows, el texto aparece en la barra de título, pero el texto de un control Button aparece en su superficie. El método Focus transfiere el foco a un control y lo convierte en el control activo. Cuando usted oprime la ficha Tab en una aplicación Windows en ejecución, los controles reciben el orden especificado mediante su propiedad TabIndex. Visual Studio establece esta propiedad con base en el orden en el que se agregan los controles a un formulario, pero usted puede cambiar el orden de tabulación. TabIndex es útil para los usuarios que introducen información en muchos controles, como en un conjunto de controles TextBox que representan el nombre, la dirección y el número telefónico de un usuario. Éste puede introducir información y seleccionar rápidamente el siguiente control, con sólo oprimir Tab. La propiedad Enabled indica si el usuario puede interactuar con un control para generar un evento. A menudo, si se deshabilita un control, es debido a que no está disponible una opción para el usuario en ese momento. Por ejemplo, las aplicaciones de edición de texto deshabilitan con frecuencia el comando “pegar” hasta que el usuario copia algo de texto. En la mayoría de los casos, el texto de un control deshabilitado aparece en gris, en vez de negro). También puede ocultar un control al usuario sin deshabilitarlo, estableciendo la propiedad Visible a false o llamando al método Hide. En cada caso, el control aún existe pero no está visible en el formulario. Puede usar el anclaje y el acoplamiento para especificar la distribución de los controles dentro de un contendor (tal como Form). El anclaje hace que los controles permanezcan a una distancia fija desde los extremos del contenedor, incluso aunque éste cambie su tamaño. El anclaje mejora la experiencia del usuario. Por ejemplo, si el usuario espera que un control aparezca en cierta esquina de la aplicación, el anclaje asegura que el control siempre esté en esa esquina; aun si el usuario cambia el tamaño del formulario. El acoplamiento une un control a un contenedor, de tal forma que el control se estira a lo largo de todo un extremo del contenedor. Por ejemplo, un botón acoplado en la parte superior de un contenedor se estira a lo largo de toda la parte superior de ese contenedor, sin importar la anchura del contenedor. Cuando se cambia el tamaño de los contenedores padre, los controles anclados se desplazan (y posiblemente cambian de tamaño) de tal forma que la distancia desde los lados hasta donde están anclados no varíe. De manera predeterminada, la mayoría de los controles se anclan a la esquina superior izquierda del formulario. Para ver los efectos de anclar un control, cree una aplicación Windows simple que contenga dos controles Button. Ancle un control a los extremos derecho e inferior, estableciendo la propiedad Anchor como se muestra en la figura 13.12. Deje el otro control sin anclar. Ejecute la aplicación y aumente el tamaño del formulario. Observe que el control Button anclado a la esquina superior derecha siempre se encuentra a la misma distancia desde la esquina inferior derecha del formulario (figura 13.13), pero el otro control permanece a su distancia original desde la esquina superior izquierda del formulario.

13.4 Propiedades y distribución de los controles

423

Ventana de anclaje

Haga clic en la flecha hacia abajo en la propiedad Anchor para mostrar la ventana de anclaje

Las barras de color oscuro indican el (los) extremo(s) del contenedor al (los) cual(es) está anclado el control; haga clic con el ratón para seleccionar o deseleccionar una barra

Figura 13.12 | Manipulación de la propiedad Anchor de un control.

Antes de cambiar el tamaño

Después de cambiar el tamaño

Distancia constante hacia los extremos derecho e inferior

Figura 13.13 | Demostración del anclaje. Algunas veces es conveniente que un control abarque todo un extremo completo del formulario, aun cuando éste cambie su tamaño. Por ejemplo, un control tal como una barra de estado debe permanecer comúnmente en la parte inferior del formulario. El acoplamiento permite que un control abarque todo un extremo completo (izquierda, derecha, superior o inferior) de su contenedor padre, o que llene todo el contenedor. Cuando se cambia el tamaño del control padre, el control acoplado también cambia su tamaño. En la figura 13.14, un control Button está acoplado a la parte superior del formulario (abarcando la parte superior). Cuando se cambia

Antes de cambiar el tamaño

Después de cambiar el tamaño

El control se extiende a lo largo de toda la parte superior del formulario

Figura 13.14 | Acoplar un control Button a la parte superior de un formulario.

424

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

el tamaño del formulario, el control Button cambia su tamaño para ajustarse a la nueva anchura del formulario. Los formularios tienen una propiedad Padding, la cual especifica la distancia entre los controles acoplados y los bordes del formulario. Esta propiedad especifica cuatro valores (una para cada extremo), y cada valor se establece a 0 de manera predeterminada. En la figura 13.15 se muestra un resumen de algunas propiedades comunes para distribuir controles. Las propiedades Anchor y Dock de un objeto Control se establecen con respecto al contenedor padre del objeto Control, el cual podría ser un formulario o algún otro contenedor padre (como un Panel, que veremos en la sección 13.6). Los tamaños mínimo y máximo del formulario (o de otro Control) pueden establecerse mediante las propiedades MinimumSize y MaximumSize, respectivamente. Ambas son de tipo Size, que tiene las propiedades Width y Height para especificar el tamaño del formulario. Las propiedades MinimumSize y MaximumSize le permiten diseñar la distribución de la GUI para un rango de tamaño dado. El usuario no puede hacer un formulario más pequeño que el tamaño especificado por la propiedad MinimumSize, y no puede hacer un formulario más grande que el tamaño especificado por la propiedad MaximumSize. Para establecer un formulario a un tamaño fijo (en donde el usuario no pueda cambiar su tamaño), asigne el mismo valor a sus tamaños mínimo y máximo o establezca su propiedad FormBorderStyle a FixedSingle. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 13.2 Para los formularios que cambian de tamaño, asegúrese de que la distribución de la GUI aparezca consistente a lo largo de varios tamaños del formulario.

Uso de Visual Studio para editar la distribución de una GUI Visual Studio cuenta con herramientas que nos ayudan con la distribución de la GUI. Tal vez usted se haya dado cuenta que, cuando arrastra un control sobre un formulario, aparecen líneas azules (conocidas como líneas de ajuste) para ayudarlo a posicionar el control con respecto a otros controles (figura 13.16) y los bordes del formulario. Esta nueva característica de Visual Studio 2005 hace que el control que usted arrastra “ajuste su posición” con respecto a los demás controles. Visual Studio también cuenta con el menú Formato, el cual contiene varias opciones para modificar la distribución de su GUI. El menú Formato no aparece en el IDE, a menos que usted seleccione un control (o conjunto de controles) en la vista de diseño. Cuando selecciona varios controles, puede usar el submenú Alinear del menú Formato para alinearlos. El menú Formato también le permite modificar la cantidad de espacio entre los controles, o centrar un control en el formulario.

Propiedades de la distribucion de Control Anchor Dock Padding

Location Size MinimumSize, MaximumSize

Descripción Hace que un control permanezca a una distancia fija desde el (los) extremo(s) del contenedor, aun cuando se cambie su tamaño. Permite que un control abarque un extremo de su contenedor, o que llene todo el contenedor. Establece el espacio entre los bordes de un contenedor y los controles acoplados. El valor predeterminado es 0, lo cual hace que el control parezca estar empotrado a los extremos del contenedor Especifica la ubicación (como un conjunto de coordenadas) de la esquina superior izquierda del control, en relación con su contenedor. Especifica el tamaño del control en píxeles como un objeto Size, que tiene las propiedades Width (anchura) y Height (altura). Indica los tamaños mínimo y máximo de un objeto Control, respectivamente.

Figura 13.15 | Propiedades de distribución de Control.

13.5 Controles Label, TextBox y Button

425

Línea de ajuste para ayudar a alinear los controles en sus extremos izquierdos Línea de ajuste que indica cuando un control alcanza la distancia mínima recomendada, desde el borde de un formulario

Figura 13.16 | Las líneas de ajuste en Visual Studio 2005,

13.5 Controles Label, TextBox y Button Los controles Label proporcionan información de texto (y también imágenes opcionales) y se definen con la clase Label (una clase derivada de Control). Un control Label muestra texto que el usuario no puede modificar directamente. El texto de un control Label puede cambiarse mediante la programación, modificando la propiedad Text. La figura 13.17 lista las propiedades comunes de Label. Un cuadro de texto (clase TextBox) es un área en la que un programa puede mostrar texto o el usuario puede escribir texto a través del teclado. Un cuadro de texto de contraseña es un control TextBox que oculta la información que introduce el usuario. A medida que el usuario escribe caracteres, el cuadro de texto de contraseña enmascara la entrada del usuario, mostrando un carácter que usted puede especificar. (Por lo general, *). Si establece la propiedad PasswordChar, el control TextBox se convierte en un cuadro de texto de contraseña. A menudo, los usuarios se encuentran con ambos tipos de controles TextBox, cuando inician sesión en una computadora o sitio Web; el control TextBox para el nombre de usuario permite a los usuarios introducir sus nombres de usuario; el control TextBox de contraseña permite a los usuarios introducir sus contraseñas. La figura 13.18 lista las propiedades comunes y un evento común de los controles TextBox.

Propiedades comunes de Label Font Text TextAlign

Descripción La fuente del texto en el control Label. El texto en el control Label. La alineación del texto en el control Label; horizontal (izquierda, centro o derecha) y vertical (superior, media o inferior).

Figura 13.17 | Propiedades comunes de Label. Propiedades y evento de TextBox

Descripción

Propiedades comunes AcceptsReturn

Si es true en un control TextBox con múltiples líneas, al oprimir Intro en el control TextBox se crea una nueva línea. Si es false, oprimir Intro es lo mismo que oprimir el control Button predeterminado en el formulario. El control Button predeterminado es el que se asigna a la propiedad AcceptButton de un formulario.

Figura 13.18 | Propiedades y evento de TextBox. (Parte 1 de 2).

426

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

Propiedades y evento de TextBox Multiline PasswordChar

ReadOnly ScrollBars

Text

Descripción Si es true, el control TextBox puede abarcar varias líneas. El valor predeterminado es false. Cuando se asigna un carácter a esta propiedad, el control TextBox se convierte en un cuadro de texto de contraseña, y el carácter especificado enmascara cada carácter que escribe el usuario. Si no se especifica un carácter, el control TextBox muestra el texto que se escribió. Si es true, el control TextBox tiene un fondo de color gris y no se puede editar su texto. El valor predeterminado es false. Para los cuadros de texto con múltiples líneas, esta propiedad indica cuáles barras de desplazamiento deben aparecer: ninguna (None), Horizontal, Vertical o ambas (Both). El contenido de texto del control TextBox.

Evento común TextChanged

Se genera cuando cambia el texto en un control TextBox (es decir, cuando el usuario agrega o elimina caracteres). Al hacer doble clic en el control TextBox en el modo de Diseño, se genera un manejador de eventos vacío para este evento.

Figura 13.18 | Propiedades y evento de TextBox. (Parte 2 de 2). Un botón es un control que el usuario oprime para desencadenar una acción específica, o para seleccionar una opción en un programa. Como veremos más adelante, un programa puede usar varios tipos de botones, tales como casillas de verificación y botones de opción. Todas las clases de botones se derivan de la clase ButtonBase (espacio de nombres System.Windows.Forms), la cual define las características comunes de los botones. En esta sección veremos la clase Button, que por lo general permite a un usuario enviar un comando a una aplicación. La figura 13.19 lista las propiedades comunes y un evento común de la clase Button. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 13.3 Aunque los controles Label, TextBox y otros pueden responder a los clics del ratón, los controles Button son más naturales para este propósito.

Propiedades y evento de Button

Descripción

Propiedades comunes Text FlatStyle

Especifica el texto a mostrar en la superficie del control Button. Modifica la apariencia de un control Button: los atributos Flat. (Para que el botón se muestre sin una apariencia tridimensional), Popup. (Para que el botón aparezca plano hasta que el usuario mueva el puntero del ratón sobre el botón), Standard (tridimensional) y System, en donde el sistema operativo es el que controla la apariencia del botón. El valor predeterminado es Standard.

Evento común Click

Se genera cuando el usuario hace clic en el control Button. Al hacer doble clic en un control Button en vista de diseño, se crea un manejador de eventos vacío para este evento.

Figura 13.19 | Propiedades y evento de Button.

13.5 Controles Label, TextBox y Button

427

La figura 13.20 utiliza un control TextBox, un Button y un Label. El usuario introduce texto en un cuadro de contraseña y hace clic en el control Button, haciendo que se muestre la entrada de texto en el control Label. Por lo general no se muestra este texto; el propósito de los cuadros de texto de contraseña es ocultar el texto que introduce el usuario. Cuando éste hace clic en el botón Muéstrame, esta aplicación extrae el texto que escribió el usuario en el cuadro de texto de contraseña y lo muestra en otro control TextBox. Primero, cree la GUI arrastrando los controles (un control TextBox, un Button y un Label) en el formulario. Una vez posicionados los controles, cambie sus nombres en la ventana Propiedades, de los valores predeterminados textBox1, button1 y label1, a unos más descriptivos: mostrarContraseniaLabel, mostrarContraseniaButton e introducirContraseniaTextBox. La propiedad (Name) en la ventana Propiedades nos permite modificar el nombre de la variable para un control. Visual Studio crea el código necesario y lo coloca en el método InitializeComponent de la clase parcial, en el archivo PruebaLabelTextBoxButtonForm.Designer.cs. Después establecemos la propiedad Text de mostrarContraseniaButton a "Mostrarme" y borramos las propiedades Text de mostrarContraseniaLabel e introducirContraseniaTextBox, de manera que estén en blanco cuando el programa empiece a ejecutarse. La propiedad BorderStyle de mostrarContraseniaLabel se estableció a fixed3D, para que el control Label tenga una apariencia tridimensional. La propiedad BorderStyle de todos los controles TextBox se establece a Fixed3D de manera predeterminada. El carácter de contraseña para introducirContraseniaTextBox se estableció mediante la asignación del carácter de asterisco (*) a la propiedad PasswordChar. Esta propiedad sólo acepta un carácter. Para crear un manejador de eventos para mostrarContraseniaButton, hicimos doble clic en este control, en modo de Diseño. Agregamos la línea 22 al cuerpo del manejador de eventos. Cuando el usuario hace clic en el botón Mostrarme de la aplicación en ejecución, la línea 22 obtiene el texto introducido por el usuario en introducirContraseniaTextBox y muestra el texto en mostrarContraseniaLabel. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

// Fig. 13.20: PruebaLabelTextBoxButtonForm.cs // Uso de los controles TextBox, Label y Button para mostrar // el texto oculto en un cuadro de texto de contraseña. using System; using System.Windows.Forms; // formulario que crea un cuadro de texto de contraseña y // un control Label para mostrar el contenido del control TextBox public partial class PruebaLabelTextBoxButtonForm : Form { // constructor predeterminado public PruebaLabelTextBoxButtonForm() { InitializeComponent(); } // fin del constructor // muestra la entrada del usuario en el control Label private void mostrarContraseniaButton_Click( object sender, EventArgs e ) { // muestra el texto que escribió el usuario mostrarContraseniaLabel.Text = introducirContraseniaTextBox.Text; } // fin del método mostrarContraseniaButton_Click } // end class PruebaLabelTextBoxButtonForm

Figura 13.20 | Programa para mostrar el texto oculto en un cuadro de texto de contraseña.

428

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

13.6 Controles GroupBox y Panel Los controles GroupBox y Panel ordenan los controles en una GUI. Por lo general, estos controles se utilizan para agrupar en una GUI los controles con una funcionalidad similar, o varios controles relacionados. Todos los controles dentro de un control GroupBox o Panel se mueven en conjunto, cuando se mueve el control GroupBox o Panel. La principal diferencia entre estos dos controles es que el control GroupBox puede mostrar una leyenda (es decir, texto) y no incluye barras de desplazamiento, mientras que el control Panel puede incluir barras de desplazamiento pero no incluye una etiqueta. Los controles GroupBox tienen bordes delgados de manera predeterminada; los controles Panel pueden establecerse de manera que también tengan bordes, con sólo cambiar su propiedad BorderStyle. Las figuras 13.21 y 13.22 listan las propiedades comunes de los controles GroupBox y Panel, respectivamente. Archivo Nuevo Abrir... Cerrar

Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 13.4 Los controles Panel y GroupBox pueden contener otros controles Panel y GroupBox para lograr diseños más complejos.

Observación de apariencia visual 13.5 Puede organizar una GUI anclando y acoplando los controles dentro de un control GroupBox o Panel. Después, el control GroupBox o Panel puede anclarse o acoplarse dentro de un formulario. Esto divide los controles en “grupos” funcionales, que pueden ordenarse con facilidad.

Para crear un control GroupBox, arrastre su icono del Cuadro de herramientas hacia un formulario. Después, arrastre nuevos controles del Cuadro de herramientas hacia el control GroupBox. Estos controles se agregan a la propiedad Controls del control GroupBox y se convierten en parte del control GroupBox. La propiedad Text del control GroupBox especifica la leyenda. Para crear un control Panel, arrastre su icono desde el Cuadro de herramientas, hasta el formulario. Después puede agregar los controles directamente al control Panel, arrastrándolos desde el Cuadro de herramientas hasta el control Panel. Para habilitar las barras de desplazamiento, establezca la propiedad AutoScroll del control Panel a true. Si el control Panel cambia su tamaño y no puede mostrar todos sus controles, aparecen barras de desplazamiento (figura 13.23). Las barras de desplazamiento pueden usarse para ver todos los controles dentro del control Panel; tanto en tiempo de diseño como en tiempo de ejecución. En la figura 13.23 establecemos la propiedad BorderStyle del control Panel a FixedSingle, para que pueda ver el control Panel en el formulario.

Propiedades de Groupbox Descripción Controls Text

El conjunto de controles que contiene el control GroupBox. Especifica el texto de la leyenda que se muestra en la parte superior del control GroupBox.

Figura 13.21 | Propiedades de GroupBox.

Propiedades de Panel

Descripción

AutoScroll

Indica si aparecen barras de desplazamiento cuando el control Panel es demasiado pequeño para mostrar todos sus controles. El valor predeterminado es false. Establece el borde del control Panel. El valor predeterminado es None; las otras opciones son Fixed3D y FixedSingle. El conjunto de controles que contiene el control Panel.

BorderStyle Controls

Figura 13.22 | Propiedades de Panel.

13.6 Controles GroupBox y Panel

Control dentro del Panel

Barras de desplazamiento del Panel

429

Panel

El Panel se cambió de tamaño

Figura 13.23 | Creación de un control Panel con barras de desplazamiento.

Observación de apariencia visual 13.6

Archivo Nuevo Abrir... Cerrar

Use controles Panel con barras de desplazamiento para evitar atestar una GUI, y para reducir su tamaño.

El programa de la figura 13.24 utiliza un control GroupBox y un control Panel para ordenar controles Al hacer clic en estos controles Button, sus manejadores de eventos cambian el texto en un control

Button. Label.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

// Fig. 13.24: EjemploGroupboxPanelForm.cs // Uso de los controles GroupBox y Panel para guardar controles Buttons. using System; using System.Windows.Forms; // formulario que muestra un control GroupBox y un control Panel public partial class EjemploGroupBoxPanelForm : Form { // constructor predeterminado public EjemploGroupBoxPanelForm() { InitializeComponent(); } // fin del constructor // manejador de eventos para el boton Hola private void holaButton_Click( object sender, EventArgs e ) { mensajeLabel.Text = "Se oprimió Hola"; // cambia el texto en el control Label } // fin del método holaButton_Click // manejador de eventos para el botón Adiós private void adiosButton_Click( object sender, EventArgs e ) { mensajeLabel.Text = "Se oprimió Adiós"; // cambia el texto en el control Label } // fin del método adiosButton_Click

Figura 13.24 | Empleo del control GroupBox y el control Panel para ordenar los controles Button. (Parte 1 de 2).

430

26 27 28 29 30 31 32 33 34 35 36 37 38

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

// manejador de eventos para el botón Izquierda private void izquierdaButton_Click( object sender, EventArgs e ) { mensajeLabel.Text = "Se oprimió Izquierda"; // cambia el texto en el control Label } // fin del método izquierdaButton_Click // manejador de eventos para el botón Derecha private void derechaButton_Click( object sender, EventArgs e ) { mensajeLabel.Text = "Se oprimió Derecha"; // cambia el texto en el control Label } // fin del método derechaButton_Click } // fin de la clase EjemploGroupBoxPanelForm

Figura 13.24 | Empleo del control GroupBox y el control Panel para ordenar los controles Button. (Parte 2 de 2). El control GroupBox (llamado principalGroupBox) tiene dos controles Button: holaButton (que muestra el texto Hola) y adiosButton (que muestra el texto Adiós). El control Panel (llamado principalPanel) también tiene dos controles Button: izquierdoButton (que muestra el texto Izquierda) y derechoButton (que muestra el texto Derecha). El control principalPanel tiene su propiedad AutoScroll establecida en true, lo cual permite que aparezcan barras de desplazamiento cuando el contenido del control Panel requiere más espacio que su área visible. Al principio, el control Label (llamado mensajeLabel) está en blanco. Para agregar controles a principalGroupBox o principalPanel, Visual Studio llama al método Add de la propiedad Controls de cada contenedor. Este código se coloca en la clase parcial ubicada en el archivo EjemploGroupBoxPanelForm.Designer.cs. Los manejadores de eventos para los cuatro controles Button se encuentran en las líneas 16-37. Agregamos una línea en cada manejador de eventos (líneas 18, 24, 30 y 36) para modificar el texto de mensajeLabel, de manera que indique cuál control Button oprimió el usuario.

13.7 Controles CheckBox y RadioButton

C# tiene dos tipos de botones de estado, que pueden estar en los estados encendido/apagado o verdadero/falso: CheckBox y RadioButton. Al igual que la clase Button, las clases CheckBox y RadioButton se derivan de la clase ButtonBase.

Controles CheckBox Un control CheckBox es un pequeño cuadro, que está en blanco o contiene una marca de verificación. Cuando el usuario hace clic en un control CheckBox para seleccionarlo, aparece una marca de verificación en el cuadro. Si el usuario hace clic en el control CheckBox otra vez para deseleccionarlo, se elimina la marca de verificación. Puede seleccionarse cualquier número de controles CheckBox en un momento dado. En la figura 13.25 aparece una lista de propiedades y eventos comunes de los controles CheckBox.

13.7 Controles CheckBox y RadioButton

Propiedades y eventos de CheckBox

431

Descripción

Propiedades comunes Checked CheckState

Text

Indica si el control CheckBox está seleccionado (contiene una marca de verificación) o deseleccionado (en blanco). Esta propiedad devuelve un valor Boolean. Indica si el control CheckBox está seleccionado o deseleccionado con un valor de la enumeración CheckState (Checked, Unchecked o Indeterminate). Indeterminate se utiliza cuando no está claro si el estado debe ser Checked (seleccionado) o Unchecked (deseleccionado). Por ejemplo, en Microsoft Word, cuando seleccionamos un párrafo que contiene varios formatos de caracteres y después seleccionamos Formato > Fuente, algunos de los controles CheckBox aparecen en el estado Indeterminate. Cuando CheckState se establece a Indeterminate, por lo general el control CheckBox se muestra en color gris. Especifica el texto que aparece a la derecha del control CheckBox.

Eventos comunes CheckedChanged

CheckStateChanged

Se genera cuando cambia la propiedad Checked. Éste es un evento predeterminado del control CheckBox. Cuando un usuario hace doble clic en el control CheckBox en vista de diseño, se genera un manejador de eventos vacíos para este evento. Se genera cuando cambia la propiedad CheckState.

Figura 13.25 | Propiedades y eventos de CheckBox. El programa en la figura 13.26 permite al usuario seleccionar controles CheckBox para modificar el estilo de fuente de un control Label. El manejador de eventos para un control CheckBox aplica negrita y el manejador de eventos para el otro control aplica cursiva. Si ambos controles CheckBox están seleccionados, el estilo de fuente se establece en negrita y cursiva. Al principio, ningún control CheckBox está seleccionado. El control negritaCheckBox tiene en su propiedad Text el valor Negrita. El control cursivaCheckBox tiene en su propiedad Text el valor Cursiva. La propiedad Text de salidaLabel contiene el texto Observe cómo cambia el estilo de la fuente. Después de crear los controles, definimos sus manejadores de eventos. Al hacer doble clic en los controles CheckBox en tiempo de compilación, se crean los manejadores de eventos CheckedChanged vacíos. Para modificar el estilo de fuente en un control Label, debe establecer su propiedad Font a un nuevo objeto Font (líneas 21-23 y 31-33). El constructor de Font que utilizamos aquí recibe el nombre, el tamaño y el estilo de la fuente como argumentos. Los primeros dos argumentos (salidaLabel.Font.Name y salidaLabel.Font. Size) usan el nombre y tamaño de fuente originales de salidaLabel. El estilo se especifica mediante un miembro de la enumeración FontStyle, que contiene Regular, Bold, Italic, Strikeout y Underline. (El estilo Strikeout muestra el texto con una línea a través de éste). La propiedad Style de un objeto Font es de sólo lectura, de manera que puede establecerse sólo a la hora de crear el objeto Font. 1 2 3 4 5 6 7 8

// Fig. 13.26: PruebaCheckBoxForm.cs // Uso de controles CheckBox para activar los estilos cursiva y negrita. using System; using System.Drawing; using System.Windows.Forms; // el formulario contiene controles CheckBox para permitir al usuario modificar el texto de ejemplo public partial class PruebaCheckBoxForm : Form

Figura 13.26 | Uso de controles CheckBox para modificar los estilos de las fuentes. (Parte 1 de 2).

432

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

{ // constructor predeterminado public PruebaCheckBoxForm() { InitializeComponent(); } // fin del constructor // alterna el estilo de fuente entre negrita // y normal, con base en la configuración actual private void negritaCheckBox_CheckedChanged( object sender, EventArgs e ) { salidaLabel.Font = new Font( salidaLabel.Font.Name, salidaLabel.Font.Size, salidaLabel.Font.Style ^ FontStyle.Bold ); } // fin del método negritaCheckBox_CheckedChanged // alterna el estilo de fuente entre cursiva y // normal, con base en la configuración actual private void cursivaCheckBox_CheckedChanged( object sender, EventArgs e ) { salidaLabel.Font = new Font( salidaLabel.Font.Name, salidaLabel.Font.Size, salidaLabel.Font.Style ^ FontStyle.Italic ); } // fin del método cursivaCheckBox_CheckedChanged } // fin de la clase PruebaCheckBoxForm

Figura 13.26 | Uso de controles CheckBox para modificar los estilos de las fuentes. (Parte 2 de 2). Los estilos pueden combinarse mediante operadores a nivel de bit: operadores que realizan manipulaciones con bits de información. En el capítulo 1 vimos que todos los datos se representan en la computadora como combinaciones de 0s y 1s. Cada 0 o 1 representa a un bit. FontStyle tiene un atributo llamado System.FlagAttribute, lo que significa que los valores de bit de FontStyle se seleccionan de una manera que nos permita combinar distintos elementos FontStyle para crear estilos compuestos, mediante el uso de operadores a nivel de bit. Estos estilos no son mutuamente exclusivos, por lo que podemos combinar distintos estilos y eliminarlos sin afectar la combinación de los elementos FontStyle anteriores. Podemos combinar estos diversos estilos de fuente, usando ya sea el operador OR lógico ( | ) o el operador OR exclusivo (^). Cuando se aplica el operador OR lógico a dos bits, si por lo menos uno de los dos bits tiene el valor 1, entonces el resultado es 1. La combinación de estilos mediante el uso del operador OR condicional funciona de la siguiente forma. Suponga que el estilo FontStyle.Bold se representa mediante los bits 01 y que FontStyle.Italic se representa mediante los bits 10. Si utilizamos el OR condicional ( | | ) para combinar los estilos, obtenemos los bits 11.

13.7 Controles CheckBox y RadioButton 01 10 -11

= =

Bold Italic

=

Bold e Italic

433

El operador OR condicional nos ayuda a crear combinaciones de estilos. No obstante, ¿qué ocurre si queremos deshacer una combinación de estilos, como en la figura 13.26? El operador lógico OR exclusivo nos permite combinar estilos y deshacer las configuraciones de estilo existentes. Cuando se aplica el OR lógico exclusivo a dos bits, si ambos bits tienen el mismo valor, entonces el resultado es 0. Si ambos bits son distintos, entonces el resultado es 1. La combinación de estilos mediante el uso de OR lógico exclusivo funciona de la siguiente manera. Suponga de nuevo que FontStyle.Bold se representa mediante los bits 01 y que FontStyle.Italic se representa mediante los bits 10. Al utilizar el OR exclusivo lógico (^) en ambos estilos, obtenemos los bits 11. 01 10 -11

= =

Bold Italic

=

Bold e Italic

Ahora suponga que queremos eliminar el estilo FontStyle.Bold de la combinación anterior entre Fonty FontStyle.Italic. La manera más sencilla de hacerlo es volver a aplicar el operador OR exclusivo lógico (^) al estilo compuesto y a FontStyle.Bold. Style.Bold

11 01 -10

= =

Bold e Italic Bold

=

Italic

Éste es un ejemplo simple. Las ventajas de utilizar operadores a nivel de bit para combinar valores FontStyle se hacen más evidentes si consideramos que hay cinco valores distintos de FontStyle (Bold, Italic, Regular, Strikeout y Underline), lo que produce 16 combinaciones distintas. Al utilizar los operadores a nivel de bit para combinar los estilos de fuente, se reduce en forma considerable la cantidad de código requerido para comprobar todas las combinaciones de fuente posibles. En la figura 13.26 necesitamos establecer el estilo de fuente, de manera que el texto aparezca en negrita si no estaba así originalmente, y viceversa. Observe que la línea 23 utiliza el operador OR exclusivo lógico a nivel de bits para hacer esto. Si salidaLabel.Font.Style está en negrita, entonces el estilo resultante debe ser sin negrita. Si el texto está originalmente en cursiva, el estilo resultante debe ser negrita y cursiva, en vez de sólo negrita. Lo mismo se aplica para FontStyle.Italic en la línea 33. Si no utilizáramos los operadores a nivel de bits para componer los elementos FontStyle, tendríamos que evaluar el estilo actual y cambiarlo de manera acorde. Por ejemplo, en el manejador de eventos negritaCheckBox_CheckChanged podríamos evaluar si tiene el estilo regular y convertirlo en negrita y cursiva; evaluar si tiene el estilo negrita y hacerlo regular; evaluar si tiene el estilo cursiva y hacerlo negrita y cursiva; y evaluar si tiene el estilo negrita y cursiva y hacerlo cursiva. Esto es incómodo, ya que para cada nuevo estilo que agregamos, duplicamos el número de combinaciones. Si agregáramos un control CheckBox para el estilo subrayado se requeriría evaluar ocho estilos adicionales. Si agregáramos un control CheckBox para el estilo tachado se requeriría evaluar 16 estilos adicionales.

Controles RadioButton Los botones de opción (que se definen con la clase RadioButton) son similares a los controles CheckBox en cuanto a que también tienen dos estados: seleccionado y no seleccionado (también conocido como deseleccionado). Sin embargo, por lo general los controles RadioButton aparecen como un grupo, en el cual sólo puede seleccionarse un control RadioButton a la vez. Al seleccionar un control RadioButton en el grupo, se obliga a que todos los demás queden deseleccionados. Por lo tanto, los controles RadioButton se utilizan para representar un conjunto de opciones mutuamente exclusivas (es decir, un conjunto en el cual no pueden seleccionarse varias opciones al mismo tiempo).

434

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

Archivo Nuevo Abrir... Cerrar

Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 13.7 Use controles RadioButton cuando el usuario sólo debe seleccionar una opción en un grupo.

Observación de apariencia visual 13.8 Use controles CheckBox cuando el usuario deba poder seleccionar varias opciones en un grupo.

Todos los controles RadioButton que se agregan a un contenedor forman parte del mismo grupo. Para separar controles RadioButton en varios grupos, los controles deben agregarse a controles GroupBox o Panel. En la figura 13.27 se listan las propiedades comunes y un evento común de la clase RadioButton.

Observación de ingeniería de software 13.2 Los controles Form, GroupBox y Panel pueden actuar como grupos lógicos para los controles RadioButton. Los controles RadioButton dentro de cada grupo son mutuamente exclusivos entre sí, pero no con los controles RadioButton en distintos grupos lógicos.

El programa en la figura 13.28 utiliza controles RadioButton para permitir a los usuarios seleccionar opciones para un cuadro de diálogo MessageBox. Después de seleccionar los atributos deseados, el usuario oprime el botón Mostrar para mostrar el cuadro de diálogo MessageBox. Un control Label en la esquina inferior izquierda muestra el resultado del cuadro de diálogo MessageBox (es decir, cuál control Button oprimió el usuario: Sí, No, Cancelar, etcétera). Para almacenar las opciones del usuario, creamos e inicializamos los objetos tipoIcono y tipoBoton (líneas 11-12). El objeto tipoIcono es de tipo MessageBoxIcon, y puede tener los valores Asterisk (asterisco), Error, Exclamation (exclamación), Hand (mano), Information (información), None (ninguno), Question (pregunta), Stop (alto) y Warning (advertencia). Los resultados de ejemplo sólo muestran los iconos Error, Exclamation, Information y Question. El objeto tipoBoton es de tipo MessageBoxButtons y puede tener los valores AbortRetryIgnore (abortar/ reintentar/ignorar), OK (aceptar), OKCancel (aceptar/cancelar), RetryCancel (reintentar/cancelar), YesNo (sí/no) y YesNoCancel (sí/no/cancelar). El nombre indica las opciones presentes para el usuario en el cuadro de diálogo MessageBox. Las ventanas de resultados de ejemplo muestran cuadros de diálogo MessageBox para todos los valores de la enumeración MessageBoxButtons. Creamos dos controles GroupBox, uno para cada conjunto de valores de enumeración. Las leyendas de los controles GroupBox son Tipo de botón e Icono. Los controles GroupBox contienen controles RadioButton para las opciones de enumeración correspondientes, y las propiedades Text de estos controles se establecen en forma apropiada. Como los controles RadioButton están agrupados, sólo puede seleccionarse un control RadioButton de cada control GroupBox. También hay un control Button (mostrarButton) con la etiqueta Mostrar. Cuando

Propiedades y evento de RadioButton

Descripción

Propiedades comunes Checked Text

Indica si el control RadioButton está seleccionado. Especifica el texto del control RadioButton.

Evento común CheckedChanged

Se genera cada vez que se selecciona o deselecciona el control RadioButton. Al hacer doble clic en un control RadioButton en vista de diseño, se genera un manejador de eventos vacío para este evento.

Figura 13.27 | Propiedades y evento de RadioButton.

13.7 Controles CheckBox y RadioButton

435

un usuario hace clic en este control Button, se muestra un cuadro de diálogo MessageBox personalizado. Un control Label (mostrarLabel) muestra cuál botón oprimió el usuario dentro del cuadro de diálogo MessageBox. El manejador de eventos para los controles RadioButton maneja el evento CheckedChanged de cada control RadioButton. Cuando se selecciona un control RadioButton contenido dentro del control GroupBox llamado Tipo de botón, el manejador de eventos correspondiente al control RadioButton seleccionado establece tipoBoton al valor apropiado. Las líneas 21-45 contienen el manejo de eventos para estos controles RadioButton. De manera similar, cuando el usuario selecciona los controles RadioButton que pertenecen al control GroupBox llamado Icono, los manejadores de eventos asociados con estos eventos (líneas 48-80) establecen tipoIcono a su valor correspondiente.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

// Fig. 13.28: PruebaRadioButtonsForm.cs // Uso de controles RadioButton para establecer las opciones de una ventana de mensajes. using System; using System.Windows.Forms; // el formulario contiene varios controles RadioButton--el usuario selecciona // uno de cada grupo para crear un cuadro de diálogo MessageBox personalizado public partial class PruebaRadioButtonsForm : Form { // crea variables que almacenan la selección de opciones del usuario private MessageBoxIcon tipoIcono; private MessageBoxButtons tipoBoton; // constructor predeterminado public PruebaRadioButtonsForm() { InitializeComponent(); } // fin del constructor // cambia los controles Button con base en la opción elegida por el emisor private void tipoBoton_CheckedChanged( object sender, EventArgs e ) { if ( sender == aceptarButton ) // muestra botón Aceptar tipoBoton = MessageBoxButtons.OK; // muestra botones Aceptar y Cancelar else if ( sender == aceptarCancelarButton ) tipoBoton = MessageBoxButtons.OKCancel; // muestra botones Abortar, Reintentar e Ignorar else if ( sender == abortarReintentarIgnorarButton ) tipoBoton = MessageBoxButtons.AbortRetryIgnore; // muestra botones Sí, No y Cancelar else if ( sender == siNoCancelarButton ) tipoBoton = MessageBoxButtons.YesNoCancel; // muestra botones Sí y No else if ( sender == siNoButton ) tipoBoton = MessageBoxButtons.YesNo; // sólo queda una opción: mostrar botones Reintentar y Cancelar else tipoBoton = MessageBoxButtons.RetryCancel; } // fin del método tipoBoton_Changed

Figura 13.28 | Uso de controles RadioButton para establecer las opciones de la ventana de mensajes. (Parte 1 de 3).

436

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

// cambia el icono con base en la opción elegida por el emisor private void tipoIcono_CheckedChanged( object sender, EventArgs e ) { if ( sender == asteriscoButton ) // muestra icono de asterisco tipoIcono = MessageBoxIcon.Asterisk; // muestra icono de error else if ( sender == errorButton ) tipoIcono = MessageBoxIcon.Error; // muestra icono de signo de exclamación else if ( sender == exclamacionButton ) tipoIcono = MessageBoxIcon.Exclamation; // muestra icono de mano else if ( sender == manoButton ) tipoIcono = MessageBoxIcon.Hand; // muestra icono de información else if ( sender == informacionButton ) tipoIcono = MessageBoxIcon.Information; // muestra icono de signo de interrogación else if ( sender == preguntaButton ) tipoIcono = MessageBoxIcon.Question; // muestra icono de alto else if ( sender == altoButton ) tipoIcono = MessageBoxIcon.Stop; // sólo queda una opción: muestra icono de advertencia else tipoIcono = MessageBoxIcon.Warning; } // fin del método tipoIcono_CheckChanged // muestra MessageBox y Button que oprimió el usuario private void mostrarButton_Click( object sender, EventArgs e ) { // muestra MessageBox y almacena el // valor del control Button que se oprimió DialogResult resultado = MessageBox.Show( "Éste es su MessageBox personalizado.", "MessageBox personalizado", tipoBoton, tipoIcono, 0, 0 ); // comprueba qué control Button se oprimió en el MessageBox // cambia el texto mostrado de manera acorde switch (resultado) { case DialogResult.OK: mostrarLabel.Text = "Se oprimió Aceptar."; break; case DialogResult.Cancel: mostrarLabel.Text = "Se oprimió Cancelar."; break; case DialogResult.Abort: mostrarLabel.Text = "Se oprimió Abortar."; break; case DialogResult.Retry: mostrarLabel.Text = "Se oprimió Reintentar.";

Figura 13.28 | Uso de controles RadioButton para establecer las opciones de la ventana de mensajes. (Parte 2 de 3).

13.7 Controles CheckBox y RadioButton

437

106 break; 107 case DialogResult.Ignore: 108 mostrarLabel.Text = "Se oprimió Ignorar."; 109 break; 110 case DialogResult.Yes: 111 mostrarLabel.Text = "Se oprimió Sí."; 112 break; 113 case DialogResult.No: 114 mostrarLabel.Text = "Se oprimió No."; 115 break; 116 } // fin de switch 117 } // fin del método mostrarButton_Click 118 } // fin de la clase PruebaRadioButtonsForm a)

b)

c) Tipo de botón OKCancel

d) Tipo de botón OK

e) Tipo de botón AbortRetryIgnore

f) Tipo de botón YesNoCancel

g) Tipo de botón YesNo

h) Tipo de botón RetryCancel

Figura 13.28 | Uso de controles RadioButton para establecer las opciones de la ventana de mensajes. (Parte 3 de 3).

438

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

El manejador de eventos Click para mostrarButton (líneas 83-117) crea un cuadro de diálogo MessageBox (líneas 87-89). Las opciones del MessageBox se especifican con los valores almacenados en tipoIcono y tipoBoton. Cuando el usuario hace clic en uno de los botones del cuadro de diálogo MessageBox, el resultado del cuadro de mensajes se devuelve a la aplicación. Este resultado es un valor de la enumeración DialogResult que contiene Abort (abortar), Cancel (cancelar), Ignore (ignorar), No, None (ninguno), OK (aceptar), Retry (reintentar) o Yes (sí). La instrucción switch en las líneas 93-116 evalúa el resultado y establece el valor de mostrarLabel.Text de manera apropiada.

13.8 Controles PictureBox Un control PictureBox muestra una imagen. Esta imagen puede tener uno de varios formatos, como mapa de bits, GIF (Formato de intercambio de gráficos) y JPEG. (En el capítulo 17, Gráficos y multimedia, hablaremos sobre las imágenes). La propiedad Image de un control PictureBox especifica la imagen que se mostrará, y la propiedad SizeMode indica cómo se mostrará: Normal, StretchImage (estirar imagen), Autosize (tamaño automático) o CenterImage (centrar imagen). La figura 13.29 describe las propiedades comunes y un evento común del control PictureBox. La figura 13.30 utiliza un control PictureBox llamado imagenPictureBox para mostrar una de tres imágenes de mapa de bits: imagen0, imagen1 o imagen2. Estas imágenes están ubicadas en el directorio imagenes, dentro de los directorios bin/Debug y bin/Release del proyecto. Cada vez que el usuario hace clic en el botón Propiedades y evento de PictureBox

Descripción

Propiedades comunes Image Establece la imagen a mostrar en el control PictureBox. SizeMode Enumeración que controla el tamaño y el posicionamiento de la imagen. Los valores son Normal (predeterminado), StretchImage, AutoSize y CenterImage. Normal coloca la imagen en la esquina superior izquierda del control PictureBox, y CenterImage coloca la imagen en el centro. Estas dos opciones truncan la imagen si es demasiado grande. StretchImage cambia el tamaño de la imagen para que se ajuste al control PictureBox. AutoSize cambia el tamaño del control PictureBox para que muestre toda la imagen. Evento común Click

Ocurre cuando el usuario hace clic en el control. Al hacer doble clic en este control en la vista de diseño, se crea un manejador de eventos para este evento.

Figura 13.29 | Propiedades y evento de PictureBox. 1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 13.30: PruebaPictureBoxForm.cs // Uso de un control PictureBox para mostrar imágenes. using System; using System.Drawing; using System.Windows.Forms; using System.IO; // formulario para mostrar distintas imágenes en PictureBox al hacer clic en el botón public partial class PruebaPictureBoxForm : Form { private int numImagen = -1; // determina cuál imagen se va a mostrar // constructor predeterminado public PruebaPictureBoxForm()

Figura 13.30 | Uso de un control PictureBox para mostrar imágenes. (Parte 1 de 2).

13.8 Controles PictureBox

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

{ InitializeComponent(); } // fin del constructor // cambia la imagen cada vez que se hace clic en el botón Siguiente imagen private void siguienteButton_Click( object sender, EventArgs e ) { numImagen = ( numImagen + 1 ) % 3; // numImagen itera de 0 a 2 // crea objeto Image del archivo, muestra en control PicutreBox imagenPictureBox.Image = Image.FromFile( Directory.GetCurrentDirectory() + @"\imagenes\imagen" + numImagen + ".bmp" ); } // fin del método siguienteButton_Click } // fin de la clase PruebaPictureBoxForm a)

b)

c)

Figura 13.30 | Uso de un control PictureBox para mostrar imágenes. (Parte 2 de 2).

439

440

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

Siguiente imagen, ésta cambia a la siguiente imagen en la secuencia. Cuando se muestra la última imagen y el usuario hace clic en el botón Siguiente imagen, se muestra de nuevo la primera imagen. Dentro del manejador de eventos siguienteButton_Click (líneas 20-28) utilizamos un int (numImagen) para almacenar el número de la imagen que deseamos mostrar. Después establecemos la propiedad Image de imagenPictureBox para asignarle un objeto Image (líneas 25-27).

13.9 Controles ToolTip En el capítulo 2 demostramos el uso de los cuadros de información sobre herramientas (tool tips): el útil texto que aparece cuando colocamos el puntero del ratón sobre un elemento en una GUI. Recuerde que los cuadros de información sobre herramientas que se muestran en Visual Studio le ayudan a familiarizarse con las características del IDE, y le sirven como recordatorios útiles para la funcionalidad de los iconos de cada una de las barras de herramientas. Muchos programas utilizan los cuadros de información sobre herramientas para recordar a los usuarios el propósito de cada control. Por ejemplo, Microsoft Word cuenta con cuadros de información sobre herramientas que ayudan a los usuarios a determinar el propósito de cada uno de los iconos de la aplicación. En esta sección le demostraremos cómo usar el componente ToolTip para agregar, a sus aplicaciones, cuadros de información sobre herramientas. La figura 13.31 describe las propiedades comunes y un evento común de la clase ToolTip. Cuando agregamos un componente ToolTip desde el Cuadro de herramientas, aparece en la bandeja de componentes: la región gris debajo del formulario en el modo de Diseño. Una vez que se agrega un componente ToolTip a un formulario, aparece una nueva propiedad en la ventana Propiedades para los demás controles del formulario. Esta propiedad aparece en la ventana Propiedades como ToolTip en, seguida del nombre del componente ToolTip. Por ejemplo, si el componente ToolTip de nuestro formulario se llamara ayudaToolTip, estableceríamos el valor de la propiedad Tooltip en ayudaToolTip de un control para especificar el texto del cuadro de información sobre herramientas de ese control. La figura 13.32 demuestra el uso del componente ToolTip.

Propiedades y evento de Tooltip

Descripción

Propiedades comunes AutoPopDelay InitialDelay ReshowDelay

La cantidad de tiempo (en milisegundos) que aparece el cuadro de información sobre herramientas, mientras el ratón se coloca sobre un control. La cantidad de tiempo (en milisegundos) que debe colocarse un ratón sobre un control para que aparezca un cuadro de información sobre herramientas. La cantidad de tiempo (en milisegundos) entre la cual aparecen dos cuadros de información sobre herramientas distintos (cuando el ratón se desplaza de un control a otro).

Evento común Draw

Se genera cuando se muestra el cuadro de información sobre herramientas. Este evento permite a los programadores modificar la apariencia del cuadro de información sobre herramientas.

Figura 13.31 | Propiedades y evento de ToolTip. 1 2 3 4 5 6 7

// Fig. 13.32: EjemploToolTipForm.cs // Demostración del componente ToolTip. using System; using System.Windows.Forms; public partial class EjemploToolTipForm : Form {

Figura 13.32 | Demostración del componente ToolTip. (Parte 1 de 2).

13.9 Controles ToolTip

8 9 10 11 12 13 14 15 16

441

// constructor predeterminado public EjemploToolTipForm() { InitializeComponent(); } // fin del constructor // no se necesitan manejadores de eventos para este ejemplo } // fin de la clase ToolTipExampleForm a)

b)

Figura 13.32 | Demostración del componente ToolTip. (Parte 2 de 2).

Para este ejemplo, crearemos una GUI que contenga dos controles Label, para poder demostrar el uso de distinta información de texto en el cuadro de información sobre herramientas para cada control Label. Para mejorar la legibilidad de los resultados de ejemplo, establecimos la propiedad BorderStyle de cada control Label a FixedSingle, la cual muestra un borde sólido. Como no hay código para manejar eventos en este ejemplo, la clase de la figura 13.32 sólo contiene un constructor. En este ejemplo, al componente ToolTip le llamamos etiquetasToolTip. La figura 13.33 muestra el componente ToolTip en la bandeja de componentes. Establecimos el texto del cuadro de información sobre herramientas para el primer control Label a "Primera etiqueta" y el cuadro de información sobre herramientas para el segundo control Label a "Segunda etiqueta". La figura 13.34 demuestra cómo establecer el texto del cuadro de información sobre herramientas para el primer control Label.

El control ToolTip en la bandeja de componentes

Figura 13.33 | Demostración de la bandeja de componentes.

442

Capítulo 13 Conceptos de interfaz gráfica de usuario: parte I

Propiedad para establecer los cuadros de información sobre herramientas

Cuadros de información sobre herramientas

Figura 13.34 | Establecer el texto del cuadro de información sobre herramientas de un control.

13.10 Control NumericUpDown A veces es conveniente restringir las opciones de entrada de un usuario a un rango específico de valores numéricos. Éste es el propósito del control NumericUpDown. Este control aparece como un control TextBox, con dos pequeños controles Button del lado derecho: uno con una flecha hacia arriba y el otro con una flecha hacia abajo. De manera predeterminada, un usuario puede escribir valores numéricos en este control como si fuera un control TextBox, o puede hacer clic en las flechas hacia arriba o hacia abajo para incrementar o decrementar el valor en el control, respectivamente. Los valores más grande y más pequeño del rango se especifican mediante las propiedades Maximum y Minimum, respectivamente (ambas de tipo decimal). La propiedad Increment (también de tipo decimal) especifica por cuánto cambia el número actual en el control cuando el usuario hace clic en las flechas hacia arriba o hacia abajo del control. La figura 13.35 describe las propiedades comunes y un evento común de la clase NumericUpDown.

Propiedades y evento de NumericUpDown

Descripción

Propiedades comunes Increment Maximum Minimum UpDownAlign

Value

Especifica por cuánto cambia el número actual en el control, cuando el usuario hace clic en las flechas hacia arriba y hacia abajo. El valor más grande en el rango del control. El valor más pequeño en el rango del control. Modifica la alineación de los controles Button arriba y abajo en el control. NumericDownControl. Esta propiedad se puede utilizar para mostrar estos controles Button, ya sea a la izquierda o a la derecha del control. El valor numérico actual que se muestra en el control.

Evento común ValueChanged

Este evento se genera cuando cambia el valor en el control. Es el evento predeterminado para el control NumericUpDown.

Figura 13.35 | Propiedades y evento de NumericUpDown.

13.10 Control NumericUpDown

443

La figura 13.36 demuestra el uso de un control NumericUpDown para una GUI que calcula la tasa de interés. Los cálculos que se realizan en esta aplicación son similares a los que se realizan en la figura 6.6. Se utilizan controles TextBox para introducir los montos del capital y la tasa de interés, y un control NumericUpDown para introducir el número de años para los que deseamos calcular el interés. Para el control NumericUpDown llamado anioUpDown, establecemos la propiedad Minimum en 1 y la propiedad Maximum en 10. Dejamos la propiedad Increment establecida en 1, su valor predeterminado. Estas configuraciones especifican que los usuarios pueden introducir un número de años en el rango de 1 a 10, en incrementos de 1. Si establecemos la propiedad Increment en 0.5, también podemos introducir valores como 1.5 o 2.5. Establecemos la propiedad ReadOnly del control NumericUpDown en true para indicar que el usuario no puede introducir un número en el control para realizar una selección. Por ende, el usuario debe hacer clic en las flechas hacia arriba o hacia abajo para modificar el valor en el control. De manera predeterminada, la propiedad ReadOnly se establece en false. Los resultados de esta aplicación se muestran en un control TextBox

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

// Fig. 13.36: calculadoraInteresesForm.cs // Demostración del control NumericUpDown. using System; using System.Windows.Forms; public partial class calculadoraInteresesForm : Form { // constructor predeterminado public calculadoraInteresesForm() { InitializeComponent(); } // fin del constructor private void calcularButton_Click( object sender, EventArgs e ) { // declara las variables para guardar entrada del usuario decimal principal; // guarda principal double tasa; // guarda tasa de interés int anio; // guarda número de años decimal monto; // guarda monto string salida; // guarda salida // extrae la entrada del usuario principal = Convert.ToDecimal( principalTextBox.Text ); tasa = Convert.ToDouble( interesTextBox.Text ); anio = Convert.ToInt32( anioUpDown.Value ); // establece encabezado de salida salida = "Año\tMonto en depósito\r\n"; // calcula el monto para cada año y lo adjunta a la salida for ( int contadorAnios = 1; contadorAnios Ejecutar…). Para las aplicaciones que conoce Windows no se requieren los nombres de ruta completos y, por lo general puede omitirse la extensión .exe. Para abrir un archivo cuyo tipo reconozca Windows, sólo necesita usar el nombre de ruta completo del archivo. El sistema operativo Windows debe poder utilizar la aplicación asociada con la extensión del archivo dado para abrirlo. El manejador de eventos para el evento LinkClicked de unidadLinkLabel explora la unidad C: (líneas 17-24). La línea 21 establece la propiedad LinkVisited a true, la cual modifica el color del vínculo, de azul a púrpura (los colores de LinkVisited se pueden configurar a través de la ventana Propiedades en Visual Studio). Después, el manejador de eventos pasa @"C:\" al método Start (línea 23), el cual abre una ventana del Explorador de Windows. El símbolo @ que colocamos antes de "C:\" indica que todos los caracteres en el objeto string deben interpretarse en forma literal. Por ende, la barra diagonal inversa dentro del objeto string no se considera como el primer carácter de una secuencia de escape. Esto simplifica los objetos string que representan rutas de directorios, ya que no necesitamos usar \\ por cada carácter \ en la ruta. El manejador de eventos para el evento LinkClicked de deitelLinkLabel (líneas 27-35) abre la página Web www.deitel.com en Internet Explorer. Para lograr esto, pasamos la dirección de la página Web como un objeto string (líneas 33-34), con lo cual se abre Internet Explorer. La línea 31 establece la propiedad LinkVisited a true. El manejador de eventos para el evento LinkClicked de blocDeNotasLinkLabel (líneas 38-47) abre la aplicación Bloc de notas. La línea 42 establece la propiedad LinkVisited a true, de manera que el vínculo aparezca como visitado. La línea 46 pasa el argumento "notepad" al método Start, que ejecuta notepad.exe. Observe que en la línea 46 no se requiere la extensión .exe; Windows puede determinar si reconoce el argumento que se proporciona al método Start como un archivo ejecutable.

14.6 Control ListBox

469

14.6 Control ListBox El control ListBox permite al usuario ver y seleccionar varios elementos en una lista. Estos controles son entidades estáticas de la GUI, lo cual significa que deben agregarse elementos a la lista mediante la programación. Se pueden proporcionar al usuario controles TextBox y Button con los cuales pueda especificar los elementos que desea agregar a la lista, pero el verdadero proceso de agregarlos debe realizarse en el código. El control CheckedListBox (sección 14.7) extiende a un control ListBox mediante la inclusión de controles CheckBox a un lado de cada elemento de la lista. Esto permite a los usuarios colocar marcas de verificación en varios elementos a la vez, como se hace con los controles CheckBox. (Los usuarios también pueden seleccionar varios elementos de un control ListBox mediante la propiedad SelectionMode, que veremos en breve). La figura 14.15 muestra un control ListBox y un control CheckedListBox. En ambos controles aparecen barras de desplazamiento si el número de elementos es mayor que el área visible del control ListBox. La figura 14.16 lista las propiedades y métodos comunes de ListBox, y un evento común. La propiedad SelectionMode determina el número de elementos que pueden seleccionarse. Esta propiedad tiene los posibles valores None, One, MultiSimple y MultiExtended (de la enumeración SelectionMode); en la figura 14.16 explicamos las diferencias entre estas opciones. El evento SelectedIndexChanged ocurre cuando el usuario selecciona un nuevo elemento.

Control

Aparecen barras de desplazamiento, si es necesario

Elementos seleccionados

Elemento seleccionado

Control CheckedListBox

Figura 14.15 | Controles ListBox y CheckedListBox en un formulario.

Propiedades, métodos y evento de ListBox

Descripción

Propiedades comunes Items

La colección de elementos en el control ListBox.

MultiColumn

Indica si el control ListBox puede dividir una lista en varias columnas, con lo cual se eliminan las barras de desplazamiento verticales de la pantalla.

SelectedIndex

Devuelve el índice del elemento seleccionado. Si no se han seleccionado elementos, la propiedad devuelve –1. Si el usuario selecciona varios elementos, esta propiedad devuelve sólo uno de los índices seleccionados. Por esta razón, si desea seleccionar varios elementos, debe usar la propiedad SelectedIndices.

SelectedIndices

Devuelve una colección que contiene los índices para todos los elementos seleccionados.

Figura 14.16 | Propiedades, métodos y evento de ListBox. (Parte 1 de 2).

470

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

Propiedades, métodos y evento de ListBox

Descripción

SelectedItem

Devuelve una referencia al elemento seleccionado. Si se seleccionan varios elementos, devuelve el elemento con el número de índice más bajo.

SelectedItems

Devuelve una colección del (los) elemento(s) seleccionado(s).

SelectionMode

Determina el número de elementos que pueden seleccionarse, y el medio a través del cual pueden seleccionarse varios elementos. Los valores son None, One, MultiSimple (se permite la selección múltiple) o MultiExtended (se permite la selección múltiple mediante el uso de una combinación de teclas de flechas, o los clics del ratón y las teclas Mayús y Ctrl.

Sorted

Indica si los elementos están ordenados en forma alfabética. Al establecer el valor de esta propiedad a true se ordenan los elementos. El valor predeterminado es false.

Evento común ClearSelected

Deselecciona todos los elementos.

GetSelected

Recibe un índice como argumento y devuelve true si se selecciona el elemento correspondiente.

Evento común SelectedIndexChanged

Se genera cuando cambia el índice seleccionado. Éste es el evento predeterminado cuando se hace doble clic en el control, estando en el diseñador.

Figura 14.16 | Propiedades, métodos y evento de ListBox. (Parte 2 de 2). Tanto el control ListBox como CheckedListBox tienen las propiedades Items, SelectedItem y SelectedLa propiedad Items devuelve todos los elementos de la lista, en forma de una colección. Las colecciones son una manera común de administrar listas de objetos en el .NET Framework. Muchos componentes .NET de la GUI (por ejemplo, controles ListBox) usan colecciones para exponer listas de objetos internos (por ejemplo, los elementos contenidos dentro de un control ListBox). En el capítulo 26 hablaremos más acerca de las colecciones. La colección que devuelve la propiedad Items se representa como un objeto de tipo ObjectCollection. La propiedad SelectedItem devuelve el elemento actual seleccionado de ListBox. Si el usuario puede seleccionar varios elementos, use la colección SelectedItems para devolver todos los elementos seleccionados como una colección. La propiedad SelectedIndex devuelve el índice del elemento seleccionado; si pudiera haber más de uno, use la propiedad SelectedIndices. Si no hay elementos seleccionados, la propiedad SelectedIndex devuelve –1. El método GetSelected recibe un índice y devuelve true si está seleccionado el elemento correspondiente. Para agregar elementos a un control ListBox o CheckedListBox, debemos agregar objetos a su colección Items. Para lograr esto podemos llamar al método Add, para agregar un objeto string a la colección Items de ListBox o CheckedListBox. Por ejemplo, podríamos escribir Index.

miListBox.Items.Add( miListItem )

para agregar el objeto string miListItem al control ListBox llamado miListBox. Para agregar varios objetos, puede llamar al método Add varias veces, o al método AddRange para agregar un arreglo de objetos. Las clases ListBox y CheckedListBox pueden llamar al método ToString del objeto enviado para determinar el control Label para la entrada del objeto correspondiente en la lista. Esto le permite agregar distintos objetos a un control ListBox o CheckedListBox, los cuales pueden devolverse después a través de las propiedades SelectedItem y SelectedItems. De manera alternativa, puede agregar elementos a controles ListBox y CheckedListBox en forma visual, examinando la propiedad Items en la ventana Propiedades. Al hacer clic en el botón de elipsis se abre el Editor de la colección Cadena, el cual contiene un área de texto para agregar elementos; cada elemento aparece en una línea separada (figura 14.17). Después, Visual Studio escribe el código para agregar estos objetos string a la colección Items dentro del método InitializeComponent.

14.6 Control ListBox

471

Figura 14.17 | Editor de la colección Cadena. La figura 14.18 utiliza la clase PruebaListBoxForm para agregar, quitar y borrar elementos del control llamado mostrarListBox. La clase PruebaListBoxForm utiliza el control TextBox entradaTextBox para permitir al usuario introducir un nuevo elemento. Cuando el usuario hace clic en el botón Add, el nuevo elemento aparece en mostrarListBox. De manera similar, si el usuario selecciona un elemento y hace clic en Quitar, el elemento se elimina. Al hacer clic en Borrar, se eliminan todas las entradas en mostrarListBox. Para terminar la aplicación, el usuario hace clic en Salir. El manejador de eventos agregarButton_Click (líneas 18-22) llama al método Add de la colección Items en el control ListBox. Este método recibe un objeto string como el elemento que se agregará a mostrarListBox. En este caso, el objeto string utilizado es el texto introducido por el usuario, o entradaTextBox.Text (línea 20). Después de agregar el elemento, se borra el contenido de entradaTextBox.Text (línea 21). El manejador de eventos quitarButton_Click (líneas 25-30) utiliza el método RemoveAt para quitar un elemento del control ListBox. El manejador de eventos quitarButton_Click primero utiliza la propiedad SelectedIndex para determinar cuál índice está seleccionado. Si SelectedIndex no es -1 (es decir, que haya un elemento seleccionado), la línea 29 quita el elemento que corresponde al índice seleccionado. El manejador de eventos borrarButton_Click (líneas 33-36) llama al método Clear de la colección Items (línea 35). Esto elimina todas las entradas en mostrarListBox. Por último, el manejador de eventos salirButton_Click (líneas 39-42) termina la aplicación llamando al método Application.Exit (línea 41). ListBox

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

// Fig. 14.18: PruebaListBoxForm.cs // Programa para agregar, quitar y borrar elementos de un control ListBox using System; using System.Windows.Forms; // el formulario utiliza un control TextBox y controles Button para // agregar, quitar y borrar elementos de un control ListBox public partial class PruebaListBoxForm : Form { // constructor predeterminado public PruebaListBoxForm() { InitializeComponent(); } // fin del constructor // agrega nuevo elemento al control ListBox (texto de entradaTextBox) // y borra el control TextBox de entrada

Figura 14.18 | Programa que agrega, quita y borra elementos de un control ListBox. (Parte 1 de 2).

472

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

private void agregarButton_Click( object sender, EventArgs e ) { mostrarListBox.Items.Add( entradaTextBox.Text ); entradaTextBox.Clear(); } // fin del método agregarButton_Click // quita un elemento, si está seleccionado private void quitarButton_Click( object sender, EventArgs e ) { // comprueba si el elemento está seleccionado, si es así lo quita if ( mostrarListBox.SelectedIndex != -1 ) mostrarListBox.Items.RemoveAt( mostrarListBox.SelectedIndex ); } // fin del método quitarButton_Click // borra todos los elementos del control ListBox private void borrarButton_Click( object sender, EventArgs e ) { mostrarListBox.Items.Clear(); } // fin del método borrarButton_Click // sale de la aplicación private void salirButton_Click( object sender, EventArgs e ) { Application.Exit(); } // fin del método salirButton_Click } // fin de la clase PruebaListBoxForm a)

b)

c)

d)

Figura 14.18 | Programa que agrega, quita y borra elementos de un control ListBox. (Parte 2 de 2).

14.7 Control CheckedListBox

473

14.7 Control CheckedListBox El control CheckedListBox se deriva de la clase ListBox, e incluye un control CheckBox enseguida de cada elemento. Al igual que en los controles ListBox, pueden agregarse elementos a través de los métodos Add y AddRange, o a través del Editor de la colección Cadena. En los controles CheckedListBox se pueden seleccionar varios elementos, y los únicos valores posibles para la propiedad SelectionMode son None y One. One permite la selección múltiple, ya que en los controles CheckBox no hay restricciones lógicas sobre los elementos; el usuario puede seleccionar todos los que requiera. Por ende, la única opción es si se va a permitir que el usuario realice selecciones múltiples o no. Esto mantiene el comportamiento del control CheckedListBox consistente con el de los controles CheckBox. En la figura 14.19 aparecen las propiedades y eventos comunes, y un método común de los controles CheckBox.

Error común de programación 14.1 El IDE muestra un mensaje de error si usted trata de establecer la propiedad SelectionMode a MultiSimple o MultiExtended en la ventana Propiedades de un control CheckedListBox. Si este valor se establece mediante la programación, se produce un error en tiempo de ejecución.

El evento ItemCheck ocurre cada vez que un usuario selecciona o deselecciona un elemento CheckedListLas propiedades CurrentValue y NewValue del argumento del evento devuelven valores CheckState para el estado actual y nuevo del elemento, respectivamente. Una comparación de estos valores le permite determinar si el elemento CheckedListBox estaba seleccionado o deseleccionado. El control CheckedListBox retiene las propiedades SelectedItems y SelectedIndices (las hereda de la clase ListBox). No obstante, también incluye las propiedades CheckedItems y CheckedIndices, las cuales devuelven información acerca de los elementos e índices seleccionados. Box.

Propiedades, métodos y evento de CheckedlListBox

Descripción

Propiedades comunes

(Todas las propiedades, métodos y eventos de ListBox las hereda el control CheckedListBox.)

CheckedItems

Contiene la colección de elementos que están seleccionados. Esto es distinto del elemento seleccionado, que está resaltado (pero no necesariamente activado). [Nota: puede haber cuando mucho un elemento seleccionado en un momento dado.]

CheckedIndices

Devuelve los índices de todos los elementos seleccionados.

SelectionMode

Determina cuántos elementos pueden seleccionarse. Los únicos valores posibles son (permite seleccionar varios elementos) o None (no permite seleccionar ningún elemento).

One

Método común GetItemChecked

Recibe un índice y devuelve true si el elemento correspondiente está seleccionado.

Evento común (argumentos de evento ItemCheckEventArgs) ItemCheck

Se genera cuando se selecciona o deselecciona un elemento.

Propiedades de ItemCheckEventArgs CurrentValue

Indica si el elemento actual está seleccionado o deseleccionado. Los posibles valores son Checked, Unchecked e Indeterminate.

ItemCheck

Devuelve el índice con base cero del elemento que cambió.

NewValue

Especifica el nuevo estado del elemento.

Figura 14.19 | Propiedades, método y evento de CheckedListBox.

474

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

En la figura 14.20, la clase PruebaCheckedListBoxForm usa un control CheckedListBox y un control para mostrar la selección de libros de un usuario. El control CheckedListBox permite al usuario seleccionar varios títulos. En el Editor de la colección Cadena se agregaron elementos para algunos libros de Deitel: C++, Java™, Visual Basic, Internet y WWW, Perl, Python, Internet inalámbrica y Java avanzado (el acrónimo HTP significa “Cómo programar”). El control ListBox (llamado mostrarListBox) muestra la selección del usuario. En las capturas de pantalla que acompañan este ejemplo, el control CheckedListBox aparece a la izquierda y el control ListBox a la derecha. Cuando el usuario selecciona o deselecciona un elemento en entradaCheckedListBox, se produce un evento ItemCheck y se ejecuta el manejador de eventos entradaCheckedListBox_ItemCheck (líneas 17-29). Una instrucción if…else (líneas 25-28) determina si el usuario seleccionó o deseleccionó un elemento en el control CheckedListBox. La línea 25 utiliza la propiedad NewValue para determinar si el elemento se está seleccionando (CheckState.Checked). Si el usuario selecciona un elemento, la línea 26 agrega una entrada seleccionada al control ListBox mostrarListBox. Si el usuario deselecciona un elemento, la línea 28 quita el elemento correspondiente de mostrarListBox. Este manejador de eventos se creó seleccionando el control CheckedListBox en modo de Diseño, viendo los eventos del control en la ventana Propiedades y haciendo doble clic en el evento ItemCheck. ListBox

14.8 Control ComboBox

El control ComboBox combina las características del control TextBox con una lista desplegable: un componente de la GUI que contiene una lista, de la cual se puede seleccionar un valor. Por lo general, un control ComboBox

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

// Fig. 14.20: PruebaCheckedListBoxForm.cs // Uso del control CheckedListBox para agregar elementos al control mostrarListBox using System; using System.Windows.Forms; // el formulario usa un control CheckedListBox para agregar elementos al control mostrarListBox public partial class PruebaCheckedListBoxForm : Form { // constructor predeterminado public PruebaCheckedListBoxForm() { InitializeComponent(); } // fin del constructor // elemento a punto de cambiar // lo agrega o lo quita de mostrarListBox private void entradaCheckedListBox_ItemCheck( object sender, ItemCheckEventArgs e ) { // obtiene referencia del elemento seleccionado string elemento = entradaCheckedListBox.SelectedItem.ToString(); // si el elemento está seleccionado, se agrega al control ListBox // en caso contrario, se quita del control ListBox if ( e.NewValue == CheckState.Checked ) mostrarListBox.Items.Add( elemento ); else mostrarListBox.Items.Remove( elemento ); } // fin del método entradaCheckedListBox_ItemCheck } // fin de la clase CheckedListBoxTestForm

Figura 14.20 | Uso de los controles CheckedListBox y ListBox en un programa para mostrar la selección de un usuario. (Parte 1 de 2).

14.8 Control ComboBox

475

a)

b)

c)

d)

Figura 14.20 | Uso de los controles CheckedListBox y ListBox en un programa para mostrar la selección de un usuario. (Parte 2 de 2). aparece como un control TextBox con una flecha hacia abajo de lado derecho. De manera predeterminada, el usuario puede introducir texto en el control TextBox o puede hacer clic en la flecha hacia abajo para mostrar una lista de elementos predefinidos. Si un usuario selecciona un elemento de la lista, ese elemento se muestra en el control TextBox. Si la lista contiene más elementos de los que se pueden mostrar en la lista desplegable, aparece una barra de desplazamiento. El número máximo de elementos que puede mostrar una lista desplegable a la vez se establece mediante la propiedad MaxDropDownItems. La figura 14.21 muestra un control ComboBox de ejemplo en tres estados distintos. Al igual que con el control ListBox, puede agregar objetos a la colección Items mediante programación, usando los métodos Add y AddRange, o en forma visual con el Editor de la colección Cadena. La figura 14.22 lista propiedades comunes y un evento común de la clase ComboBox.

476

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

Haga clic en la flecha hacia abajo para mostrar los elementos en la lista desplegable

Al seleccionar un elemento de la lista desplegable, el texto cambia en la parte correspondiente al control TextBox

Figura 14.21 | Demostración del control ComboBox. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 14.4 Use un control ComboBox para ahorrar espacio en una GUI. Una desventaja de ello es que, a diferencia de un control ListBox, el usuario no puede ver los elementos disponibles sin expandir la lista desplegable.

La propiedad DropDownStyle determina el tipo de control ComboBox, y se representa como un valor de la enumeración ComboBoxStyle, que contiene los valores Simple, DropDown y DropDownList. La opción Simple no muestra una flecha hacia abajo, sino que aparece una barra de desplazamiento a un lado del control, lo que permite al usuario seleccionar una opción de la lista. El usuario también puede introducir una selección desde el teclado. El estilo DropDown (predeterminado) muestra una lista desplegable cuando se hace clic en la flecha hacia

Propiedades y evento de ComboBox

Descripción

Propiedades comunes DropDownStyle

Determina el tipo de control ComboBox. El valor Simple significa que la porción del texto puede editarse, y la porción de la lista siempre está visible. El valor DropDown (predeterminado) significa que la porción del texto es editable, pero el usuario debe hacer clic en el botón de flecha para ver la porción de lista. El valor DropDownList significa que la porción de texto no puede editarse, y el usuario debe hacer clic en el botón de flecha hacia abajo para ver la porción de la lista.

Items

La colección de elementos en el control ComboBox.

MaxDropDownItems

Especifica el máximo número de elementos (entre 1 y 100) que puede mostrar la lista desplegable. Si el número de elementos excede al número máximo de elementos a mostrar, aparece una barra de desplazamiento.

SelectedIndex

Devuelve el índice del elemento seleccionado. Si no hay un elemento seleccionado, se devuelve -1.

SelectedItem

Devuelve una referencia al elemento seleccionado.

Sorted

Indica si los elementos están ordenados alfabéticamente. Al establecer el valor de esta propiedad a true se ordenan los elementos. El valor predeterminado es false.

Evento común SelectedIndexChanged

Se genera cuando cambia el índice seleccionado (como cuando se selecciona un elemento distinto). Es el evento predeterminado cuando se hace doble clic en el control, estando en el diseñador.

Figura 14.22 | Propiedades y evento de ComboBox.

14.8 Control ComboBox

477

abajo (o cuando se oprime la tecla fecha hacia abajo). El usuario puede escribir un nuevo elemento en el control ComboBox. El último estilo es DropDownList, el cual muestra una lista desplegable pero no permite al usuario escribir en el control TextBox. El control ComboBox cuenta con las propiedades Items (una colección), SelectedItem y SelectedIndex, las cuales son similares a las propiedades correspondientes en ListBox. Puede haber cuando menos un elemento seleccionado en un ComboBox. Si no hay elementos seleccionados, entonces SelectedIndex es -1. Cuando cambia el elemento seleccionado, se produce un evento SelectedIndexChanged. La clase PruebaComboBoxForm (figura 14.23) permite a los usuarios seleccionar una figura para dibujar: círculo, elipse, cuadrado o pastel (con relleno o sin relleno), mediante el uso de un control ComboBox. En este ejemplo, el control ComboBox no puede editarse, por lo que el usuario no puede escribir en el control TextBox.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

// Fig. 14.23: PruebaComboBoxForm.cs // Uso de un control ComboBox para seleccionar una figura a dibujar. using System; using System.Drawing; using System.Windows.Forms; // el formulario usa un control ComboBox para seleccionar distintas figuras a dibujar public partial class PruebaComboBoxForm : Form { // constructor predeterminado public PruebaComboBoxForm() { InitializeComponent(); } // fin del constructor // obtiene el índice de la figura seleccionada, dibuja la figura private void imagenComboBox_SelectedIndexChanged( object sender, EventArgs e ) { // crea objetos Graphics, Pen y SolidBrush Graphics miGraphics = base.CreateGraphics(); // crea objeto Pen usando el color DarkRed (rojo oscuro) Pen miPen = new Pen( Color.DarkRed ); // crea objeto SolidBrush, usando el color rojo oscuro SolidBrush miSolidBrush = new SolidBrush( Color.DarkRed ); // borra el área de dibujo y la establece de color blanco miGraphics.Clear( Color.White ); // busca índice, dibuja la figura apropiada switch ( imagenComboBox.SelectedIndex ) { case 0: // se selecciona el caso del Círculo miGraphics.DrawEllipse( miPen, 50, 50, 150, 150 ); break; case 1: // se selecciona el caso del Rectángulo miGraphics.DrawRectangle( miPen, 50, 50, 150, 150 ); break; case 2: // se selecciona el caso de la Elipse miGraphics.DrawEllipse( miPen, 50, 85, 150, 115 ); break; case 3: // se selecciona el caso del Pastel

Figura 14.23 | Uso de un control ComboBox para dibujar una figura seleccionada. (Parte 1 de 2).

478

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

miGraphics.DrawPie( miPen, 50, 50, 150, 150, 0, 45 ); break; case 4: // se selecciona el caso del Círculo relleno miGraphics.FillEllipse( miSolidBrush, 50, 50, 150, 150 ); break; case 5: // se selecciona el caso del Rectángulo relleno miGraphics.FillRectangle( miSolidBrush, 50, 50, 150, 150 ); break; case 6: // se selecciona el caso del Elipse relleno miGraphics.FillEllipse( miSolidBrush, 50, 85, 150, 115 ); break; case 7: // se selecciona el caso del Pastel relleno miGraphics.FillPie( miSolidBrush, 50, 50, 150, 150, 0, 45 ); break; } // fin del switch miGraphics.Dispose(); // libera el objeto Graphics } // fin del método imagenComboBox_SelectedIndexChanged } // fin de la clase PruebaComboBoxForm a)

b)

c)

d)

Figura 14.23 | Uso del control ComboBox para dibujar una figura seleccionada. (Parte 2 de 2).

Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 14.5 Haga las listas (como las de los controles ComboBox) editables sólo si el programa está diseñado para aceptar elementos que introduzca el usuario. De no ser así, el usuario podría tratar de introducir un elemento personalizado que sea inadecuado para los fines de su aplicación.

Después de crear el control ComboBox imagenComboBox, haga que no sea posible editarlo estableciendo su propiedad DropDownStyle a DropDownList en la ventana Propiedades. Después agregue los elementos Círculo, Cuadrado, Elipse, Pastel, Círculo relleno, Cuadrado relleno, Elipse rellena y Pastel relleno a la colección Items, mediante el Editor de la colección Cadena. Cada vez que el usuario selecciona un elemento de imagenComboBox, se produce un evento SelectedIndexChanged y se ejecuta el manejador de eventos imagenComboBox_SelectedIndexChanged (líneas 17-60). Las líneas 21-27 crean un objeto Graphics, un objeto Pen y

14.9 Control TreeView

479

un objeto SolidBrush, los cuales se utilizan para dibujar en el formulario. El objeto Graphics (línea 21) permite que una pluma o brocha dibuje en un componente, usando uno de varios métodos de Graphics. Los métodos DrawEllipse, DrawRectangle y DrawPie (líneas 36, 39, 42 y 45) utilizan el objeto Pen (línea 24) para dibujar los contornos de sus correspondientes figuras. Los métodos FillEllipse, FillRectangle y FillPie (líneas 48, 51, 54 y 57) utilizan el objeto SolidBrush (línea 27) para rellenar sus correspondientes figuras sólidas. La línea 30 colorea todo el formulario de blanco (White), mediante el uso del método Clear de Graphics. En el capítulo 17, Gráficos y multimedia, hablaremos sobre estos métodos con mayor detalle. La aplicación dibuja una figura con base en el índice del elemento seleccionado. La instrucción switch (líneas 33-59) utiliza imagenComboBox.SelectedIndex para determinar cuál elemento seleccionó el usuario. El método DrawEllipse (línea 36) de Graphics recibe un objeto Pen, las coordenadas x y y del centro, y la anchura y altura de la elipse para dibujarla. El origen del sistema de coordenadas se encuentra en la esquina superior izquierda del formulario; la coordenada x se incrementa a la derecha, y la coordenada y se incrementa hacia abajo. Un círculo es un caso especial de elipse (la altura y la anchura son iguales). La línea 36 dibuja un círculo. La línea 42 dibuja una elipse con valores distintos para la anchura y la altura. El método DrawRectangle de la clase Graphics (línea 39) recibe un objeto Pen, las coordenadas x y y de la esquina superior izquierda, y la anchura y altura del rectángulo para dibujarlo. El método DrawPie (línea 45) dibuja un pastel como una porción de una elipse. La elipse está delimitada por un rectángulo. El método DrawPie recibe un objeto Pen las coordenadas x y y de la esquina superior izquierda del rectángulo, su anchura y altura, el ángulo inicial (en grados) y el ángulo de barrido (en grados) del pastel. Los ángulos se incrementan en sentido a favor de las manecillas del reloj. Los métodos FillEllipse (líneas 48 y 54), FillRectangle (línea 51) y FillPie (línea 57) son similares a sus contrapartes sin relleno, sólo que reciben un objeto SolidBrush en vez de un objeto Pen. En las capturas de pantalla de la figura 14.23 se muestran algunas de las figuras que se dibujan.

14.9 Control TreeView

El control TreeView muestra nodos de manera jerárquica en un árbol. Tradicionalmente, los nodos son objetos que contienen valores y pueden hacer referencia a otros nodos. Un nodo padre contiene nodos hijos, y los nodos hijos pueden ser padres de otros nodos. Dos nodos hijos que tienen el mismo nodo padre se consideran nodos hermanos. Un árbol es una colección de nodos que por lo general se organizan en forma jerárquica. El primer nodo padre de un árbol es el nodo raíz (un control TreeView puede tener varios nodos raíz). Por ejemplo, el sistema de archivos de una computadora puede representarse como un árbol. El directorio de nivel superior (tal vez C:) sería la raíz, cada subcarpeta de C: sería un nodo hijo, y cada carpeta hija tendría sus propios hijos. Los controles TreeView son útiles para mostrar información jerárquica, como la estructura de archivos que acabamos de mencionar. En el capítulo 24, Estructuras de datos, veremos los nodos y los árboles con más detalle. La figura 14.24 muestra un ejemplo de un control TreeView en un formulario.

Haga clic en + para expander el nodo y mostrar los nodos hijos

Haga clic en – para contraer el nodo y ocultar los nodos hijos

Nodo raíz

Nodos hijos (de Gerente2)

Figura 14.24 | El control TreeView que muestra un árbol de ejemplo.

480

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

Para expandir o contraer un nodo padre, hay que hacer clic en el cuadro con el signo más o en el cuadro con el signo menos que se encuentra a su derecha. Los nodos sin hijos no tienen estos cuadros. Los nodos en un control TreeView son instancias de la clase TreeNode. Cada objeto TreeNode tiene una colección Nodes (tipo TreeNodeCollection), la cual contiene una lista de otros objetos TreeNode, conocidos como sus hijos. La propiedad Parent devuelve una referencia al nodo padre (o null si es un nodo raíz). Las figuras 14.25 y 14.26 listan las propiedades comunes de los controles TreeView y TreeNode, los métodos comunes de TreeNode y un evento común de TreeView.

Propiedades y evento de TreeView

Descripción

Propiedades comunes CheckBoxes

Indica si aparecen controles CheckBox a un lado de los nodos. Un valor de true muestra los controles CheckBox. El valor predeterminado es false.

ImageList

Especifica un objeto ImageList que contiene los iconos de los nodos. Un objeto ImageList es una colección que contiene objetos Image.

Nodes

Lista la colección de objetos TreeNode en el control. Contiene los métodos Add (agrega un objeto TreeNode), Clear (elimina toda la colección) y Remove (elimina un nodo específico). Al eliminar un nodo padre se eliminan todos sus hijos.

SelectedNodes

El nodo seleccionado.

Evento común (argumentos TreeViewEventArgs del evento) AfterSelect

Se genera cuando cambia el nodo seleccionado. Éste es el evento predeterminado cuando se hace doble clic en el control, estando en el diseñador.

Figura 14.25 | Propiedades y evento de TreeView.

Propiedades y métodos de TreeNode

Descripción

Propiedades comunes Checked

Indica si el objeto TreeNode está seleccionado (la propiedad CheckBoxes debe ser en el objeto TreeView padre).

true FirstNode

Especifica el primer nodo en la colección Nodes (es decir, el primer hijo en el árbol).

FullPath

Indica la ruta del nodo, empezando en la raíz del árbol.

ImageIndex

Especifica el índice de la imagen que se muestra cuando se deselecciona el nodo.

LastNode

Especifica el último nodo en la colección Nodes (es decir, el último hijo en el árbol).

NextNode

El siguiente nodo hermano.

Nodes

Colección de objetos TreeNode contenida en el nodo actual (es decir, todos los hijos del nodo actual). Contiene los métodos Add (agrega un objeto TreeNode), Clear (borra toda la colección) y Remove (elimina un nodo específico). Al eliminar a un nodo padre se eliminan todos sus hijos.

PrevNodes

Nodo hermano anterior.

SelectedImageIndex

Especifica el índice de la imagen a usar cuando se seleccione el nodo.

Text

Especifica el texto del control TreeNode.

Figura 14.26 | Propiedades y métodos de TreeNode. (Parte 1 de 2).

14.9 Control TreeView

Propiedades y métodos de TreeNode

481

Descripción

Métodos comunes Collapse

Contrae un nodo.

Expand

Expande un nodo.

ExpandAll

Expande todos los hijos de un nodo.

GetNodeCount

Devuelve el número de nodos hijos.

Figura 14.26 | Propiedades y métodos de TreeNode. (Parte 2 de 2).

Para agregar nodos al control TreeView en forma visual, haga clic en la elipsis que está a un lado de la propiedad Nodes, en la ventana Propiedades. A continuación se abrirá el Editor TreeNode (figura 14.27), el cual muestra un árbol vacío que representa el control TreeView. Hay botones para crear un nodo raíz, y para agregar o eliminar un nodo. A la derecha están las propiedades del nodo actual. Aquí puede cambiar el nombre del nodo. Para agregar nodos por medio de la programación, primero cree un nodo raíz. Cree un nuevo objeto TreeNode y pase a este objeto un string para mostrarlo en pantalla. Después llame al método Add para agregar este nuevo objeto TreeNode a la colección Nodes de TreeView. Por ende, para agregar un nodo raíz al control TreeView llamado miTreeView, escriba miTreeView.Nodes.Add( new TreeNode( raizLabel ) );

en donde miTreeView es el objeto TreeView al cual estamos agregando nodos, y raizLabel es el texto que se va a mostrar en miTreeView. Para agregar hijos a un nodo raíz, agregue nuevos objetos TreeNode a su colección Nodes. Para seleccionar el nodo raíz apropiado del control TreeView, escribimos miTreeView.Nodes[ miIndice ]

Elimina el nodo actual

Figura 14.27 | El Editor TreeNode.

482

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

en donde miIndice es el índice del nodo raíz en la colección Nodes de miTreeView. Para agregar nodos a los hijos nodos, utilizamos el mismo proceso mediante el cual agregamos nodos raíz a miTreeView. Para agregar un hijo al nodo raíz en el índice miIndice, escribimos miTreeView.Nodes[ miIndice ].Nodes.Add( new TreeNode( hijoLabel ) );

La clase EstructuraDirectorioTreeViewForm (figura 14.28) utiliza un control TreeView para mostrar el contenido de un directorio seleccionado por el usuario. Para especificar el directorio, se utilizan un control TextBox y un control Button. Primero es necesario escribir la ruta completa del directorio que deseamos que se muestre en pantalla. Después hay que hacer clic en el control Button para establecer el directorio especificado

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

// Fig. 14.28: EstructuraDirectorioTreeViewForm.cs // Uso del control TreeView para mostrar una estructura de directorio. using System; using System.Windows.Forms; using System.IO; // el formulario usa TreeView para mostrar la estructura del directorio public partial class EstructuraDirectorioTreeViewForm : Form { string subcadenaDirectorio; // almacena la última parte del nombre de ruta completo // constructor predeterminado public EstructuraDirectorioTreeViewForm() { InitializeComponent(); } // fin del constructor // llena el nodo actual con subirectorios public void LlenarTreeView( string valorDirectorio, TreeNode nodoPadre ) { // arreglo que almacena todos los subdirectorios en el directorio string[] arregloDirectorios = Directory.GetDirectories( valorDirectorio ); // llena el nodo actual con subdirectorios try { // comprueba si hay subdirectorios if ( arregloDirectorios.Length != 0 ) { // para cada subdirectorio, crea nuevo objeto TreeNode, // lo agrega como hijo del nodo actual y llena en forma // recursiva los nodos hijos con subdirectorios foreach ( string directorio in arregloDirectorios ) { // obtiene la última parte del nombre de ruta completo // buscando la última ocurrencia de "\" y devolviendo la // parte del nombre de ruta que va después de esta ocurrencia subcadenaDirectorio = directorio.Substring( directorio.LastIndexOf( '\\' ) + 1, directorio.Length - directorio.LastIndexOf( '\\' ) - 1 ); // crea objeto TreeNode para el directorio actual

Figura 14.28 | Uso del control TreeView para mostrar una estructura de directorio. (Parte 1 de 2).

14.9 Control TreeView

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87

TreeNode miNodo = new TreeNode( subcadenaDirectorio ); // agrega el directorio actual al nodo padre nodoPadre.Nodes.Add( miNodo ); // llena en forma recursiva cada subdirectorio LlenarTreeView( directorio, miNodo ); } // fin de foreach } // fin de if } // fin de try // atrapa excepción catch ( UnauthorizedAccessException ) { nodoPadre.Nodes.Add( "Acceso denegado" ); } // fin de catch } // fin del método LlenarTreeView // maneja evento click de entrarButton private void entrarButton_Click( object sender, EventArgs e ) { // borra todos los nodos directorioTreeView.Nodes.Clear(); // // // if {

comprueba si existe el directorio introducido por el usuario si existe, llena el objeto TreeView, si no existe, muestra cuadro MessageBox de error ( Directory.Exists( entradaTextBox.Text ) ) // agrega el nombre de ruta completo a directorioTreeView directorioTreeView.Nodes.Add( entradaTextBox.Text );

// inserta subcarpetas LlenarTreeView( entradaTextBox.Text, directorioTreeView.Nodes[ 0 ] ); } // fin de if // muestra MessageBox de error si no encontró el directorio else MessageBox.Show( entradaTextBox.Text + " no se pudo encontrar.", "No se encontró el directorio", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin del método entrarButton_Click } // fin de la clase EstructuraDirectorioTreeViewForm a)

b)

Figura 14.28 | Uso del control TreeView para mostrar una estructura de directorio. (Parte 2 de 2).

483

484

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

como el nodo raíz en el control TreeView. Cada subdirectorio de este directorio se convierte en un nodo hijo. Este esquema es similar al que se utiliza en el Explorador de Windows. Las carpetas pueden expandirse o contraerse haciendo clic en los cuadros con los signos más o menos que aparecen a su izquierda. Cuando el usuario hace clic en el botón entrarButton, se borran todos los nodos en directorioTreeView (línea 67). Después se utiliza la ruta introducida en entradaTextBox para crear el nodo raíz. La línea 75 agrega el directorio a directorioTreeView como el nodo raíz, y las líneas 78-79 llaman al método LlenarTreeView (líneas 19-61), el cual recibe un directorio (un string) y un nodo padre. Luego, el método LlenarTreeView crea nodos hijos que corresponden a los subdirectorios del directorio que recibe como argumento. El método LlenarTreeView (líneas 19-61) obtiene una lista de subdirectorios mediante el uso del método GetDirectories de la clase Directory (espacio de nombres System.IO) en las líneas 23-24. El método GetDirectories recibe un objeto string (el directorio actual) y devuelve un arreglo de objetos string (los subdirectorios). Si no se puede acceder a un directorio por razones de seguridad, se lanza una excepción UnauthorizedAccessException. Las líneas 57-60 atrapan esta excepción y agregan un nodo que contiene “Acceso denegado”, en vez de mostrar los subdirectorios. Si hay directorios accesibles, las líneas 40-42 utilizan el método Substring para mejorar la legibilidad, acortando el nombre de ruta completo a sólo el nombre del directorio. A continuación, cada objeto string en arregloDirectorios se utiliza para crear un nuevo nodo hijo (línea 45). Utilizamos el método Add (línea 48) para agregar cada nodo hijo al padre. Después se hacen llamadas recursivas al método LlenarTreeView con cada subdirectorio (línea 51), lo que en cierto momento llena el control TreeView con toda la estructura de directorios. Observe que nuestro algoritmo recursivo puede provocar un retraso cuando el programa carga directorios extensos. No obstante, una vez que se agregan los nombres de las carpetas a la colección Nodes apropiada, pueden expandirse y contraerse sin retraso. En la siguiente sección, presentaremos un algoritmo alternativo para resolver este problema.

14.10 Control ListView El control ListView es similar a un control ListBox, en cuanto a que ambos muestran listas de las que el usuario puede seleccionar uno o más elementos (en la figura 14.31 encontrará un ejemplo de un control ListView). La diferencia importante entre las dos clases es que un control ListView puede mostrar iconos enseguida de los elementos de la lista (los cuales se controlan mediante su propiedad ImageList). La propiedad MultiSelect (de tipo Boolean) determina si pueden seleccionarse varios elementos. Pueden incluirse controles CheckBox, para lo cual se establece la propiedad CheckBoxes (de tipo Boolean) a true, para dar al control ListView una apariencia similar a la de un control CheckedListBox. La propiedad View especifica el diseño del control ListBox. La propiedad Activation determina el método mediante el cual el usuario selecciona un elemento de la lista. En la figura 14.29 se explican los detalles de estas propiedades, junto con el evento ItemActivate.

Propiedades y evento de ListView

Descripción

Propiedades comunes Activation

Determina la forma en que el usuario activa un elemento. Esta propiedad recibe un valor en la enumeración ItemActivation. Los posibles valores son OneClick (activación por un solo clic), TwoClick (activación por dos clics, el elemento cambia de color cuando se selecciona) y Standard (activación con doble clic, el elemento no cambia de color).

CheckBoxes

Indica si los elementos aparecen con controles CheckBox. Si es true, se muestran los controles CheckBox. El valor predeterminado es false.

LargeImageList

Especifica el objeto ImageList que contiene iconos grandes para mostrar.

Figura 14.29 | Propiedades y evento de ListView. (Parte 1 de 2).

14.10 Control ListView

485

Propiedades y evento de ListView

Descripción

Items

Devuelve la colección de objetos ListViewItem en el control.

MultiSelect

Determina si se permite la selección múltiple. El valor predeterminado es true, que permite la selección múltiple.

SelectedItems

Obtiene la colección de elementos seleccionados.

SmallImageList

Especifica el objeto ImageList que contiene iconos pequeños para mostrar.

View

Determina la apariencia de los objetos ListViewItem. Los posibles valores son (se muestra un icono grande, los elementos pueden estar en varias columnas), SmallIcon (se muestra un icono pequeño, los elementos pueden estar en varias columnas), List (se muestran iconos pequeños, los elementos aparecen en una sola columna), Details (igual que List, pero pueden mostrarse varias columnas de información por cada elemento) y Tile (se muestran iconos grandes, la información se proporciona a la derecha del icono, válido sólo en Windows XP o posterior). LargeIcon

Evento común ItemActivate

Se genera cuando se activa un elemento en el control ListView. No contiene los detalles específicos acerca de cuál elemento se activó.

Figura 14.29 | Propiedades y evento de ListView. (Parte 2 de 2). El control ListView le permite definir las imágenes que se usarán como iconos para sus elementos. Para mostrar imágenes se requiere un componente ImageList. Para crear uno, arrástrelo hacia un formulario desde el Cuadro de herramientas. Después seleccione la propiedad Images en la ventana Propiedades para mostrar el Editor de la colección Imágenes (figura 14.30). Aquí puede explorar en busca de imágenes que desea agregar al componente ImageList, el cual contiene un arreglo de objetos Image. Una vez definidas las imágenes, establezca la propiedad SmallImageList del control ListView con el nuevo objeto ImageList. La propiedad SmallImageList especifica la lista de imágenes para los iconos pequeños. La propiedad LargeImageList establece la lista de imágenes para los iconos grandes. Cada uno de los elementos en un control ListView son de tipo ListViewItem. Los iconos para los elementos de ListView se seleccionan estableciendo la propiedad ImageIndex del elemento con el índice apropiado.

Figura 14.30 | La ventana Editor de la colección Imágenes para un componente ImageList.

486

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

La clase PruebaListViewForm (figura 14.31) muestra los archivos y carpetas en un control ListView, junto con pequeños iconos que representan a cada archivo o carpeta. Si un archivo o carpeta es inaccesible debido a las configuraciones de los permisos, aparece un cuadro de diálogo MessageBox. El programa explora el contenido del directorio a medida que navega por él, en vez de indexar toda la unidad de una sola vez. Para mostrar iconos al lado de los elementos de la lista, cree un componente ImageList para el control ListView exploradorListView. Primero, arrastre y suelte un componente ImageList en el formulario, y abra el Editor de la colección Imágenes. Selecciona nuestras dos imágenes de mapa de bits simples, que se encuentran en la carpeta bin\Release de este ejemplo; uno para una carpeta (índice de arreglo 0) y el otro para un archivo (índice de arreglo 1). Después establezca la propiedad SmallImageList del objeto exploradorListView con el nuevo componente ImageList en la ventana Propiedades. El método CargarArchivosEnDirectorio (líneas 63-110) llena el control exploradorListView con el directorio que recibe como argumento (valorDirectorioActual). Borra exploradorListView y agrega el elemento "Subir un nivel". Cuando el usuario hace clic en este elemento, el programa trata de subir un nivel (en breve veremos cómo). Después, el método crea un objeto DirectoryInfo que se inicializa con el objeto string directorioActual (líneas 74-75). Si no se otorga el permiso para explorar el directorio, se lanza una excepción (y se atrapa en la línea 104). El método CargarArchivosEnDirectorio trabaja en forma distinta al método

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

// Fig. 14.31: PruebaListViewForm.cs // Mostrar directorios y su contenido en un control ListView. using System; using System.Drawing; using System.Windows.Forms; using System.IO; // el formulario contiene un control ListView, el cual muestra // las carpetas y los archivos en un directorio public partial class PruebaListViewForm : Form { // almacena el directorio actual string directorioActual = Directory.GetCurrentDirectory(); // constructor predeterminado public PruebaListViewForm() { InitializeComponent(); } // fin del constructor // explora directorio en el que hizo clic el usuario, o sube un nivel private void exploradorListView_Click( object sender, EventArgs e ) { // asegura que se seleccione un elemento if ( exploradorListView.SelectedItems.Count != 0 ) { // si se seleccionó el primer elemento, sube un nivel if ( exploradorListView.Items[ 0 ].Selected ) { // crea objeto DirectoryInfo para el directorio DirectoryInfo objetoDirectorio = new DirectoryInfo( directorioActual ); // si el directorio tiene padre, lo carga if ( objetoDirectorio.Parent != null ) CargarArchivosEnDirectorio( objetoDirectorio.Parent.FullName ); } // fin de if

Figura 14.31 | Control ListView que muestra archivos y carpetas. (Parte 1 de 3).

14.10 Control ListView

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92

// seleccionó directorio o archivo else { // se eligió un directorio o un archivo string elegido = exploradorListView.SelectedItems[ 0 ].Text; // si el elemento seleccionado es un directorio, carga el directorio seleccionado if ( Directory.Exists( directorioActual + @"\" + elegido ) ) { // si se encuentra actualmente en C:\, no necesita '\'; en caso contrario sí if ( directorioActual == @"C:\" ) CargarArchivosEnDirectorio( directorioActual + elegido ); else CargarArchivosEnDirectorio( directorioActual + @"\" + elegido ); } // fin de if } // fin de else // actualiza mostrarLabel mostrarLabel.Text = directorioActual; } // fin de if } // fin del método exploradorListView_Click // muestra archivos/subdirectorios del directorio actual public void CargarArchivosEnDirectorio( string valorDirectorioActual ) { // carga información del directorio y la muestra en pantalla try { // borra control ListView y establece el primer elemento exploradorListView.Items.Clear(); exploradorListView.Items.Add( "Subir un nivel" ); // actualiza directorio actual directorioActual = valorDirectorioActual; DirectoryInfo nuevoDirectorioActual = new DirectoryInfo( directorioActual ); // coloca archivos y directorios en arreglos DirectoryInfo[] arregloDirectorios = nuevoDirectorioActual.GetDirectories(); FileInfo[] arregloArchivos = nuevoDirectorioActual.GetFiles(); // agrega los nombres de los directorios al control ListView foreach ( DirectoryInfo dir in arregloDirectorios ) { // agrega directorio a ListView ListViewItem nuevoElementoDirectorio = exploradorListView.Items.Add( dir.Name ); nuevoElementoDirectorio.ImageIndex = 0; directorio } // fin de foreach

// establece imagen del

// agrega los nombres de archivo a ListView

Figura 14.31 | Control ListView que muestra archivos y carpetas. (Parte 2 de 3).

487

488

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

93 foreach ( FileInfo file in arregloArchivos ) 94 { 95 // agrega archivo a ListView 96 ListViewItem nuevoElementoArchivo = 97 exploradorListView.Items.Add( file.Name ); 98 99 nuevoElementoArchivo.ImageIndex = 1; // establece imagen del archivo 100 } // fin de foreach 101 } // fin de try 102 103 // acceso denegado 104 catch ( UnauthorizedAccessException ) 105 { 106 MessageBox.Show( "Advertencia: tal vez algunos campos no estén " + 107 "visibles debido a la configuración de los permisos", 108 "Atención", 0, MessageBoxIcon.Warning ); 109 } // fin de catch 110 } // fin del método CargarArchivosEnDirectorio 111 112 // maneja evento de carga cuando el formulario se muestra por primera vez 113 private void PruebaListViewForm_Load( object sender, EventArgs e ) 114 { 115 // establece lista de objetos Image 116 Image imagenCarpeta = Image.FromFile( 117 directorioActual + @"\imagenes\carpeta.bmp" ); 118 119 Image imagenArchivo = Image.FromFile( 120 directorioActual + @"\imagenes\archivo.bmp" ); 121 122 archivoCarpeta.Images.Add( imagenCarpeta ); 123 archivoCarpeta.Images.Add( imagenArchivo ); 124 125 // carga el directorio actual en el control exploradorListView 126 CargarArchivosEnDirectorio( directorioActual ); 127 mostrarLabel.Text = directorioActual; 128 } // fin del método PruebaListViewForm_Load 129 } // fin de la clase PruebaListViewForm

a)

b)

c)

Figura 14.31 | Control ListView que muestra archivos y carpetas. (Parte 3 de 3).

14.11 Control TabControl

489

LlenarTreeView en el programa anterior (figura 14.28). En vez de cargar todas las carpetas en el disco duro, el método CargarArchivosEnDirectorio sólo carga las carpetas en el directorio actual. La clase DirectoryInfo (espacio de nombres System.IO) nos permite explorar o manipular la estructura de directorios con facilidad. El método GetDirectories (línea 79) devuelve un arreglo de objetos DirectoryInfo, los cuales contienen los subdirectorios del directorio actual. De manera similar, el método GetFiles (línea 80) devuelve un arreglo de objetos de la clase FileInfo, que contienen los archivos en el directorio actual. La propiedad Name (tanto de la clase DirectoryInfo como de la clase FileInfo) sólo contiene el nombre del directorio o archivo, como temp en vez de C:\micarpeta\temp. Para acceder al nombre completo, use la propiedad FullName.

Las líneas 83-90 y las líneas 93-100 iteran a través de los subdirectorios y archivos del directorio actual, y los agregan a exploradorListView. Las líneas 89 y 99 establecen las propiedades ImageIndex de los elementos recién creados. Si un elemento es un directorio, establecemos su icono a un icono de directorio (índice 0); si es un archivo, establecemos su icono a un icono de archivo (índice 1); El método exploradorListView_Click (líneas 22-60) responde cuando el usuario hace clic en el control exploradorListView. La línea 25 comprueba si hay algo seleccionado. Si se hizo una selección, la línea 28 determina si el usuario seleccionó el primer elemento en exploradorListView. El primer elemento siempre es Subir un nivel; si está seleccionado, el programa trata de subir un nivel. Las líneas 31-32 crean un objeto DirectoryInfo para el directorio actual. La línea 35 establece la propiedad Parent para asegurar que el usuario no se encuentre en la raíz del árbol de directorios. La propiedad Parent indica el directorio padre como un objeto DirectoryInfo; si no existe un directorio padre, Parent devuelve el valor null. Si existe un directorio padre, entonces la línea 36 pasa el nombre completo del directorio padre al método CargarArchivosEnDirectorio. Si el usuario no seleccionó el primer elemento en exploradorListView, las líneas 40-55 permiten que el usuario continúe navegando a través de la estructura de directorios. La línea 43 crea el objeto string elegido, el cual recibe el texto del elemento seleccionado (el primer elemento en la colección SelectedItems). La línea 46 determina si el usuario seleccionó un directorio válido (en vez de un archivo). El programa combina las variables directorioActual y elegido (el nuevo directorio), separadas por una barra diagonal inversa (\), y pasa este valor al método Exists de la clase Directory. Este método devuelve true si su parámetro string es un directorio. Si esto ocurre, el programa pasa el objeto string al método CargarArchivosEnDirectorio. Como el directorio C:\ ya incluye una barra diagonal inversa, no se necesita una para combinar directorioActual y elegido (línea 50). No obstante, otros directorios deben incluir la barra diagonal (líneas 52-53). Por último, mostrarLabel se actualiza con el nuevo directorio (línea 58). Este programa se carga rápidamente, ya que sólo indexa los archivos en el directorio actual. Esto significa que puede ocurrir un pequeño retraso cuando se carga un nuevo directorio. Además, los cambios en la estructura de directorios pueden mostrarse al recargar un directorio. El programa anterior (figura 14.28) podría tener un largo retraso inicial al cargar toda una estructura de directorios completa. Este tipo de concesión es muy común en el mundo del software.

Observación de ingeniería de software 14.2 Al diseñar aplicaciones que se ejecutan durante periodos extensos de tiempo, podría ser conveniente elegir un largo retraso inicial para mejorar el rendimiento durante el resto del programa. No obstante, en aplicaciones que se ejecutan por periodos cortos, los desarrolladores por lo general prefieren tiempos de carga inicial rápidos y pequeños retrasos después de cada acción.

14.11 Control TabControl El control TabControl crea ventanas con fichas, como las que hemos visto en Visual Studio (figura 14.32). Esto le permite especificar más información en el mismo espacio de un formulario. Los controles TabControl contienen objetos TabPage, que son similares a los controles Panel y GroupBox en cuanto a que los controles TabPage pueden contener controles. Primero se agregan controles a los objetos TabPage y después se agregan los objetos TabPage al control TabControl. Sólo se muestra un objeto TabPage a la vez. Para agregar objetos a TabPage y TabControl, escriba miTabPage.Controls.Add(miControl) miTabControl.Controls.Add(miTabPage)

490

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

Estas instrucciones llaman al método Add de la colección Controls. El ejemplo agrega el control TabControl miControl al objeto TabPage miTabPage, y después agrega miTabPage a miTabControl. De manera alternativa, podemos usar el método AddRange para agregar un arreglo de objetos TabPage o controles a un control TabControl o a un objeto TabPage, respectivamente. La figura 14.33 describe el uso de un control TabControl de ejemplo. Para agregar controles TabControl en forma visual, arrástrelos y suéltelos en un formulario, en modo de Diseño. Para agregar objetos TabPage en modo de Diseño, haga clic derecho en el control TabControl y seleccione Agregar ficha (figura 14.34). Como alternativa, haga clic en la propiedad TabPages en la ventana Propiedades y agregue fichas en el cuadro de diálogo que aparezca. Para modificar la etiqueta de una ficha, establezca la propiedad Text del objeto TabPage. Observe que al hacer clic en las fichas, se selecciona el control TabControl; para seleccionar el objeto TabPage, haga clic en el área de control debajo de las fichas. Puede agregar controles al objeto TabPage, arrastrando y soltando elementos desde el Cuadro de herramientas. Para ver distintos objetos TabPage, haga clic en la ficha apropiada (ya sea en modo de diseño, o en modo de ejecución). En la figura 14.35 se describen las propiedades comunes y un evento común de los controles TabControl. Ventanas de fichas

Figura 14.32 | Ventanas con fichas en Visual Studio.

TabPage

Controles dentro de TabPage

Figura 14.33 | Ejemplo de un control TabControl con objetos TabPage.

TabControl

14.11 Control TabControl

491

Cada objeto TabPage genera un evento Click cuando se hace clic en su ficha. Para crear los manejadores de eventos para este evento, haga doble clic en el cuerpo del control TabPage. La clase UsoDeFichasForm (figura 14.36) utiliza un control TabControl para mostrar varias opciones relacionadas con el texto en una etiqueta (Color, Size y Message). El último objeto TabPage muestra un mensaje Acerca de, el cual describe el uso de los controles TabControl.

Figura 14.34 | Agregar objetos TabPages a un control TabControl.

Propiedades y evento de TabControl

Descripción

Propiedades comunes ImageList

Especifica las imágenes que se van a mostrar en las fichas.

ItemSize

Especifica el tamaño de las fichas.

Multiline

Indica si pueden mostrarse varias filas de fichas.

SelectedIndex

Índice del objeto TabPage seleccionado.

SelectedTab

El objeto TabPage seleccionado.

TabCount

Devuelve el número de páginas de fichas.

TabPages

Collección de objetos TabPages dentro del control TabControl.

Evento común SelectedIndexChanged

Se genera cuando cambia la propiedad SelectedIndex (es decir, cuando se selecciona otro objeto TabPage).

Figura 14.35 | Propiedades y evento de TabControl. 1 2 3 4 5 6

// Fig. 14.36: UsoDeFichasForm.cs // Uso del control TabControl para mostrar varias opciones de fuente. using System; using System.Drawing; using System.Windows.Forms;

Figura 14.36 | Uso del control TabControl para mostrar varias opciones de fuente. (Parte 1 de 3).

492

7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

// el formulario usa fichas y botones de opción para mostrar varias opciones de fuente public partial class UsoDeFichasForm : Form { // constructor predeterminado public UsoDeFichasForm() { InitializeComponent(); } // fin del constructor // manejador de eventos para el control RadioButton Negro private void negroRadioButton_CheckedChanged( object sender, EventArgs e ) { mostrarLabel.ForeColor = Color.Black; // cambia color de fuente a negro } // fin del método negroRadioButton_CheckedChanged // manejador de eventos para el control RadioButton Rojo private void rojoRadioButton_CheckedChanged( object sender, EventArgs e ) { mostrarLabel.ForeColor = Color.Red; // cambia color de fuente a rojo } // fin del método rojoRadioButton_CheckedChanged // manejador de eventos para el control RadioButton Verde private void verdeRadioButton_CheckedChanged( object sender, EventArgs e ) { mostrarLabel.ForeColor = Color.Green; // cambia color de fuente a verde } // fin del método verdeRadioButton_CheckedChanged // manejador de eventos para el control RadioButton de 12 puntos private void tamanio12RadioButton_CheckedChanged( object sender, EventArgs e ) { // cambia el tamaño de la fuente a 12 mostrarLabel.Font = new Font( mostrarLabel.Font.Name, 12 ); } // fin del método tamanio12RadioButton_CheckedChanged // manejador de eventos para el control RadioButton de 12 puntos private void tamanio16RadioButton_CheckedChanged( object sender, EventArgs e ) { // cambia el tamaño de la fuente a 16 mostrarLabel.Font = new Font( mostrarLabel.Font.Name, 16 ); } // fin del método tamanio16RadioButton_CheckedChanged // manejador de eventos para el control RadioButton de 20 puntos private void tamanio20RadioButton_CheckedChanged( object sender, EventArgs e ) { // cambia el tamaño de la fuente a 20 mostrarLabel.Font = new Font( mostrarLabel.Font.Name, 20 ); } // fin del método tamanio20RadioButton_CheckedChanged // manejador de eventos para el control RadioButton ¡Hola! private void holaRadioButton_CheckedChanged( object sender, EventArgs e ) {

Figura 14.36 | Uso del control TabControl para mostrar varias opciones de fuente. (Parte 2 de 3).

14.11 Control TabControl

65 66 67 68 69 70 71 72 73 74

493

mostrarLabel.Text = "¡Hola!"; // cambia el texto a ¡Hola! } // fin del método holaRadioButton_CheckedChanged // manejador de eventos para el control RadioButton ¡Adiós! private void adiosRadioButton_CheckedChanged( object sender, EventArgs e ) { mostrarLabel.Text = "¡Adiós!"; // cambia el texto a ¡Adiós! } // fin del método adiosRadioButton_CheckedChanged } // fin de la clase UsoDeFichasForm a)

b)

c)

d)

Figura 14.36 | Uso del control TabControl para mostrar varias opciones de fuente. (Parte 3 de 3). El control opcionesDeTextoTabControl y las fichas colorTabPage, tamanioTabPage, mensajeTabPage y acercaDeTabPage se crean en el diseñador (como describimos antes). La ficha colorTabPage contiene tres controles RadioButton para los colores negro (negroRadioButton), rojo (rojoRadioButton) y verde (verdeRadioButton). Este objeto TabPage se muestra en la figura 14.36(a). El manejador de eventos CheckChanged para cada control RadioButton actualiza el color del texto en mostrarLabel (líneas 20, 27 y 34). La ficha tamanioTabPage (figura 14.36(b)) tiene tres controles RadioButton, los cuales corresponden a los tamaños de fuente de 12 (tamanio12RadioButton), 16 (tamanio16RadioButton) y 20 (tamanio20RadioButton), que modifican el tamaño de la fuente de mostrarLabel (líneas 42, 50 y 58, respectivamente). La ficha mensajeTabPage (figura 14.36(c)) contiene dos controles RadioButton para los mensajes ¡Hola! (holaRadioButton) y ¡Adiós! (adiosRadioButton). Los dos controles RadioButton determinan el texto en mostrarLabel (líneas 65 y 72, respectivamente). La ficha acercaDeTabPage (figura 14.36(d)) contiene un control Label (mensajeLabel) que describe el propósito de los controles TabControl.

Observación de ingeniería de software 14.3 Un objeto TabPage puede actuar como un contenedor para un solo grupo lógico de controles RadioButton, para cumplir con su exclusividad mutua. Para colocar varios grupos de controles RadioButton dentro de un solo objeto TabPage, debe agrupar los controles RadioButton dentro de controles Panel o GroupBox que se encuentren dentro del objeto TabPage.

494

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

14.12 Ventanas de la interfaz de múltiples documentos (MDI) En capítulos anteriores sólo hemos construido aplicaciones con la interfaz de un solo documento (SDI). Tales programas (incluyendo el Bloc de notas y Paint de Microsoft) sólo pueden soportar una ventana o documento abierto a la vez. Por lo general, las aplicaciones SDI tienen habilidades limitadas; por ejemplo, Paint y Bloc de notas tienen características limitadas de edición de imágenes y texto. Para editar varios documentos, el usuario debe ejecutar otra instancia de la aplicación SDI. Las aplicaciones más recientes son programas con la interfaz de múltiples documentos (MDI), que permiten a los usuarios editar varios documentos a la vez (por ejemplo, los productos de Microsoft Office). Los programas MDI también tienden a ser más complejos; PaintShop Pro y Photoshop tienen un mayor número de características para editar imágenes que Paint. A la ventana de aplicación principal de un programa MDI se le conoce como ventana padre, y a cada ventana dentro de la aplicación se le conoce como ventana hija. Aunque una aplicación MDI puede tener muchas ventanas hijas, cada una tiene sólo una ventana padre. Lo que es más, sólo puede haber como máximo una ventana hija activa a la vez. Las ventanas hijas no pueden ser padres y tampoco pueden moverse fuera de su padre. De no ser así, una ventana hija se comporta igual que cualquier otra ventana (con respecto a cerrar, minimizar, cambiar de tamaño, etc.). La funcionalidad de una ventana hija puede ser distinta de la funcionalidad de otras ventanas hijas del padre. Por ejemplo, una ventana hija podría permitir al usuario editar imágenes, otra podría permitir al usuario editar texto y una tercera podría mostrar el tráfico de la red en forma gráfica, pero todas podrían pertenecer al mismo padre MDI. La figura 14.37 ilustra una aplicación MDI de ejemplo. Para crear un formulario MDI, cree un nuevo formulario y establezca su propiedad IsMdiContainer a true. El formulario cambiará de apariencia, como en la figura 14.38.

Padre MDI Hija MDI Hija MDI

Figura 14.37 | Ventana padre MDI y ventanas hijas MDI.

Interfaz de un solo documento (SDI)

Figura 14.38 | Formularios SDI y MSI.

Interfaz de múltiples documentos (MDI)

14.12 Ventanas de la interfaz de múltiples documentos (MDI)

495

A continuación, cree una clase Form hija para agregarla al formulario. Para ello, haga clic con el botón derecho del ratón en el Explorador de soluciones del proyecto, seleccione Proyecto > Agregar Windows Forms… y nombre el archivo. Edite el formulario según lo desee. Para agregar e formulario hijo al padre, debemos crear un nuevo objeto Form hijo, asignar a la propiedad MdiParent el formulario padre y llamar al método Show del formulario. En general, para agregar un formulario hijo a un padre, escribimos ClaseFormHijo hijoForm = New ClaseFormHijo(); hijoForm.MdiParent = padreForm; hijoForm.Show();

En la mayoría de los casos, el formulario padre crea al hijo, por lo que la referencia padreForm es this. Por lo general, el código para crear un hijo se encuentra dentro de un manejador de eventos, el cual crea una nueva ventana en respuesta a una acción del usuario. Las selecciones de menús (como Archivo, seguido de la opción de submenú Nuevo, seguido de la opción de submenú Ventana) son técnicas comunes para crear nuevas ventanas hijas. La propiedad MdiChildren de la clase Form devuelve un arreglo de referencias Form hijas. Esto es útil si la ventana padre desea comprobar el estado de todas sus hijas (por ejemplo, asegurar que todas se guarden antes de que cierre la ventana padre). La propiedad ActiveMdiChild devuelve una referencia a la ventana hija activa; devuelve Nothing si no hay ventanas hijas activas. En la figura 14.39 se describen otras características de las ventanas MDI. Las ventanas hijas se pueden minimizar, maximizar y cerrar en forma independiente unas de otras, y de la ventana padre. La figura 14.40 muestra dos imágenes: una contiene dos ventanas hijas minimizadas y la otra contiene una ventana hija minimizada. Cuando el padre se minimiza o se cierra, las ventanas hijas también se minimizan o se cierran. Observe que la barra de título en la figura 14.40(b) es Form1 – [Hijo2]. Cuando se maximiza una ventana hija, el texto de su barra de título se inserta en la barra de título de la ventana padre. Cuando se minimiza o maximiza una ventana hija, su barra de título muestra un icono de restauración, el cual puede usarse para regresar la ventana hija a su tamaño anterior (al que tenía antes de maximizarse o minimizarse). Propiedades, un método y un evento de los formularios MDI

Descripción

Propiedades comunes de los hijos MDI IsMdiChild

Indica si el formulario es un hijo MDI. Si es true, el formulario es un hijo MDI (propiedad de sólo lectura).

MdiParent

Especifica el formulario padre MDI del hijo.

Propiedades comunes de los padres MDI ActiveMdiChild

Devuelve el formulario hijo MDI que está activo en un momento dado (devuelve null si no hay hijos activos).

IsMdiContainer

Indica si un formulario puede ser un padre MDI. Si es true, el formulario puede ser un padre MDI. El valor predeterminado es false.

MdiChildren

Devuelve los hijos MDI como un arreglo de objetos Form.

Método común LayoutMdi

Determina la distribución de los formularios hijos en un padre MDI. El método recibe como parámetro una enumeración MdiLayour con los posibles valores ArrangeIcons, Cascade, TileHorizontal y TileVertical. La figura 14.42 ejemplifica los efectos de estos valores.

Evento común MdiChildActivate

Se genera cuando se cierra o se activa un formulario hijo MDI.

Figura 14.39 | Propiedades, métodos y eventos de las ventanas padre MDI y las ventanas hijas MDI.

496

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

C# cuenta con una propiedad que ayuda a llevar la cuenta de cuáles ventanas hijas están abiertas en un contenedor MDI. La propiedad MdiWindowListItem de la clase MenuStrip especifica qué menú (si lo hay) va a mostrar una lista de ventanas hijas abiertas. Cuando se abre una nueva ventana hija, se agrega una entrada a la lista (como en la primera pantalla de la figura 14.41). Si se abren nueve ventanas hijas o más, la lista incluye la opción Más ventanas…, la cual permite al usuario seleccionar una ventana de una lista en un cuadro de diálogo.

Iconos de la ventana padre: minimizar, maximizar y cerrar a)

Iconos de la ventana hija maximizada: minimizar, restaurar y cerrar b)

Iconos de la ventana hija minimizada: restaurar, maximizar y cerrar

La barra de título de la ventana padre indica la ventana hija maximizada

Figura 14.40 | Ventanas hijas minimizadas y maximizadas.

Lista de ventanas hijas

9 o más ventanas habilitan la opción Más ventanas…

Figura 14.41 | Ejemplo de la propiedad MdiList de MenuItem.

14.12 Ventanas de la interfaz de múltiples documentos (MDI)

497

Buena práctica de programación 14.1 Al crear aplicaciones MDI, incluya un menú que muestre una lista de las ventanas hijas abiertas. Esto ayuda al usuario a seleccionar una ventana hija con rapidez, en vez de tener que buscarla en la ventana padre.

Los contenedores MDI le permiten organizar la colocación de las ventanas hijas. Para ordenar las ventanas hijas en una aplicación MDI, se hace una llamada al método LayoutMdi del formulario padre. El método LayoutMdi recibe una enumeración MdiLayout, la cual puede tener los valores ArrangeIcons, Cascade, TileHorizontal y TileVertical. Las ventanas en mosaico llenan por completo la ventana padre y no se traslapan; dichas ventanas pueden organizarse en forma horizontal (el valor TileHorizontal) o vertical (el valor TileVertical). Las ventanas en cascada (el valor Cascade) se traslapan; cada una es del mismo tamaño y muestra una barra de título visible, si es posible. El valor ArrangeIcons ordena los iconos de todas las ventanas hijas minimizadas. Si hay ventanas minimizadas esparcidas alrededor de la ventana padre, el valor ArrangeIcons las ordena cerca de la esquina inferior izquierda de la ventana padre. La figura 14.42 ilustra los valores de la enumeración MdiLayout. La clase UsoDeMDIForm (figura 14.43) demuestra el uso de las ventanas MDI. La clase UsoDeMDIForm tiene tres instancias del formulario hijo HijoForm (figura 14.44), cada una de las cuales contiene un control PictureBox que muestra una imagen. El formulario MDI padre contiene un menú que permite a los usuarios crear y ordenar los formularios hijos. El programa en la figura 14.43 es la aplicación. El formulario MDI padre, que se crea primero, contiene dos menús de nivel superior. El primero de estos menús, llamado Archivo (archivoToolStripMenuItem), contiene un elemento Salir (salirToolStripMenuItem) y un submenú Nuevo (nuevoToolStripMenuItem), que

a) ArrangeIcons

b) Cascade

c) TileHorizontal

d) TileVertical

Figura 14.42 | Valores de la enumeración MdiLayout.

498

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

contiene los elementos para cada ventana hija. El segundo menú, llamado Ventana (ventanaToolStripMenuproporciona opciones para distribuir en pantalla los formularios MDI hijos, además de una lista de los hijos MDI activos.

Item),

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

// Fig. 14.43: UsoDeMDIForm.cs // Demostración del uso de ventanas MDI padre e hijas. using System; using System.Windows.Forms; // el formulario demuestra el uso de las ventanas MDI padre e hijas public partial class UsoDeMDIForm : Form { // constructor predeterminado public UsoDeMDIForm() { InitializeComponent(); } // fin del constructor // crea ventana Hijo 1 cuando se hace clic en hijo1ToolStripMenuItem private void hijo1ToolStripMenuItem_Click( object sender, EventArgs e ) { // crea nuevo hijo HijoForm formHijo = new HijoForm( "Hijo 1", @"\imagenes\csharphtp1.jpg" ); formHijo.MdiParent = this; // set parent formHijo.Show(); // display child } // fin del método hijo1ToolStripMenuItem_Click // crea ventana Hijo 2 cuando se hace clic en hijo2ToolStripMenuItem private void hijo2ToolStripMenuItem_Click( object sender, EventArgs e ) { // crea nuevo hijo HijoForm formHijo = new HijoForm( "Hijo 2", @"\imagenes\vbnethtp2.jpg" ); formHijo.MdiParent = this; // establece el padre formHijo.Show(); // muestra el hijo } // fin del método hijo2ToolStripMenuItem_Click // crea ventana Hijo 3 cuando se hace clic en hijo3ToolStripMenuItem private void hijo3ToolStripMenuItem_Click( object sender, EventArgs e ) { // crea nuevo hijo HijoForm formHijo = new HijoForm( "Hijo 3", @"\imagenes\pythonhtp1.jpg" ); formHijo.MdiParent = this; // establece el padre formHijo.Show(); // muestra el hijo } // fin del método hijo3MenuItem_Click // sale de la aplicación private void salirToolStripMenuItem_Click( object sender, EventArgs e ) { Application.Exit(); } // fin del método salirToolStripMenuItem_Click

Figura 14.43 | Clase de ventana MDI padre. (Parte 1 de 2).

14.12 Ventanas de la interfaz de múltiples documentos (MDI)

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74

499

// establece esquema en Cascada private void cascadaToolStripMenuItem_Click( object sender, EventArgs e ) { this.LayoutMdi( MdiLayout.Cascade ); } // fin del método cascadaToolStripMenuItem_Click // establece esquema en Mosaico horizontal private void mosaicoHorizontalToolStripMenuItem_Click( object sender, EventArgs e ) { this.LayoutMdi( MdiLayout.TileHorizontal ); } // fin del método mosaicoHorizontalToolStripMenuItem // establece esquema en Mosaico vertical private void mosaicoVerticalToolStripMenuItem_Click( object sender, EventArgs e ) { this.LayoutMdi( MdiLayout.TileVertical ); } // fin del método mosaicoVerticalToolStripMenuItem_Click } // fin de la clase UsoDeMDIForm

a)

b)

c)

d)

Figura 14.43 | Clase de ventana MDI padre. (Parte 2 de 2).

En la ventana Propiedades establecimos la propiedad IsMdiContainer del formulario a true, convirtiéndolo en un formulario MDI padre. Además, establecimos la propiedad MdiWindowListItem de MenuStrip a ventanaToolStripMenuItem. Esto permite que el menú Ventana contenga la lista de ventanas MDI hijas.

500

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

El elemento de menú Cascada (cascadaToolStripMenuItem) tiene un manejador de eventos (cascadaToolStripMenuItem_Click, líneas 55-59) que ordena las ventanas hijas en cascada. El manejador de eventos llama al método LayoutMdi con el argumento Cascade de la enumeración MdiLayout (línea 58). El elemento de menú Mosaico horizontal (mosaicoHorizontalToolStripMenuItem) tiene un manejador de eventos (mosaicoHorizontalToolStripMenuItem_Click, líneas 62-66) que ordena las ventanas hijas en forma horizontal. El manejador de eventos llama al método LayoutMdi con el argumento TileHorizontal de la enumeración MdiLayout (línea 65). Por último, el elemento de menú Mosaico vertical (mosaicoVerticalToolStripMenuItem) tiene un manejador de eventos (mosaicoVerticalToolStripMenuItem_Click, líneas 69-73) que ordena las ventanas hijas en forma vertical. El manejador de eventos llama al método LayoutMdi con el argumento TileVertical de la enumeración MdiLayout (línea 72). En este punto, la aplicación aún está incompleta; debemos definir la clase de formulario MDI hijo. Para ello, haga clic con el botón derecho del ratón en el proyecto, dentro del Explorador de soluciones, y seleccione Agregar > Windows Forms…. Después use el nombre HijoForm para la nueva clase en el cuadro de diálogo (figura 14.44). A continuación, hay que agregar un control PictureBox (mostrarImag) a HijoForm. En el constructor, la línea 15 establece el texto de la barra de título. Las líneas 18-19 establecen la propiedad Image de HijoForm a un objeto Image, usando el método FromFile. Después de definir la clase de formulario MDI hijo, el formulario MDI padre (figura 14.43) puede crear nuevas ventanas hijas. Los manejadores de eventos en las líneas 16-46 crean un nuevo formulario hijo, que corresponde al elemento del menú en el que se hizo clic. Las líneas 20-21, 31-32 y 42-43 crean nuevas instancias de HijoForm. Las líneas 22, 33 y 44 establecen la propiedad MdiParent de cada ventana hija, para asignarle el formulario padre. Las líneas 23, 34 y 45 llaman al método Show para mostrar cada formulario hijo.

14.13 Herencia visual En el capítulo 10 vimos cómo crear clases heredando de otras clases. También hemos utilizado la herencia para crear formularios que muestren una GUI, derivando nuestras nuevas clases de formularios de la clase System. Windows.Forms.Form. Éste es un ejemplo de herencia visual. La clase Form derivada contiene la funcionalidad de su clase base Form, incluyendo sus propiedades, métodos, variables y controles. La clase derivada también hereda de su clase base todos los aspectos visuales: tamaño, distribución de los componentes, espacio entre los componentes de la GUI, colores y fuentes.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

// Fig. 14.44: HijoForm.cs // Ventana hija del padre MDI. using System; using System.Drawing; using System.Windows.Forms; using System.IO; public partial class HijoForm : Form { public HijoForm( string titulo, string nombreArchivo ) { // Requerido para el soporte del Diseñador de Windows Form InitializeComponent(); Text = titulo; // establece el texto del título // establece imagen a mostrar en control PictureBox mostrarImag.Image = Image.FromFile( Directory.GetCurrentDirectory() + nombreArchivo ); } // fin del constructor } // fin de la clase HijoForm

Figura 14.44 | El formulario MDI hijo HijoForm.

14.13 Herencia visual

501

La herencia visual le permite lograr una consistencia visual en todas las aplicaciones. Por ejemplo, podría definir un formulario base que contenga el logotipo de un producto, un color de fondo específico, una barra de menús predefinida y otros elementos. Después podría usar el formulario base a lo largo de toda una aplicación para obtener uniformidad y reconocimiento de marca. La clase HerenciaVisualForm (figura 14.45) se deriva de Form. Los resultados ilustran el funcionamiento del programa. La GUI contiene dos controles Label con el texto, Errores, Errores, Errores y Copyright 2006, por deitel.com, así como un control Button que muestra el texto Aprenda más. Cuando un usuario oprime el botón Aprenda más, se invoca el método aprendaMasButton_Click (líneas 16-22). Este método muestra un cuadro de diálogo MessageBox que proporciona cierto texto informativo. Para permitir que otros formularios hereden de HerenciaVisualForm, debemos empaquetar a VisualHerenciaForm como un archivo .dll (biblioteca de clases). Haga clic con el botón derecho del ratón en el nombre del proyecto, dentro del Explorador de soluciones, y seleccione Propiedades; después seleccione la ficha Aplicación. En la lista desplegable Tipo de resultado, sustituya Aplicación para Windows por Biblioteca de clases. Al generar el proyecto se produce el archivo .dll. Para heredar en forma visual de HerenciaVisualForm, primero hay que crear una aplicación para Windows. En esta aplicación, agregue una referencia al archivo .dll que acaba de crear (se encuentra en la carpeta bin/ Release de la aplicación anterior). Después abra el archivo que define la GUI de la nueva aplicación y modifique la primera línea de la clase, de manera que herede de la clase HerenciaVisualForm. Sólo tendrá que especificar

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// Fig. 14.45: HerenciaVisualForm.cs // Formulario base para usarlo con la herencia visual. using System; using System.Windows.Forms; // el formulario base se utiliza para demostrar la herencia visual public partial class HerenciaVisualForm : Form { // constructor predeterminado public HerenciaVisualForm() { InitializeComponent(); } // fin del constructor // muestra MessageBox cuando se hace clic en el control Button private void aprendaMasButton_Click( object sender, EventArgs e ) { MessageBox.Show( "Errores, Errores, Errores es un producto de deitel.com", "Aprenda más", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // fin del método aprendaMasButton_Click } // fin de la clase HerenciaVisualForm

Figura 14.45 | La clase HerenciaVisualForm, que hereda de la clase Form, contiene un control Button (Aprenda más).

502

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

Figura 14.46 | Formulario que demuestra la herencia visual. el nombre de la clase. En la vista de Diseño, el formulario de la nueva aplicación deberá mostrar los controles del formulario base (figura 14.46). Aún podemos agregar más componentes al formulario. La clase PruebaHerenciaVisualForm (figura 14.47) es una clase derivada de HerenciaVisualForm. Los resultados ilustran la funcionalidad del programa. La GUI contiene los componentes derivados de la clase HerenciaVisualForm, así como también un control Button adicional, con el texto Aprenda el programa. Cuando un usuario oprime este control Button, se invoca el método aprendaProgramaButton_Click (líneas 17-22). Este método muestra otro cuadro de diálogo MessageBox que proporciona un texto informativo distinto. La figura 14.47 demuestra que la clase PruebaHerenciaVisual hereda los componentes, la distribución de los mismos y la funcionalidad de la clase base HerenciaVisualForm (figura 14.45). Si un usuario hace clic en el botón Aprenda más, el manejador de eventos aprendaMasButton_Click de la clase base muestra un cuadro de diálogo MessageBox. HerenciaVisualForm utiliza un modificador de acceso private para declarar sus controles, por lo que la clase PruebaHerenciaVisualForm no puede modificar los controles heredados de la clase HerenciaVisualForm.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// Fig. 14.47: PruebaHerenciaVisualForm.cs // Formulario derivado mediante el uso de la herencia visual. using System; using System.Windows.Forms; // formulario derivado mediante la herencia visual public partial class PruebaHerenciaVisualForm : HerenciaVisualForm // código para la herencia { // constructor predeterminado public PruebaHerenciaVisualForm() { InitializeComponent(); } // fin del constructor // muestra MessageBox cuando se hace clic en el control Button private void aprendaProgramaButton_Click(object sender, EventArgs e) { MessageBox.Show( "Este programa fue creado por Deitel & Associates", "Aprenda el programa", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // fin del método aprendaProgramaButton_Click } // fin de la clase PruebaHerenciaVisualForm

Figura 14.47 | La clase PruebaHerenciaVisualForm, que hereda de la clase HerenciaVisualForm, contiene un control Button adicional. (Parte 1 de 2).

14.14 Controles definidos por el usuario

La clase derivada no puede modificar estos controles

503

La clase derivada puede modificar este control

Figura 14.47 | La clase PruebaHerenciaVisualForm, que hereda de la clase HerenciaVisualForm, contiene un control Button adicional. (Parte 2 de 2).

14.14 Controles definidos por el usuario

El .NET Framework le permite crear controles personalizados. Estos controles personalizados aparecen en el Cuadro de herramientas del usuario y pueden agregarse a controles Form, Panel o GroupBox de la misma forma que se agregan los controles Button, Label y otros controles predefinidos. La manera más simple de crear un control personalizado es derivar una clase de un control existente, como Label. Esto es útil si queremos agregar funcionalidad a un control existente, en vez de tener que volver a implementar el control existente para incluir la funcionalidad deseada. Por ejemplo, puede crear un nuevo tipo de control Label que se comporte como un control Label normal, pero que tenga una apariencia distinta. Para ello, hay que heredar de la clase Label y redefinir el método OnPaint. Todos los controles contienen el método OnPaint, que el sistema llama cuando un componente debe volver a dibujarse (como cuando se cambia su tamaño). El método OnPaint recibe un objeto PaintEventArgs, el cual contiene información gráfica: la propiedad Graphics es el objeto gráfico que se utiliza para dibujar, y la propiedad ClipRectangle define el límite rectangular del control. Cada vez que el sistema produce el evento Paint, la clase base de nuestro control atrapa el evento. Mediante el polimorfismo se hace una llamada al método OnPaint de nuestro control. Como no se hace la llamada a la implementación de OnPaint de nuestra clase base, debemos de llamarla en forma implícita desde nuestra implementación de OnPaint antes de ejecutar nuestro código personalizado para pintar en la pantalla. En la mayoría de los casos, es conveniente hacer esto para asegurar que se ejecute el código original para pintar en la pantalla, además del código que usted defina en la clase del control personalizado. Por otro lado, si no deseamos que se ejecute el método OnPaint de la clase base, no lo llamamos. Para crear un nuevo control compuesto de controles existentes, use la clase UserControl. Los controles que se agregan a un control personalizado se llaman controles constituyentes. Por ejemplo, un programador podría crear un control UserControl compuesto de un control Button, un control Label y un control TextBox, cada uno de ellos asociado con cierta funcionalidad (por ejemplo, que el control Button establezca el texto del control Label y le asigne el texto que contiene el control TextBox). El control UserControl actúa como un contenedor para los controles que se agregan a él. El control UserControl contiene controles constituyentes, por lo que no determina la forma en que éstos se muestran en pantalla. El método OnPaint del control UserControl no puede redefinirse. Para controlar la apariencia de cada control constituyente, debe manejar el evento Paint de cada control. El manejador de eventos de Paint recibe un objeto PaintEventArgs, el cual puede usarse para dibujar gráficos (líneas, rectángulos, etc.) en los controles constituyentes. Mediante el uso de otra técnica, un programador puede crear un control completamente nuevo, heredando de la clase Control. Esta clase no define ningún comportamiento específico; esa tarea se deja para el programador. La clase Control maneja los elementos asociados con todos los controles, como los eventos y los controles de tamaño. El método OnPaint debe contener una llamada al método OnPaint de la clase base, el cual debe llamar

504

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

a los manejadores de eventos de Paint. Después, usted debe agregar código para dibujar gráficos personalizados dentro del método OnPaint redefinido, a la hora de dibujar el control. Esta técnica permite la mayor flexibilidad, pero también requiere la mayor planeación. En la figura 14.48 se muestra un resumen de los tres métodos.

Técnicas para controles personalizado y propiedades de PaintEventArgs

Descripción

Técnicas para controles personalizados Heredar del control de Windows Forms

Puede hacer esto para agregar funcionalidad a un control ya existente. Si redefine el método OnPaint, llame al método OnPaint de la clase base. Sólo puede agregar elementos a la apariencia del control original, no rediseñarlo.

Crear un control

Puede crear un control UserControl compuesto de varios controles ya existentes (por ejemplo, para combinar su funcionalidad). No puede redefinir los métodos OnPaint de los controles personalizados. En vez de ello, debe colocar el código de dibujo en un manejador de eventos Paint. De nuevo. hay que tener en cuenta que sólo se pueden agregar elementos a la apariencia original del control, no rediseñarlo.

UserControl

Heredar de la clase Control

Defina un control completamente nuevo. Redefina el método OnPaint y después llame al método OnPaint de la clase base e incluya métodos para dibujar el control. Con este método puede personalizar la apariencia y la funcionalidad del control.

Propiedades de PaintEventArgs Graphics

El objeto gráfico del control. Se utiliza para dibujar en el control.

ClipRectangle

Especifica el rectángulo que indica el contorno del control.

Figura 14.48 | Creación de controles personalizados.

En la figura 14.49 creamos un control tipo “reloj”. Éste es un control UserControl compuesto de un control y un control Timer; cada vez que el control Timer genera un evento, el control Label se actualiza para reflejar la hora actual. Los controles Timer (espacio de nombres System.Windows.Forms) son componentes invisibles que residen en un formulario, generando eventos Tick a un intervalo establecido. Este intervalo se establece mediante la propiedad Interval del control Timer, la cual define el número de milisegundos (milésimas de un segundo) que deben transcurrir entre cada evento. De manera predeterminada, los temporizadores están deshabilitados y no generan eventos. Esta aplicación contiene un control de usuario (RelojUserControl) y un formulario que muestra el control de usuario. Empezamos por crear una aplicación para Windows. Después, creamos una clase UserControl para el proyecto, seleccionando Proyecto > Agregar control de usuario…. A continuación debe aparecer un cuadro de diálogo, en el que podemos seleccionar el tipo de control que deseamos agregar; ya están seleccionados los controles del usuario. Después asignamos el nombre RelojUserControl al archivo (y a la clase). Nuestro control RelojUserControl se muestra como un rectángulo de color gris. Puede tratar a este control como un formulario Windows Forms, lo que significa que puede agregar controles mediante el Cuadro de herramientas y establecer propiedades mediante la ventana Propiedades. No obstante, en vez de crear una aplicación, sólo estamos creando un nuevo control compuesto de otros controles. Agregue un control Label (mostrarLabel) y un control Timer (relojTimer) al control UserControl. Establezca el intervalo del control Timer a 1000 milisegundos y establezca el texto de mostrarLabel con cada evento (líneas 16-20). Para generar eventos, relojTimer debe habilitarse estableciendo la propiedad Enabled a true en la ventana Propiedades. Label

14.14 Controles definidos por el usuario

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

505

// Fig. 14.49: RelojUserControl.cs // Control definido por el usuario, con un control Timer y un control Label. using System; using System.Windows.Forms; // control UserControl que muestra la hora en un control Label public partial class RelojUserControl : UserControl { // constructor predeterminado public RelojUserControl() { InitializeComponent(); } // fin del constructor // actualiza el control Label en cada evento Tick private void relojTimer_Tick(object sender, EventArgs e) { // obtiene la hora actual (Now), la convierte en string mostrarLabel.Text = DateTime.Now.ToLongTimeString(); } // fin del método relojTimer_Tick } // fin de la clase RelojUserControl

Figura 14.49 | Reloj definido mediante el control UserControl.

La estructura DateTime (espacio de nombres System) contiene la propiedad Now, que es la hora actual. El método ToLongTimeString convierte Now en un objeto string que contiene la hora, los minutos y segundos actuales (junto con el indicador AM o PM). Utilizamos esta información para establecer la hora en mostrarLabel, en la línea 19. Una vez creado, nuestro control tipo reloj aparece como un elemento en el Cuadro de herramientas. Tal vez necesite cambiar al formulario de la aplicación para que el elemento aparezca en el Cuadro de herramientas. Para usar el control, sólo arrástrelo al formulario y ejecute la aplicación para Windows. Usamos un color de fondo blanco para el objeto RelojUserControl, para hacerlo que resalte en el formulario. La figura 14.49 muestra el resultado de RelojForm, que contiene nuestro control RelojUserControl. No hay manejadores de eventos en RelojForm, por lo que sólo mostramos el código para RelojUserControl. Visual Studio le permite compartir controles personalizados con otros desarrolladores. Para crear un control UserControl que pueda exportarse a otras soluciones, haga lo siguiente: 1. Cree un nuevo proyecto de Biblioteca de clases. 2. Elimine el archivo Class1.cs que se incluye al principio con la aplicación. 3. Haga clic con el botón derecho del ratón en el proyecto, dentro del Explorador de soluciones, y seleccione Agregar > Control de usuario…. En el cuadro de diálogo que aparezca, escriba un nombre para el archivo del control de usuario y haga clic en Agregar. 4. Dentro del proyecto, agregue controles y funcionalidad al control UserControl (figura 14.50). 5. Genere el proyecto. Visual Studio creará un archivo .dll para el control UserControl en el directorio de salida (bin/Release). El archivo no es ejecutable; las bibliotecas de clases se utilizan para definir clases que pueden reutilizarse en otras aplicaciones ejecutables. 6. Cree una nueva aplicación para Windows.

506

Capítulo 14 Conceptos de interfaz gráfica de usuario: parte 2

Figura 14.50 | Creación de un control personalizado. 7. En la nueva aplicación para Windows, haga clic con el botón derecho del ratón en el Cuadro de herramientas y seleccione la opción Elegir elementos…. En el cuadro de diálogo Elegir elementos del cuadro de herramientas que aparezca, haga clic en Examinar…. Busque el archivo .dll de la biblioteca de clases que creó en los Pasos del 1 al 5. Después aparecerá el elemento en el cuadro de diálogo Elegir elementos del cuadro de herramientas (figura 14.51). Ahora este control se puede agregar al formulario, como si fuera cualquier otro control (figura 14.52).

Figura 14.51 | Control personalizado agregado al Cuadro de herramientas.

Nuevo Cuadro de herramientas

Control insertado nuevamente

Figura 14.52 | Control personalizado agregado al formulario.

14.15 Conclusión

507

14.15 Conclusión Muchas de las aplicaciones comerciales de hoy en día cuentan con GUIs que son fáciles de usar y manipular. Debido a esta demanda de GUIs amigables para el usuario, la habilidad de diseñar GUIs sofisticadas es una habilidad de programación esencial. El IDE de Visual Studio ayuda a que el desarrollo de GUIs sea rápido y sencillo. En los capítulos 13 y 14 presentamos técnicas de desarrollo de GUIs básicas. En el capítulo14 demostramos cómo crear menús, los cuales proporcionan a los usuarios un fácil acceso a la funcionalidad de una aplicación. En este capítulo aprendió a usar los controles DateTimePicker y MonthCalendar, que permiten a los usuarios introducir valores de fecha y hora. Demostramos los controles LinkLabel, que se utilizan para vincular al usuario con una aplicación o página Web. Utilizó varios controles que proporcionan listas de datos al usuario: ListBox, CheckedListBox y ListView. Utilizamos el control ComboBox para crear listas desplegables, y el control TreeView para mostrar datos en forma jerárquica. Después presentamos GUIs complejas que utilizan ventanas con fichas e interfaces de múltiples documentos. Concluimos este capítulo con demostraciones de la herencia simple y la creación de controles personalizados. En el siguiente capítulo exploraremos el concepto de subprocesamiento múltiple. En muchos lenguajes de programación puede crear varios subprocesos, para que varias actividades se lleven a cabo en paralelo.

15

Subprocesamiento múltiple

No bloquees el camino de la investigación —Charles Sanders Peirce

OBJETIVOS En este capítulo aprenderá lo siguiente:

Una persona con un reloj sabe qué hora es; una persona con dos relojes nunca estará segura.

Q

Qué son los subprocesos y por qué son útiles.

Q

Cómo nos permiten los subprocesos administrar actividades concurrentes.

Q

El ciclo de vida de un subproceso.

Aprenda a laborar y esperar.

Q

Las prioridades y la programación de subprocesos.

—Henry Wadsworth Longfellow

Q

A crear y ejecutar objetos Thread.

Q

Acerca de la sincronización de subprocesos.

Q

Qué son las relaciones productor/consumidor y cómo se implementan con el subprocesamiento múltiple.

Q

A mostrar resultados de múltiples subprocesos en una GUI.

—Proverbio

La definición más general de la belleza… múltiple deidad en la unidad. —Samuel Taylor Coleridge

El mundo avanza tan rápido estos días que el hombre que dice que no puede hacer algo, por lo general se ve interrumpido por alguien que lo está haciendo. —Elbert Hubbard

Pla n g e ne r a l

510

Capítulo 15 Subprocesamiento múltiple

15.1 15.2 15.3 15.4 15.5 15.6 15.7 15.8 15.9 15.10

Introducción Estados de los subprocesos: ciclo de vida de un subproceso Prioridades y programación de subprocesos Creación y ejecución de subprocesos Sincronización de subprocesos y la clase Monitor Relación productor/consumidor sin sincronización de subprocesos Relación productor/consumidor con sincronización de subprocesos Relación productor/consumidor: búfer circular Subprocesamiento múltiple con GUIs Conclusión

15.1 Introducción Sería bueno si pudiéramos realizar una acción a la vez, y realizarla bien, pero por lo general eso es difícil. El cuerpo humano realiza una gran variedad de operaciones en paralelo; o, como diremos en este capítulo, operaciones concurrentes. Por ejemplo, la respiración, la circulación de la sangre y la digestión pueden ocurrir de manera concurrente. Todos los sentidos (vista, tacto, olfato, gusto y oído) pueden emplearse al mismo tiempo. Las computadoras también pueden realizar operaciones concurrentes. Para su computadora es tarea común compilar un programa, enviar un archivo a una impresora y recibir mensajes de correo electrónico a través de una red, de manera concurrente. Aunque suene irónico, la mayoría de los lenguajes de programación no permiten a los programadores especificar actividades concurrentes. Por lo general, los lenguajes de programación proporcionan sólo un simple conjunto de instrucciones de control, las cuales permiten a los programadores realizar una acción a la vez, procediendo a la siguiente acción una vez que la anterior haya terminado. Históricamente, el tipo de concurrencia que las computadoras realizan en la actualidad se ha implementado en forma general como funciones “primitivas” del sistema operativo, disponibles sólo para los “programadores de sistemas” altamente experimentados. El lenguaje de programación Ada, desarrollado por el Departamento de Defensa de los Estados Unidos, hizo que las primitivas de concurrencia estuvieran disponibles ampliamente para los contratistas de defensa dedicados a la construcción de sistemas militares de comando y control. Sin embargo, Ada no se ha utilizado de manera general en las universidades o en la industria comercial. La Biblioteca de clases del .NET Framework cuenta con primitivas de concurrencia. Usted especifica que las aplicaciones contienen “subprocesos de ejecución”, cada uno de los cuales designa una porción de un programa que puede ejecutarse en forma concurrente con otros subprocesos; a esta capacidad se le conoce como subprocesamiento múltiple, y está disponible para todos los lenguajes de programación .NET, incluyendo C#, Visual Basic y Visual C++. La Biblioteca de clases del .NET Framework incluye herramientas de subprocesamiento múltiple en el espacio de nombres System.Threading.

Tip de rendimiento 15.1 Un problema con las aplicaciones de un solo subproceso es que las actividades que llevan mucho tiempo deben completarse antes de que puedan comenzar otras. En una aplicación con subprocesamiento múltiple, los subprocesos pueden distribuirse entre varios procesadores (si hay disponibles), de manera que se realicen varias tareas en forma concurrente, lo cual permite a la aplicación operar con más eficiencia. El subprocesamiento múltiple también puede incrementar el rendimiento en sistemas con un solo procesador que simulan la concurrencia; cuando un subproceso no puede proceder, otro puede usar el procesador.

Hablaremos sobre muchas aplicaciones de la programación concurrente. Cuando los programas descargan archivos extensos como clips de audio o video desde Internet, los usuarios no desean esperar hasta que se descargue todo un clip completo para empezar a reproducirlo. Para resolver este problema, podemos poner varios subprocesos a trabajar; un subproceso descarga un clip mientras otro lo reproduce. Estas actividades se realizan en forma concurrente para evitar que la reproducción del clip tenga interrupciones, sincronizamos los subprocesos

15.2 Estados de los subprocesos: ciclo de vida de un subproceso

511

de manera que el subproceso de reproducción no empiece sino hasta que haya una cantidad suficiente del clip en memoria, como para mantener ocupado al subproceso de reproducción. Otro ejemplo de subprocesamiento múltiple es la recolección automática de basura del CLR. C y C++ requieren que los programadores reclamen en forma explícita la memoria asignada de manera dinámica. El CLR cuenta con un subproceso recolector de basura, el cual reclama la memoria asignada en forma dinámica que ya no se necesita.

Buena práctica de programación 15.1 Establezca a null la referencia a un objeto, cuando el programa ya no necesite ese objeto. Esto permite que el recolector de basura determine lo más pronto posible si el objeto puede recolectarse como basura. Si dicho objeto tiene otras referencias hacia él, no puede recolectarse.

Escribir programas con subprocesamiento múltiple puede ser un proceso complicado. Aunque la mente humana puede realizar funciones de manera concurrente, a las personas se les dificulta saltar de un “tren de pensamiento” paralelo a otro. Para ver por qué el subprocesamiento múltiple puede ser difícil de programar y comprender, intente realizar el siguiente experimento: abra tres libros en la página 1 y trate de leerlos concurrentemente. Lea unas cuantas palabras del primer libro; después unas cuantas del segundo y por último unas cuantas del tercero, luego regrese a leer las siguientes palabras del primer libro, etc. Después de este experimento apreciará los retos que implica el subprocesamiento múltiple: alternar entre los libros, leer brevemente, recordar en dónde se quedó en cada libro, acercar el libro que está leyendo para poder verlo, hacer a un lado los libros que no está leyendo y, entre todo este caos, ¡tratar de comprender el contenido de cada libro!

15.2 Estados de los subprocesos: ciclo de vida de un subproceso En cualquier momento dado, se dice que un subproceso se encuentra en uno de varios estados de subproceso, los cuales se muestran en el diagrama de estados de UML en la figura 15.1. En esta sección hablaremos sobre estos estados y las transiciones entre los mismos. Dos clases imprescindibles para las aplicaciones con subprocesamiento múltiple son Thread y Monitor (espacio de nombres System.Threading). En esta sección hablaremos también sobre varios métodos de las clases Thread y Monitor que producen transiciones de estado. En las siguientes secciones hablaremos sobre algunos de los términos en el diagrama. Un objeto Thread empieza su ciclo de vida en el estado Unstarted (inactivo) cuando el programa crea el objeto y pasa un delegado ThreadStart al constructor del objeto. El delegado ThreadStart, que especifica la acción que realizará el subproceso durante su ciclo de vida, debe inicializarse con un método que devuelva void y no reciba argumentos. [Nota: .NET 2.0 también incluye un delegado ParameterizedThreadStart, al cual le puede pasar un método que reciba argumentos. Para obtener más información, visite los sitios msdn2. microsoft.com/en-us/library/xzehzsds (en inglés) y msdn2.microsoft.com/es-es/library/system. threading.parameterizedthreadstart(VS.80).aspx (en español).] El subproceso permanece en el estado Unstarted hasta que el programa llama al método Start de Thread, el cual coloca el subproceso en el estado Running y devuelve de inmediato el control a la parte del programa que llamó a Start. Después, el subproceso que acaba de pasar al estado Running y cualquier otro subproceso en el programa se pueden ejecutar en forma concurrente en un sistema con varios procesadores, o pueden compartir el procesador en un sistema con un solo procesador. Mientras se encuentre en el estado Running (en ejecución), tal vez el subproceso no esté en ejecución todo el tiempo. El subproceso se ejecuta en el estado Running sólo cuando el sistema operativo asigna un procesador al subproceso. Cuando un subproceso en ejecución recibe un procesador por primera vez, empieza a ejecutar el método especificado por su delegado ThreadStart. Un subproceso en ejecución entra al estado Stopped (detenido) o Aborted (abortado) cuando termina su delegado ThreadStart, lo que por lo general indica que el subproceso ha completado su tarea. Observe que un programa puede forzar a un subproceso a que entre en el estado Stopped, llamando al método Abort de Thread en el objeto Thread apropiado. El método Abort lanza una excepción ThreadAbortException en el subproceso, lo que por lo general ocasiona que el subproceso termine. Cuando un subproceso se encuentra en el estado Stopped y no hay referencias al objeto subproceso, el recolector de basura puede eliminar el objeto subproceso de la memoria. [Nota: Cuando se llama internamente al método Abort de un subproceso, éste entra en el estado

512

Capítulo 15 Subprocesamiento múltiple

JchiVgiZY

Sus pen d

Res ume

in Jo

,

e ls Pu

VZ e^g :m

! WaZ H c^ :$ ed YZ Y^h c d X^‹ ! jZ Zi^ WaZ c df e c^ ^‹ 7a k^Vg ed gVX :c Y^h eZ d d jZ aV H df i‹ :$ 7a eaZ YZ db ZX

e

HjheZcYZY

H t or Ab aZid

LV^iHaZZe?d^c

8db

a^c In , P iZg te ul kV rr se ad up A YZ t ll Wa ^c , , it VX , i^k Sl ^YV ee Y p

Start

Gjcc^c\

HideeZY

7adX`ZY

Figura 15.1 | Ciclo de vida de un subproceso. AbortRequested antes de entrar al estado Stopped. El subproceso permanece en el estado AbortRequested mientras espera recibir la excepción ThreadAbortException que está pendiente. Cuando se hace una llamada a Abort, si el subproceso se encuentra en el estado WaitSleepJoin, Suspended o Blocked, el subproceso reside en su estado actual y en el estado AbortRequested, por lo que no puede recibir la excepción ThreadAbortReception sino hasta que deje su estado actual.] Un subproceso se considera bloqueado si no puede utilizar un procesador, aun si hay uno disponible. Por ejemplo, un subproceso se bloquea cuando envía una petición de entrada/salida (E/S). El sistema operativo bloquea la ejecución del subproceso hasta que pueda completarse la petición de E/S por la que el subproceso está esperando. En ese punto, el subproceso regresa al estado Running, para poder seguir ejecutándose. Otro caso en el cual se bloquea un subproceso es en la sincronización de subprocesos (sección 15.5). Un subproceso que se sincronizará debe adquirir un bloqueo sobre un objeto, mediante una llamada al método Enter de la clase Monitor. Si no hay un bloqueo disponible, el subproceso se bloquea hasta que esté disponible el bloqueo deseado. [Nota: el estado Blocked en realidad no es un estado en .NET. Es un estado conceptual que describe a un subproceso que no se encuentra en el estado Running.] Hay tres formas en las que un subproceso en el estado Running entra al estado WaitSleepJoin. Si un subproceso encuentra código que no puede ejecutar todavía (por lo general, debido a que no se cumple cierta condición), puede llamar al método Wait de Monitor para entrar al estado WaitSleepJoin. Una vez en este estado, un subproceso regresa al estado Running cuando otro subproceso invoca al método Pulse o PulseAll de Monitor. El método Pulse cambia el siguiente subproceso en espera, de vuelta al estado Running. El método PulseAll cambia todos los subprocesos en espera, de vuelta al estado Running. Un subproceso en ejecución puede llamar al método Sleep de Thread para entrar al estado WaitSleepJoin durante un periodo de varios milisegundos, que se especifican como el argumento para Sleep. Un subproceso inactivo regresa al estado Running cuando expira su tiempo de inactividad designado. Los subprocesos inactivos no pueden usar un procesador, aunque haya uno disponible. Cualquier subproceso que entra al estado WaitSleepJoin mediante una llamada al método Wait de Monitor, o mediante una llamada al método Sleep de Thread, también sale del estado WaitSleepJoin y regresa al estado

15.3 Prioridades y programación de subprocesos

513

Running si otro subproceso en el programa hace una llamada al método Interrupt del subproceso inactivo o en espera. El método Interrupt lanza una excepción ThreadInterruptionException en el subproceso que se interrumpe. Si un subproceso no puede seguir ejecutándose (a éste le llamaremos el subproceso dependiente) a menos que termine otro subproceso, el subproceso dependiente llama al método Join del otro subproceso para “unir” los dos subprocesos. Cuando se “unen” dos subprocesos, el subproceso dependiente sale del estado WaitSleepJoin y vuelve a entrar al estado Running cuando el otro subproceso termina su ejecución (y entra al estado Stopped ). Si se llama al método Suspend de un subproceso en ejecución, éste entra al estado Suspended (suspendido). Un subproceso en el estado Suspended regresa al estado Running cuando otro subproceso en el programa invoca al método Resume del subproceso suspendido. [Nota: Internamente, cuando se hace una llamada al método Suspend, el subproceso en realidad entra al estado SuspendRequested (suspensión solicitada) antes de entrar al estado Suspended. El subproceso permanece en el estado SuspendRequested mientras espera a responder a la petición Suspend. Si el subproceso se encuentra en el estado WaitSleepJoin o si se bloquea cuando se hace la llamada a su método Suspend, el subproceso reside en su estado actual y en el estado SuspendRequest, y no puede responder a la petición Suspend hasta que salga de su estado actual.] Los métodos Suspend y Resume ahora están obsoletos y no deben usarse. En la sección 15.9 le mostraremos cómo emular estos métodos mediante la sincronización de subprocesos. Si la propiedad IsBackground de un subproceso es true, el subproceso reside en el estado Background (segundo plano) (no se muestra en la figura 15.1). Un subproceso puede residir en el estado Background y en cualquier otro estado al mismo tiempo. Para que un proceso pueda terminar, debe esperar a que todos los subprocesos en primer plano (los subprocesos que no se encuentran en el estado Background ) terminen de ejecutarse y entren en el estado Stopped (detenidos) antes de que finalice el proceso. No obstante, si los únicos subprocesos restantes en un proceso son subprocesos en segundo plano, el CLR termina cada subproceso invocando a su método Abort, y el proceso termina.

15.3 Prioridades y programación de subprocesos Todo subproceso tiene una prioridad en el rango entre ThreadPriority.Lowest y ThreadPriority.Highest. Estos valores provienen de la enumeración ThreadPriority (espacio de nombres System.Threading), la cual consiste de los valores Lowest, BelowNormal, Normal, AboveNormal y Highest. De manera predeterminada, cada subproceso tiene la prioridad Normal. El sistema operativo Windows soporta un concepto conocido como partición de tiempo, el cual permite a los subprocesos de igual prioridad compartir un procesador. Sin la partición de tiempo, cada subproceso en un conjunto de subprocesos de igual prioridad se ejecuta hasta completarse (a menos que el subproceso salga del estado Running y entre al estado WaitSleepJoin, Suspended o Blocked ), antes de que otros subprocesos de igual prioridad tengan una oportunidad para ejecutarse. Con la partición de tiempo, cada subproceso recibe un breve lapso de tiempo del procesador, conocido como cuanto, durante el cual puede ejecutarse. Cuando el cuanto expira, incluso aunque el subproceso no haya terminado de ejecutarse, el procesador se quita de ese subproceso y se asigna al siguiente de igual prioridad, si hay uno disponible. El trabajo del programador de subprocesos es mantener el subproceso de mayor prioridad en ejecución en todo momento y, si hay más de un proceso de mayor prioridad, debe asegurarse que todos los subprocesos se ejecuten durante un cuanto cada uno, en forma cíclica (round-robin). En la figura 15.2 se muestra la cola de prioridades multinivel para los subprocesos. En la figura, suponiendo que hay una computadora con un solo procesador, los subprocesos A y B se ejecutan cada uno durante un cuanto, en forma cíclica (round-robin) hasta que ambos subprocesos terminan su ejecución. Esto significa que A obtiene un cuanto de tiempo para ejecutarse. Después B obtiene un cuanto. Luego A obtiene otro cuanto; y después B, otro. Esto continúa hasta que un subproceso se completa. Entonces, el procesador dedica todo su poder al subproceso restante (a menos que se inicie otro subproceso de esa prioridad). A continuación, el subproceso C se ejecuta hasta completarse. Los subprocesos D, E y F se ejecutan cada uno durante un cuanto, en forma cíclica hasta que todos terminan de ejecutarse. Este proceso continúa hasta que todos los subprocesos terminan de ejecutarse. Observe que, dependiendo del sistema operativo, los nuevos procesos de mayor prioridad podrían aplazar (tal vez en forma indefinida) la ejecución de los subprocesos de menor prioridad. Comúnmente, a dicho aplazamiento indefinido se le conoce, en forma más colorida, como inanición.

514

Capítulo 15 Subprocesamiento múltiple

HjWegdXZhdha^hidh

Eg^dg^YVY=^\]ZhibVndg

6

Eg^dg^YVY6WdkZCdgbVa b{hYZadcdgbVa

8

7

Eg^dg^YVYCdgbVa

Eg^dg^YVY7ZadlCdgbVa bZcdhYZadcdgbVa

9

Eg^dg^YVYAdlZhib{hWV_V


?

7^i

Figura 18.1 | Jerarquía de datos. Por lo general, un registro (que puede representarse como una clase) está compuesto de varios campos relacionados. Por ejemplo, en un sistema de nóminas, un registro para un empleado específico podría incluir los siguientes campos: 1. Número de identificación del empleado. 2. Nombre. 3. Dirección. 4. Sueldo por hora. 5. Número de excepciones reclamadas. 6. Ingresos desde inicio de año a la fecha. 7. Monto de impuestos retenidos. En el ejemplo anterior, cada campo está asociado con el mismo empleado. Un archivo es un grupo de registros relacionados.1 Por lo general, el archivo de nóminas de una empresa contiene un registro para cada empleado. Un archivo de nóminas para una pequeña compañía podría contener sólo 22 registros, mientras que el de una compañía grande podría contener 100,000 registros. Es común para una compañía tener muchos archivos, algunos de ellos conteniendo millones, billones o incluso trillones de caracteres de información. 1.

Por lo general, un archivo puede contener datos arbitrarios en formatos arbitrarios. En algunos sistemas operativos, un archivo se ve simplemente como una colección de bytes, y cualquier organización de los bytes en un archivo (como organizar los datos en registros) es una vista creada por el programador de aplicaciones.

636

Capítulo 18 Archivos y flujos

Para facilitar la recuperación de registros específicos de un archivo, debe seleccionarse cuando menos un campo en cada registro como clave de registro, la cual identifica que un registro pertenece a una persona o entidad específica, y distingue a ese registro de todos los demás. Por ejemplo, en un registro de nóminas se utiliza, por lo general, el número de identificación de empleado como clave de registro. Existen muchas formas de organizar los registros en un archivo. Una organización común se conoce como archivo secuencial, en el que por lo general los registros se almacenan en orden, mediante un campo que es la clave de registro. En un archivo de nóminas, es común que los registros se coloquen en orden, en base al número de identificación del empleado. El registro del primer empleado en el archivo contiene el número de identificación de empleado más pequeño, y los registros subsiguientes contienen números cada vez mayores. La mayoría de las empresas usan muchos archivos distintos para almacenar datos. Por ejemplo, una compañía podría tener archivos de nóminas, archivos de cuentas por cobrar (listas del dinero que deben los clientes), archivos de cuentas por pagar (listas del dinero que se debe a los proveedores), archivos de inventarios (listas de información acerca de todos los artículos que maneja la empresa) y muchos otros tipos de archivos. A menudo, un grupo de archivos relacionados se almacena en una base de datos. A una colección de programas diseñados para crear y administrar bases de datos se le conoce como sistema de administración de bases de datos (DBMS). En el capítulo 20 hablaremos sobre las bases de datos.

18.3 Archivos y flujos

C# considera a cada archivo como un flujo secuencial de bytes (figura 18.2). Cada archivo termina ya sea con un marcador de fin de archivo, o en un número específico de byte que se registra en una estructura de datos administrativa, mantenida por el sistema. Cuando se abre un archivo, se crea un objeto que se asocia con un flujo. Cuando se ejecuta un programa, el entorno en tiempo de ejecución crea tres objetos flujo, a los cuales se puede acceder mediante las propiedades Console.Out, Console.In y Console.Error, respectivamente. Estos objetos facilitan la comunicación entre un programa y un archivo o dispositivo específico. Console.In se refiere al objeto flujo de entrada estándar, el cual permite a un programa introducir datos desde el teclado. Console.Out se refiere al objeto flujo de salida estándar, el cual permite a un programa imprimir datos en la pantalla. Console.Error se refiere al objeto flujo de error estándar, el cual permite a un programa imprimir mensajes de error en la pantalla. Hemos estado usando Console.Out y Console.In en nuestras aplicaciones de consola; los métodos Write y WriteLine utilizan Console.Out para las operaciones de salida, y los métodos Read y ReadLine de Console utilizan Console.In para las operaciones de entrada. Existen muchas clases de procesamiento de archivos en la FCL. El espacio de nombres System.IO incluye clases de flujos como StreamReader (para introducir texto desde un archivo), StreamWriter (para enviar texto a un archivo) y FileStream (para introducir y enviar texto desde/hacia un archivo). Estas clases de flujos heredan de las clases abstract TextReader, TextWriter y Stream, respectivamente. En realidad, las propiedades Console.In y Console.Out son de tipo TextReader y TextWriter, respectivamente. El sistema crea objetos de las clases derivadas TextReader y TextWriter para inicializar las propiedades Console.In y Console.Out de Console. La clase abstracta Stream ofrece la funcionalidad para representar flujos como bytes. Las clases FileStream, MemoryStream y BufferedStream (todas del espacio de nombres System.IO) heredan de la clase Stream. La clase FileStream puede utilizarse para escribir y leer datos hacia y desde, archivos respectivamente. La clase MemoryStream habilita la transferencia de datos directamente desde/hacia la memoria; esto es mucho más rápido que leer desde, y escribir hacia, dispositivos externos. La clase BufferedStream la técnica de uso de un búfer para transferir datos desde/hacia un flujo. El uso de búfer es una técnica para mejorar el rendimiento de E/S, en donde cada operación de salida se dirige a una región en memoria, llamada búfer, que es lo bastante grande como para almacenar los datos provenientes de muchas operaciones de salida. Después, la verdadera transferencia hacia el dispositivo de salida se realiza en una sola operación de salida física extensa, cada vez que se llena el búfer. Las

0

1

2

3

4

5

6

7

8

9

... ...

Figura 18.2 | La manera en que C# ve a un archivo de n bytes.

n-1

bVgXVYdgYZ[^cYZVgX]^kd

18.4 Las clases File y Directory

637

operaciones de salida dirigidas al búfer de salida en memoria se conocen comúnmente como operaciones lógicas de salida. Los búferes también se pueden utilizar para agilizar las operaciones de entrada, ya que al principio se leen más datos de los que se requieren y se colocan en un búfer, de manera que las operaciones subsiguientes de lectura obtengan los datos de memoria, en vez de obtenerlos de un dispositivo externo. En este capítulo usaremos clases de flujo claves para implementar programas de procesamiento de archivos que crean y manipulan archivos de acceso secuencial. En el capítulo 23, Redes: sockets basados en flujos y datagramas, usaremos las clases de flujos para implementar aplicaciones de red.

18.4 Las clases File y Directory La información se almacena en archivos, que se organizan en directorios. Las clases File y Directory permiten a los programas manipular archivos y directorios en el disco. La clase File puede determinar información acerca de los archivos y puede usarse para abrir archivos en modo de lectura o de escritura. En las secciones subsiguientes hablaremos sobre las técnicas para escribir hacia, y leer desde, archivos. La figura 18.3 lista varios métodos de la clase static File para manipular y determinar información acerca de los archivos. En la figura 18.5 demostramos varios de estos métodos. La clase Directory cuenta con herramientas para manipular directorios. La figura 18.4 lista algunos de los métodos static de la clase Directory para manipulación de directorios. La figura 18.5 demuestra también varios de estos métodos. El objeto DirectoryInfo devuelto por el método CreateDirectory contiene información acerca de un directorio. Gran parte de la información contenida en la clase DirectoryInfo también puede utilizarse a través de los métodos de la clase Directory.

Demostración de las clases File y Directory La clase PruebaArchivosForm (figura 18.5) utiliza los métodos de File y Directory para acceder a la información de archivos y directorios. Este formulario contiene el control entradaTextBox, en el que el usuario

Método static

Descripción

AppendText

Devuelve un objeto StreamWriter que adjunta texto a un archivo existente, o crea un archivo si no existe. Copia un archivo a un archivo nuevo. Crea un archivo y devuelve su objeto FileStream asociado. Crea un archivo de texto y devuelve su objeto StreamWriter asociado. Elimina el archivo especificado. Devuelve true si existe el archivo especificado, y false en caso contrario. Devuelve un objeto DateTime que representa la fecha y hora en la que se creó el archivo. Devuelve un objeto DateTime que representa la fecha y hora del último acceso al archivo. Devuelve un objeto DateTime que representa la fecha y hora de la última modificación del archivo. Mueve el archivo especificado a una ubicación especificada. Devuelve un objeto FileStream asociado con el archivo especificado y equipado con los permisos de lectura/escritura especificados. Devuelve un objeto FileStream de sólo lectura, asociado con el archivo especificado. Devuelve un objeto StreamReader asociado con el archivo especificado. Devuelve un objeto FileStream de lectura/escritura, asociado con el archivo especificado.

Copy Create CreateText Delete Exists GetCreationTime GetLastAccessTime GetLastWriteTime Move Open OpenRead OpenText OpenWrite

Figura 18.3 | Métodos static de la clase File (lista parcial).

638

Capítulo 18 Archivos y flujos

Método static

Descripción

CreateDirectory

Crea un directorio y devuelve su objeto DirectoryInfo asociado. Elimina el directorio especificado. Devuelve true si existe el directorio especificado y false en caso contrario. Devuelve un arreglo string que contiene los nombres de los subdirectorios en el directorio especificado. Devuelve un arreglo string que contiene los nombres de los archivos en el directorio especificado. Devuelve un objeto DateTime que representa la fecha y hora de creación del directorio. Devuelve un objeto DateTime que representa la fecha y hora del último acceso al directorio. Devuelve un objeto DateTime que representa la fecha y hora en que se escribieron los últimos elementos en el directorio. Mueve el directorio especificado a una ubicación especificada.

Delete Exists GetDirectories GetFiles GetCreationTime GetLastAccessTime GetLastWriteTime Move

Figura 18.4 | Métodos static de la clase Directory. introduce el nombre de un archivo o directorio. Para cada tecla que oprima el usuario mientras escribe en el control TextBox, el programa llama al manejador de eventos entradaTextBox_KeyDown (líneas 17-74). Si el usuario oprime Intro (línea 20), este método muestra el contenido del archivo o del directorio, dependiendo del texto introducido por el usuario. (Si el usuario no oprime Intro, este método regresa sin mostrar ningún contenido.) La línea 28 utiliza el método Exists de File para determinar si el texto especificado por el usuario es el nombre de algún archivo existente. Si es así, la línea 32 invoca al método private ObtenerInformacion (líneas 77-97), el cual llama a los métodos GetCreationTime (línea 86), GetLastWriteTime (línea 90) y GetLastAccessTime (línea 94) de File para acceder a la información del archivo. Cuando el método ObtenerInformacion regresa, la línea 38 crea una instancia de un objeto StreamReader para leer texto del archivo. El constructor de StreamReader recibe como argumento un objeto string que contiene el nombre del archivo que se debe abrir. La línea 39 llama al método ReadToEnd de StreamReader para leer todo el contenido del archivo como un objeto string, y después adjunta ese objeto string al contenido del control salidaTextBox.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

// Fig 18.5: PruebaArchivosForm.cs // Uso de las clases File y Directory. using System; using System.Windows.Forms; using System.IO; // muestra el contenido de los archivos y los directorios public partial class PruebaArchivosForm : Form { // constructor sin parámetros public PruebaArchivosForm() { InitializeComponent(); } // fin del constructor // se invoca cuando el usuario oprime una tecla private void entradaTextBox_KeyDown( object sender, KeyEventArgs e )

Figura 18.5 | Prueba de las clases File y Directory. (Parte 1 de 3).

18.4 Las clases File y Directory

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76

639

{ // determina si el usuario oprimió la tecla Intro if ( e.KeyCode == Keys.Enter ) { string nombreArchivo; // nombre del archivo o directorio // obtiene el archivo o directorio especificado por el usuario nombreArchivo = entradaTextBox.Text; // determina si nombreArchivo es un archivo if ( File.Exists( nombreArchivo ) ) { // obtiene la fecha de creación del archivo, // su fecha de modificación, etc. salidaTextBox.Text = ObtenerInformacion( nombreArchivo ); // muestra el contenido del archivo a través de StreamReader try { // obtiene lector y contenido del archivo StreamReader flujo = new StreamReader( nombreArchivo ); salidaTextBox.Text += flujo.ReadToEnd(); } // fin de try // maneja excepción si StreamReader no está disponible catch ( IOException ) { MessageBox.Show( "Error al leer del archivo", "Error de archivo", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin de if // determina si nombreArchivo es un directorio else if ( Directory.Exists( nombreArchivo ) ) { string[] listaDirectorios; // arreglo para los directorios // obtiene la fecha de creación del directorio, // su fecha de modificación, etc. salidaTextBox.Text = ObtenerInformacion( nombreArchivo ); // obtiene la lista de archivos/directorios del directorio especificado listaDirectorios = Directory.GetDirectories( nombreArchivo ); salidaTextBox.Text += "\r\n\r\nContenido del directorio:\r\n"; // imprime en pantalla el contenido de listaDirectorios for ( int i = 0; i < listaDirectorios.Length; i++ ) salidaTextBox.Text += listaDirectorios[ i ] + "\r\n"; } // fin de else if else { // notifica al usuario que no existe el directorio o archivo MessageBox.Show( entradaTextBox.Text + " no existe", "Error de archivo", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de else } // fin de if } // fin del método entradaTextBox_KeyDown // obtiene información sobre el archivo o directorio

Figura 18.5 | Prueba de las clases File y Directory. (Parte 2 de 3).

640

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

Capítulo 18 Archivos y flujos

private string ObtenerInformacion( string nombreArchivo ) { string informacion; // imprime mensaje indicando que existe el archivo o directorio informacion = nombreArchivo + " existe\r\n\r\n"; // imprime en pantalla la fecha y hora de creación del archivo o directorio informacion += "Creación: " + File.GetCreationTime( nombreArchivo ) + "\r\n"; // imprime en pantalla la fecha de la última modificación del archivo o directorio informacion += "Última modificación: " + File.GetLastWriteTime( nombreArchivo ) + "\r\n"; // imprime en pantalla la fecha y hora del último acceso al archivo o directorio informacion += "Último acceso: " + File.GetLastAccessTime( nombreArchivo ) + "\r\n" + "\r\n"; return informacion; } // fin del método ObtenerInformacion } // fin de la clase PruebaArchivosForm

a)

b)

c)

d)

Figura 18.5 | Prueba de las clases File y Directory. (Parte 3 de 3). Si la línea 28 determina que el texto especificado por el usuario no es un archivo, la línea 49 determina si es un directorio, usando el método Exists de Directory. Si el usuario especificó un directorio existente, la línea 55 invoca al método ObtenerInformacion para acceder a la información del directorio. La línea 58 llama al método GetDirectories de Directory para obtener un arreglo string que contiene los nombres de los subdirectorios en el directorio especificado. Las líneas 63-64 muestran a cada elemento en el arreglo string. Observe que, si la línea 49 determina que el texto especificado por el usuario no es un nombre de directorio, las líneas 69-71 notifican al usuario (a través de un cuadro MessageBox) que el nombre que introdujo no existe como archivo o directorio.

18.4 Las clases File y Directory

641

Búsqueda de directorios con expresiones regulares Ahora consideraremos otro ejemplo que utiliza las herramientas de manipulación de archivos y directorios de C#. La clase BuscarArchivoForm (figura 18.6) utiliza las clases File y Directory, y las herramientas de expresiones regulares para reportar el número de archivos de cada tipo que exista en la ruta del directorio especificado. El programa también sirve como herramienta de “limpieza”; cuando encuentra un archivo que tiene la extensión .bak (es decir, un archivo de respaldo), el programa muestra un cuadro MessageBox, pidiendo al usuario si desea eliminar el archivo y después responde en forma apropiada a la entrada del usuario. Cuando el usuario oprime Intro o hace clic en el botón Buscar directorio, el programa invoca al método buscarButton_Click (líneas 34-71), el cual busca en forma recursiva a través de la ruta del directorio que proporcionó el usuario. Si el usuario introduce texto en el control TextBox, la línea 40 llama al método Exists de Directory para determinar si ese texto es la ruta y el nombre de un directorio válido. Si no es así, las líneas 51-52 notifican al usuario sobre el error. Si el usuario especifica un directorio válido, la línea 60 pasa el nombre del directorio como argumento para el método private BuscarDirectorio (líneas 74-153). Este método localiza los archivos que coinciden con la expresión regular definida en las líneas 82-83. Esta expresión regular coincide con cualquier secuencia de números o letras, seguida de un punto y de una o más letras. Observe la subcadena de formato (?\w+) en el argumento para el constructor Regex (línea 83). Esto indica que la parte de la cadena que coincide con \w+ (es decir, la extensión de archivo que aparece después de un punto en el nombre del archivo) debe colocarse en la

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

// Fig 18.6: ArchivoBuscarForm.cs // Uso de expresiones regulares para determinar los tipos de archivos. using System; using System.Windows.Forms; using System.IO; using System.Text.RegularExpressions; using System.Collections.Specialized; // usa expresiones regulares para determinar los tipos de archivos public partial class BuscarArchivoForm : Form { string directorioActual = Directory.GetCurrentDirectory(); string[] listaDirectorios; // subdirectorios string[] arregloArchivos; // almacena las extensiones encontradas y el número encontrado NameValueCollection encontrados = new NameValueCollection(); // constructor sin parámetros public BuscarArchivoForm() { InitializeComponent(); } // fin del constructor // se invoca cuando el usuario escribe en el cuadro de texto private void entradaTextBox_KeyDown( object sender, KeyEventArgs e ) { // determina si el usuario oprimió Intro if ( e.KeyCode == Keys.Enter ) buscarButton_Click( sender, e ); } // fin del método entradaTextBox_KeyDown // se invoca cuando el usuario hace clic en el botón "Buscar directorio" private void buscarButton_Click( object sender, EventArgs e ) { // comprueba la entrada del usuario; el valor predeterminado es el directorio actual

Figura 18.6 | Expresión regular utilizada para determinar los tipos de archivos. (Parte 1 de 4).

642

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

Capítulo 18 Archivos y flujos

if ( entradaTextBox.Text != "" ) { // verifica que la entrada del usuario sea un nombre de directorio válido if ( Directory.Exists( entradaTextBox.Text ) ) { directorioActual = entradaTextBox.Text; // restablece el cuadro de texto de entrada y actualiza la pantalla directorioLabel.Text = "Directorio actual:" + "\r\n" + directorioActual; } // fin de if else { // muestra error si el usuario no especifica un directorio válido MessageBox.Show( "Directorio inválido", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de else } // fin de if // borra el contenido de los cuadros de texto entradaTextBox.Text = ""; salidaTextBox.Text = ""; BuscarDirectorio( directorioActual );

// busca en el directorio

// sintetiza e imprime los resultados en pantalla foreach ( string actual in encontrados ) { salidaTextBox.Text += "* Se encontraron " + encontrados[ actual ] + " " archivos " + actual + ".\r\n"; } // fin de foreach // borra la salida para una nueva búsqueda encontrados.Clear(); } // fin del método buscarButton_Click // busca en el directorio usando expresión regular private void BuscarDirectorio( string directorioActual ) { // para el nombre de archivo sin ruta de directorio try { string nombreArchivo = ""; // expresión regular para las extensiones que coinciden con el patrón Regex expresionRegular = new Regex( @"[a-zA-Z0-9]+\.(?\w+)" ); // almacena resultado de coincidencias de la expresión regular Match resultadoCoincidencias; string extensionArchivo; // almacena las extensiones de archivo // número de archivos con la extensión dada en el directorio int cuentaExtension; // obtiene los directorios listaDirectorios = Directory.GetDirectories( directorioActual );

Figura 18.6 | Expresión regular utilizada para determinar los tipos de archivos. (Parte 2 de 4).

18.4 Las clases File y Directory

643

96 // obtiene la lista de archivos en el directorio actual 97 arregloArchivos = Directory.GetFiles( directorioActual ); 98 99 // itera a través de la lista de archivos 100 foreach ( string miArchivo in arregloArchivos ) 101 { 102 // elimina la ruta del directorio del nombre del archivo 103 nombreArchivo = miArchivo.Substring( miArchivo.LastIndexOf( @"\" ) + 1 ); 104 105 // obtiene resultado de la búsqueda de la expresión regular 106 resultadoCoincidencias = expresionRegular.Match( nombreArchivo ); 107 108 // verifica que haya coincidencia 109 if ( resultadoCoincidencias.Success ) 110 extensionArchivo = resultadoCoincidencias.Result( "${extension}" ); 111 else 112 extensionArchivo = "[sin extensión]"; 113 114 // almacena el valor del contenedor 115 if ( encontrados[ extensionArchivo ] == null ) 116 encontrados.Add( extensionArchivo, "1" ); 117 else 118 { 119 cuentaExtension = Int32.Parse( encontrados[ extensionArchivo ] ) + 1; 120 encontrados[ extensionArchivo ] = cuentaExtension.ToString(); 121 } // fin de else 122 123 // busca archivos de respaldo( .bak ) 124 if ( extensionArchivo == "bak" ) 125 { 126 // pregunta al usuario si desea eliminar el archivo ( .bak ) 127 DialogResult resultado = 128 MessageBox.Show( "Se encontró archivo de respaldo " + 129 nombreArchivo + ". ¿Desea eliminarlo?", "Eliminar archivo de respaldo", 130 MessageBoxButtons.YesNo, MessageBoxIcon.Question ); 131 132 // elimina el archivo si el usuario hizo clic en 'sí' 133 if ( resultado == DialogResult.Yes ) 134 { 135 File.Delete( miArchivo ); 136 cuentaExtension = Int32.Parse( encontrados[ "bak" ] ) - 1; 137 encontrados[ "bak" ] = cuentaExtension.ToString(); 138 } // fin de if 139 } // fin de if 140 } // fin de foreach 141 142 // llamada recursiva para buscar archivos en subdirectorio 143 foreach ( string miDirectorio in listaDirectorios ) 144 BuscarDirectorio( miDirectorio ); 145 } // fin de try 146 // maneja excepción si los archivos tienen acceso no autorizado 147 catch ( UnauthorizedAccessException ) 148 { 149 MessageBox.Show( "Tal vez algunos archivos no puedan verse" + 150 " debido a la configuración de los permisos", "Advertencia", 151 MessageBoxButtons.OK, MessageBoxIcon.Information ); 152 } // fin de catch 153 } // fin del método BuscarDirectorio 154 } // fin de la clase FileSearchForm

Figura 18.6 | Expresión regular utilizada para determinar los tipos de archivos. (Parte 3 de 4).

644

Capítulo 18 Archivos y flujos

Figura 18.6 | Expresión regular utilizada para determinar los tipos de archivos. (Parte 4 de 4). variable de la expresión regular llamada extension. El valor de esta variable se extrae posteriormente del objeto Match llamado resultadoCoincidencias para obtener la extensión de archivo, de manera que podamos sintetizar los tipos de archivos en el directorio. La línea 94 llama al método GetDirectories de Directory para recuperar los nombres de todos los subdirectorios que pertenecen al directorio actual. La línea 97 llama al método GetFiles de Directory para almacenar en el arreglo string arregloArchivos los nombres de los archivos en el directorio actual. El ciclo foreach en las líneas 100-140 busca todos los archivos con la extensión .bak. El ciclo en las líneas 143-144 llama después al método BuscarDirectorio en forma recursiva (línea 144) para cada subdirectorio dentro del directorio actual. La línea 103 elimina la ruta del directorio, por lo que el programa puede evaluar sólo el nombre del archivo al utilizar la expresión regular. La línea 106 utiliza el método Match de Regex para relacionar la expresión regular con el nombre de archivo y después asigna el resultado al objeto Match resultadoCoincidencias. Si hubo coincidencia, la línea 110 usa el método Result de Match para asignar a extensionArchivo el valor de la variable de expresión regular extension del objeto resultadoCoincidencias. Si no hubo coincidencia, la línea 112 establece extensionArchivo a “[sin extensión]”. La clase ArchivoBuscarForm utiliza una instancia de la clase NameValueCollection (declarada en la línea 17) para almacenar cada tipo de extensión de nombre de archivo y el número de archivos para cada tipo. Un objeto NameValueCollection (espacio de nombres System.Collections.Specialized) contiene una colección de pares clave-valor de objetos string, y proporciona el método Add para agregar un par clave-valor a la colección. El indexador para esta clase puede indexar de acuerdo con el orden en que se agregaron los elementos, o de acuerdo con las claves. La línea 115 utiliza el objeto NameValueCollection encontrado para determinar si ésta es la primera ocurrencia de la extensión de archivo (la expresión devuelve null si la colección no contiene un par clave-valor para la extensión de archivo especificada en extensionArchivo). Si ésta es la primera ocurrencia, la línea 116 agrega esa extensión a encontrado como una clave con el valor 1. En caso contrario, la línea 119 incrementa el valor asociado con la extensión en encontrado para indicar otra ocurrencia de esa extensión de archivo, y la línea 120 asigna el nuevo valor al par clave-valor. La línea 124 determina si extensionArchivo es igual a “bak” (es decir, si el archivo es de respaldo). De ser así, las líneas 127-130 preguntan al usuario si desea eliminar el archivo; si la respuesta es Sí (línea 133), las líneas 135-137 eliminan el archivo y decrementan el valor para el tipo de archivo “bak” en encontrado. Las líneas 143-144 llaman al método BuscarDirectorio para cada subdirectorio. Mediante el uso de la recursividad, nos aseguramos que el programa lleve a cabo la misma lógica para buscar archivos .bak en cada subdirectorio. Después de revisar si hay archivos .bak en cada subdirectorio, el método BuscarDirectorio se completa y las líneas 64-67 muestran los resultados.

18.5 Creación de un archivo de texto de acceso secuencial

645

18.5 Creación de un archivo de texto de acceso secuencial C# no impone una estructura en los archivos. Por ende, el concepto de un “registro” no existe en los archivos de C#. Esto significa que usted debe estructurar los archivos para cumplir con los requerimientos de sus aplicaciones. En los siguientes ejemplos, usaremos texto y caracteres especiales para organizar nuestro propio concepto de un “registro”.

Clase BancoUIForm Los siguientes ejemplos demuestran el procesamiento de archivos en una aplicación de mantenimiento de cuentas bancarias. Estos programas tienen interfaces de usuario similares, por lo que creamos la clase reutilizable BancoUIForm (figura 18.7 del diseñador de Windows Forms) para encapsular la GUI de una clase base (vea la captura de pantalla de la figura 18.7). La clase BancoUIForm contiene cuatro controles Label y cuatro controles TextBox. Los métodos BorrarControlesTextBox (líneas 26-40), EstablecerValoresControlesTextBox (líneas 43-61) y ObtenerValoresControlesTextBox (líneas 64-75) borran, establecen los valores de, y obtienen los valores del texto en los controles TextBox, respectivamente.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

// Fig. 18.7: BancoUIForm.cs // Un formulario Windows Form reutilizable para los ejemplos en este capítulo. using System; using System.Windows.Forms; public partial class BancoUIForm : Form { protected int CuentaTextBox = 4; // número de controles TextBox en el formulario // las constantes de la enumeración especifican los índices de los controles TextBox public enum IndicesTextBox { CUENTA, NOMBRE, APELLIDO, SALDO } // fin de enum // constructor sin parámetros public BancoUIForm() { InitializeComponent(); } // fin del constructor // borra todos los controles TextBox public void BorrarControlesTextBox() { // itera a través de cada Control en el formulario for ( int i = 0; i < Controls.Count; i++ ) { Control miControl = Controls[ i ]; // obtiene el control // determina si el Control es TextBox if ( miControl is TextBox ) { // borra la propiedad Text ( la establece a una cadena vacía ) miControl.Text = ""; } // fin de if } // fin de for } // fin del método BorrarControlesTextBox

Figura 18.7 | Clase base para las GUIs en nuestras aplicaciones de procesamiento de archivos. (Parte 1 de 2).

646

41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76

Capítulo 18 Archivos y flujos

// establece los valores de los controles TextBox con el arreglo string valores public void EstablecerValoresControlesTextBox( string[] valores ) { // determina si el arreglo string tiene la longitud correcta if ( valores.Length != CuentaTextBox ) { // lanza excepción si no tiene la longitud correcta throw( new ArgumentException( "Debe haber " + ( CuentaTextBox + 1 ) + " objetos string en el arreglo" ) ); } // fin de if // establece el arreglo valores si el arreglo tiene la longitud correcta else { // establece el arreglo valores con los valores de los controles TextBox cuentaTextBox.Text = valores[ ( int ) IndicesTextBox.CUENTA ]; primerNombreTextBox.Text = valores[ ( int ) IndicesTextBox.NOMBRE ]; apellidoPaternoTextBox.Text = valores[ ( int ) IndicesTextBox.APELLIDO ]; saldoTextBox.Text = valores[ ( int ) IndicesTextBox.SALDO ]; } // fin de else } // fin del método EstablecerValoresControlesTextBox // devuelve los valores de los controles TextBox como un arreglo string public string[] ObtenerValoresControlesTextBox() { string[] valores = new string[ CuentaTextBox ]; // copia valores[ valores[ valores[ valores[

los campos de los controles TextBox al arreglo string ( int ) IndicesTextBox.CUENTA ] = cuentaTextBox.Text; ( int ) IndicesTextBox.NOMBRE ] = primerNombreTextBox.Text; ( int ) IndicesTextBox.APELLIDO ] = apellidoPaternoTextBox.Text; ( int ) IndicesTextBox.SALDO ] = saldoTextBox.Text;

return valores; } // fin del método ObtenerValoresControlesTextBox } // fin de la clase BankUIForm

Figura 18.7 | Clase base para las GUIs en nuestras aplicaciones de procesamiento de archivos. (Parte 2 de 2). Para reutilizar la clase BancoUIForm, debe compilar la GUI en un archivo DLL, creando un proyecto de tipo Biblioteca de controles de Windows (a nuestro proyecto lo nombramos BibliotecaBanco). Esta biblioteca se proporciona con el código para este capítulo. Tal vez tenga que cambiar las referencias a esta biblioteca en nuestros ejemplos cuando los copie a su sistema, ya que es muy probable que la biblioteca resida en una ubicación distinta en su sistema.

18.5 Creación de un archivo de texto de acceso secuencial

647

Clase Registro La figura 18.8 contiene la clase Registro que utilizan las figuras 18.9, 18.11 y 18.12 para mantener la información en cada registro que se escribe hacia, o se lee desde, un archivo. Esta clase también pertenece al archivo DLL de la BibliotecaBanco, por lo que se encuentra en el mismo proyecto que la clase BancoUIForm. La clase Registro contiene las variables de instancia private cuenta, primerNombre, apellidoPaterno y saldo (líneas 9-12), que en conjunto representan toda la información para un registro. El constructor sin parámetros (líneas 15-17) establece todos estos miembros, mediante una llamada al constructor de cuatro argumentos, con 0 para el número de cuenta, cadenas vacías ("") para el primer nombre y el apellido paterno, y 0.0M para el saldo. El constructor de cuatro argumentos (líneas 20-27) establece estos miembros a los valores de los parámetros especificados. La clase Registro también proporciona las propiedades Cuenta (líneas 30-40), PrimerNombre (líneas 43-53), ApellidoPaterno (líneas 56-66) y Saldo (líneas 69-79) para acceder al número de cuenta, primer nombre, apellido paterno y saldo de cada registro, respectivamente. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

// Fig. 18.8: Registro.cs // Clase serializable que representa a un registro de datos. using System; using System.Collections.Generic; using System.Text; public class Registro { private int cuenta; private string primerNombre; private string apellidoPaterno; private decimal saldo; // el constructor sin parámetros establece los miembros a los valores predeterminados public Registro() : this( 0, "", "", 0.0M ) { } // fin del constructor // el constructor sobrecargado, establece los miembros a los valores de los parámetros public Registro( int valorCuenta, string valorPrimerNombre, string valorApellidoPaterno, decimal valorSaldo ) { Cuenta = valorCuenta; PrimerNombre = valorPrimerNombre; ApellidoPaterno = valorApellidoPaterno; Saldo = valorSaldo; } // fin del constructor // propiedad que obtiene y establece Cuenta public int Cuenta { get { return cuenta; } // fin de get set { cuenta = value; } // fin de set } // fin de la propiedad Cuenta // propiedad que obtiene y establece PrimerNombre public string PrimerNombre

Figura 18.8 | Registro para las aplicaciones de procesamiento de archivos de acceso secuencial. (Parte 1 de 2).

648

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

Capítulo 18 Archivos y flujos

{ get { return primerNombre; } // fin de get set { primerNombre = value; } // fin de set } // fin de la propiedad PrimerNombre // propiedad que obtiene y establece ApellidoPaterno public string ApellidoPaterno { get { return apellidoPaterno; } // fin de get set { apellidoPaterno = value; } // fin de set } // fin de la propiedad ApellidoPaterno // propiedad que obtiene y establece Saldo public decimal Saldo { get { return saldo; } // fin de get set { saldo = value; } // fin de set } // fin de la propiedad Saldo } // fin de la clase Registro

Figura 18.8 | Registro para las aplicaciones de procesamiento de archivos de acceso secuencial. (Parte 2 de 2).

Uso de un flujo de caracteres para crear un archivo de salida La clase CrearArchivoForm (figura 18.9) utiliza instancias de la clase Registro para crear un archivo de acceso secuencial que podría usarse en un sistema de cuentas por cobrar (es decir, un programa que organice los datos en relación con el dinero que deben los clientes de crédito a una compañía. Para cada cliente, el programa obtiene un número de cuenta y el primer nombre, apellido paterno y saldo del cliente (es decir, el monto de dinero que el cliente debe a la compañía por los bienes y servicios que recibió previamente). Los datos obtenidos para cada cliente constituyen un registro para ese cliente. En esta aplicación, el número de cuenta se utiliza como la clave del registro; los archivos se crean y se mantienen en orden por número de cuenta. Este programa asume que el usuario introduce los registros en orden por número de cuenta. No obstante, un sistema de cuentas por cobrar completo proporcionaría una herramienta para ordenar, para que el usuario pudiera introducir los registros en cualquier orden. La clase CrearArchivoForm crea o abre un archivo (dependiendo de que exista uno) y después permite al usuario escribir registros en ese archivo. La directiva using en la línea 6 nos permite utilizar las clases del espacio de nombres BibliotecaBanco; este espacio de nombres contiene la clase BancoUIForm, y la clase CrearArchivoForm hereda de esta clase (línea 8). La GUI de la clase CrearArchivoForm agrega funcionalidad a la GUI de la clase BancoUIForm a través de los botones Guardar como, Entrar y Salir. Cuando el usuario hace clic en el botón Guardar como, el programa invoca al manejador de eventos guardarButton_Click (líneas 20-63). La línea 23 instancia un objeto de la clase SaveFileDialog (espacio de nombres

18.5 Creación de un archivo de texto de acceso secuencial

649

System.Windows.Forms). Los objetos de esta clase se utilizan para seleccionar archivos (vea la segunda pantalla en la figura 18.9). La línea 24 llama al método ShowDialog de la clase SaveFileDialog para mostrar el cuadro de diálogo. Al aparecer, un cuadro de diálogo SaveFileDialog evita que el usuario interactúe con cualquier otra ventana en el programa hasta que lo cierre, haciendo clic ya sea en Guardar o en Cancelar. Los cuadros de diálogo que se comportan de esta manera se conocen como cuadros de diálogo modales. El usuario selecciona la unidad, directorio y nombre de archivo apropiados, después hace clic en Guardar. El método ShowDialog devuelve un objeto DialogResult, el cual especifica cuál fue el botón (Guardar o Cancelar) en el que el usuario hizo clic para cerrar el cuadro de diálogo. Esto se asigna a la variable DialogResult llamada resultado (línea 24). La línea 30 evalúa si el usuario hizo clic en Cancelar mediante la comparación de este valor con DialogResult.Cancel. Si los valores son iguales, el método guardarButton_Click regresa (línea 31). En caso contrario, la línea 33 utiliza la propiedad FileName de SaveFileDialog para obtener el archivo seleccionado por el usuario.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

// Fig. 18.9: CrearArchivoForm.cs // Creación de un archivo de acceso secuencial. using System; using System.Windows.Forms; using System.IO; using BibliotecaBanco; public partial class CrearArchivoForm : BancoUIForm { private StreamWriter archivoWriter; // escribe datos en el archivo de texto private FileStream salida; // mantiene la conexión con el archivo // constructor sin parámetros public CrearArchivoForm() { InitializeComponent(); } // fin del constructor // manejador de eventos para el botón Guardar private void guardarButton_Click( object sender, EventArgs e ) { // crea un cuadro de diálogo que permite al usuario guardar el archivo SaveFileDialog selectorArchivo = new SaveFileDialog(); DialogResult resultado = selectorArchivo.ShowDialog(); string nombreArchivo; // nombre del archivo en el que se van a guardar los datos selectorArchivo.CheckFileExists = false; // permite al usuario crear el archivo // sale del manejador de eventos si el usuario hace clic en "Cancelar" if ( resultado == DialogResult.Cancel ) return; nombreArchivo = selectorArchivo.FileName; // obtiene el nombre del archivo especificado // muestra error si el usuario especificó un archivo inválido if ( nombreArchivo == "" || nombreArchivo == null ) MessageBox.Show( "Nombre de archivo inválido", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); else { // guarda el archivo mediante el objeto FileStream, si el usuario especificó un archivo válido try

Figura 18.9 | Creación de un archivo de acceso secuencial para escribir en él. (Parte 1 de 4).

650

43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101

Capítulo 18 Archivos y flujos

{ // abre el archivo con acceso de escritura salida = new FileStream( nombreArchivo, FileMode.OpenOrCreate, FileAccess.Write ); // establece el archivo para escribir los datos archivoWriter = new StreamWriter( salida ); // deshabilita el botón Guardar y habilita el botón Entrar guardarButton.Enabled = false; entrarButton.Enabled = true; } // fin de try // maneja la excepción si hay un problema al abrir el archivo catch ( IOException ) { // notifica al usuario si el archivo no existe MessageBox.Show( "Error al abrir el archivo", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin de else } // fin del método guardarButton_Click // manejador para entrarButton Click private void entrarButton_Click( object sender, EventArgs e ) { // almacena el arreglo string de valores de los controles TextBox string[] valores = ObtenerValoresControlesTextBox(); // Registro que contiene los valores de los controles TextBox a serializar Registro registro = new Registro(); // determina si el campo del control TextBox está vacío if ( valores[ ( int ) IndicesTextBox.CUENTA ] != "" ) { // almacena los valores de los controles TextBox en Registro y lo serializa try { // obtiene el valor del número de cuenta del control TextBox int numeroCuenta = Int32.Parse( valores[ ( int ) IndicesTextBox.CUENTA ] ); // determina si numeroCuenta es válido if ( numeroCuenta > 0 ) { // almacena los campos TextBox en Registro registro.Cuenta = numeroCuenta; registro.PrimerNombre = valores[ ( int ) IndicesTextBox.NOMBRE ]; registro.ApellidoPaterno = valores[ ( int ) IndicesTextBox.APELLIDO ]; registro.Saldo = Decimal.Parse( valores[ ( int ) IndicesTextBox.SALDO ] ); // escribe el Registro al archivo, los campos separados por comas archivoWriter.WriteLine( registro.Cuenta + "," + registro.PrimerNombre + "," + registro.ApellidoPaterno + "," + registro.Saldo ); } // fin de if else { // notifica al usuario si el número de cuenta es inválido

Figura 18.9 | Creación de un archivo de acceso secuencial para escribir en él. (Parte 2 de 4).

18.5 Creación de un archivo de texto de acceso secuencial

102 103 104 105 106 107 108 109 110 111 112

651

MessageBox.Show( "Número de cuenta inválido", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de else } // fin de try // notifica al usuario si ocurre un error en la serialización catch ( IOException ) { MessageBox.Show( "Error al escribir en archivo", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch // notifica al usuario si ocurre un error en relación con el formato de los parámetros catch ( FormatException ) { MessageBox.Show( "Formato inválido", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin de if

113 114 115 116 117 118 119 120 BorrarControlesTextBox(); // borra los valores de los controles TextBox 121 } // fin del método entrarButton_Click 122 123 // manejador para el evento Click de salirButton 124 private void salirButton_Click( object sender, EventArgs e ) 125 { 126 // determina si el archivo existe o no 127 if ( salida != null ) 128 { 129 try 130 { 131 archivoWriter.Close(); // cierra StreamWriter 132 salida.Close(); // cierra el archivo 133 } // fin de try 134 // notifica al usuario del error al cerrar el archivo 135 catch ( IOException ) 136 { 137 MessageBox.Show( "No se puede cerrar el archivo", "Error", 138 MessageBoxButtons.OK, MessageBoxIcon.Error ); 139 } // fin de catch 140 } // fin de if 141 142 Application.Exit(); 143 } // fin del método salirButton_Click 144 } // fin de la clase CrearArchivoForm a)

Interfaz gráfica de usuario BancoUI

Figura 18.9 | Creación de un archivo de acceso secuencial para escribir en él. (Parte 3 de 4).

652

Capítulo 18 Archivos y flujos

b) SaveFileDialog

Archivo y directorios

c)

d)

e)

f)

g)

h)

Figura 18.9 | Creación de un archivo de acceso secuencial para escribir en él. (Parte 4 de 4).

18.5 Creación de un archivo de texto de acceso secuencial

653

Puede abrir archivos para realizar la manipulación de texto mediante la creación de objetos FileStream. En este ejemplo, queremos abrir el archivo en modo de salida, por lo que las líneas 45-46 crean un objeto FileStream. El constructor de FileStream que usamos recibe tres argumentos: un objeto string que contiene la ruta y el nombre del archivo a abrir, una constante que describe cómo abrir el archivo y una constante que describe los permisos del archivo. La constante FileMode.OpenOrCreate (línea 46) indica que el objeto FileStream debe abrir el archivo si éste existe, o debe crearlo si no existe. Hay otras constantes de FileMode que describen cómo abrir archivos; introduciremos estas constantes a medida que las usemos en ejemplos. La constante FileAccess.Write indica que el programa sólo puede realizar operaciones de escritura con el objeto FileStream. Hay otras dos constantes para el tercer parámetro del constructor: FileAccess.Read para el acceso de sólo lectura y FileAccess.ReadWrite para el acceso de lectura y escritura. La línea 56 atrapa una excepción IOException si hay un problema al abrir el archivo, o al crear el objeto StreamWriter. De ser así, el programa muestra un mensaje de error (líneas 59-60). Si no ocurre una excepción, el archivo se abre en modo de escritura.

Buena práctica de programación 18.1 Al abrir archivos, use la enumeración FileAccess para controlar el acceso del usuario a estos archivos.

Error común de programación 18.1 Si no se abre un archivo antes de intentar hacer referencia al mismo en un programa, se produce un error lógico.

Una vez que el usuario escribe información en cada uno de los controles TextBox, hace clic en el botón el cual llama al manejador de eventos entrarButton_Click (líneas 66-121) para guardar los datos de los controles TextBox en el archivo especificado por el usuario. Si el usuario introdujo un número de cuenta válido (es decir, un entero mayor que cero), las líneas 88-92 almacenan los valores de los controles TextBox en un objeto de tipo Registro (creado en la línea 72). Si el usuario introdujo datos inválidos en uno de los controles TextBox (como caracteres no numéricos en el campo Saldo), el programa lanza una excepción FormatException. El bloque catch en las líneas 113-117 maneja este tipo de excepciones, notificando al usuario (a través de un cuadro de diálogo MessageBox) sobre el formato inapropiado. Si el usuario escribió datos válidos, las líneas 95-97 escriben el registro en el archivo mediante la invocación al método WriteLine del objeto StreamWriter que se creó en la línea 49. El método WriteLine escribe una secuencia de caracteres a un archivo. El objeto StreamWriter se construye con un argumento FileStream, que especifica el archivo al que el objeto StreamWriter enviará texto. La clase StreamWriter pertenece al espacio de nombres System.IO. Cuando el usuario hace clic en el botón Salir, el manejador de eventos salirButton_Click (líneas 124-143) sale de la aplicación. La línea 131 cierra el objeto StreamWriter y la línea 132 cierra el objeto FileStream; después la línea 142 termina el programa. Observe que la llamada al método Close se encuentra en un bloque try. El método Close lanza una excepción IOException si el archivo o flujo no pueden cerrarse en forma apropiada. En este caso, es importante notificar al usuario que la información en el archivo o flujo podría estar corrupta. Entrar,

Tip de rendimiento 18.1 Cierre cada archivo en forma explícita cuando el programa ya no necesite hacer referencia al mismo. Esto puede reducir el uso de recursos en los programas que siguen ejecutándose mucho tiempo después de que terminan de usar un archivo específico. La práctica de cerrar los archivos en forma explícita también mejora la legibilidad del programa.

Tip de rendimiento 18.2 Al liberar recursos en forma explícita cuando ya no se necesitan, éstos se vuelven disponibles de inmediato para que otros programas los reutilicen, con lo cual se mejora la utilización de recursos.

En la ejecución de ejemplo para el programa en la figura 18.9, introdujimos información para las cinco cuentas que se muestran en la figura 18.10. El programa no ilustra cómo se representan los registros de datos en el archivo. Para verificar que el archivo se haya creado con éxito, creamos un programa en la siguiente sección para leer y mostrar el archivo. Como éste es un archivo de texto, puede abrirlo en cualquier editor de texto para ver su contenido.

654

Capítulo 18 Archivos y flujos

Número de cuenta

Primer nombre

Apellido paterno

Saldo

100

Nancy

Brown

-25.54

200

Stacey

Duna

314.33

300

Doug

Barrer

0.00

400

Dave

Smith

258.34

500

Sam

Stone

34.98

Figura 18.10 | Datos de ejemplo para el programa de la figura 18.9.

18.6 Lectura de datos desde un archivo de texto de acceso secuencial La sección anterior demostró cómo crear un archivo para usarlo en aplicaciones con acceso secuencial. En esta sección veremos cómo leer (o recuperar) datos de un archivo en forma secuencial. La clase LeerArchivoAccesoSecuencialForm (figura 18.11) lee registros del archivo creado por el programa en la figura 18.9 y después muestra el contenido de cada registro. La mayor parte del código en este ejemplo es similar al de la figura 18.9, por lo que sólo hablaremos de los aspectos únicos de la aplicación. Cuando el usuario hace clic en el botón Abrir archivo, el programa llama al manejador de eventos abrirButton_Click (líneas 20-50). La línea 23 crea un cuadro de diálogo OpenFileDialog, y la línea 24 llama a su método ShowDialog para mostrar el cuadro de diálogo Abrir (vea la segunda captura de pantalla en la figura 18.11). El comportamiento y la GUI para los tipos de cuadros de diálogo Guardar y Abrir es idéntico, excepto que Guardar se sustituye por Abrir. Si el usuario introduce un nombre de archivo válido, las líneas 41-42 crean un 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

// Fig. 18.11: LeerArchivoAccesoSecuencialForm.cs // Lectura de un archivo de acceso secuencial. using System; using System.Windows.Forms; using System.IO; using BibliotecaBanco; public partial class LeerArchivoAccesoSecuencialForm : BancoUIForm { private FileStream entrada; // mantiene la conexión a un archivo private StreamReader archivoReader; // lee datos de un archivo de texto // constructor sin parámetros public LeerArchivoAccesoSecuencialForm() { InitializeComponent(); } // fin del constructor // se invoca cuando el usuario hace clic en el botón Abrir private void abrirButton_Click( object sender, EventArgs e ) { // crea un cuadro de diálogo que permite al usuario abrir el archivo OpenFileDialog selectorArchivo = new OpenFileDialog(); DialogResult resultado = selectorArchivo.ShowDialog(); string nombreArchivo; // nombre del archivo que contiene los datos // sale del manejador de eventos si el usuario hace clic en Cancelar if ( resultado == DialogResult.Cancel ) return;

Figura 18.11 | Lectura de archivos de acceso secuencial. (Parte 1 de 4).

18.6 Lectura de datos desde un archivo de texto de acceso secuencial

30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

655

nombreArchivo = selectorArchivo.FileName; // obtiene el nombre de archivo especificado BorrarControlesTextBox(); // muestra error si el usuario especifica un archivo inválido if ( nombreArchivo == "" || nombreArchivo == null ) MessageBox.Show( "Nombre de archivo inválido", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); else { // crea objeto FileStream para obtener acceso de lectura al archivo entrada = new FileStream( nombreArchivo, FileMode.Open, FileAccess.Read ); // establece el archivo del que se van a leer los datos archivoReader = new StreamReader( entrada ); abrirButton.Enabled = false; // deshabilita el botón Abrir archivo siguienteButton.Enabled = true; // habilita el botón Siguiente registro } // fin de else } // fin del método abrirButton_Click // se invoca cuando el usuario hace clic en el botón Siguiente private void siguienteButton_Click( object sender, EventArgs e ) { try { // obtiene el siguiente registro disponible en el archivo string registroEntrada = archivoReader.ReadLine(); string[] camposEntrada; // almacena piezas individuales de datos if ( registroEntrada != null ) { camposEntrada = registroEntrada.Split( ',' ); Registro registro = new Registro( Convert.ToInt32( camposEntrada[ 0 ] ), camposEntrada[ 1 ], camposEntrada[ 2 ], Convert.ToDecimal( camposEntrada[ 3 ] ) ); // copia los valores del arreglo string a los valores de los controles TextBox EstablecerValoresControlesTextBox( camposEntrada ); } // fin de if else { archivoReader.Close(); // cierra StreamReader entrada.Close(); // cierra FileStream si no hay registros en el archivo abrirButton.Enabled = true; // habilita el botón Abrir archivo siguienteButton.Enabled = false; // deshabilita el botón Siguiente registro BorrarControlesTextBox(); // notifica al usuario si no hay registros en el archivo MessageBox.Show( "No hay más registros en el archivo", "", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // fin de else } // fin de try catch ( IOException )

Figura 18.11 | Lectura de archivos de acceso secuencial. (Parte 2 de 4).

656

86 87 88 89 90 91

Capítulo 18 Archivos y flujos

{ MessageBox.Show( "Error al leer del archivo", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin del método siguienteButton_Click } // fin de la clase LeerArchivoAccesoSecuencialForm a)

b)

c)

d)

Figura 18.11 | Lectura de archivos de acceso secuencial. (Parte 3 de 4).

18.6 Lectura de datos desde un archivo de texto de acceso secuencial

e)

f)

g)

h)

657

Figura 18.11 | Lectura de archivos de acceso secuencial. (Parte 4 de 4).

objeto FileStream y lo asignan a la referencia entrada. Pasamos la constante FileMode.Open como el segundo argumento para el constructor FileStream, para indicar que el objeto FileStream debe abrir el archivo si existe, o debe lanzar una excepción FileNotFoundException si el archivo no existe. (En este ejemplo, el constructor de FileStream no lanza una excepción FileNotFoundException, ya que el cuadro de diálogo OpenFileDialog requiere que el usuario introduzca el nombre de un archivo que exista.) En el ejemplo anterior (figura 18.9), escribimos texto en el archivo usando un objeto FileStream con acceso de sólo escritura. En este ejemplo (figura 18.11), especificamos un acceso de sólo lectura para el archivo, pasando la constante FileAccess.Read como el tercer argumento para el constructor de FileStream. El objeto FileStream se utiliza para crear un objeto StreamReader en la línea 45. El objeto FileStream especifica el archivo desde el que el objeto StreamReader leerá texto.

Tip de prevención de errores 18.1 Abra un archivo con el modo de apertura de archivo FileAccess.Read si el contenido del archivo no debe modificarse. Esto evita que el contenido se modifique de manera accidental.

Cuando el usuario hace clic en el botón Siguiente registro, el programa llama al manejador de eventos (líneas 53-90), el cual lee el siguiente registro del archivo especificado por el usuario. (El usuario debe hacer clic en Siguiente registro después de abrir el archivo, para poder ver el primer registro.) La línea 58 llama al método ReadLine de StreamReader para leer el siguiente registro. Si ocurre un error mientras se lee el archivo, se lanza una excepción IOException (la cual se atrapa en la línea 85), y se le notifica al usuario (líneas 87-88). En caso contrario, la línea 61 determina si el método ReadLine de StreamReader devolvió null (es decir, que no hay más texto en el archivo). En caso contrario, la línea 63 usa el método Split de la clase string para separar el flujo de caracteres que se leyeron del archivo en objetos string que representen las propiedades de Registro. Después, estas propiedades se almacenan mediante la construcción de un objeto siguienteButton_Click

658

Capítulo 18 Archivos y flujos

Registro, usando las propiedades como argumentos (líneas 65-67). La línea 70 muestra los valores de Registro en los controles TextBox. Si ReadLine devuelve null, el programa cierra los objetos StreamReader (línea 74) y FileStream (línea 75), y después notifica al usuario que ya no hay más registros (líneas 81-82).

Búsqueda en un archivo de acceso secuencial Para recuperar los datos en forma secuencial de un archivo, lo común es que un programa empiece desde el principio del archivo, leyendo en forma consecutiva hasta encontrar los datos deseados. Algunas veces es necesario procesar un archivo en forma secuencial varias veces (desde el principio del archivo) durante la ejecución de un programa. Un objeto FileStream puede reposicionar su apuntador de posición de archivo (el cual contiene el número del siguiente byte a leer desde, o escribir en, el archivo) a cualquier posición en el archivo. Cuando se abre un objeto FileStream, su apuntador de posición de archivo se establece a la posición de byte 0 (es decir, el principio del archivo). Ahora presentaremos un programa que se basa en los conceptos empleados en la figura 18.11. La clase ConsultaCreditoForm (figura 18.12) es un programa de consulta de créditos, que permite a un gerente de créditos buscar y mostrar la información sobre las cuentas de los clientes que tienen saldos con crédito (es decir, los clientes que deben dinero a la compañía), saldos en cero (es decir, los clientes que no deben dinero a la compañía) y saldos con débito (es decir, los clientes que pagan dinero por los bienes y servicios recibidos previamente). Utilizamos un control RichTextBox en el programa para mostrar la información de las cuentas. Los controles RichTextBox ofrecen más funcionalidad que los controles TextBox ordinarios; por ejemplo, los controles RichTextBox ofrecen el método Find para buscar en cadenas individuales y el método LoadFile para mostrar el contenido de un archivo. Las clases RichTextBox y TextBox heredan de la clase abstract System.Windows.Forms.TextBoxBase. Elegimos un control RichTextBox en este ejemplo, ya que muestra varias líneas de texto de manera predeterminada, mientras que un control TextBox ordinario sólo muestra una. De manera alternativa, podríamos haber especificado que un objeto TextBox mostrara varias líneas de texto, estableciendo su propiedad Multiline a true.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

// Fig. 18.12: ConsultaCreditoForm.cs // Lee un archivo en forma secuencial y muestra el contenido con base en // el tipo de cuenta especificado por el usuario ( saldos con crédito, débito o en cero ). using System; using System.Windows.Forms; using System.IO; using BibliotecaBanco; public partial class ConsultaCreditoForm : Form { private FileStream entrada; // mantiene la conexión con el archivo private StreamReader archivoReader; // lee datos del archivo de texto // nombre del archivo que almacena los saldos con crédito, débito o en cero private String nombreArchivo; // constructor sin parámetros public ConsultaCreditoForm() { InitializeComponent(); } // fin del constructor // se invoca cuando el usuario hace clic en el botón Abrir archivo private void abrirButton_Click( object sender, EventArgs e ) { // crea un cuadro de diálogo que permite al usuario abrir un archivo OpenFileDialog selectorArchivo = new OpenFileDialog(); DialogResult result = selectorArchivo.ShowDialog();

Figura 18.12 | Programa de consulta de créditos. (Parte 1 de 5).

18.6 Lectura de datos desde un archivo de texto de acceso secuencial

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

659

// sale del manejador de eventos si el usuario hace clic en Cancelar if ( result == DialogResult.Cancel ) return; nombreArchivo = selectorArchivo.FileName; // obtiene el nombre de archivo del usuario // muestra error si el usuario especificó un archivo inválido if ( nombreArchivo == "" || nombreArchivo == null ) MessageBox.Show( "Nombre de archivo inválido", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); else { // crea objeto FileStream para obtener acceso de lectura al archivo entrada = new FileStream( nombreArchivo, FileMode.Open, FileAccess.Read ); // establece el archivo del que se van a leer los archivos archivoReader = new StreamReader( entrada ); // habilita todos los botones de la GUI, excepto Abrir archivo abrirButton.Enabled = false; creditoButton.Enabled = true; debitoButton.Enabled = true; ceroButton.Enabled = true; } // fin de else } // fin del método abrirButton_Click // se invoca cuando el usuario hace clic en el botón de saldos con crédito, // saldos con débito o saldos en cero private void obtenerSaldos_Click( object sender, System.EventArgs e ) { // convierte el emisor explícitamente a un objeto de tipo Button Button emisorButton = ( Button )sender; // obtiene el texto del botón en el que se hizo clic, y que almacena el tipo de la cuenta string tipoCuenta = emisorButton.Text; // lee y muestra la información del archivo try { // regresa al principio del archivo entrada.Seek( 0, SeekOrigin.Begin ); mostrarTextBox.Text = "Las cuentas son:\r\n"; // recorre el archivo hasta llegar a su fin while ( true ) { string[] camposEntrada; // almacena piezas de datos individuales Registro registro; // almacena cada Registro a medida que se lee el archivo decimal saldo; // almacena el saldo de cada Registro // obtiene el siguiente Registro disponible en el archivo string registroEntrada = archivoReader.ReadLine(); // cuando está al final del archivo, sale del método

Figura 18.12 | Programa de consulta de créditos. (Parte 2 de 5).

660

86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144

Capítulo 18 Archivos y flujos

if ( registroEntrada == null ) return; camposEntrada = registroEntrada.Split( ',' ); // analiza la entrada // crea el Registro a partir de entrada registro = new Registro( Convert.ToInt32( camposEntrada[ 0 ] ), camposEntrada[ 1 ], camposEntrada[ 2 ], Convert.ToDecimal( camposEntrada[ 3 ] ) ); // almacena el último campo del registro en saldo saldo = registro.Saldo; // determina si va a mostrar el saldo o no if ( DebeMostrar( saldo, tipoCuenta ) ) { // muestra el registro string salida = registro.Cuenta + "\t" + registro.PrimerNombre + "\t" + registro.ApellidoPaterno + "\t"; // muestra el saldo con el formato monetario correcto salida += String.Format( "{0:F}", saldo ) + "\r\n"; mostrarTextBox.Text += salida; // copia la salida a la pantalla } // fin de if } // fin de while } // fin de try // maneja la excepción cuando no puede leerse el archivo catch ( IOException ) { MessageBox.Show( "No se puede leer el archivo", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin del método obtenerSaldos_Click // determina si se va a mostrar el registro dado private bool DebeMostrar( decimal saldo, string tipoCuenta ) { if ( saldo > 0 ) { // muestra los saldos con crédito if ( tipoCuenta == "Saldos con crédito" ) return true; } // fin de if else if ( saldo < 0 ) { // mostrar los saldos con débito if ( tipoCuenta == "Saldos con débito" ) return true; } // fin de else if else // saldo == 0 { // muestra los saldos en cero if ( tipoCuenta == "Saldos en cero" ) return true; } // fin de else return false; } // fin del método DebeMostrar

Figura 18.12 | Programa de consulta de créditos. (Parte 3 de 5).

18.6 Lectura de datos desde un archivo de texto de acceso secuencial

145 146 // se invoca cuando el usuario hace clic en el botón Terminar 147 private void terminarButton_Click( object sender, EventArgs e ) 148 { 149 // determina si existe el archivo o no 150 if ( entrada != null ) 151 { 152 // cierra el archivo y el objeto StreamReader 153 try 154 { 155 entrada.Close(); 156 archivoReader.Close(); 157 } // fin de try 158 // maneja la excepción si el objeto FileStream no existe 159 catch( IOException ) 160 { 161 // notifica al usuario del error al cerrar el archivo 162 MessageBox.Show( "No se puede cerrar el archivo", "Error", 163 MessageBoxButtons.OK, MessageBoxIcon.Error ); 164 } // fin de catch 165 } // fin de if 166 167 Application.Exit(); 168 } // fin del método terminarButton_Click 169 } // fin de la clase ConsultaCreditoForm a)

b)

Figura 18.12 | Programa de consulta de créditos. (Parte 4 de 5).

661

662

Capítulo 18 Archivos y flujos

c)

d)

e)

Figura 18.12 | Programa de consulta de créditos. (Parte 5 de 5). El programa muestra botones que permiten a un gerente de crédito obtener la información sobre los créditos. El botón Abrir archivo abre un archivo para recopilar datos. El botón Saldos con crédito muestra una lista de cuentas que tienen saldos con crédito, el botón Saldos con débito muestra una lista de cuentas que tienen saldos con débito y el botón Saldos en cero muestra una lista de las cuentas que tienen saldos en cero. El botón Terminar sale de la aplicación. Cuando el usuario hace clic en el botón Abrir archivo, el programa llama al manejador de eventos abrirButton_Click (líneas 24-55). La línea 27 crea un cuadro de diálogo OpenFileDialog y la línea 28 llama a su método ShowDialog para mostrar el cuadro de diálogo Abrir, en el cual el usuario selecciona el archivo a abrir. Las líneas 43-44 crean un objeto FileStream con acceso de sólo lectura para el archivo, y lo asignan a la referencia entrada. La línea 47 crea un objeto StreamReader que utilizamos para leer texto del objeto FileStream. Cuando el usuario hace clic en Saldos con crédito, Saldos con débito o Saldos en cero, el programa invoca al método obtenerSaldos_Click (líneas 59-119). La línea 62 convierte el parámetro sender, el cual es una referencia object al control que generó el evento, en un objeto Button. La línea 65 extrae el texto del objeto Button, que el programa utiliza para determinar el tipo de cuentas a mostrar. La línea 71 usa el método Seek de FileStream para restablecer el apuntador de posición del archivo de vuelta al principio del archivo. El método Seek de FileStream nos permite restablecer el apuntador de posición del archivo especificando el número de bytes que debe desplazarse a partir del principio del archivo, del final o de la posición actual. La parte del archivo a partir de la cual queremos desplazarnos se elige mediante el uso de constantes de la enumeración SeekOrigin. En este caso, nuestro flujo se desplaza 0 bytes a partir del principio del archivo (SeekOrigin.Begin). Las líneas 76-111 definen un ciclo while que utiliza el método private DebeMostrar (líneas 122-144) para determinar si se debe mostrar cada registro en el archivo. El ciclo while obtiene cada registro llamando repetidas veces al método ReadLine de StreamReader (línea 83), y dividiendo el texto en símbolos (tokens) que se utilizan para inicializar el objeto registro (líneas 89-94). La línea 86 determina si

18.8 Creación de un archivo de acceso secuencial mediante el uso de la serialización de objetos

663

el apuntador de posición del archivo ha llegado al final del archivo. De ser así, el programa regresa del método obtenerSaldos_Click (línea 87).

18.7 Serialización La sección 18.5 demostró cómo escribir los campos individuales de un objeto Registro en un archivo de texto, y la sección 18.6 demostró cómo leer esos campos de un archivo y colocar sus valores en un objeto Registro en memoria. En los ejemplos, Registro se utilizó para juntar la información para un solo registro. Cuando las variables de instancia para un Registro se enviaron a un archivo en disco, se perdió cierta información como el tipo de cada valor. Por ejemplo, si se lee el valor "3" de un archivo, no hay forma de saber si el valor proviene de un int, de un string o de un decimal. Sólo tenemos datos en el disco, no información sobre el tipo. Si el programa que va a leer estos datos “sabe” a qué tipo de objeto corresponden los datos, entonces pueden leerse directamente en objetos de ese tipo. Por ejemplo, en la figura 18.9 sabemos que introduciremos un int (el número de cuenta), seguido de dos objetos string (el primer nombre y el apellido paterno) y un decimal (el saldo). También sabemos que estos valores están separados por comas, y sólo hay un registro en cada línea. Por lo tanto, podemos analizar los objetos string y convertir el número de cuenta en un int y el balance en un decimal. Algunas veces sería más sencillo leer o escribir objetos completos. C# cuenta con un mecanismo así, al cual se le conoce como serialización de objetos. Un objeto serializado se representa como una secuencia de bytes que incluye los datos de ese objeto, así como información acerca del tipo del objeto y los tipos de los datos almacenados en ese objeto. Una vez que se escribe un objeto serializado en un archivo, puede leerse desde ese archivo y deserializarse; esto es, la información sobre el tipo y los bytes que representan al objeto y sus datos pueden usarse para recrear al objeto en la memoria. La clase BinaryFormatter (espacio de nombres System.Runtime.Serialization.Formatters.Binary) permite escribir o leer objetos completos en/desde un flujo. El método Serialize de BinaryFormatter escribe en un archivo la representación de un objeto. El método Deserialize de BinaryFormatter lee esta representación de un archivo y reconstruye el objeto original. Ambos métodos lanzan una excepción SerializationException si ocurre un error durante la serialización o la deserialización. Ambos métodos requieren un objeto Stream (por ejemplo, FileStream) como parámetro, para que el objeto BinaryFormatter pueda acceder al flujo correcto. Como veremos en el capítulo 23, Redes: sockets basados en flujos y datagramas, la serialización puede usarse para transmitir objetos entre aplicaciones en una red. En las secciones 18.8-18.9 creamos y manipulamos archivos de acceso secuencial, usando la serialización de objetos, que se realiza con flujos basados en bytes, de manera que los archivos secuenciales que se creen y se manipulen serán archivos binarios. Los humanos no podemos leer archivos binarios. Por esta razón, escribimos una aplicación separada que lee y muestra en pantalla objetos serializados.

18.8 Creación de un archivo de acceso secuencial mediante el uso de la serialización de objetos Empezaremos por crear y escribir objetos serializados en un archivo de acceso secuencial. En esta sección reutilizaremos la mayor parte del código de la sección 18.5, por lo que nos concentraremos sólo en las nuevas características.

Definición de la clase RegistroSerializable Comenzaremos por modificar nuestra clase Registro (figura 18.8), de manera que los objetos de esta clase puedan serializarse. La clase RegistroSerializable (figura 18.13) está marcada con el atributo [Serializable] (línea 5), el cual indica al CLR que los objetos de la clase Registro pueden serializarse. Las clases para los objetos que queremos escribir o leer en/desde un flujo deben incluir este atributo en sus declaraciones, o deben implementar la interfaz ISerializable. La clase RegistroSerializable contiene los miembros de datos private cuenta, primerNombre, apellidoPaterno y saldo. Esta clase también cuenta con propiedades public para acceder a los campos private. En una clase marcada con el atributo [Serializable] o que implementa la interfaz ISerializable, debemos asegurarnos que toda variable de instancia de la clase también sea serializable. Todas las variables de tipos simples y los objetos string son serializables. Para las variables de tipos por referencia, hay que comprobar la declaración de la clase (y posiblemente sus clases base) para asegurarnos que el tipo sea serializable. Los objetos tipo arreglo son serializables de manera predeterminada. No obstante, si el arreglo contiene referencias a otros objetos, esos objetos podrían o no ser serializables.

664

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Capítulo 18 Archivos y flujos

// Fig. 18.13: RegistroSerializable.cs // Clase serializable que representa a un registro de datos. using System; [ Serializable ] public class RegistroSerializable { private int cuenta; private string primerNombre; private string apellidoPaterno; private decimal saldo; // el constructor predeterminado establece los miembros a los valores predeterminados public RegistroSerializable() : this( 0, "", "", 0.0M ) { } // fin del constructor // el constructor sobrecargado establece los miembros a los valores de los parámetros public RegistroSerializable( int valorCuenta, string valorPrimerNombre, string valorApellidoPaterno, decimal valorSaldo ) { Cuenta = valorCuenta; PrimerNombre = valorPrimerNombre; ApellidoPaterno = valorApellidoPaterno; Saldo = valorSaldo; } // fin del constructor // propiedad que obtiene y establece Cuenta public int Cuenta { get { return cuenta; } // fin de get set { cuenta = value; } // fin de set } // fin de la propiedad Cuenta // propiedad que obtiene y establece PrimerNombre public string PrimerNombre { get { return primerNombre; } // fin de get set { primerNombre = value; } // fin de set } // fin de la propiedad PrimerNombre // propiedad que obtiene y establece ApellidoPaterno public string ApellidoPaterno { get {

Figura 18.13 | La clase RegistroSerializable para objetos serializables. (Parte 1 de 2).

18.8 Creación de un archivo de acceso secuencial mediante el uso de la serialización de objetos

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

665

return apellidoPaterno; } // fin de get set { apellidoPaterno = value; } // fin de set } // fin de la propiedad ApellidoPaterno // propiedad que obtiene y establece Saldo public decimal Saldo { get { return saldo; } // fin de get set { saldo = value; } // fin de set } // fin de la propiedad Saldo } // fin de la clase RegistroSerializable

Figura 18.13 | La clase RegistroSerializable para objetos serializables. (Parte 2 de 2).

Uso de un flujo de serialización para crear un archivo de salida Ahora crearemos un archivo de acceso secuencial mediante la serialización (figura 18.14). La línea 13 crea un objeto BinaryFormatter para escribir objetos serializados. Las líneas 48-49 abren el objeto FileStream, en el que este programa escribe los objetos serializados. El argumento string que se pasa al constructor de FileStream representa el nombre y la ruta del archivo que se abrirá. Este argumento especifica el archivo en el cual se van a escribir los objetos serializados.

Error común de programación 18.2 Es un error lógico abrir un archivo existente en modo de salida, cuando el usuario desea preservar el archivo, ya que se perderá el contenido original del archivo.

Este programa asume que los datos se introducen en forma correcta y en el orden numérico apropiado de los registros. El manejador de eventos introducirButton_Click (líneas 66-119) realiza la operación de escritura. La línea 72 crea un objeto RegistroSerializable, al cual se le asignan los valores en las líneas 88-92. La línea 95 llama al método Serialize para escribir el objeto RegistroSerializable en el archivo de salida. El método Serialize recibe el objeto FileStream como el primer argumento, de manera que el objeto BinaryFormatter pueda escribir su segundo argumento en el archivo correcto. Observe que sólo se requiere una instrucción para escribir todo el objeto. En la ejecución de ejemplo para el programa en la figura 18.14, introducimos información para cinco cuentas; la misma información que se muestra en la figura 18.10. El programa no muestra cómo aparecen en realidad los registros de datos en el archivo. Recuerde que ahora estamos usando archivos binarios, que no pueden leer los humanos. Para verificar que el archivo se haya creado con éxito, la siguiente sección presenta un programa para leer el contenido del archivo. 1 2 3 4 5

// Fig 18.14: CrearArchivoForm.cs // Creación de un archivo de acceso secuencial mediante la serialización. using System; using System.Windows.Forms; using System.IO;

Figura 18.14 | Archivo secuencial creado mediante la serialización. (Parte 1 de 5).

666

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62

Capítulo 18 Archivos y flujos

using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization; using BibliotecaBanco; public partial class CrearArchivoForm : BancoUIForm { // objeto para serializar Registros en formato binario private BinaryFormatter aplicadorFormato = new BinaryFormatter(); private FileStream salida; // flujo para escribir en un archivo // constructor sin parámetros public CrearArchivoForm() { InitializeComponent(); } // fin del constructor // manejador para guardarButton_Click private void guardarButton_Click( object sender, EventArgs e ) { // crea un cuadro de diálogo que permite al usuario guardar el archivo SaveFileDialog selectorArchivo = new SaveFileDialog(); DialogResult resultado = selectorArchivo.ShowDialog(); string nombreArchivo; // nombre del archivo para guardar los datos selectorArchivo.CheckFileExists = false; // permite al usuario crear el archivo // sale del manejador de eventos si el usuario hace clic en "Cancelar" if ( resultado == DialogResult.Cancel ) return; nombreArchivo = selectorArchivo.FileName; // obtiene el nombre de archivo especificado // muestra error si el usuario especificó un archivo inválido if ( nombreArchivo == "" || nombreArchivo == null ) MessageBox.Show( "Nombre de archivo inválido", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); else { // guarda el archivo a través del objeto FileStream si el usuario especificó un archivo válido try { // abre el archivo con acceso de escritura salida = new FileStream( nombreArchivo, FileMode.OpenOrCreate, FileAccess.Write ); // deshabilita el botón Guardar y habilita el botón Introducir guardarButton.Enabled = false; introducirButton.Enabled = true; } // fin de try // maneja la excepción si hay un problema al abrir el archivo catch ( IOException ) { // notifica al usuario si el archivo no existe MessageBox.Show( "Error al abrir el archivo", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin de else

Figura 18.14 | Archivo secuencial creado mediante la serialización. (Parte 2 de 5).

18.8 Creación de un archivo de acceso secuencial mediante el uso de la serialización de objetos

63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120

667

} // fin del método guardarButton_Click // manejador para el evento Click de introducirButton private void introducirButton_Click( object sender, EventArgs e ) { // almacena los valores de los controles TextBox en el arreglo string valores string[] valores = ObtenerValoresControlesTextBox(); // Registro que contiene los valores de los controles TextBox a serializar RegistroSerializable registro = new RegistroSerializable(); // determina si el campo del control TextBox de la cuenta está vacío if ( valores[ ( int ) IndicesTextBox.CUENTA ] != "" ) { // almacena los valores de los controles TextBox en Registro y lo serializa try { // obtiene el valor del número de cuenta del control TextBox int numeroCuenta = Int32.Parse( valores[ ( int ) IndicesTextBox.CUENTA ] ); // determina si el numeroCuenta es válido if ( numeroCuenta > 0 ) { // almacena los campos de los controles TextBox en Registro registro.Cuenta = numeroCuenta; registro.PrimerNombre = valores[ ( int ) IndicesTextBox.NOMBRE ]; registro.ApellidoPaterno = valores[ ( int ) IndicesTextBox.APELLIDO ]; registro.Saldo = Decimal.Parse( valores[ ( int ) IndicesTextBox.SALDO ] ); // escribe Record al objeto FileStream ( serializa el objeto ) aplicadorFormato.Serialize( salida, registro ); } // fin de if else { // notifica al usuario si el número de cuenta es inválido MessageBox.Show( "Número de cuenta inválido", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de else } // fin de try // notifica al usuario si ocurre un error en la serialización catch ( SerializationException ) { MessageBox.Show( "Error al escribir en el archivo", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch // notifica al usuario si ocurre un error en relación con el formato de los parámetros catch ( FormatException ) { MessageBox.Show( "Formato inválido", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin de if BorrarControlesTextBox(); // borra los valores de los controles TextBox } // fin del método introducirButton_Click

Figura 18.14 | Archivo secuencial creado mediante la serialización. (Parte 3 de 5).

668

Capítulo 18 Archivos y flujos

121 // manejador para el evento Click de salirButton 122 private void salirButton_Click( object sender, EventArgs e ) 123 { 124 // determina si existe el archivo 125 if ( salida != null ) 126 { 127 // cierra el archivo 128 try 129 { 130 salida.Close(); 131 } // fin de try 132 // notifica al usuario del error al cerrar el archivo 133 catch ( IOException ) 134 { 135 MessageBox.Show( "No se pude cerrar el archivo", "Error", 136 MessageBoxButtons.OK, MessageBoxIcon.Error ); 137 } // fin de catch 138 } // fin de if 139 140 Application.Exit(); 141 } // fin del método salirButton_Click 142 } // fin de la clase CrearArchivoForm a)

Interfaz gráfica de usuario BancoUI

b) SaveFileDialog

Archivo y directorios

Figura 18.14 | Archivo secuencial creado mediante la serialización. (Parte 4 de 5).

18.9 Lectura y deserialización de datos de un archivo de texto de acceso secuencial

c)

d)

e)

f)

669

g)

Figura 18.14 | Archivo secuencial creado mediante la serialización. (Parte 5 de 5).

18.9 Lectura y deserialización de datos de un archivo de texto de acceso secuencial La sección anterior mostró cómo crear un archivo de acceso secuencial usando la serialización de objetos. En esta sección veremos cómo leer objetos serializados de un archivo en forma secuencial. La figura 18.15 lee y muestra el contenido del archivo creado por el programa en la figura 18.14. La línea 13 crea el objeto BinaryFormatter que se utilizará para leer objetos. El programa abre el archivo en modo de 1 2 3 4

// Fig. 18.15: LeerArchivoAccesoSecuencialForm.cs // Lectura de un archivo de acceso secuencial mediante la serialización. using System; using System.Windows.Forms;

Figura 18.15 | Lectura de un archivo secuencial mediante la deserialización. (Parte 1 de 4).

670

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

Capítulo 18 Archivos y flujos

using using using using

System.IO; System.Runtime.Serialization.Formatters.Binary; System.Runtime.Serialization; BibliotecaBanco;

public partial class LeerArchivoAccesoSecuencialForm : BancoUIForm { // objeto para deserializar el Registro en formato binario private BinaryFormatter lector = new BinaryFormatter(); private FileStream entrada; // flujo para leer de un archivo // constructor sin parámetros public LeerArchivoAccesoSecuencialForm() { InitializeComponent(); } // fin del constructor // se invoca cuando el usuario hace clic en el botón Abrir private void abrirButton_Click( object sender, EventArgs e ) { // crea un cuadro de diálogo que permite al usuario abrir un archivo OpenFileDialog selectorArchivo = new OpenFileDialog(); DialogResult resultado = selectorArchivo.ShowDialog(); string nombreArchivo; // nombre del archivo que contiene los datos // sale del manejador de eventos si el usuario hace clic en Cancelar if ( resultado == DialogResult.Cancel ) return; nombreArchivo = selectorArchivo.FileName; // obtiene el nombre de archivo especificado BorrarControlesTextBox(); // muestra error si el usuario especificó un archivo inválido if ( nombreArchivo == "" || nombreArchivo == null ) MessageBox.Show( "Nombre de archivo inválido", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); else { // crea objeto FileStream para obtener acceso de lectura al archivo entrada = new FileStream( nombreArchivo, FileMode.Open, FileAccess.Read ); abrirButton.Enabled = false; // deshabilita el botón Abrir archivo siguienteButton.Enabled = true; // habilita el botón Siguiente registro } // fin de else } // fin del método abrirButton_Click // se invoca cuando el usuario hace clic en el botón Siguiente private void siguienteButton_Click( object sender, EventArgs e ) { // deserializa el Registro y almacena los datos en controles TextBox try { // obtiene el siguiente RegistroSerializable disponible en el archivo RegistroSerializable registro = ( RegistroSerializable ) lector.Deserialize( entrada ); // almacena los valores del Registro en un arreglo string temporal string[] valores = new string[] {

Figura 18.15 | Lectura de un archivo secuencial mediante la deserialización. (Parte 2 de 4).

18.9 Lectura y deserialización de datos de un archivo de texto de acceso secuencial

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86

671

registro.Cuenta.ToString(), registro.PrimerNombre.ToString(), registro.ApellidoPaterno.ToString(), registro.Saldo.ToString() }; // copia los valores del arreglo string a los controles TextBox EstablecerValoresControlesTextBox( valores ); } // fin de try // maneja la excepción cuando no hay registros en el archivo catch( SerializationException ) { entrada.Close(); // cierra objeto FileStream si no hay registros en el archivo abrirButton.Enabled = true; // habilita el botón Abrir archivo siguienteButton.Enabled = false; // deshabilita el botón Siguiente registro BorrarControlesTextBox(); // notifica al usuario si no hay registros en el archivo MessageBox.Show( "No hay más registros en el archivo", "", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // fin de catch } // fin del método siguienteButton_Click } // fin de la clase LeerArchivoAccesoSecuencialForm

Figura 18.15 | Lectura de un archivo secuencial mediante la deserialización. (Parte 3 de 4).

672

Capítulo 18 Archivos y flujos

Figura 18.15 | Lectura de un archivo secuencial mediante la deserialización. (Parte 4 de 4). entrada mediante la creación de un objeto FileStream (líneas 44-45). El nombre del archivo a abrir se especifica como el primer argumento para el constructor de FileStream. El programa lee objetos de un archivo en el manejador de eventos siguienteButton_Click (líneas 53-85). Utilizamos el método Deserialize (del objeto BinaryFormatter creado en la línea 13) para leer los datos (líneas 59-60). Observe que convertimos el resultado de Deserialize al tipo RegistroSerializable (línea 60); esta conversión es necesaria, ya que Deserialize devuelve una referencia de tipo object y necesitamos acceder a propiedades que pertenecen a la clase RegistroSerializable. Si ocurre un error durante la deserialización, se lanza una excepción SerializationException, y el se cierra el objeto FileStream (línea 75).

18.10 Conclusión

673

18.10 Conclusión En este capítulo vimos cómo utilizar el procesamiento de archivos para manipular datos persistentes. Aprendió que los datos se almacenan en las computadoras como 0s y 1s, y que las combinaciones de estos valores se utilizan para formar bytes, campos, registros y, en un momento dado, archivos. Vimos las generalidades acerca de las diferencias entre los flujos basados en carácter y los flujos basados en byte, así como varias clases de procesamiento de archivos del espacio de nombres System.IO. Utilizó la clase File para manipular archivos y la clase Directory para manipular directorios. Después aprendió a utilizar el procesamiento de archivos de acceso secuencial para manipular registros en archivos de texto. Más adelante hablamos sobre las diferencias entre el procesamiento de archivos de texto y la serialización de objetos, y utilizamos la serialización para almacenar objetos completos en, y recuperar objetos completos de, los archivos. En el siguiente capítulo presentamos el Lenguaje de marcado extensible (XML): una tecnología para describir datos con amplio soporte. Mediante el uso de XML, podemos describir cualquier tipo de datos, como fórmulas matemáticas, música y reportes financieros. Le demostraremos cómo describir datos con XML y cómo escribir programas que puedan procesar los datos codificados en XML.

19

Lenguaje de marcado extensible (XML)

Conociendo a los árboles, comprendo el significado de la paciencia. Conociendo al césped, puedo apreciar la persistencia.

OBJETIVOS

—Hal Borland

En este capítulo aprenderá lo siguiente:

Al igual que todo lo metafísico, la armonía entre el pensamiento y la realidad se encuentra en la gramática del lenguaje.

Q

Marcar datos mediante el uso de XML.

Q

Cómo los espacios de nombres de XML ayudan a proporcionar nombres únicos para los elementos y atributos de XML.

Q

Crear DTDs y esquemas para especificar y validar la estructura de un documento XML.

Q

Crear y utilizar hojas de estilo XSL simples para representar los datos de documentos XML.

Q

Recuperar y modificar los datos XML mediante la programación, usando las clases del .NET Framework.

Q

Validar los documentos XML contra los esquemas, usando la clase XmlReader.

Q

Transformar documentos XML en XHTML, mediante el uso de la clase XslCompiledTransform.

—Ludwig Wittgenstein

Jugué con una idea, y crecí voluntarioso; la sacudí en el aire; la transformé, dejé que escapara y la recobré; la hice iridiscente con la suposición, y dejé que volara con paradoja. —Oscar Wilde

Pla n g e ne r a l

676

19.1 19.2 19.3 19.4 19.5 19.6 19.7 19.8 19.9 19.10 19.11 19.12

Capítulo 19 Lenguaje de marcado extensible (XML)

Introducción Fundamentos de XML Estructuración de datos Espacios de nombres de XML Definiciones de tipo de documento (DTDs) Documentos de esquemas XML del W3C (Opcional) Lenguaje de hojas de estilos extensible y transformaciones XSL (Opcional) Modelo de objetos de documento (DOM) (Opcional) Validación de esquemas con la clase XmlReader (Opcional) XSLT con la clase XslCompiledTransform Conclusión Recursos Web

19.1 Introducción

El Lenguaje de marcado extensible (XML) fue desarrollado en 1996 por el Grupo de trabajo de XML del Consorcio World Wide Web (W3C ). XML es una tecnología abierta (es decir, tecnología no propietaria) con amplio soporte para describir datos, y se ha convertido en el formato estándar para el intercambio de datos entre aplicaciones a través de Internet. El .NET Framework hace un uso extenso de XML. La Biblioteca de clases del .NET Framework proporciona un conjunto extenso de clases relacionadas con el XML, y gran parte de la implementación interna de Visual Studio también emplea XML. Las secciones 19.2-19.6 introducen el XML y las tecnologías relacionadas con XML: los espacios de nombres de XML para proporcionar nombres únicos para los elementos y atributos de XML, y las Definiciones de tipo de documento (DTDs) y los Esquemas de XML para validar documentos de XML. Estas secciones son obligatorias para soportar el uso de XML en los capítulos 20 al 22. Las secciones 19.7-19.10 presentan tecnologías de XML adicionales y clases clave del .NET Framework para crear y manipular documentos XML mediante la programación; este material es opcional, pero lo recomendamos para los lectores que planeen emplear XML en sus propias aplicaciones en C#.

19.2 Fundamentos de XML

XML permite a los autores de documentos crear marcado (es decir, una notación basada en texto para describir datos) para casi cualquier tipo de información. Esto permite a los autores de documentos crear lenguajes de marcado completamente nuevos para describir cualquier tipo de datos, como las fórmulas matemáticas, las instrucciones de configuración de software, las estructuras moleculares químicas, la música, las noticias, las recetas y los reportes financieros. XML describe los datos en una forma que tanto los seres humanos como las computadoras pueden comprender. La figura 19.1 es un documento XML simple que describe la información para un jugador de béisbol. Nos enfocaremos en las líneas 5-11 para presentar la sintaxis básica de XML. En la sección 19.3 aprenderá acerca de los demás elementos de este documento. Los documentos de XML contienen texto que representa contenido (es decir, datos), por ejemplo, John (línea 6 de la figura 19.1), y elementos que especifican la estructura del documento, como primerNombre (línea 6 de la figura 19.1). Los documentos de XML delimitan los elementos con etiquetas iniciales y etiquetas finales. Una etiqueta inicial consiste en el nombre del elemento entre un signo menor que, (por ejemplo, y en las líneas 5 y 6, respectivamente). Una etiqueta final consiste en el nombre del elemento precedido por una barra diagonal (/) entre corchetes (por ejemplo, y en las líneas 6 y 11, respectivamente). Las etiquetas inicial y final de un elemento encierran texto que representa una pieza de datos (por ejemplo, el primerNombre del jugador, John, en la línea 6, que va encerrado por la etiqueta inicial y la etiqueta final ). Todo documento de XML debe

19.2 Fundamentos de XML

1 2 3 4 5 6 7 8 9 10 11

677



John Doe 0.375

Figura 19.1 | XML que describe la información de un jugador de béisbol.

tener exactamente un elemento raíz que contenga a todos los demás elementos. En la figura 19.1, jugador (líneas 5-11) es el elemento raíz. Algunos lenguajes de marcado basados en XML incluyen XHTML (Lenguaje de marcado de hipertexto extensible: el sustituto de HTML para marcar el contenido Web), MathML (para matemáticas), VoiceXML™ (para voz), CML (Lenguaje de marcado químico: para química) y XBRL (Lenguaje de informes empresariales extensible: para el intercambio de datos financieros). A estos lenguajes de marcado se les conoce como vocabularios de XML, y proporcionan los medios para describir tipos específicos de datos, en formas estandarizadas y estructuradas. En la actualidad se almacenan cantidades masivas de datos en Internet, en una variedad de formatos (por ejemplo: bases de datos, páginas Web, archivos de texto). Con base en las tendencias actuales, es probable que la mayor parte de estos datos, en especial los que se intercambian entre sistemas, pronto tomen la forma de XML. Las organizaciones ven a XML como el futuro de la codificación de los datos. Los grupos de tecnología de información están planeando formas de integrar XML en sus sistemas. Los grupos industriales están desarrollando vocabularios de XML personalizados para la mayoría de las grandes industrias, para permitir que las aplicaciones empresariales basadas en computadora se comuniquen en lenguajes comunes. Por ejemplo, los servicios Web, que veremos en el capítulo 22, permiten que las aplicaciones basadas en Web intercambien datos sin problemas, a través de los protocolos estándar basados en XML. Es casi un hecho que la siguiente generación de Internet y World Wide Web estará basada en XML, el cual permitirá el desarrollo de aplicaciones basadas en Web más sofisticadas. Como veremos en este capítulo, XML le permite asignar significado a lo que de otra manera estaría constituido por simples piezas aleatorias de datos. Como resultado, los programas pueden “comprender” los datos que manipulan. Por ejemplo, un explorador Web podría ver una dirección postal listada en una página Web de HTML simple como una cadena de caracteres, sin ningún significado real. Sin embargo, en un documento de XML estos datos pueden identificarse con claridad (marcarse) como una dirección. Un programa que utilice el documento puede reconocer estos datos como una dirección y proporcionar vínculos a un mapa de esa ubicación, indicaciones de manejo desde esa ubicación, o cualquier otro tipo de información específica de esa ubicación. De igual forma, una aplicación puede reconocer los nombres de personas, fechas, números ISBN y cualquier otro tipo de datos codificados en XML. Con base en estos datos, la aplicación puede presentar a los usuarios otra información relacionada, ofreciendo una experiencia más completa y significativa para el usuario.

Ver y modificar documentos de XML Los documentos de XML son altamente portables. Para ver o modificar un documento de XML (que es un archivo de texto con la extensión de archivo .xml) no se requiere software especial, aunque existen muchas herramientas de software, y con frecuencia surgen nuevos productos en el mercado que hacen más conveniente el desarrollo de aplicaciones basadas en XML. Cualquier editor de texto con soporte para los caracteres del código ASCII/Unicode puede abrir documentos de XML para verlos y editarlos. Además, la mayoría de los exploradores Web pueden desplegar documentos en XML con un formato que facilite ver la estructura del XML. En la sección 19.3 demostramos esto mediante el uso de Internet Explorer. Una característica importante de XML es que tanto los humanos como las máquinas pueden leerlo.

678

Capítulo 19 Lenguaje de marcado extensible (XML)

Procesamiento de documentos de XML

Para procesar un documento de XML se requiere un software llamado analizador de XML (o procesador de XML ). Un analizador hace que los datos del documento estén disponibles para las aplicaciones. Mientras lee el contenido de un documento XML, el analizador comprueba que el documento cumpla con las reglas de sintaxis especificadas por la Recomendación de XML del consorcio W3C (www.w3.org/XML). La sintaxis de XML requiere un solo elemento raíz, una etiqueta inicial y una etiqueta final para cada elemento, y etiquetas propiamente anidadas (es decir, la etiqueta final para un elemento anidado debe aparecer antes de la etiqueta final del elemento de cierre). Lo que es más, XML es sensible a mayúsculas o minúsculas, por lo que debe utilizarse la capitalización apropiada en los elementos. Un documento que se conforma a esta sintaxis es un documento de XML bien formado, y es sintácticamente correcto. En la sección 19.3 presentaremos la sintaxis fundamental de XML. Si un analizador de XML puede procesar un documento de XML con éxito, ese documento está bien formado. Los analizadores pueden proporcionar acceso a los datos codificados en XML sólo en documentos bien formados. A menudo, los analizadores de XML están integrados en software como Visual Studio, o están disponibles para descargarse a través de Internet. Algunos analizadores populares son Microsoft XML Core Services (MSXML), Xerces de la Fundación de software Apache (xml.apache.org) y el software de código fuente abierto Expat XML Parser (expat.sourceforge.net). En este capítulo utilizaremos MSXML.

Validación de documentos de XML

Un documento de XML puede hacer referencia de manera opcional a una Definición de tipo de documento (DTD ) o a un esquema que defina la estructura apropiada del documento de XML. Cuando un documento de XML hace referencia a una DTD o a un esquema, algunos analizadores (conocidos como analizadores de validación) pueden leer la DTD/esquema y comprobar que el documento de XML cumpla con la estructura definida por la DTD/esquema. Si el documento de XML se conforma a la DTD/esquema (es decir, que el documento tenga la estructura apropiada), es válido. Por ejemplo, si en la figura 19.1 hacemos referencia a una DTD que especifica que un elemento jugador debe tener los elementos primerNombre, apellidoPaterno y promedioBateo, y después omitimos el elemento apellidoPaterno (línea 8 en la figura 19.1), el documento de XML jugador.xml sería inválido. No obstante, este documento aún estaría bien formado, ya que cumple con la sintaxis apropiada de XML (es decir, tiene un elemento raíz, y cada elemento tiene una etiqueta inicial y una etiqueta final). Por definición, un documento de XML válido está bien formado. Los analizadores que no pueden comprobar la conformidad del documento con DTDs o esquemas son analizadores no validadores (sólo determinan si un documento de XML está bien formado, no si es válido). En las secciones 19.5 y 19.6 hablaremos sobre la validación, las DTDs y los esquemas, así como de las diferencias clave entre estos dos tipos de especificaciones de estructura. Por ahora, sólo tome en cuenta que los esquemas son documentos de XML en sí, mientras que las DTDs no lo son. Como aprenderá en la sección 19.6, esta diferencia presenta varias ventajas en cuanto al uso de esquemas en vez de DTDs.

Observación de ingeniería de software 19.1 Las DTDs y los esquemas son esenciales para las transacciones de negocio a negocio (B2B) y los sistemas de misión crítica. Validar los documentos de XML asegura que sistemas distintos puedan manipular datos estructurados en formas estandarizadas, y evita los errores producidos por datos faltantes o mal formados.

Formato y manipulación de documentos de XML Los documentos de XML sólo contienen datos y no instrucciones de formato, por lo que las aplicaciones que procesan documentos de XML deben decidir cómo manipular o mostrar los datos de cada documento. Por ejemplo, un PDA (asistente personal digital) puede representar a un documento de XML de manera distinta a como lo representarían un teléfono inalámbrico o una computadora de escritorio. Usted puede usar el Lenguaje de hojas de estilos extensible (XSL) para especificar instrucciones de representación para distintas plataformas. En la sección 19.7 hablaremos sobre el XSL. Los programas de procesamiento de XML pueden también buscar, ordenar y manipular datos de XML mediante el uso de tecnologías tales como XSL. Algunas otras tecnologías relacionadas con XML son XPath (Lenguaje de rutas XML: un lenguaje para acceder a las partes de un documento XML), XSL-FO (Objetos de formato XML: un vocabulario de XML utilizado para describir el formato de los documentos) y XSLT (Transformaciones

19.3 Estructuración de datos

679

XSL: un lenguaje para transformar documentos de XML en otros documentos). En la sección 19.7 presentaremos el XSLT. También presentaremos a XPath en la sección 19.7, y después hablaremos sobre este lenguaje con más detalle en la sección 19.8.

19.3 Estructuración de datos En esta sección y a lo largo de este capítulo, crearemos nuestro propio marcado de XML. Este lenguaje nos permite describir datos con precisión, en un formato bien estructurado.

Marcado de XML para un artículo En la figura 19.2 presentamos un documento de XML que marca un artículo simple mediante el uso de XML. Los números de línea que se muestran son para fines de referencia solamente, por lo que no forman parte del documento de XML. Este documento comienza con una declaración XML (línea 1), la cual identifica al documento como un documento de XML. El atributo version especifica la versión de XML a la cual se conforma el documento. El estándar actual de XML es la versión 1.0. Aunque el consorcio W3C publicó la especificación de la versión 1.1 en febrero de 2004, esta versión más reciente aún no cuenta con un soporte amplio. El consorcio W3C podría seguir publicando nuevas versiones a medida que XML evolucione, para cumplir con los requerimientos de distintos campos.

Tip de portabilidad 19.1 Los documentos deben incluir la declaración XML para identificar la versión utilizada de XML. Podríamos asumir que un documento sin declaración XML se conforma a la versión más reciente de XML; pero si no es así, podrían producirse errores.

Error común de programación 19.1 Es un error colocar caracteres de espacio en blanco antes de la declaración XML.

Los comentarios de XML (líneas 2-3), que comienzan con



-->



Figura 19.9 | Definición de tipo de documento (DTD) para una carta de negocios. (Parte 1 de 2).

19.5 Definiciones de tipo de documento (DTDs)

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

direccion1 ( #PCDATA )> direccion2 ( #PCDATA )> ciudad ( #PCDATA )> estado ( #PCDATA )> cp ( #PCDATA )> telefono ( #PCDATA )> bandera EMPTY> bandera genero (M | F) "M">

cierre ( #PCDATA )> parrafo ( #PCDATA )> firma ( #PCDATA )>

689

Figura 19.9 | Definición de tipo de documento (DTD) para una carta de negocios. (Parte 2 de 2).

Error común de programación 19.8 Para los documentos validados con DTDs, cualquier documento que utilice elementos, atributos o relaciones que no estén definidos explícitamente mediante una DTD es un documento inválido.

Definición de elementos en una DTD

La declaración de tipo de elemento ELEMENT en las líneas 4-5 define las reglas para el elemento carta. En este caso, carta contiene uno o más elementos contacto, un elemento saludo, uno o más elementos parrafo, un elemento cierre y un elemento firma, en esa secuencia. El indicador de ocurrencia de signo positivo (+) especifica que la DTD permite una o más ocurrencias de un elemento. Otros indicadores de ocurrencia son el asterisco (*), que indica un elemento opcional que puede ocurrir cero o más veces, y el signo de interrogación (?), que indica que un elemento opcional puede ocurrir cuando menos una vez (es decir, cero ocurrencias o una). Si un elemento no tiene un indicador de ocurrencia, la DTD permite exactamente una ocurrencia de este elemento. La declaración del tipo del elemento contacto (líneas 7-8) especifica que un elemento contacto contiene los elementos hijos nombre, direccion1, direccion2, ciudad, estado, cp, telefono y bandera, en ese orden. La DTD requiere sólo una ocurrencia de cada uno de estos elementos.

Definición de atributos en una DTD

La línea 9 utiliza la declaración de lista de atributos ATTLIST para definir un atributo llamado tipo, para el elemento contacto. La palabra clave #IMPLIED especifica que, si el analizador encuentra un elemento contacto sin un atributo tipo, puede elegir un valor arbitrario para el atributo, o puede ignorarlo. De cualquier forma, el documento aún sería válido (si el resto del documento es válido); si se omite un atributo tipo no se invalidará el documento. Otras palabras clave que pueden usarse en lugar de #IMPLIED en una declaración ATTLIST son #REQUIRED y #FIXED. La palabra clave #REQUIRED especifica que el atributo debe estar presente en el elemento, y la palabra clave #FIXED especifica que el atributo (si está presente) debe tener el valor fijo dado. Por ejemplo,

indica que el atributo cp (si está presente en el elemento direccion) debe tener el valor 01757 para que el documento sea válido. Si el atributo no está presente, entonces el analizador, de manera predeterminada, usará el valor fijo que especifique la declaración ATTLIST.

Comparación entre datos tipo carácter y datos tipo carácter analizados

La palabra clave CDATA (línea 9) especifica que el atributo type contiene datos de carácter (es decir, una cadena). Un analizador pasará tales datos a una aplicación sin modificarlos.

690

Capítulo 19 Lenguaje de marcado extensible (XML)

Observación de ingeniería de software 19.4 La sintaxis de las DTDs no cuenta con un mecanismo para describir el tipo de datos de un elemento (o atributo). Por ejemplo, una DTD no puede especificar que un elemento o atributo específico puede contener sólo datos enteros.

La palabra clave #PCDATA (línea 11) especifica que un elemento (por ejemplo, nombre) puede contener datos tipo carácter analizados (es decir, datos procesados por un analizador de XML). Los elementos con datos tipo carácter analizados no pueden contener caracteres de marcado, como los signos menor que () o &. El autor del documento debe sustituir cualquier carácter de marcado en un elemento #PCDATA con la correspondiente referencia a entidad de carácter de ese carácter. Por ejemplo, la referencia a entidad de carácter < debe utilizarse en lugar del símbolo menor que (). El autor de un documento que desee utilizar un símbolo & literal, debe utilizar en su defecto la referencia a entidad &. Los datos tipo carácter analizados pueden contener símbolos &, sólo para insertar entidades. En el apéndice H podrá ver una lista de otras referencias a entidades de carácter.

Error común de programación 19.9 El uso de caracteres de marcado (por ejemplo: y &) en datos tipo carácter analizados es un error. Use en su defecto referencias a entidades de carácter (por ejemplo: y &).

Definición de elementos vacíos en una DTD La línea 18 define un elemento vacío llamado bandera. La palabra clave EMPTY especifica que el elemento no contiene datos entre sus etiquetas inicial y final. Por lo general, los elementos vacíos describen datos mediante atributos. Por ejemplo, los datos de bandera aparecen en su atributo genero (línea 19). La línea 19 especifica que el valor del atributo genero debe ser uno de los valores enumerados (M o F) encerrados entre paréntesis y delimitados por una barra vertical (|) que significa “o”. Observe que la línea 19 también indica que genero tiene un valor predeterminado de M.

Comparación entre documentos bien formados y documentos válidos En la sección 19.3 demostramos cómo usar el Microsoft XML Validator para validar un documento de XML, comparándolo con su DTD especificada. La validación reveló que el documento de XML carta.xml (figura 19.4) está bien formado y es válido; se conforma a carta.dtd (figura 19.9). Recuerde que un documento bien formado es sintácticamente correcto (es decir, cada etiqueta inicial tiene su correspondiente etiqueta final, el documento sólo contiene un elemento raíz, etc.), y un documento válido contiene los elementos apropiados con los atributos apropiados, en la secuencia apropiada. Un documento de XML no puede ser válido a menos que esté bien formado. Cuando un documento no se conforma a una DTD o a un esquema, Microsoft XML Validator muestra un mensaje de error. Por ejemplo, la DTD en la figura 19.10 indica que un elemento contacto debe contener el elemento hijo nombre. Un documento que omite a este elemento hijo sigue estando bien formado, pero no es válido. En dicho escenario, Microsoft XML Validator muestra el mensaje de error que aparece en la figura 19.10.

Figura 19.10 | XML Validator muestra un mensaje de error.

19.6 Documentos de esquemas XML del W3C

691

19.6 Documentos de esquemas XML del W3C En esta sección presentaremos los esquemas para especificar la estructura de un documento de XML y validar documentos de XML. Muchos desarrolladores en la comunidad de XML creen que las DTDs no son lo bastante flexibles como para satisfacer las necesidades de programación actuales. Por ejemplo, las DTDs carecen de un medio para indicar qué tipo específico de datos (por ejemplo: numérico, texto) puede contener un elemento, y las DTDs no son documentos de XML en sí. Éstas y otras limitaciones han conducido al desarrollo de los esquemas. A diferencia de las DTDs, los esquemas no utilizan gramática EBNF. En vez de ello, utilizan sintaxis de XML y, de hecho, son documentos de XML que los programas pueden manipular. Al igual que las DTDs, los analizadores de validación utilizan los esquemas para validar documentos. En esta sección, nos concentraremos en el vocabulario XML Schema del W3C (observe la letra “S” mayúscula en “Schema”). Utilizaremos el término XML Schema durante el resto del capítulo, cada vez que hagamos referencia al vocabulario XML Schema del W3C. Para obtener la información más reciente acerca de XML Schema, visite el sitio www.w3.org/XML/Schema. Si desea ver tutoriales sobre los conceptos de XML Schema que estén más allá de lo que presentamos aquí, visite el sitio www.w3schools.com/schema/default.asp. Una DTD describe la estructura de un documento de XML, no el contenido de sus elementos. Por ejemplo, 5

contiene datos tipo carácter. Si el documento que contiene el elemento cantidad hace referencia a una DTD, un analizador de XML puede validar el documento para confirmar que, ciertamente, el contenido de este elemento es PCDATA. Sin embargo, el analizador no puede validar que el contenido sea numérico; las DTDs no proporcionan esta capacidad. Así que, desafortunadamente, el analizador también considera que hola

es válido. Una aplicación que utilice el documento de XML que contenga este marcado debe comprobar que los datos en el elemento cantidad sean numéricos, y debe realizar las acciones apropiadas en caso de que no sea así. XML Schema permite a los autores de esquemas especificar que los datos del elemento cantidad deben ser numéricos o, más específicamente, de tipo entero. Un analizador que valide el documento de XML y lo compare con este esquema puede determinar que el 5 está en conformidad, y que hola no lo está. Un documento de XML que se conforma a un documento de esquema es válido para el esquema, y un documento que no se conforma es inválido para el esquema. Los esquemas son documentos de XML y, por lo tanto, también deben ser válidos.

Validación con base en un documento de esquema de XML La figura 19.11 muestra un documento de XML válido para el esquema, llamado libro.xml, y la figura 19.12 muestra el documento de XML Schema pertinente (libro.xsd) que define la estructura para libro.xml. Por convención, los esquemas utilizan la extensión .xsd. Nosotros usamos un validador de esquemas XSD proporcionado por Microsoft en la dirección apps.gotdotnet.com/xmltools/xsdvalidator

1 2 3 4 5 6 7 8



Visual Basic 2005 Cómo programar, 3/e

Figura 19.11 | Documento de XML válido para el esquema, que describe una lista de libros. (Parte 1 de 2).

692

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

Capítulo 19 Lenguaje de marcado extensible (XML)

Visual C# 2005 Cómo programar

Java Cómo programar, 6/e

C++ Cómo programar, 5/e

Internet y World Wide Web Cómo programar, 3/e

Figura 19.11 | Documento de XML válido para el esquema, que describe una lista de libros. (Parte 2 de 2). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23











Figura 19.12 | Documento de XML Schema para libro.xml.

19.6 Documentos de esquemas XML del W3C

693

para asegurarnos que el documento de XML de la figura 19.11 se conforme al esquema de la figura 19.12. Para validar el documento del esquema en sí (es decir, libro.xsd) y producir la salida que se muestra en la figura 19.12, utilizamos un XSV en línea (Validador de XML Schema) proporcionado por el consorcio W3C en la dirección www.w3.org/2001/03/webdata/xsv

Estas herramientas son gratuitas y cumplen con las especificaciones del consorcio W3C, en relación con los esquemas de XML y la validación de los mismos. La sección 19.12 lista varios validadores en línea de XML Schema. La figura 19.11 contiene marcado que describe varios libros de Deitel. El elemento libros (línea 5) tiene el prefijo de espacio de nombres deitel, que indica que el elemento libros forma parte del espacio de nombres http://www.deitel.com/listalibros. Observe que declaramos el prefijo de espacio de nombres deitel en la línea 5.

Creación de un documento de XML Schema La figura 19.12 presenta el documento de XML Schema que especifica la estructura de libro.xml (figura 19.11). Este documento define un lenguaje basado en XML (es decir, un vocabulario) para escribir documentos de XML acerca de colecciones de libros. El esquema define los elementos, atributos y relaciones padre-hijo que un documento de este tipo puede (o debe) incluir. Este esquema también especifica el tipo de datos que pueden contener estos elementos y atributos. El elemento raíz schema (figura 19.12, líneas 5-23) contiene elementos que definen la estructura de un documento de XML como libro.xml. La línea 5 especifica como espacio de nombres predeterminado el URI del espacio de nombres de XML Schema del W3C: http://www.w3.org/2001/XMLSchema. Este espacio de nombres contiene elementos predefinidos (por ejemplo: el elemento raíz schema) que conforman el vocabulario XML Schema: el lenguaje utilizado para escribir un documento de XML Schema.

Tip de portabilidad 19.3 Los autores de esquemas de XML del W3C especifican el URI http://www.w3.org/2001/XMLSchema cuando hacen referencia al espacio de nombres XML Schema. Este espacio de nombres contiene elementos predefinidos que conforman el vocabulario XML Schema. Al especificar este URI, se asegura que las herramientas de validación identifiquen en forma correcta los elementos de XML Schema, y que no los confundan con los elementos definidos por los autores de documentos.

La línea 6 enlaza el URI http://www.deitel.com/listalibros al prefijo de espacio de nombres deitel. Como veremos en unos momentos, el esquema utiliza este espacio de nombres para diferenciar los nombres que nosotros creamos, de los nombres que forman parte del espacio de nombres XML Schema. La línea 7 también especifica a http://www.deitel.com/listalibros como el espacio de nombres de destino (targetNamespace) del esquema. Este atributo identifica el espacio de nombres del vocabulario de XML que define este esquema. Observe que el valor para targetNamespace de libro.xsd es el mismo que el espacio de nombres referenciado en la línea 5 de libro.xml (figura 19.11). Esto es lo que “conecta” al documento de XML con el esquema que define su estructura. Cuando un validador de esquemas de XML analice los archivos libro.xml y a libro.xsd, reconocerá que libro.xml utiliza elementos y atributos del espacio de nombres http://www.deitel.com/listalibros. El validador también reconocerá que este espacio de nombres es el que está definido en libro.xsd (es decir, el elemento targetNamespace del esquema). Por ende, el validador sabe en dónde buscar las reglas estructurales para los elementos y atributos utilizados en libro.xml.

Definición de un elemento en XML Schema En XML Schema, la etiqueta element (línea 9) define a un elemento que se incluirá en un documento de XML que se conforme a ese esquema. En otras palabras, elemento especifica los elementos actuales que pueden usarse para marcar datos. La línea 9 define el elemento libros, que utilizamos como elemento raíz en libro.xml (figura 19.11). Los atributos nombre y tipo especifican el nombre y tipo de datos del elemento, respectivamente. El tipo de datos de un elemento indica los datos que puede contener. Los posibles tipos de datos incluyen los tipos definidos por XML Schema (por ejemplo: string, double) y los tipos definidos por el usuario (por ejemplo:

694

Capítulo 19 Lenguaje de marcado extensible (XML)

TipoLibros,

que se define en las líneas 11-16). La figura 19.13 lista varios de los muchos tipos integrados de XML Schema. Para obtener una lista completa de tipos integrados, consulte la sección 3 de la especificación en www.w3.org/TR/xmlschema-2. En este ejemplo, libros se define como un elemento con el tipo de datos deitel:TipoLibros (línea 9). TipoLibros es un tipo definido por el usuario (líneas 11-16) en el espacio de nombres http://www.deitel. com/listalibros y, por lo tanto, debe tener el prefijo de espacio de nombres deitel. No es un tipo de datos existente de XML Schema.

Tipo(s) de datos de XML Schema

Descripción

string

Una cadena de caracteres.

boolean

Verdadero o falso.

true, false

true

decimal

Un número decimal.

( ), en donde i es un entero y n es un entero menor o igual a cero.

5, -12, -45.78

float

Un número de punto flotante.

m * (2e),

en donde m es un entero cuyo valor absoluto es menor que 224 y e es un entero en el rango de -149 a 104. Más tres números adicionales: infinito positivo, infinito negativo y “no es un número” (NaN).

0, 12, -109.375, NaN

double

Un número de punto flotante.

m * (2e),

en donde m es un entero cuyo valor absoluto es menor que 253 y e es un entero en el rango de -1075 a 970. Más tres números adicionales: infinito positivo, infinito negativo y “no es un número” (NaN).

0, 12, -109.375, NaN

long

Un número entero.

-9223372036854775808 9223372036854775807,

Rangos o estructuras

Ejemplos "hola"

i*

10n

a

1234567890, -1234567890

inclusive int

Un número entero.

-2147483648

a 2147483647,

inclusive short

Un número entero.

-32768

date

Una fecha que consiste en el año, mes y día.

aaaa-mm con un valor dd opcional y una zona horaria opcional, en donde aaaa es de cuatro dígitos; mm y dd son de dos dígitos.

2005-05-10

time

Una hora que consiste en horas, minutos y segundos.

hh:mm:ss con una zona horaria opcional, en donde hh, mm y ss son de dos dígitos.

16:30:25-05:00

Figura 19.13 | Algunos tipos de datos de XML Schema.

a 32767, inclusive

1234567890, -1234567890 12, -345

19.6 Documentos de esquemas XML del W3C

695

En XML Schema existen dos categorías de tipos de datos: tipos simples y tipos complejos. La única diferencia entre estos dos tipos es que los tipos simples no pueden contener atributos o elementos hijos, y los tipos complejos sí. Un tipo definido por el usuario que contiene atributos o elementos hijos debe definirse como tipo complejo. Las líneas 11-16 utilizan el elemento complexType para definir a TipoLibros como un tipo complejo que tiene un elemento hijo llamado libro. El elemento sequence (líneas 12-15) nos permite especificar el orden secuencial en el que deben aparecer los elementos hijos. El elemento element (líneas 13-14) anidado dentro del elemento complexType indica que un elemento TipoLibros (por ejemplo, libros) puede contener elementos hijos llamados libro de tipo deitel:TipoLibroSencillo (definido en las líneas 18-22). El atributo minOccurs (línea 14), que tiene el valor 1, especifica que los elementos de tipo TipoLibros deben contener como mínimo un elemento libro. El atributo maxOccurs (línea 14), con el valor unbounded, especifica que los elementos de tipo TipoLibros pueden tener cualquier número de elementos hijos libro. Las líneas 18-22 definen el tipo complejo TipoLibroSencillo. Un elemento de este tipo contiene un elemento hijo llamado titulo. La línea 20 define el elemento title como del tipo simple string. Recuerde que los elementos de un tipo simple no pueden contener atributos o elementos hijos. La etiqueta final schema (, línea 23) declara el fin del documento de XML Schema.

Análisis detallado de los tipos en XML Schema Cada elemento en XML Schema tiene un tipo. Los tipos pueden ser los tipos integrados proporcionados por XML Schema (figura 19.13), o los tipos definidos por el usuario (por ejemplo, TipoLibroSencillo en la figura 19.12). Cada tipo simple define una restricción sobre un tipo definido por XML Schema, o una restricción sobre un tipo definido por el usuario. Las restricciones limitan los posibles valores que puede guardar un elemento. Los tipos complejos se dividen en dos grupos: los que tienen contenido simple y los que tienen contenido complejo. Ambos pueden contener atributos, pero sólo el contenido complejo puede contener elementos hijos. Los tipos complejos con contenido simple deben extender o restringir a algún otro tipo existente. Los tipos complejos con contenido complejo no tienen esta limitación. En el siguiente ejemplo demostraremos los tipos complejos con cada tipo de contenido. El documento de esquema en la figura 19.14 crea tanto tipos simples como tipos complejos. El documento de XML en la figura 19.15 (laptop.xml) sigue la estructura definida en la figura 19.14 para describir partes de una computadora laptop. Un documento como laptop.xml que se conforma a un esquema, se conoce como documento de instancia XML: el documento es una instancia (es decir, ejemplo) del esquema.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21











Figura 19.14 | Documento de XML Schema que define tipos simples y complejos. (Parte 1 de 2).

696

22 23 24 25 26 27 28 29 30 31 32 33 34

Capítulo 19 Lenguaje de marcado extensible (XML)









Figura 19.14 | Documento de XML Schema que define tipos simples y complejos. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12



Intel 17 2.4 256

Figura 19.15 | Documento de XML que usa el elemento laptop definido en computadora.xsd. La línea 5 declara el espacio de nombres predeterminado para que sea el espacio de nombres estándar de XML Schema; se asumirá que cualquier elemento sin prefijo se encuentra en el espacio de nombres de XML Schema. La línea 6 enlaza el prefijo de espacio de nombres computadora al espacio de nombres http://www. deitel.com/computadora. La línea 7 identifica a este espacio de nombres como el elemento targetNameSpace: el espacio de nombres que está siendo definido por el documento actual de XML Schema. Para diseñar los elementos de XML para describir computadoras laptop, primero creamos un tipo simple en las líneas 9-13 usando el elemento simpleType. Nombramos a este tipo simple gigahertz debido a que se utilizará para describir la velocidad del reloj del procesador en gigahertz. Los tipos simples son restricciones de un tipo, que por lo general se denomina tipo base. Para este tipo simple, la línea 10 declara el tipo base como decimal, y restringimos el valor para que sea cuando menos 2.1, mediante el uso del elemento minInclusive en la línea 11. A continuación declaramos un tipo complejo llamado CPU, que tiene contenido simple (simpleContent) (líneas 16-20). Recuerde que un tipo complejo con contenido simple puede tener atributos, pero no elementos hijos. Además recuerde que los tipos complejos con contenido simple deben extender o restringir algún tipo de XML Schema o algún tipo definido por el usuario. El elemento extension con el atributo base (línea 17) establece el tipo base a string. En este tipo complejo, extendemos el tipo base string con un atributo. El elemento attribute (línea 18) proporciona al tipo complejo un atributo de tipo string llamado modelo. Por ende, un elemento de tipo CPU debe contener texto string (debido a que el tipo base es string) y puede contener un atributo modelo que también es de tipo string. Por último, definimos el tipo portatil, que es un tipo complejo con contenido complejo (líneas 23-31). Dichos tipos pueden tener elementos hijos y atributos. El elemento all (líneas 24-29) encierra elementos que deben incluirse una vez cada uno en el correspondiente documento de instancia de XML. Estos elementos pueden incluirse en cualquier orden. Este tipo complejo guarda cuatro elementos: procesador, monitor, VelocidadCPU

19.7 (Opcional) Lenguaje de hojas de estilos extensible y transformaciones XSL

697

y RAM. Estos elementos reciben los tipos CPU, int, gigahertz e int, respectivamente. Al utilizar los tipos CPU y gigahertz, debemos incluir el prefijo de espacio de nombres computadora, ya que estos tipos definidos por el usuario son parte del espacio de nombres computadora (http://www.deitel.com/computadora): el espacio de nombres definido en el documento actual (línea 7). Además, portatil contiene un atributo definido en la línea 30. El elemento attribute indica que los elementos de tipo portatil contienen un atributo de tipo string, llamado fabricante. La línea 33 declara el elemento actual que utiliza los tres tipos definidos en el esquema. El elemento se llama laptop y es de tipo portatil. Debemos utilizar el prefijo de espacio de nombres computadora en frente de portatil. Ahora hemos creado un elemento llamado laptop que contiene los elementos hijos procesador, monitor, VelocidadCPU y RAM, y un atributo llamado fabricante. La figura 19.15 utiliza el elemento laptop definido en el esquema computadora.xsd. Una vez más, utilizamos un validador de esquemas XSD en línea para asegurar que este documento de instancia de XML se adhiera a las reglas estructurales del esquema. La línea 5 declara el prefijo de espacio de nombres computadora. El elemento laptop requiere este prefijo, ya que forma parte del espacio de nombres http://www.deitel.com/computadora. La línea 6 establece el atributo fabricante de la laptop, y las líneas 8-11 utilizan los elementos definidos en el esquema para describir las características de la laptop. En esta sección presentamos los documentos del XML Schema del W3C para definir la estructura de documentos de XML, y validamos los documentos de instancia de XML, comparándolos con esquemas mediante el uso de un validador de esquemas XSD en línea. La sección 19.9 demuestra cómo validar los documentos de XML mediante la programación, comparándolos con esquemas mediante el uso de clases del .NET Framework. Esto le permite asegurar que un programa en C# manipule sólo documentos válidos; manipular un documento inválido al que le falten piezas requeridas de datos podría provocar errores en el programa.

19.7 (Opcional) Lenguaje de hojas de estilos extensible y transformaciones XSL

Los documentos en Lenguaje de hojas de estilos extensible (XSL) especifican la forma en que los programas deben representar los datos de un documento de XML. XSL es un grupo de tres tecnologías: XSL-FO (Objetos de formato XSL), XPath (Lenguaje de rutas XML) y XSLT (Transformaciones XSL). XSL-FO es un vocabulario para especificar el formato, y XPath es un lenguaje basado en cadenas, de expresiones utilizadas por XML y muchas de sus tecnologías relacionadas, para localizar en forma efectiva y eficiente las estructuras y los datos (como los elementos y atributos específicos) en documentos de XML. La tercera porción de XSL, las Transformaciones XSL (XSLT), es una tecnología para transformar documentos de XML en otros documentos; es decir, transformar la estructura de los datos del documento de XML en otra estructura. XSLT proporciona elementos que definen reglas para transformar un documento de XML y producir un documento de XML distinto. Esto es útil cuando queremos utilizar datos en varias aplicaciones o plataformas, cada una de las cuales puede estar diseñada para trabajar con documentos escritos en un vocabulario específico. Por ejemplo, XSLT nos permite convertir un documento de XML simple en un documento XHTML (Lenguaje de marcado de hipertexto extensible) que presente los datos del documento de XML (o un subconjunto de los datos) en un formato para visualizarlo en un explorador Web. (En la figura 19.16 encontrará una vista de ejemplo tipo “antes” y “después” de dicha transformación.) XHTML es la recomendación técnica que sustituye a HTML para marcar el contenido Web. Para obtener más información sobre XHTML, consulte el Apéndice F, Introducción al lenguaje XHTML: parte 1 y el Apéndice G, Introducción a XHTML: parte 2, y visite el sitio www.w3.org. Para transformar un documento de XML mediante el uso de XSLT se requieren dos estructuras tipo árbol: el árbol de origen (es decir, el documento de XML que se va a transformar) y el árbol de resultados (es decir, el documento de XML que se va a crear). XPath se utiliza para localizar partes del documento del árbol de origen que coinciden con plantillas definidas en una hoja de estilos XSL. Cuando ocurre una coincidencia (es decir, cuando un nodo coincide con una plantilla), se ejecuta la plantilla coincidente y agrega su resultado al árbol de resultados. Cuando ya no hay coincidencias, XSLT ha transformado el árbol de origen en el árbol de resultados. XSLT no analiza cada nodo del árbol de origen; navega en forma selectiva por el árbol de origen, utilizando los atributos select y match de XPath. Para que XSLT funcione, el árbol de origen debe estar estructurado en forma apropiada. Los esquemas, las DTDs y los analizadores de validación pueden validar la estructura de un documento antes de utilizar XPath y XSLTs.

698

Capítulo 19 Lenguaje de marcado extensible (XML)

Un ejemplo simple de XSL La figura 19.16 lista un documento de XML que describe varios deportes. La salida muestra el resultado de la transformación (especificada en la plantilla XSLT de la figura 19.17), representado por Internet Explorer 6. Para realizar transformaciones, se requiere un procesador de XSLT. Algunos de los procesadores de XSLT populares son: MSXML de Microsoft y Xalan 2 de la Fundación de software Apache (xml.apache.org). El documento de XML que se muestra en la figura 19.16 se transforma en un documento XHTML mediante MSXML, cuando el documento se carga en Internet Explorer. MSXML es tanto un analizador de XML como un procesador de XSLT. La línea 2 (figura 19.16) es una instrucción de procesamiento (PI) que hace referencia a la hoja de estilos XSL deportes.xsl (figura 19.17). Una instrucción de procesamiento se incrusta en un documento de XML y proporciona información específica de la aplicación a cualquier procesador de XML que utilice esa aplicación. En este caso específico, la instrucción de procesamiento especifica la ubicación de un documento XSLT con el que se va a

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31



Cricket

Más popular entre las comunidades de naciones.

Béisbol

Más popular en América.

Soccer (Fútbol)

El deporte más popular en el mundo.



Figura 19.16 | Documento de XML que describe varios deportes.

19.7 (Opcional) Lenguaje de hojas de estilos extensible y transformaciones XSL

699

transformar el documento de XML. Los caracteres (línea 2, figura 19.16) delimitan una instrucción de procesamiento, la cual consiste en un destino de la PI (por ejemplo, xml:stylesheet) y un valor de la PI (por ejemplo, type = “text/xsl” href. = “deportes.xsl”). El atributo type del valor de la PI especifica que deportes.xsl es un archivo tipo text/xsl (es decir, un archivo de texto con contenido XSL). El atributo href. especifica el nombre y la ubicación de la hoja de estilo que se va a aplicar; en este caso, deportes.xsl en el directorio actual.

Observación de ingeniería de software 19.5 XSL permite a los autores de documentos separar la presentación de los datos (especificada en documentos de XSL) de su descripción (especificada en documentos de XML).

La figura 19.17 muestra el documento de XSL para transformar los datos estructurados del documento de XML de la figura 19.16 en un documento de XHTML para presentarlo en pantalla. Por convención, los documentos XSL tienen la extensión de archivo .xsl. Las líneas 6-7 empiezan la hoja de estilos XSL con la etiqueta inicial stylesheet. El atributo version especifica la versión de XSLT a la cual se conforma este documento. La línea 7 enlaza el prefijo de espacio de nombres xsl al URI de XSLT del W3C (es decir, http://www.w3.org/1999/XSL/Transform). Las líneas 9-12 usan el elemento xsl:output para escribir una declaración de tipo de documento XHTML (DOCTYPE) en el árbol de resultados (es decir, el documento de XML que se va a crear). DOCTYPE identifica a XHTML como el tipo del documento resultante. Al atributo method se le asigna “xml”, lo cual indica que se va a enviar XML al árbol de resultados. (Recuerde que XHTML es un tipo de XML.) El atributo omit-xmldeclaration especifica si la transformación debe escribir la declaración XML en el árbol de resultados. En este caso, no queremos omitir la declaración XML, por lo que asignamos a este atributo el valor “no”. Los atributos doctype-system y doctype-public escriben la información DOCTYPE de la DTD en el árbol de resultados.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30





Deportes





Figura 19.17 | XSLT que crea elementos y atributos en un documento de XHTML. (Parte 1 de 2).

700

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

Capítulo 19 Lenguaje de marcado extensible (XML)



ID Deporte Información




Figura 19.17 | XSLT que crea elementos y atributos en un documento de XHTML. (Parte 2 de 2). XSLT utiliza plantillas (es decir, elementos xsl:template) para describir cómo transformar nodos específicos del árbol de origen al árbol de resultados. Se aplica una plantilla a los nodos que se especifican en el atributo match requerido. La línea 14 utiliza el atributo match para seleccionar la raíz del documento (es decir, la parte conceptual del documento que contiene el elemento raíz y todo lo que está debajo de éste) de XML de origen (es decir, deportes.xml). El carácter / (una barra diagonal) de XPath siempre selecciona la raíz del documento. Recuerde que XPath es un lenguaje basado en cadenas, que se utiliza para localizar partes de un documento de XML con facilidad. En XPath, una barra diagonal especifica que estamos usando direccionamiento absoluto (es decir, empezamos desde la raíz y definimos rutas hacia abajo por el árbol de origen). En el documento de XML de la figura 19.16, los nodos hijos de la raíz del documento son los dos nodos de instrucción de procesamiento (líneas 1-2), los dos nodos de comentario (líneas 4-5) y el nodo del elemento deportes (líneas 7-31). La plantilla en la figura 19.17, línea 14, coincide con un nodo (es decir, el nodo raíz), por lo que ahora se agrega el contenido de la plantilla al árbol de resultados. El procesador MSXML escribe el XHTML en las líneas 16-29 (figura 19.17) en el árbol de resultados, exactamente como aparece en el documento de XSL. Ahora el árbol de resultados consiste en la definición DOCTYPE y el código XHTML de las líneas 16-29. Las líneas 33-39 utilizan los elementos xsl:for-each para iterar a través del documento XML de origen, buscando elementos juego. El elemento xsl:for-each es similar a la instrucción foreach de C#. El atributo select es una expresión XPath que especifica los nodos (se le conoce como conjunto de nodos) en los que opera el elemento xsl:for-each. De nuevo, la primera barra diagonal significa que estamos usando direccionamiento absoluto. La barra diagonal entre deportes y juego indica que juego es un nodo hijo de deportes. Por ende, el elemento xsl:for-each busca nodos juego que son hijos del nodo deportes. El documento deportes.xml contiene sólo un nodo deportes, que también es el nodo raíz del documento. Después de buscar los elementos que coincidan con el criterio de búsqueda, el elemento xsl:for-each procesa cada elemento con el código en las líneas 34-38 (estas líneas producen una fila en una tabla, cada vez que se ejecutan) y coloca el resultado de las líneas 34-38 en el árbol de resultados. La línea 35 utiliza el elemento value-of para recuperar el valor del atributo id y colocarlo en un elemento td en el árbol de resultados. El símbolo @ de XPath especifica que id es un nodo de atributo del nodo de contexto juego. Las líneas 36-37 colocan los valores de los elementos nombre y parrafo en elementos td y los insertan en el árbol de resultados. Cuando una expresión de XPath no tiene barra diagonal al principio, utiliza direccionamiento relativo. Al omitir la barra diagonal al principio se indica a las instrucciones xsl:value-of select que deben buscar elementos nombre y parrafo que sean hijos del nodo de contexto, no del nodo raíz. Debido a la última selección de la expresión de XPath, el nodo de contexto actual es juego, el cual sin duda tiene un atributo id, un elemento hijo nombre y un elemento hijo parrafo.

Uso de XSLT para ordenar y dar formato a los datos La figura 19.18 presenta un documento de XML (ordenamiento.xml) que marca información acerca de un libro. Observe que varios elementos del marcado que describen al libro aparecen fuera de orden (por ejemplo, el

19.7 (Opcional) Lenguaje de hojas de estilos extensible y transformaciones XSL

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

701



Deitel's XML Primer

Jane Blue





XML Fundamentals



Figura 19.18 | Documento de XML que contiene información sobre un libro. elemento que describe al capítulo 3 aparece antes del elemento que describe al capítulo 2). Los ordenamos de esta forma a propósito, para demostrar que la hoja de estilos XSL a la que se hace referencia en la línea 5 (ordenamiento. xsl) puede ordenar los datos del archivo XML para fines de presentación. La figura 19.19 presenta un documento de XSL (ordenamiento.xsl) para transformar a ordenamiento. xml (figura 19.18) en XHTML. Recuerde que un documento de XSL navega por un árbol de origen y crea un árbol de resultados. En este ejemplo, el árbol de origen es XML, y el árbol de salida es XHTML. La línea 14 de la figura 19.19 coincide con el elemento raíz del documento en la figura 19.18. La línea 15 envía una etiqueta inicial html al árbol de resultados. El elemento (línea 16) especifica que el procesador de XSLT debe aplicar los elementos xsl:template definidos en este documento XSL a los hijos del nodo actual (es decir, la raíz del documento). El contenido que resulta al aplicar las plantillas se envía en el elemento html que termina en la línea 17. Las líneas 21-84 especifican una plantilla que coincide con el elemento libro. La plantilla indica cómo dar formato de XHTML a la información contenida en los elementos libro de ordenamiento.xml (figura 19.18).

1 2 3 4 5 6 7



Figura 19.19 | Documento de XSL que transforma a ordenamiento.xml en XHTML. (Parte 1 de 3).

702

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66

Capítulo 19 Lenguaje de marcado extensible (XML)





\r\n"; break; case XmlNodeType.Text: // Texto de XML, lo muestra ImprimirTab( profundidad ); // inserta tabuladores txtSalida.Text += "\t" + lector.Value + "\r\n"; break; // Declaracion XML la muestra case XmlNodeType.XmlDeclaration: ImprimirTab( profundidad ); // inserta tabuladores txtSalida.Text += "\r\n"; break; case XmlNodeType.EndElement: // EndElement de XML, lo muestra ImprimirTab( profundidad ); // inserta tabuladores txtSalida.Text += "\r\n"; profundidad--; // reduce la profundidad break; } // fin de switch } // fin de while } // fin del método XmlReaderTextForm_Load // inserta tabuladores private void ImprimirTab( int numero ) { for ( int i = 0; i < numero; i++ ) txtSalida.Text += "\t"; } // fin del método ImprimirTab } // fin de la clase PruebaXmlReaderForm } // fin del espacio de nombres PruebaXmlReader

Figura 19.22 | Un objeto XmlReader iterando a través de un documento de XML. (Parte 2 de 2). El método Read de XmlReader lee un nodo del árbol DOM. Al llamar a este método en la condición del ciclo (línea 27), lector lee todos los nodos del documento. La instrucción switch (líneas 27-58) procesa cada nodo. Se da formato a la propiedad Name (líneas 32, 50 y 55), que contiene el nombre del nodo, o a la propiedad Value (líneas 40 y 44), que contiene los datos del nodo, y se concatena al objeto string asignado a la propiedad Text del control TextBox. La propiedad NodeType del objeto XmlReader especifica si el nodo es un elemento, comentario, while

708

Capítulo 19 Lenguaje de marcado extensible (XML)

texto, declaración XML o elemento final. Observe que cada caso especifica un tipo de nodo mediante el uso de constantes de la enumeración XmlNodeType. Por ejemplo, XmlNodeType.Element (línea 31) indica la etiqueta inicial de un elemento. Los resultados que se muestran en pantalla enfatizan la estructura del documento de XML. La variable profundidad (línea 23) mantiene el número de caracteres de tabulación para aplicar sangría a cada elemento. La profundidad se incrementa cada vez que el programa encuentra un tipo de nodo Element, y se decrementa cada vez que el programa encuentra un tipo de nodo EndElement o un elemento vacío. Utilizamos una técnica similar en el siguiente ejemplo, para enfatizar la estructura de árbol del documento de XML que se mostrará en pantalla.

Mostrar el gráfico de un árbol DOM en un control TreeView Los objetos XmlReader no cuentan con herramientas para mostrar su contenido en forma gráfica. En este ejemplo, mostramos el contenido de un documento de XML mediante el uso de un control TreeView. Utilizamos la clase TreeNode para representar a cada nodo en el árbol. Las clases TreeView y TreeNode forman parte del espacio de nombres System.Windows.Forms. Los objetos TreeNode se agregan al control TreeView para enfatizar la estructura del documento de XML. El programa en C# de la figura 19.23 demuestra cómo manipular un árbol DOM mediante la programación, para mostrar su gráfico en un control TreeView. La GUI para esta aplicación contiene un control TreeView llamado xmlTreeView (declarado en XmlDom.Designer.cs). La aplicación carga el archivo carta.xml (figura 19.24) en XmlReader (línea 27) y después muestra la estructura del árbol del documento en el control TreeView.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

// Fig. 19.23: XmlDom.cs // Demuestra la manipulación de un árbol DOM. using System; using System.Windows.Forms; using System.Xml; namespace XmlDom { public partial class XmlDomForm : Form { public XmlDomForm() { InitializeComponent(); } // fin del constructor private TreeNode arbol; // referencia TreeNode // inicializa las variables de instancia private void XmlDomForm_Load( object sender, EventArgs e ) { // crea el objeto de Xml ReaderSettings y // establece la propiedad IgnoreWhitespace XmlReaderSettings configuraciones = new XmlReaderSettings(); configuraciones.IgnoreWhitespace = true; // crea el objeto XmlReader XmlReader lector = XmlReader.Create( "carta.xml", configuraciones ); arbol = new TreeNode(); // crea instancia de TreeNode arbol.Text = "carta.xml"; // asigna un nombre a TreeNode treeXml.Nodes.Add( arbol ); // agrega TreeNode al control TreeView CrearArbol( lector, arbol ); // crea la jerarquía de nodo y árbol

Figura 19.23 | Estructura DOM de un documento XML, visualizada en un control TreeView. (Parte 1 de 3).

19.8 (Opcional) Modelo de objetos de documento (DOM)

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91

709

} // fin del método XmlDomForm_Load // construye TreeView con base en el árbol DOM private void CrearArbol( XmlReader lector, TreeNode nodoArbol ) { // nodoArbol que se va a agregar al árbol existente TreeNode nuevoNodo = new TreeNode(); while ( lector.Read() ) { // crea árbol con base en el tipo de nodo switch ( lector.NodeType ) { // si es nodo de texto, agrega su valor al árbol case XmlNodeType.Text: nuevoNodo.Text = lector.Value; nodoArbol.Nodes.Add( nuevoNodo ); break; case XmlNodeType.EndElement: // si es EndElement, se mueve hacia arriba del árbol nodoArbol = nodoArbol.Parent; break; // si es nuevo elemento, agrega el nombre y recorre el árbol case XmlNodeType.Element: // determina si el elemento tiene contenido if ( !lector.IsEmptyElement ) { // asigna texto al nodo, agrega nuevoNodo como hijo nuevoNodo.Text = lector.Name; nodoArbol.Nodes.Add( nuevoNodo ); // establece nodoArbol al último hijo nodoArbol = nuevoNodo; } // fin de if else // no recorre elementos vacíos { // asigna cadena de NodeType a nuevoNodo // y lo agrega al árbol nuevoNodo.Text = lector.NodeType.ToString(); nodoArbol.Nodes.Add( nuevoNodo ); } // fin de else break; default: // para todos los demás tipos, muestra el tipo de nodo nuevoNodo.Text = lector.NodeType.ToString(); nodoArbol.Nodes.Add( nuevoNodo ); break; } // fin de switch nuevoNodo = new TreeNode(); } // fin de while // actualiza el control TreeView treeXml.ExpandAll(); // expande los nodos del árbol en el control TreeView treeXml.Refresh(); // obliga al control TreeView a actualizarse } // fin del método CrearArbol } // fin de la clase XmlDomForm } // fin del espacio de nombres XmlDom

Figura 19.23 | Estructura DOM de un documento XML, visualizada en un control TreeView. (Parte 2 de 3).

710

Capítulo 19 Lenguaje de marcado extensible (XML)

Figura 19.23 | Estructura DOM de un documento XML, visualizada en un control TreeView. (Parte 3 de 3).

[Nota: la versión de carta.xml en la figura 19.24 es casi idéntica a la de la figura 19.4, sólo que la figura 19.24 no hace referencia a una DTD, como en la línea 5 de la figura 19.4.] En el manejador del evento Load de XmlDomForm (líneas 19-34), las líneas 23-24 crean un objeto XmlReaderSettings y establecen su propiedad IgnoreWhitespace a true, de manera que se ignoren los espacios en blanco insignificantes en el documento de XML. Después, la línea 27 invoca al método static Create de XmlReader para analizar y cargar el archivo carta.xml. La línea 29 crea el objeto TreeNode llamado arbol (que se declara en la línea 16). Este objeto TreeNode se utiliza como una representación gráfica de un nodo de árbol DOM en el control TreeView. La línea 31 asigna el nombre del documento de XML (es decir, carta.xml) a la propiedad Text de arbol. La línea 32 llama al método Add para agregar el nuevo objeto TreeNode a la colección Nodes de TreeView. La línea 33 llama al método BuildTree para actualizar el control TreeView, de manera que muestre el árbol DOM de origen completo. El método CrearArbol (líneas 37-89) recibe un objeto XmlReader para leer el documento de XML, y un objeto TreeNode que hace referencia a la ubicación actual en el árbol (es decir, el objeto TreeNode que se agregó más recientemente al control TreeView). La línea 40 declara la referencia TreeNode llamada nuevoNodo, que se utilizará para agregar nuevos nodos al control TreeView. Las líneas 42-84 iteran a través de cada nodo en el árbol DOM del documento de XML. La instrucción switch en las líneas 45-81 agregan un nodo al control TreeView, con base en el nodo actual de XmlReader. Cuando se encuentra un nodo de texto, a la propiedad Text del nuevo objeto TreeNode (nuevoNodo) se le asigna el valor del nodo actual (línea 49). La línea 50 agrega este objeto TreeNode a la lista de nodos de nodoArbol (es decir, agrega el nodo al control TreeView). La línea 52 coincide con un tipo de nodo EndElement. Esta instrucción case se mueve hacia arriba del árbol, hasta el padre del nodo, ya que se encontró el final de un elemento. La línea 53 accede a la propiedad Parent de nodoArbol para recuperar el padre actual del nodo. La línea 57 coincide con los tipos de nodos Element. Cada tipo de nodo Element que no esté vacío (línea 60) incrementa la profundidad del árbol; por ende, asignamos el nombre (Name) del lector actual a la propiedad Text del nuevoNodo y agregamos ese nuevoNodo a la lista de nodos de nodoArbol (líneas 63-64). La línea 67 asigna la referencia de nuevoNodo a nodoArbol, para asegurar que nodoArbol haga referencia al último NodoArbol hijo en la lista de nodos. Si el nodo Element actual es un elemento vacío (línea 69), asignamos a la propiedad Texto de nuevoNodo la representación de cadena del TipoNodo (línea 73). A continuación, el nuevoNodo se agrega a la lista de nodos de nodoArbol (línea 74). La instrucción case predeterminada (líneas 77-80) asigna la representación de cadena del tipo de nodo a la propiedad Text de nuevoNodo, y después agrega el nuevoNodo a la lista de nodos NodoArbol. Una vez que se procesa el árbol DOM completo, la lista de nodos NodoArbol se muestra en el control TreeView (líneas 87-88). El método ExpandAll de TreeView hace que se muestren todos los nodos del árbol. El método Refresh de TreeView actualiza la pantalla para mostrar los objetos NodoArbol recién agregados. Observe que, mientras la aplicación se ejecuta, si hace clic en los nodos (es decir, en los cuadros + o –) en el control TreeView, éstos se expanden o se contraen.

19.8 (Opcional) Modelo de objetos de documento (DOM)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

711



Jane Doe Box 12345 15 Any Ave. Othertown Otherstate 67890 555-4321

John Doe 123 Main St.

Anytown Anystate 12345 555-1234

Estimado señor: Es nuestro privilegio informarle acerca de nuestra nueva base de datos administrada con XML. Este nuevo sistema le permite reducir la carga en su servidor de lista de inventario, al hacer que la máquina cliente realice el trabajo de ordenar y filtrar los datos.

Por favor visite nuestro sitio Web para ver la disponibilidad y los precios.

Sinceramente, Ms. Jane Doe

Figura 19.24 | Carta de negocios marcada como XML.

Localización de datos en documentos XML mediante XPath Aunque la clase XmlReader incluye métodos para leer y modificar los valores de los nodos, no es el medio más eficiente para localizar datos en un árbol DOM. La Biblioteca de clases del .NET Framework cuenta con la clase XPathNavigator en el espacio de nombres System.Xml.XPath para iterar a través de listas de nodos que coincidan con los criterios de búsqueda, que se escriben como expresiones de XPath. Recuerde que XPath (Lenguaje de rutas de XML) proporciona una sintaxis para localizar nodos específicos en los documentos de XML, en forma efectiva y eficiente. XPath es un lenguaje basado en cadenas de expresiones utilizadas por XML, y muchas de sus tecnologías relacionadas (como XSLT, que vimos en la sección 19.7). La figura 19.25 utiliza un objeto XPathNavigator para navegar a través de un documento de XML y utiliza un control TreeView, junto con objetos TreeNode, para mostrar la estructura del documento de XML. En este ejemplo, la lista de nodos NodoArbol se actualiza cada vez que el objeto XPathNavigator se posiciona en un nuevo nodo, en vez de mostrar todo el árbol DOM completo de una sola vez. Los nodos se agregan y se eliminan del control TreeView para reflejar la ubicación del objeto XPathNavigator en el árbol DOM. La figura 19.26 muestra

712

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Capítulo 19 Lenguaje de marcado extensible (XML)

// Fig. 19.25: NavegadorRutas.cs // Demuestra la clase XPathNavigator. using System; using System.Windows.Forms; using System.Xml.XPath; // contiene a XPathNavigator namespace NavegadorRutas { public partial class NavegadorRutasForm : Form { public NavegadorRutasForm() { InitializeComponent(); } // fin del constructor private XPathNavigator xPath; // navegador para recorrer el documento // referencia al documento para uso de XPathNavigator private XPathDocument documento; // referencia a la lista TreeNode para uso del control TreeView private TreeNode arbol; // inicializa las variables y el control TreeView private void NavegadorRutasForm_Load( object sender, EventArgs e ) { // carga el documento de XML documento = new XPathDocument( "deportes.xml" ); xPath = documento.CreateNavigator(); // crea el navegador arbol = new TreeNode(); // crea el nodo raíz para los objetos TreeNode arbol.Text = xPath.NodeType.ToString(); // #raíz rutaArbol.Nodes.Add( arbol ); // agrega árbol // actualiza el control TreeView rutaArbol.ExpandAll(); // expande el nodo del árbol en el control TreeView rutaArbol.Refresh(); // fuerza la actualización del control TreeView rutaArbol.SelectedNode = arbol; // resalta la raíz } // fin del método NavegadorRutasForm_Load // procesa el evento btnSeleccionar_Click private void btnSeleccionar_Click( object sender, EventArgs e ) { XPathNodeIterator iterador; // permite iterar por los nodos try // obtiene el nodo especificado del control ComboBox { // selecciona el nodo especificado iterador = xPath.Select( cboSeleccion.Text ); MostrarIterador( iterador ); // imprime la selección } // fin de try // atrapa expresiones inválidas catch ( System.Xml.XPath.XPathException argumentException ) { MessageBox.Show( argumentException.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin del método btnSeleccionar_Click

Figura 19.25 | Un objeto XPathNavigator, navegando por los nodos seleccionados. (Parte 1 de 4).

19.8 (Opcional) Modelo de objetos de documento (DOM)

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118

// se desplaza al primer hijo al ocurrir el evento btnPrimerHijo_Click private void btnPrimerHijo_Click( object sender, EventArgs e ) { TreeNode nuevoNodoArbol; // se desplaza al primer hijo if ( xPath.MoveToFirstChild() ) { nuevoNodoArbol = new TreeNode(); // crea nuevo nodo // establece la propiedad Text del nodo // con el nombre o el valor del navegador DeterminarTipo( nuevoNodoArbol, xPath ); // agrega nodos a la lista de nodos TreeNode arbol.Nodes.Add( nuevoNodoArbol ); arbol = nuevoNodoArbol; // asigna nuevoNodoArbol al arbol // actualiza el control TreeView rutaArbol.ExpandAll(); // expande el nodo en el control TreeView rutaArbol.Refresh(); // obliga a que el control TreeView se actualice rutaArbol.SelectedNode = arbol; // resalta la raíz } // fin de if else // el nodo no tiene hijos MessageBox.Show( "El nodo actual no tiene hijos.", "", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // fin del método btnPrimerHijo_Click // se desplaza al padre del nodo al ocurrir el evento btnPadre_Click private void btnPadre_Click( object sender, EventArgs e ) { // se desplaza al padre if ( xPath.MoveToParent() ) { arbol = arbol.Parent; // obtiene el número de nodos hijos, sin incluir subárboles int cuenta = arbol.GetNodeCount( false ); // elimina a todos los hijos for ( int i = 0; i < cuenta; i++ ) arbol.Nodes.Remove( arbol.FirstNode ); // elimina nodo hijo // actualiza el control TreeView rutaArbol.ExpandAll(); // expande el nodo en el control TreeView rutaArbol.Refresh(); // obliga a que el control TreeView se actualice rutaArbol.SelectedNode = arbol; // resalta la raíz } // fin de if else // si el nodo no tiene padre (nodo raíz) MessageBox.Show( "El nodo actual no tiene padre.", "", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // fin del método btnPadre_Click // busca el siguiente hermano al ocurrir el evento btnSiguiente_Click private void btnSiguiente_Click( object sender, EventArgs e ) { // declara e inicializa dos objetos TreeNode TreeNode nuevoNodoArbol = null; TreeNode nuevoNodo = null;

Figura 19.25 | Un objeto XPathNavigator, navegando por los nodos seleccionados. (Parte 2 de 4).

713

714

119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177

Capítulo 19 Lenguaje de marcado extensible (XML)

// se desplaza al siguiente hermano if ( xPath.MoveToNext() ) { nuevoNodoArbol = arbol.Parent; // obtiene el nodo padre nuevoNodo = new TreeNode(); // crea nuevo nodo // decide si va a mostrar el nodo actual DeterminarTipo( nuevoNodo, xPath ); nuevoNodoArbol.Nodes.Add( nuevoNodo ); // lo agrega al nodo padre arbol = nuevoNodo; // establece la posición actual de visualización // actualiza el control TreeView rutaArbol.ExpandAll(); // expande el nodo en el control TreeView rutaArbol.Refresh(); // obliga a que el control TreeView se actualice rutaArbol.SelectedNode = arbol; // resalta la raíz } // fin de if else // el nodo no tiene más hermanos MessageBox.Show( "El nodo actual es el último hermano.", "", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // fin del método btnSiguiente_Click // obtiene al hermano anterior al ocurrir el evento btnAnterior_Click private void btnAnterior_Click( object sender, EventArgs e ) { TreeNode nodoArbolPadre = null; // se desplaza al hermano anterior if ( xPath.MoveToPrevious() ) { nodoArbolPadre = arbol.Parent; // obtiene el nodo padre nodoArbolPadre.Nodes.Remove( arbol ); // elimina el nodo actual arbol = nodoArbolPadre.LastNode; // se desplaza al nodo anterior // actualiza el control TreeView rutaArbol.ExpandAll(); // expande el nodo del árbol en el control TreeView rutaArbol.Refresh(); // obliga a que el control TreeView se actualice rutaArbol.SelectedNode = arbol; // resalta la raíz } // fin de if else // si el nodo actual no tiene hermanos anteriores MessageBox.Show( "El nodo actual es el primer hermano.", "", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // fin del método btnAnterior_Click // imprime los valores para XPathNodeIterator private void MostrarIterador( XPathNodeIterator iterador ) { txtSeleccion.Clear(); // imprime los valores del nodo seleccionado while ( iterador.MoveNext() ) txtSeleccion.Text += iterador.Current.Value.Trim() + "\r\n"; } // fin del método MostrarIterador // determina si TreeNode debe mostrar el nombre o valor del nodo actual private void DeterminarTipo( TreeNode nodo, XPathNavigator xPath ) {

Figura 19.25 | Un objeto XPathNavigator, navegando por los nodos seleccionados. (Parte 3 de 4).

19.8 (Opcional) Modelo de objetos de documento (DOM)

715

178 switch ( xPath.NodeType ) // determina el valor de NodeType 179 { 180 case XPathNodeType.Element: // si es Element, obtiene su nombre 181 // obtiene el nombre del nodo actual y elimina los espacios en blanco 182 nodo.Text = xPath.Name.Trim(); 183 break; 184 default: // obtiene los valores del nodo 185 // obtiene el valor del nodo actual y elimina los espacios en blanco 186 nodo.Text = xPath.Value.Trim(); 187 break; 188 } // fin de switch 189 } // fin del método DeterminarTipo 190 } // fin de la clase NavegadorRutasForm 191 } // fin del espacio de nombres NavegadorRutas a)

b)

c)

d)

Figura 19.25 | Un objeto XPathNavigator, navegando por los nodos seleccionados. (Parte 4 de 4).

716

Capítulo 19 Lenguaje de marcado extensible (XML)

el documento de XML deportes.xml que utilizamos en este ejemplo. [Nota: Las versiones de deportes.xml que presentamos en las figuras 19.26 y 19.16 son casi idénticas. En el ejemplo actual no queremos aplicar una XSLT, por lo que omitimos la instrucción de procesamiento que se encuentra en la línea 2 de la figura 19.16.] El programa de la figura 19.25 carga el documento de XML deportes.xml (figura 19.26) en un objeto XPathDocument; para ello le pasa el nombre de archivo del documento al constructor de XPathDocument (línea 28). El método CreateNavigator (línea 29) crea y devuelve una referencia XPathNavigator a la estructura del árbol de XPathDocument. Los métodos de navegación de XPathNavigator son MoveToFirstChild (línea 66), MoveToParent (línea 92), MoveToNext (línea 121) y MoveToPrevious (línea 149). Cada método realiza la acción que su nombre implica. El método MoveToFirstChild se desplaza al primer hijo del nodo al que el objeto XPathNavigator hace referencia, MoveToParent se desplaza al nodo padre del nodo referenciado por el objeto XPathNavigator, MoveToNext se desplaza al siguiente hermano del nodo referenciado por el objeto XPathNavigator, y MoveToPrevious se desplaza al hermano anterior del nodo al que el objeto XPathNavigator hace referencia. Cada método devuelve un valor bool, indicando si el desplazamiento tuvo éxito. En este ejemplo, mostramos una advertencia en un cuadro MessageBox cada vez que falla una operación de desplazamiento. Lo que es más, se hace una llamada a cada método en el manejador de eventos del botón que coincide con su nombre (por ejemplo, al hacer clic en el botón Primer hijo en la figura 19.25(a) se activa primerHijoButton_Click, que hace una llamada a MoveToFirstChild). Cada vez que avanzamos usando XPathNavigator, como con MoveToFirstChild y MoveToNext, se agregan nodos a la lista de nodos TreeNode. El método private DeterminarTipo (líneas 176-189) determina si se va a asignar la propiedad Name o la propiedad Value del nodo al objeto TreeNode (líneas 182 y 186). Cada vez que se hace una llamada a MoveToParent, todos los hijos del nodo padre se eliminan de la pantalla. De manera similar, una llamada a MoveToPrevious elimina el nodo hermano actual. Observe que los nodos se eliminan sólo del control TreeView, no de la representación de árbol del documento.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29



Cricket

Más popular entre las comunidades de naciones.

Béisbol

Más popular en América.

Soccer (Fútbol)

El deporte más popular en el mundo.



Figura 19.26 | Documento de XML que describe varios deportes.

19.9 (Opcional) Validación de esquemas con la clase XmlReader

717

Expresión de XPath

Descripción

/deportes

Coincide con todos los nodos deportes que sean nodos hijos del nodo raíz del documento. Coincide con todos los nodos juego que sean nodos hijos de deportes, que es un hijo de la raíz del documento. Coincide con todos los nodos nombre que sean nodos hijos de juego. El nodo juego es un hijo de deportes, el cual es hijo de la raíz del documento. Coincide con todos los nodos parrafo que sean nodos hijos de juego. El nodo juego es un hijo de deportes, que es hijo de la raíz del documento. Coincide con todos los nodos juego que contengan un elemento hijo, cuyo nombre sea Cricket. El nodo juego es un hijo de deportes, que es un hijo de la raíz del documento.

/deportes/juego /deportes/juego/nombre

/deportes/juego/parrafo

/deportes/juego [nombre='Cricket']

Figura 19.27 | Expresiones de XPath y sus descripciones. El manejador de eventos seleccionarButton_Click (líneas 42-58) corresponde al botón Seleccionar. El método Select de XPathNavigator (línea 49) recibe los criterios de búsqueda en forma de una expresión XPathExpression, o de un objeto string que representa a la expresión XPath, y devuelve cualquier objeto que coincide con los criterios de búsqueda en forma de un objeto XNodeIterator. La figura 19.27 muestra un resumen de las expresiones de XPath que proporciona el control ComboBox de este programa. En la figura 19.25(b)-(d) mostramos el resultado de algunas de estas expresiones. El método MostrarIterador (definido en las líneas 166-173) adjunta al control seleccionTextBox los valores de los nodos del objeto XPathNodeIterator dado. Observe que llamamos al método string Trim para eliminar el espacio en blanco innecesario. El método MoveNext (línea 171) avanza al siguiente nodo, al que la propiedad Current (línea 172) puede acceder.

19.9 (Opcional) Validación de esquemas con la clase XmlReader En la sección 19.6 vimos que los esquemas proporcionan los medios para especificar la estructura de un documento de XML, y para validar los documentos de XML. Dicha validación ayuda a una aplicación a determinar si un documento específico que recibe está completo, ordenado en forma apropiada y si no le faltan datos. En la sección 19.6 utilizamos un validador de esquemas XSD en línea, para verificar que un documento XML se conforme a un esquema de XML. En esta sección le mostraremos cómo realizar el mismo tipo de validación mediante la programación, usando las clases que proporciona el .NET Framework.

Validación de un documento de XML mediante la programación La clase XmlReader puede validar un documento de XML a medida que lo lee y analiza. En este ejemplo, demostraremos cómo activar dicha validación. El programa en la figura 19.28 valida un documento de XML seleccionado por el usuario; ya sea libro.xml (figura 19.11) o falla.xml (figura 19.29), y lo compara con el documento de XML Schema libro.xsd (figura 19.12). La línea 17 crea la variable XmlSchemaSet llamada esquemas. Un objeto de la clase XmlSchemaSet almacena una colección de esquemas, para validarlos con un objeto XmlReader. La línea 23 asigna un nuevo objeto XmlSchemaSet a la variable esquemas, y la línea 26 llama al método Add de este objeto para agregar un esquema a la colección. El método Add recibe como argumentos un URI de espacio de nombres que identifica al esquema (http://www.deitel.com/listalibros), junto con el nombre y la ubicación del archivo de esquema (libro.xsd en el directorio actual). Las líneas 29-32 crean y establecen las propiedades de un objeto XmlReaderSettings. La línea 30 establece la propiedad ValidationType del objeto XmlReaderSettings al valor ValidationType.Schema, indicando que deseamos que el objeto XmlReader realice la validación con un esquema, a medida que vaya leyendo un

718

Capítulo 19 Lenguaje de marcado extensible (XML)

documento de XML. La línea 31 establece la propiedad Schemas del objeto XmlReaderSettings a esquemas. Esta propiedad establece el (los) esquema(s) utilizado(s) para validar el documento que lee el objeto XmlReader. La línea 32 registra el método ValidationError con la propiedad ValidationEventHandler del objeto configuraciones. El método ErrorValidacion (líneas 52-56) se llama si el documento que se está leyendo es inválido, o si ocurre un error (por ejemplo, si el documento no puede encontrarse). Si no se registra un método con ValidationEventHandler, se lanza una excepción (XmlException) cuando el documento XML resulta ser inválido o no se encuentra. Después de establecer las propiedades ValidationType, Schemas y ValidationEventHandler del objeto XmlReaderSettings, estamos listos para crear un objeto XmlReader de validación. Las líneas 35-36 crean un objeto XmlReader que lee el archivo que seleccionó el usuario del control ComboBox cboArchivos, y lo valida con el esquema libro.xsd. La validación se realiza nodo por nodo, mediante llamadas al método Read del objeto XmlReader (línea 39). Debido a que establecimos la propiedad ValidationType de XmlReaderSettings a ValidationType.Schema, cada llamada a Read valida el siguiente nodo en el documento. El ciclo termina cuando se validan todos los nodos, o cuando falla la validación en uno de los nodos.

Detección de un documento de XML inválido El programa en la figura 19.28 valida el documento de XML libro.xml (figura 19.12) con el esquema libro. xsd (figura 19.11) con éxito. No obstante, cuando el usuario selecciona el documento de XML de la figura 19.29,

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

// Fig. 19.28: PruebaValidacion.cs // Validación de documentos XML con esquemas. using System; using System.Windows.Forms; using System.Xml; using System.Xml.Schema; // contiene la clase XmlSchemaSet namespace PruebaValidacion { public partial class PruebaValidacionForm : Form { public PruebaValidacionForm() { InitializeComponent(); } // fin del constructor private XmlSchemaSet esquemas; // esquemas con los que se va a validar private bool valido = true; // resultado de la validación // maneja el evento Click del botón Validar private void btnValidar_Click( object sender, EventArgs e ) { esquemas = new XmlSchemaSet(); // crea la clase XmlSchemaSet // agrega el esquema a la colección esquemas.Add( "http://www.deitel.com/listalibros", "libro.xsd" ); // establece las configuraciones de validación XmlReaderSettings configuraciones = new XmlReaderSettings(); configuraciones.ValidationType = ValidationType.Schema; configuraciones.Schemas = esquemas; configuraciones.ValidationEventHandler += ErrorValidacion; // crea el objeto XmlReader

Figura 19.28 | Ejemplo de validación de esquemas. (Parte 1 de 2).

19.9 (Opcional) Validación de esquemas con la clase XmlReader

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

719

XmlReader lector = XmlReader.Create( cboArchivos.Text, configuraciones ); // analiza el archivo while ( lector.Read() ); // cuerpo vacío if ( valido ) // comprueba el resultado de la validación { lblConsola.Text = "El documento es válido"; } // fin de if valido = true; // restablece la variable lector.Close(); // cierra el flujo del lector } // fin del método btnValidar_Click // manejador de eventos para el error de validación private void ErrorValidacion( object sender , ValidationEventArgs arguments ) { lblConsola.Text = arguments.Message; valido = false; // falló la validación } // fin del método ErrorValidacion } // fin de la clase PruebaValidacionForm } // fin del espacio de nombres PruebaValidacion

Figura 19.28 | Ejemplo de validación de esquemas. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18



Visual Basic 2005 How to Program, 3/e

Visual C# 2005 How to Program

Java How to Program, 6/e

Figura 19.29 | Archivo de XML que no se conforma al documento de XML Schema en la figura 19.12. (Parte 1 de 2).

720

19 20 21 22 23 24 25 26

Capítulo 19 Lenguaje de marcado extensible (XML)

C++ How to Program, 5/e Internet and World Wide Web How to Program, 3/e

XML How to Program

Figura 19.29 | Archivo de XML que no se conforma al documento de XML Schema en la figura 19.12. (Parte 2 de 2).

la validación falla; el elemento libro en las líneas 18-21 contiene más de un elemento titulo. Cuando el programa encuentra el nodo inválido, se hace una llamada al método ErrorValidacion (líneas 51-56 de la figura 19.28), con lo cual se muestra un mensaje explicando por qué falló la validación.

19.10 (Opcional) XSLT con la clase XslCompiledTransform En la sección 19.7 vimos que XSLT proporciona elementos que definen las reglas para convertir un tipo de documento de XML en otro tipo de documento de XML. Mostramos cómo transformar documentos de XML en documentos de XHTML, y los visualizamos en Internet Explorer. El procesador XSLT incluido en Internet Explorer (es decir, MSXML) realizó las transformaciones.

Realización de una transformación XSL en C#, usando el .NET Framework La figura 19.30 aplica la hoja de estilos deportes.xsl (figura 19.17) a deportes.xml (figura 19.26) mediante programación. El resultado de la transformación se escribe en un archivo de XHTML en el disco, y se muestra en un cuadro de texto. La figura 19.30(c) muestra el documento de XHTML resultante (deportes.html), visto en Internet Explorer. La línea 5 es una declaración using para el espacio de nombres System.Xml.Xsl, el cual contiene la clase XslCompiledTransform para aplicar hojas de estilos XSLT a documentos de XML. La línea 17 declara la referencia XslCompiledTransform llamada transformador. Un objeto de este tipo sirve como un procesador XSLT (como MSXML en los ejemplos anteriores) para transformar datos XML de un formato a otro.

1 2 3 4 5 6 7 8 9

// Fig. 19.30: PruebaTransformacion.cs // Aplicación de una hoja de estilos XSLT a un documento de XML. using System; using System.Windows.Forms; using System.Xml.Xsl; // contiene la clase XslCompiledTransform namespace PruebaTransformacion { public partial class PruebaTransformacionForm : Form

Figura 19.30 | Hoja de estilos XSLT aplicada a un documento de XML. (Parte 1 de 2).

19.10 (Opcional) XSLT con la clase XslCompiledTransform

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

721

{ public PruebaTransformacionForm() { InitializeComponent(); } // fin del constructor // aplica la transformación private XslCompiledTransform transformador; // inicializa las variables private void PruebaTransformacionForm_Load( object sender, EventArgs e ) { transformador = new XslCompiledTransform(); // crea el transformador // carga y compila la hoja de estilos transformador.Load( "deportes.xsl" ); } // fin del método PruebaTransformacionForm_Load // transforma los datos XML al ocurrir el evento Click del botón Transformar XML private void btnTransformar_Click( object sender, EventArgs e ) { // realiza la transformación y almacena el resultado en un nuevo archivo transformador.Transform( "deportes.xml", "deportes.html" ); // lee y muestra el texto del documento de XHTML en un control Textbox txtConsola.Text = System.IO.File.ReadAllText( "deportes.html" ); } // fin del método btnTransformar_Click } // fin de la clase PruebaTransformacionForm } // fin del espacio de nombres PruebaTransformacion

a)

b)

c)

Figura 19.30 | Hoja de estilos XSLT aplicada a un documento de XML. (Parte 2 de 2).

722

Capítulo 19 Lenguaje de marcado extensible (XML)

En el manejador de eventos PruebaTransformacionForm_Load (líneas 20-26), la línea 22 crea un nuevo objeto XslCompiledTransform. Después la línea 25 llama al método Load del objeto XslCompiledTransform, que analiza y carga la hoja de estilos que utiliza esta aplicación. Este método recibe un argumento que especifica el nombre y la ubicación de la hoja de estilos, deportes.xsl (figura 19.17), que se encuentra en el directorio actual. El manejador de eventos transformarButton_Click (líneas 29-37) llama al método Transform de la clase XslCompiledTransform para aplicar la hoja de estilos (deportes.xsl) a deportes.xml (línea 32). Este método recibe dos argumentos string; el primero especifica el archivo XML al que debe aplicarse la hoja de estilos, y el segundo especifica el archivo en el que debe almacenarse el resultado de la transformación en el disco. Por ende, la llamada al método Transform en la línea 32 transforma a deportes.xml en XHTML y escribe el resultado en disco como el archivo deportes.html. La figura 19.30(c) muestra el nuevo documento de XHTML representado en Internet Explorer. Observe que la salida es idéntica a la de la figura 19.16; no obstante, en el ejemplo actual, el XHTML se almacena en disco, en vez de que MSXML lo genere en forma dinámica. Después de aplicar la transformación, el programa muestra el contenido del archivo deportes.html recién creado en el control TextBox llamado txtConsola, como se muestra en la figura 19.30(b). Las líneas 35-36 obtienen el texto del archivo, pasando su nombre al método ReadAllText de la clase System.IO.File que proporciona la FCL, para simplificar las tareas de procesamiento de archivos en el sistema local.

19.11 Conclusión En este capítulo estudiamos el Lenguaje de marcado extensible y varias de sus tecnologías relacionadas. Empezamos hablando sobre cierta terminología básica de XML, y presentamos los conceptos del marcado, los vocabularios de XML y los analizadores de XML (de validación y no validadores). Después demostramos cómo describir y estructurar datos en XML, ilustrando estos puntos con ejemplos, marcando un artículo y una carta de negocios. En este capítulo se describió también el concepto de un espacio de nombres de XML. Usted aprendió que un espacio de nombres tiene un nombre único, que proporciona los medios para que los autores de documentos se refieran sin ambigüedades a los elementos que tienen el mismo nombre (es decir, evitando el conflicto de nombres). Presentamos ejemplos de cómo definir dos espacios de nombres en el mismo documento, así como de establecer el espacio de nombres predeterminado para un documento. También vimos cómo crear DTDs y esquemas para especificar y validar la estructura de un documento de XML. Mostramos cómo utilizar varias herramientas para confirmar si los documentos de XML son válidos (es decir, si se conforman a una DTD o a un esquema). En este capítulo se demostró además cómo crear y utilizar documentos de XML, para especificar las reglas de conversión de documentos de XML en varios formatos. En específico, aprendió a dar formato y ordenar datos XML como XHTML, para mostrarlos en un explorador Web. Las secciones finales del capítulo presentaron usos más avanzados de XML en aplicaciones en C#. Demostramos cómo recuperar y mostrar datos de un documento de XML, usando varias clases del .NET Framework. Ilustramos cómo un árbol del Modelo de objetos de documento (DOM) representa a cada uno de los elementos de un documento de XML como un nodo en el árbol. También se demostró cómo leer datos de un documento de XML mediante el uso de la clase XmlReader, cómo mostrar árboles DOM en forma gráfica y cómo localizar datos en documentos de XML con XPath. Por último, mostramos cómo utilizar las clases XmlReader y XslCompiledTransform del .NET Framework para realizar la validación de esquemas y las transformaciones XSL, respectivamente. En el capítulo 20 empezaremos nuestra discusión sobre las bases de datos, que organizan datos de tal forma que éstos puedan seleccionarse y actualizarse con rapidez. Presentamos el Lenguaje de consultas estructurado (SQL) para escribir consultas simples de bases de datos (es decir, búsquedas) y ADO.NET para manipular la información en una base de datos a través de C#. En este capítulo aprenderá a crear documentos de XML con base en los datos contenidos en una base de datos.

19.12 Recursos Web

723

19.12 Recursos Web www.w3.org/XML

El W3C (Consorcio World Wide Web) facilita el desarrollo de protocolos comunes para asegurar la interoperabilidad en la Web. Su página XML incluye información acerca de los próximos eventos, publicaciones, software, grupos de discusión y los desarrollos más recientes en XML. www.xml.org xml.org es una referencia para XML, DTDs, esquemas y espacios de nombres. www.w3.org/Style/XSL

Este sitio del W3C proporciona información acerca de XSL, incluyendo temas tales como el desarrollo con XSL, el aprendizaje de XSL, las herramientas compatibles con XSL; la especificación XSL, FAQs y antecedentes sobre XSL. www.w3.org/TR

Éste es el sitio Web del W3C, acerca de los informes técnicos y las publicaciones. Contiene vínculos a anteproyectos funcionales, recomendaciones propuestas y otros recursos. www.xmlbooks.com

Este sitio proporciona una lista de libros sobre XML recomendados por Charles Goldfarb, uno de los diseñadores originales de GML (Lenguaje de marcado general), del cual se derivan SGML, HTML y XML. www.xml-zone.com

El sitio DevX XML Zone es un recurso completo de información sobre XML. Este sitio incluye preguntas frecuentes (FAQs), noticias, artículos y vínculos a otros sitios y grupos de noticias relacionados con XML. wdvl.internet.com/Authoring/Languages/XML

El sitio Web Developer’s Virtual Library XML incluye tutoriales, FAQs, las noticias más recientes y muchos vínculos a sitios de XML y descargas de software. www.xml.com

Este sitio proporciona las noticias e información más reciente acerca de XML, listados de conferencias, vínculos a recursos Web de XML organizados por tema, herramientas y otros recursos. msdn.microsoft.com/xml/default.aspx

El Centro de desarrollo de XML de MSDN contiene artículos sobre XML, sesiones de chat “Pregunte a los expertos”, ejemplos, demos, grupos de noticias y demás información útil. www.oasis-open.org/cover/xml.html

Este sitio incluye vínculos a FAQs, recursos en línea, iniciativas de la industria, demos, conferencias y tutoriales. www-106.ibm.com/developerworks/xml

El sitio IBM developerWorks de XML es un gran recurso para desarrolladores, ya que ofrece noticias, herramientas, una biblioteca, casos de estudio e información acerca de eventos y estándares relacionados con XML. www.devx.com/projectcool/door/7051

El sitio ProjectCool DevX incluye varios tutoriales que tratan acerca de temas de XML, desde introductorios hasta avanzados. www.ucc.ie/xml

Este sitio proporciona una FAQ detallada sobre XML. Los desarrolladores pueden leer respuestas a ciertas preguntas populares, o enviar sus propias preguntas por medio del sitio. www.w3.org/XML/Schema

Este sitio del W3C proporciona información acerca de XML Schema, incluyendo vínculos, herramientas, recursos y la especificación XML Schema. tools.decisionsoft.com/schemaValidate.html

DecisionSoft proporciona un validador de XML Schema gratuito en línea. www.sun.com/software/xml/developers/multischema

La página Web para descargar el Validador de XML Multi-Schema de Sun (MSV), el cual valida documentos de XML con varios tipos de esquemas.

724

Capítulo 19 Lenguaje de marcado extensible (XML)

en.wikipedia.org/wiki/EBNF

Este sitio proporciona información detallada acerca de la Forma de Backus-Naur extendida (EBNF). www.garshol.priv.no/download/text/bnf.html

Este sitio introduce la Forma de Backus-Naur (BNF) y la EBNF, y habla sobre las diferencias entre estas dos notaciones. www.w3schools.com/schema/default.asp

Este sitio proporciona un tutorial sobre XML Schema. www.w3.org/2001/03/webdata/xsv

Ésta es una herramienta en línea para validar documentos de XML Schema. www.w3.org/TR/xmlschema-2

Ésta es la parte 2 de la especificación XML Schema del W3C, la cual define los tipos de datos permitidos en un XML Schema.

20

Bases de datos, SQL y ADO.NET

Es un error capital teorizar antes de tener datos. —Arthur Conan Doyle

OBJETIVOS Q

Acerca del modelo de bases de datos relacionales.

Q

Escribir consultas básicas de bases de datos en SQL.

Ahora ve, escríbelo en una tablilla, grábalo en un libro, y que dure hasta el último día, para testimonio hasta siempre.

Q

Agregar orígenes de datos a los proyectos.

—Santa Biblia, Isaías 30:8

Q

Utilizar las herramientas de “arrastrar y colocar” del IDE para mostrar tablas de bases de datos en aplicaciones.

En este capítulo aprenderá lo siguiente:

Q

Utilizar las clases de los espacios de nombres System. y System.Data.SqlClient para manipular bases de datos.

Primero consiga los hechos, y después podrá distorsionarlos según le parezca.

Data

—Mark Twain

Q

Utilizar el modelo de objetos sin conexión de ADO.NET para almacenar datos de una base de datos en la memoria local.

Me gustan dos tipos de hombres: domésticos y foráneos.

Q

Crear documentos de XML a partir de orígenes de datos.

—Mae West

Pla n g e ne r a l

726

20.1 20.2 20.3 20.4

Capítulo 20 Bases de datos: SQL y ADO.NET

20.5 20.6

20.7 20.8 20.9 20.10 20.11

Introducción Bases de datos relacionales Generalidades acerca de las bases de datos relacionales: la base de datos Libros SQL 20.4.1 Consulta SELECT básica 20.4.2 Cláusula WHERE 20.4.3 Cláusula ORDER BY 20.4.4 Mezcla de datos de varias tablas: INNER JOIN 20.4.5 Instrucción INSERT 20.4.6 Instrucción UPDATE 20.4.7 Instrucción DELETE Modelo de objetos ADO.NET Programación con ADO.NET: extraer información de una base de datos 20.6.1 Mostrar una tabla de base de datos en un control DataGridView 20.6.2 Cómo funciona el enlace de datos Consulta de la base de datos Libros Programación con ADO.NET: caso de estudio de libreta de direcciones Uso de un objeto DataSet para leer y escribir XML Conclusión Recursos Web

20.1 Introducción

Una base de datos es una colección organizada de datos. Existen muchas estrategias para organizar datos, de manera que se facilite el acceso y la manipulación de los mismos. Un sistema de administración de bases de datos (DBMS) proporciona mecanismos para almacenar, organizar, recuperar y modificar datos para muchos usuarios. Los sistemas de administración de bases de datos permiten el acceso a los datos y su almacenamiento, de manera independiente a la representación interna de los datos. Los sistemas de bases de datos más populares en la actualidad son las bases de datos relacionales. SQL es el lenguaje estándar internacional que se utiliza casi de manera universal con las bases de datos relacionales, para realizar consultas (es decir, para solicitar información que cumpla con ciertos criterios dados) y para manipular datos. Algunos sistemas de administración de bases de datos relacionales (RDBMS ) son: Microsoft SQL Server, Oracle, Sybase, IBM DB2 y PostgreSQL. En la Sección 20.11, Recursos Web, proporcionamos URLs para estos sistemas. MySQL (www.mysql.com) es un RDBMS de código fuente abierto que se está volviendo cada vez más popular, el cual puede descargarse y utilizarse libremente para fines no comerciales. Tal vez usted también esté familiarizado con Microsoft Access, un sistema de bases de datos relacionales que forma parte de Microsoft Office. En este capítulo, utilizamos Microsoft SQL Server 2005 Express: una versión gratuita de SQL Server 2005 que se instala al momento en que instalamos Visual C# 2005. Nos referiremos a SQL Server 2005 Express simplemente como SQL Server desde este punto en adelante. Un lenguaje de programación se conecta e interactúa con una base de datos relacional a través de una interfaz de base de datos: un software que facilita la comunicación entre un sistema de administración de bases de datos y un programa. Los programas en C# se comunican con las bases de datos y manipulan sus datos a través de ADO.NET. La versión actual de ADO.NET es 2.0. La sección 20.5 presenta las generalidades acerca del modelo de objetos de ADO.NET, junto con los espacios de nombres y clases relevantes que le permitirán trabajar con las bases de datos en C#. Sin embargo, como aprenderá en las siguientes secciones, la mayor parte del trabajo requerido para comunicarse con una base de datos mediante el uso de ADO.NET 2.0 la realiza el mismo IDE. Usted trabajará principalmente con las herramientas y asistentes de programación visual del IDE, que simplifican

20.2 Bases de datos relacionales

727

el proceso de conectarse a, y de manipular una base de datos. A lo largo de este capítulo, nos referiremos a ADO. NET 2.0 simplemente como ADO.NET. En este capítulo se presentan los conceptos generales de las bases de datos relacionales y SQL, y después se explora ADO.NET y las herramientas del IDE para acceder a los orígenes de datos. Los ejemplos en las secciones 20.6-20.9 demuestran cómo crear aplicaciones que utilicen bases de datos para almacenar información. En los siguientes dos capítulos podrá ver otras aplicaciones prácticas de las bases de datos. El Capítulo 21, ASP.NET 2.0, formularios Web Forms y controles Web, presenta un caso de estudio de una librería basada en Web que recupera la información de usuarios y libros de una base de datos. El capítulo 22, Servicios Web, utiliza una base de datos para almacenar los datos de reservación de una aerolínea para un servicio Web (es decir, un componente de software al que se puede acceder en forma remota, a través de una red).

20.2 Bases de datos relacionales

Una base de datos relacional es una representación lógica de datos, que permite acceder a los mismos de manera independiente de su estructura física; además, organiza los datos en tablas. La figura 20.1 ilustra una tabla de ejemplo llamada Empleados, que podría usarse en un sistema de administración de personal. La tabla almacena los atributos de los empleados. Las tablas están compuestas de filas y columnas, en las que se almacenan los valores. Esta tabla consta de seis filas y cinco columnas. La columna Numero de cada fila en esta tabla es la clave primaria; una columna (o grupo de columnas), que requiere un valor único que no puede duplicarse en otras filas. Esto garantiza que pueda usarse un valor de clave primaria para identificar a una fila en forma única. Una clave primaria que está compuesta de dos o más columnas se conoce como clave compuesta. Algunos buenos ejemplos de columnas de clave primaria en otras aplicaciones son el número de identificación (ID) de un empleado en un sistema de nóminas, y el número de pieza en un sistema de inventarios; se garantiza que los valores en cada una de estas columnas serán únicos. Las filas en la figura 20.1 se muestran en orden por clave primaria. En este caso, las filas se listan en orden incremental (ascendente), pero también podrían listarse en orden decremental (descendente), o desordenadas. Como demostraremos en nuestro siguiente ejemplo, los programas pueden especificar criterios de ordenamiento al solicitar datos de una base de datos. Cada columna representa un atributo de datos distinto. Por lo general, las filas son únicas (por clave primaria) dentro de una tabla, pero algunos valores de las columnas podrían duplicarse entre las filas. Por ejemplo, tres filas distintas en la columna Departamento de la tabla Empleados contienen el número 413, lo cual indica que estos empleados trabajan en el mismo departamento. A menudo, los distintos usuarios de una base de datos se interesan en distintos datos y en distintas relaciones entre los datos. La mayoría de los usuarios sólo requiere subconjuntos de las filas y las columnas. Para obtener estos subconjuntos, los programas utilizan SQL para definir consultas que seleccionen subconjuntos de los datos de una tabla. Por ejemplo, un programa podría seleccionar datos de la tabla Empleados para crear el resultado de una consulta que muestre en dónde está ubicado cada departamento, en orden ascendente, por número de Departamento (figura 20.2). En la sección 20.4 hablaremos sobre las consultas de SQL. En la sección 20.7, aprenderá a utilizar el Generador de consultas del IDE para crear consultas de SQL.

IVWaV Empleados

;^aV

Salario

Ubicacion

413

1100

New Jersey

413

2000

New Jersey

Larson

642

1800

Los Angeles

35761

Myers

611

1400

Orlando

47132

Neumann

413

9000

New Jersey

78321

Stephens

611

8500

Orlando

Numero

Nombre

23603

Jones

24568

Kerwin

34589

8aVkZeg^bVg^V

Departamento

8dajbcV

Figura 20.1 | Datos de ejemplo de la tabla Empleados.

728

Capítulo 20 Bases de datos: SQL y ADO.NET

Departamento

Ubicacion

413 611 642

New Jersey Orlando Los Angeles

Figura 20.2 | Resultado de seleccionar distintos datos de Departamento y Ubicacion de la tabla Empleados.

20.3 Generalidades acerca de las bases de datos relacionales: la base de datos Libros Ahora veremos las generalidades sobre las bases de datos relacionales, en el contexto de una base de datos simple llamada Libros. Esta base de datos almacena información acerca de algunas publicaciones recientes de Deitel. Primero veremos las tablas de la base de datos Libros. Después presentaremos los conceptos de las bases de datos, como la manera de usar SQL para recuperar información de la base de datos Libros y manipular esos datos. Le proporcionaremos el archivo de la base de datos (Libros.mdf) con los ejemplos para este capítulo (puede descargarlos de pearsoneducacion.net/deitel, o si prefiere la versión en inglés vaya a www.deitel.com/books/ csharpforprogrammers2). Por lo general, los archivos de bases de datos de SQL Server terminan con la extensión de archivo .mdf (“archivo de datos maestro”). La sección 20.6 explica cómo utilizar este archivo en una aplicación.

Tabla Autores de la base de datos Libros La base de datos consta de tres tablas: Autores, ISBNAutor y Titulos. La tabla Autores (descrita en la figura 20.3) cuenta con tres columnas que mantienen el número de ID único para cada autor, su primer nombre y su apellido paterno, respectivamente. La figura 20.4 contiene los datos de la tabla Autores. Listamos las filas en orden, con base en la clave primaria de la tabla: IDAutor. En la sección 20.4.3 aprenderá a ordenar los datos con base en otros criterios (por ejemplo, en orden alfabético por apellido paterno) mediante el uso de la cláusula ORDER BY de SQL.

Columna

Descripción

IDAutor

El número de ID del autor; en la base de datos Libros, esta columna de tipo entero se define como una columna de identidad, también conocida como columna autoincremental; para cada fila que se inserta en la tabla, el valor de IDAutor se incrementa en 1 de manera automática, para asegurar que cada fila tenga un IDAutor único. Ésta es la clave primaria. El primer nombre del autor (una cadena de caracteres). El apellido paterno del autor (una cadena de caracteres).

PrimerNombre ApellidoPaterno

Figura 20.3 | La tabla Autores de la base de datos Libros.

IDAutor

PrimerNombre

ApellidoPaterno

1

Harvey

Deitel

2

Paul

Deitel

3

Andrew

Goldberg

4

David

Choffnes

Figura 20.4 | Datos de la tabla Autores, de la base de datos Libros.

20.3 Generalidades acerca de las bases de datos relacionales: la base de datos Libros

729

Tabla Titulos de la base de datos Libros La tabla Titulos (descrita en la figura 20.5) tienen cuatro columnas que mantienen información acerca de cada libro en la base de datos, incluyendo el ISBN, título, número de edición y año de copyright. La figura 20.6 contiene los datos de la tabla Titulos.

Tabla ISBNAutor de la base de datos Libros La tabla ISBNAutor (descrita en la figura 20.7) cuenta con dos columnas que mantienen números ISBN para cada libro y sus correspondientes números de ID del autor. Esta tabla asocia a los autores con sus libros. La columna IDAutor es una clave externa; una columna en esta tabla que coincide con la columna de clave primaria en otra tabla (es decir, IDAutor en la tabla Autores). La columna ISBN también es una clave externa; concuerda con la columna de clave primaria (es decir, ISBN) en la tabla Titulos. Tanto la columna IDAutor como ISBN, en conjunto, forman, en esta tabla, la clave primaria. Cada fila en esta tabla relaciona en forma única a un autor con el ISBN de un libro. La figura 20.8 contiene los datos de la tabla ISBNAutor, de la base de datos Libros.

Claves externas Las claves externas pueden especificarse al momento de crear una tabla. Una clave externa ayuda a mantener la Regla de integridad referencial; todo valor de clave externa debe aparecer como valor de clave primaria de otra tabla. Esto permite a los DBMS determinar si el valor de IDAutor para una fila específica de la tabla ISBNAutor es válida. Las claves externas también permiten seleccionar datos relacionados en varias tablas de esas tablas; esto se conoce como unir los datos. (En la sección 20.4.4 aprenderá a unir datos mediante el uso del operador INNER JOIN de SQL.) Hay una relación de uno a varios entre una clave primaria y su correspondiente clave externa (por ejemplo, un autor puede escribir muchos libros). Esto significa que una clave externa puede aparecer muchas veces en su propia tabla, pero sólo una vez (como la clave primaria) en otra tabla. Por ejemplo, el ISBN 0131450913 puede aparecer en varias filas de ISBNAutor (ya que este libro tiene varios autores), pero sólo puede aparecer una vez en Titulos, en donde ISBN es la clave primaria. Columna

Descripción

ISBN

El ISBN del libro (una cadena de caracteres); que es la clave primaria de la tabla. ISBN es una abreviación de “Número internacional estándar del libro”, un esquema de numeración que utilizan las editoriales en todo el mundo para dar a cada libro un número único de identificación. El título del libro (una cadena de caracteres). Número de edición del libro (un entero). Año de copyright del libro (una cadena de caracteres).

Titulo NumeroEdicion Copyright

Figura 20.5 | La tabla Titulos de la base de datos Libros.

ISBN

Titulo

NumeroEdicion

Copyright

0131426443

C How to Program

4

2004

0131450913

Internet & World Wide Web How to Program

3

2004

0131483986

Java How to Program

6

2005

0131525239

Visual C# 2005 How to Program

2

2006

0131828274

Operating Systems

3

2004

0131857576

C++ How to Program

5

2005

0131869000

Visual Basic 2005 How to Program

3

2006

Figura 20.6 | Datos de la tabla Titulos de la base de datos Libros.

730

Capítulo 20 Bases de datos: SQL y ADO.NET

Columna

Descripción

IDAutor

El número de ID del autor, una clave externa para la tabla Autores. El ISBN de un libro, una clave externa para la tabla Titulos.

ISBN

Figura 20.7 | La tabla ISBNAutor de la base de datos Libros. IDAutor

ISBN

IDAutor

ISBN

1

0131869000

2

0131450913

1

0131525239

2

0131426443

1

0131483986

2

0131857576

1

0131857576

2

0131483986

1

0131426443

2

0131525239

1

0131450913

2

0131869000

1

0131828274

3

0131450913

2

0131828274

4

0131828274

Figura 20.8 | Datos de la tabla ISBNAutor de Libros.

Diagrama de entidad relación para la base de datos Libros

La figura 20.9 es un diagrama de entidad relación (ER ) para la base de datos Libros. Este diagrama muestra las tablas en la base de datos y las relaciones entre ellas. El primer compartimiento en cada cuadro contiene el nombre de la tabla. Los nombres en cursiva son claves primarias (por ejemplo, IDAutor en la tabla Autores). La clave primaria de una tabla identifica en forma única a cada fila de la misma. Cada fila debe tener un valor en la columna de clave primaria, y el valor de la clave debe ser único en la tabla. Esto se conoce como la Regla de integridad de entidades. Observe que los nombres IDAutor e ISBN en la tabla ISBNAutor están en cursiva; en conjunto, éstos forman una clave primaria compuesta para la tabla ISBNAutor.

Error común de programación 20.1 Si no se proporciona un valor para cada columna en una clave primaria, se rompe la Regla de integridad de las entidades y el DBMS reporta un error.

Error común de programación 20.2 Si se proporciona el mismo valor para la clave primaria en varias filas, se rompe la Regla de integridad de las entidades y el DBMS reporta un error.

Las líneas que conectan a las tablas en la figura 20.9 representan las relaciones entre las tablas. Considere la línea entre las tablas Autores e ISBNAutor. En el extremo de la línea correspondiente a Autores hay un 1, y en el extremo correspondiente a ISBNAutor hay un símbolo de infinito (∞). Esto indica una relación de uno a varios; para cada autor en la tabla Autores, puede haber un número arbitrario de números ISBN para los libros escritos por ese autor en la tabla ISBNAutor (es decir, un autor puede escribir cualquier número de libros). Observe que la línea de relación vincula a la columna IDAutor de la tabla Autores (en donde IDAutor es la clave primaria) con la columna IDAutor en la tabla ISBNAutor (en donde IDAutor es una clave externa); la línea entre las tablas vincula a la clave primaria con la correspondiente clave externa.

Error común de programación 20.3 Si se proporciona un valor de clave externa que no aparezca como valor de clave primaria en otra tabla, se rompe la Regla de integridad referencial y el DBMS reporta un error.

20.4 SQL

Autores

Titulos

ISBNAutor 1

IDAutor PrimerNombre

IDAutor ISBN

ApellidoPaterno

731

1

ISBN Titulo NumeroEdicion Copyright

Figura 20.9 | Diagrama de entidad-relación para la base de datos Libros. La línea entre las tablas Titulos e ISBNAutor ilustra una relación de uno a varios; varios autores pueden escribir un libro. Observe que la línea entre las tablas vincula a la clave primaria ISBN de la tabla Titulos con la correspondiente clave externa en la tabla ISBNAutor. Las relaciones en la figura 20.9 ilustran que el único propósito de la tabla ISBNAutor es proporcionar una relación de varios a varios entre las tablas Autores y Titulos; un autor puede escribir varios libros; y un libro puede tener varios autores.

20.4 SQL Ahora veremos las generalidades acerca de SQL, en el contexto de la base de datos Libros. Más adelante en este capítulo creará aplicaciones en C# que ejecutan consultas de SQL y acceden a sus resultados mediante el uso de la tecnología ADO.NET. Aunque el IDE de Visual C# proporciona herramientas visuales que ocultan parte del SQL que se utiliza para manipular bases de datos, es sin duda importante comprender los fundamentos de SQL. Conocer los tipos de operaciones que usted puede realizar le ayudará a desarrollar aplicaciones más avanzadas, que hagan un uso extenso de las bases de datos. La figura 20.10 lista algunas palabras clave de SQL comunes, que se utilizan para formar instrucciones de SQL completas; hablaremos sobre estas palabras clave en las siguientes subsecciones. Existen otras palabras clave de SQL, pero están más allá del alcance de este libro. Para obtener información adicional acerca de SQL, consulte los URLs listados en la Sección 20.11, Recursos Web.

20.4.1 Consulta SELECT básica

Vamos a considerar varias consultas de SQL que recuperan información de la base de datos Libros. Una consulta de SQL “selecciona” filas y columnas de una o más tablas en una base de datos. Las consultas realizan dichas selecciones con la palabra clave SELECT. La forma básica de una consulta SELECT es: SELECT * FROM nombreTabla

Palabra clave de SQL

Descripción

SELECT

Recupera datos de una o más tablas. Especifica las tablas involucradas en una consulta. Se requiere en todas las consultas. Especifica criterios opcionales para la selección, los cuales determinan las filas que se van a recuperar, eliminar o actualizar. Especifica criterios opcionales para ordenar filas (por ejemplo, en orden ascendente o descendente). Especifica un operador opcional para mezclar filas de varias tablas. Inserta filas en una tabla especificada. Actualiza filas en una tabla especificada. Elimina filas de una tabla especificada.

FROM WHERE ORDER BY INNER JOIN INSERT UPDATE DELETE

Figura 20.10 | Palabras clave comunes de SQL.

732

Capítulo 20 Bases de datos: SQL y ADO.NET

en donde el asterisco (*) indica que deben recuperarse todas las columnas de la tabla nombreTabla. Por ejemplo, para recuperar todos los datos en la tabla Autores, use: SELECT * FROM Autores

Observe que no se garantiza que las filas de la tabla Autores se devuelvan en un orden específico. En la sección 20.4.3 aprenderá a especificar criterios para ordenar filas. La mayoría de los programas no requieren todos los datos en una tabla. Para recuperar sólo columnas específicas de una tabla, sustituya el asterisco (*) con una lista separada por comas de los nombres de las columnas. Por ejemplo, para recuperar sólo las columnas IDAutor y ApellidoPaterno para todas las filas en la tabla Autores, use la consulta SELECT IDAutor, ApellidoPaterno FROM Autores

Esta consulta devuelve sólo los datos que se listan en la figura 20.11.

20.4.2 Cláusula WHERE

Cuando los usuarios buscan en una base de datos filas que cumplan con ciertos criterios de selección (que formalmente se les conoce como predicados), sólo se seleccionan las filas que cumplen con esos criterios de selección. SQL utiliza la cláusula WHERE opcional en una consulta para especificar los criterios de selección de la consulta. La forma básica de una consulta con criterios de selección es: SELECT nombreColumna1, nombreColumna2, … FROM nombreTabla WHERE criterios

Por ejemplo, para seleccionar las columnas Titulo, NumeroEdicion y Copyright de la tabla Titulos, en donde la fecha de Copyright sea más reciente que 2004, use la consulta SELECT Titulo, NumeroEdicion, Copyright FROM Titulos WHERE Copyright > '2004'

La figura 20.12 muestra el resultado de la consulta anterior.

IDAutor

ApellidoPaterno

1

Deitel

2

Deitel

3

Goldberg

4

Choffnes

Figura 20.11 | Datos de IDAutor y ApellidoPaterno, de la tabla Autores.

Titulo

NumeroEdicion

Copyright

Java How to Program

6

2005

Visual C# 2005 How to Program

2

2006

C++ How to Program

5

2005

Visual Basic 2005 How to Program

3

2006

Figura 20.12 | Títulos con fechas de copyright después del 2004, de la tabla Titulos.

20.4 SQL

733

Los criterios de la cláusula WHERE pueden contener los operadores relacionales , =, = (igualdad), (desigualdad) y LIKE, así como los operadores lógicos AND, OR y NOT (que veremos en la sección 20.4.6). El operador LIKE se utiliza para las coincidencias de patrones con los caracteres comodines por ciento (%) y guión bajo ( _ ). Las coincidencias de patrones permiten a SQL buscar cadenas que coincidan con un patrón específico. Un patrón que contiene un carácter de por ciento (%) busca cadenas que tengan cero o más caracteres en la posición del carácter de por ciento en el patrón. Por ejemplo, la siguiente consulta localiza las filas de todos los autores cuyos apellidos paternos empiecen con la letra D:

SELECT IDAutor, PrimerNombre, ApellidoPaterno FROM Autores WHERE ApellidoPaterno LIKE 'D%'

La consulta anterior selecciona las dos filas que se muestran en la figura 20.13, ya que dos de los cuatro autores en nuestra base de datos tienen un apellido paterno que empieza con la letra D (seguida de cero o más caracteres). El % en el patrón LIKE de la cláusula WHERE indica que puede aparecer cualquier número de caracteres después de la letra D en la columna ApellidoPaterno. Observe que la cadena del patrón va encerrada entre comillas sencillas. Un guión bajo ( _ ) en la cadena del patrón indica un solo carácter comodín en esa posición en el patrón. Por ejemplo, la siguiente consulta localiza las filas de todos los autores cuyo apellido paterno empiece con cualquier carácter (especificado por _ ), seguido de la letra h, seguido de cualquier número de caracteres adicionales (especificados por %): SELECT IDAutor, PrimerNombre, ApellidoPaterno FROM Autores WHERE ApellidoPaterno LIKE '_h%'

La consulta anterior produce la fila que se muestra en la figura 20.14, ya que sólo hay un autor en nuestra base de datos que tenga un apellido paterno donde la segunda letra sea h.

20.4.3 Cláusula ORDER BY Las filas en el resultado de una consulta pueden almacenarse en orden ascendente o descendente, mediante el uso de la cláusula ORDER BY opcional. La forma básica de una consulta con una cláusula ORDER BY es: SELECT SELECT

nombreColumna1, nombreColumna2, … nombreColumna1, nombreColumna2, …

FROM FROM

nombreTabla nombreTabla

IDAutor

PrimerNombre

1

Harvey

Deitel

2

Paul

Deitel

ORDER BY ORDER BY

ApellidoPaterno

Figura 20.13 | Autores de la tabla Autores cuyo apellido paterno empiece con D.

IDAutor

PrimerNombre

ApellidoPaterno

4

David

Choffnes

Figura 20.14 | El único autor de la tabla Autores cuyo apellido paterno tiene una h como segunda letra.

columna columna

ASC DESC

734

Capítulo 20 Bases de datos: SQL y ADO.NET

en donde ASC especifica un orden ascendente (de menor a mayor), DESC especifica un orden descendente (de mayor a menor) y columna especifica la columna en la cual se basa el ordenamiento. Por ejemplo, para obtener la lista de autores en orden ascendente por apellido paterno (figura 20.15), utilizamos la siguiente consulta: SELECT IDAutor, PrimerNombre, ApellidoPaterno FROM Autores ORDER BY ApellidoPaterno ASC

El orden predeterminado es ascendente, por lo que ASC es opcional en la consulta anterior. Para obtener la misma lista de autores en orden descendente por apellido paterno (figura 20.16), utilizamos la siguiente consulta: SELECT IDAutor, PrimerNombre, ApellidoPaterno FROM Autores ORDER BY ApellidoPaterno DESC

Pueden usarse varias columnas para ordenar datos con una cláusula ORDER BY de la forma: ORDER BY columna1 tipoOrden, columna2 tipoOrden, …

en donde tipoOrden puede ser ASC o DESC. Observe que tipoOrden no tiene que ser idéntico para cada columna. Por ejemplo, la consulta SELECT Titulo, NumeroEdicion, Copyright FROM Titulos ORDER BY Copyright DESC, Titulo ASC

devuelve las filas de la tabla Titulos, ordenadas primero en orden descendente por fecha de copyright, después en orden ascendente por título (figura 20.17). Esto significa que las filas con valores mayores de Copyright se devuelven primero que las filas con valores menores de Copyright, y todas las filas que tenga los mismos valores de Copyright se ordenan en forma ascendente, por título. Las cláusulas WHERE y ORDER BY pueden combinarse en una sola consulta. Por ejemplo, la consulta SELECT ISBN, Titulo, NumeroEdicion, Copyright FROM Titulos WHERE Titulos LIKE '%How to Program' ORDER BY Titulo ASC

devuelve el ISBN, Titulo, NumeroEdicion y Copyright de cada libro en la tabla Titulos que tenga un Titulo que termine con “How to Program”, y los ordena en forma ascendente por Titulo. Los resultados de la consulta se muestran en la figura 20.18.

20.4.4 Mezcla de datos de varias tablas: INNER JOIN

Por lo general, los diseñadores de las bases de datos normalizan las bases de datos; es decir, dividen los datos relacionados en tablas separadas, para asegurar que una base de datos no almacene datos redundantes. Por ejemplo,

IDAutor

PrimerNombre

ApellidoPaterno

4

David

Choffnes

1

Harvey

Deitel

2

Paul

Deitel

3

Andrew

Goldberg

Figura 20.15 | Autores de la tabla Autores en orden ascendente por ApellidoPaterno.

20.4 SQL

IDAutor

PrimerNombre

ApellidoPaterno

3

Andrew

Goldberg

2

Harvey

Deitel

1

Paul

Deitel

4

David

Choffnes

735

Figura 20.16 | Autores de la tabla Autores en orden descendente por ApellidoPaterno.

Titulo

NumeroEdicion

Copyright

Visual Basic 2005 How to Program

3

2006

Visual C# 2005 How to Program

2

2006

C++ How to Program

5

2005

Java How to Program

6

2005

C How to Program

4

2004

Internet & World Wide Web How to Program

3

2004

Operating Systems

3

2004

Figura 20.17 | Datos de Titulos en orden descendente por Copyright, y en orden ascendente por Titulo.

ISBN

Titulo

NumeroEdicion

Copyright

0131426443

C How to Program

4

2004

0131857576

C++ How to Program

4

2005

0131450913

Internet & World Wide Web How to Program

4

2004

0131483986

Java How to Program

4

2005

0131869000

Visual Basic 2005 How to Program

4

2006

0131525239

Visual C# 2005 How to Program

4

2006

Figura 20.18 | Libros de la tabla Titulos, cuyos títulos terminan con How to Program en orden ascendente por Titulo.

la base de datos Libros tiene las tablas Autores y Titulos. Utilizamos una tabla ISBNAutor para almacenar “vínculos” entre los autores y los títulos. Si no separáramos esta información en tablas individuales, tendríamos que incluir la información del autor con cada entrada en la tabla Titulos. Esto significaría que la base de datos estaría almacenando información duplicada de los autores, para los autores que hayan escrito más de un libro. A menudo es conveniente mezclar los datos provenientes de varias tablas en un solo resultado. A este proceso se le conoce como unir las tablas, y se especifica mediante un operador INNER JOIN en la consulta. Una operación

736

Capítulo 20 Bases de datos: SQL y ADO.NET

INNER JOIN mezcla filas de dos tablas, relacionando los valores en una columna que sea común para las tablas. La forma básica de una operación INNER JOIN es: SELECT nombreColumna1, nombreColumna2, … FROM tabla1 INNER JOIN tabla2 ON tabla1.nombreColumna = tabla2.nombreColumna

La cláusula ON de INNER JOIN especifica las columnas que se van a comparar de cada tabla, para determinar cuáles filas se van a mezclar. Por ejemplo, la siguiente consulta produce una lista de autores, acompañada de los ISBNs para los libros escritos por cada autor: SELECT PrimerNombre, ApellidoPaterno, ISBN FROM Autores INNER JOIN ISBNAutor ON Autores.IDAutor = ISBNAutor.IDAutor ORDER BY ApellidoPaterno, PrimerNombre

La consulta combina las columnas PrimerNombre y ApellidoPaterno de la tabla Autores con la columna ISBN de la tabla ISBNAutor, y almacena los resultados en orden ascendente por ApellidoPaterno y PrimerNombre. Observe el uso de la sintaxis nombreTabla.nombreColumna en la cláusula ON. Esta sintaxis (conocida como nombre calificado) especifica las columnas de cada tabla que deben compararse para unir las tablas. La sintaxis “nombreTabla.” se requiere si las columnas tienen el mismo nombre en ambas tablas. Puede usarse la misma sintaxis en cualquier consulta, para diferenciar columnas que tengan el mismo nombre en distintas tablas.

Error común de programación 20.4 En una consulta de SQL, si no se califican los nombres para las columnas que tengan el mismo nombre en dos o más tablas, se produce un error.

Como siempre, la consulta puede contener una cláusula ORDER BY. La figura 20.19 ilustra los resultados de la consulta anterior, ordenados por PrimerNombre y ApellidoPaterno.

PrimerNombre

ApellidoPaterno

ISBN

David

Choffnes

0131828274

Harvey

Deitel

0131869000

Harvey

Deitel

0131525239

Harvey

Deitel

0131483986

Harvey

Deitel

0131857576

Harvey

Deitel

0131426443

Harvey

Deitel

0131450913

Harvey

Deitel

0131828274

Paul

Deitel

0131869000

Paul

Deitel

0131525239

Paul

Deitel

0131483986

Paul

Deitel

0131857576

Paul

Deitel

0131426443

Paul

Deitel

0131450913

Paul

Deitel

0131828274

Andrew

Goldberg

0131450913

Figura 20.19 | Autores y números ISBN para sus libros, en orden ascendente por ApellidoPaterno y PrimerNombre.

20.4 SQL

737

20.4.5 Instrucción INSERT

La instrucción INSERT inserta una fila en una tabla. La forma básica de esta instrucción es INSERT INTO nombreTabla ( nombreColumna1, nombreColumna2, …, nombreColumnaN ) VALUES ( valor1, valor2, …, valorN )

en donde nombreTabla es la tabla en la que se va a insertar la fila. El nombreTabla va seguido de una lista separada por comas de los nombres de las columnas entre paréntesis (no se requiere esta lista si la operación INSERT especifica un valor para cada columna de la tabla, en el orden correcto). La lista de nombres de columnas va seguida de la palabra clave VALUES de SQL, y de una lista separada por comas de los valores entre paréntesis. Los valores que se especifican aquí deben coincidir con las columnas especificadas después del nombre de la tabla, tanto en orden como en tipo (por ejemplo, si se supone que nombreColumna1 es la columna PrimerNombre, entonces valor1 debe ser una cadena entre comillas sencillas, que represente el primer nombre). Siempre hay que listar en forma explícita las columnas al insertar filas; si cambia el orden de las columnas en la tabla y se utiliza sólo VALUES, se puede generar un error. La instrucción INSERT INSERT INTO Autores ( PrimerNombre, ApellidoPaterno ) VALUES ( 'Sue', 'Smith' )

inserta una fila en la tabla Autores. Esta instrucción indica que los valores 'Sue' y 'Smith' se proporcionan para las columnas PrimerNombre y ApellidoPaterno, respectivamente. No especificamos un IDAutor en este ejemplo, ya que es una columna de identidad en la tabla Autores (véase la figura 20.3). Para cada fila que se agrega a esta tabla, SQL Server asigna un valor único de IDAutor, que es el siguiente valor en una secuencia autoincremental (es decir, 1, 2, 3, etc.). En este caso, a Sue Smith se le asignaría el número 5 para IDAutor. La figura 20.20 muestra la tabla Autores después de la operación INSERT. No todos los DBMS soportan columnas de identidad o de autoincremento.

Error común de programación 20.5 Es un error especificar un valor para una columna de identidad.

Error común de programación 20.6 SQL utiliza el carácter de comilla sencilla (') para delimitar cadenas. Para especificar una cadena que contenga una comilla sencilla (por ejemplo, O’Malley) en una instrucción de SQL, debe haber dos comillas sencillas en la posición en donde aparece el carácter de comilla sencilla en la cadena (es decir, 'O''Malley'). El primero de los dos caracteres de comilla sencilla actúa como carácter de escape para el segundo. Si no se escapan los caracteres de comilla sencilla en una cadena que forme parte de una instrucción de SQL, se produce un error de sintaxis.

20.4.6 Instrucción UPDATE

Una instrucción UPDATE modifica datos en una tabla. La forma básica de la instrucción UPDATE es UPDATE nombreTabla SET nombreColumna1 = valor1, nombreColumna2 = valor2, WHERE criterios

…, nombreColumnaN

= valorN

IDAutor

PrimerNombre

ApellidoPaterno

1

Harvey

Deitel

2

Paul

Deitel

3

Andrew

Goldberg

4

David

Choffnes

5

Sue

Smith

Figura 20.20 | La tabla Autores, después de una operación INSERT.

738

Capítulo 20 Bases de datos: SQL y ADO.NET

en donde nombreTabla es la tabla que se va a actualizar. El nombreTabla va seguido de la palabra clave SET y de una lista, separada por comas, de pares nombre de columna-valor, en el formato nombreColumna = valor. La cláusula WHERE opcional proporciona los criterios que determinan cuáles filas se van a actualizar. Aunque no se requiere, la cláusula WHERE se utiliza comúnmente, a menos que se requiera hacer una modificación en todas las filas. La instrucción UPDATE UPDATE Autores SET ApellidoPaterno = 'Jones' WHERE ApellidoPaterno = 'Smith' AND PrimerNombre = 'Sue'

actualiza una fila en la tabla Autores. La palabra clave AND es un operador lógico que, al igual que el operador && de C#, devuelve verdadero sí, y sólo si ambos operandos son verdaderos. Por ende, la instrucción anterior asigna el valor Jones a ApellidoPaterno para la fila en la que ApellidoPaterno es igual a Smith y PrimerNombre es igual a Sue. [Nota: si hay varias filas con el primer nombre “Sue” y el apellido paterno “Smith”, esta instrucción modifica todas esas filas para que tengan el apellido paterno “Jones”.] La figura 20.21 muestra la tabla Autores una vez que la operación UPDATE se haya realizado. SQL también proporciona otros operadores lógicos, como OR y NOT, que se comportan igual que sus contrapartes en C#.

20.4.7 Instrucción DELETE

Una instrucción DELETE elimina filas de una tabla. La forma básica de una instrucción DELETE es DELETE FROM nombreTabla WHERE criterios

en donde nombreTabla es la tabla de la cual se van a eliminar datos. La cláusula WHERE opcional especifica los criterios utilizados para determinar cuáles filas se van a eliminar. La instrucción DELETE DELETE FROM Autores WHERE ApellidoPaterno = 'Jones' AND PrimerNombre = 'Sue'

elimina la fila para Sue Jones en la tabla Autores. Las instrucciones DELETE pueden eliminar varias filas, si éstas cumplen con los criterios en la cláusula WHERE. La figura 20.22 muestra la tabla Autores, una vez que se lleva a cabo la operación DELETE.

IDAutor

PrimerNombre

ApellidoPaterno

1

Harvey

Deitel

2

Paul

Deitel

3

Andrew

Goldberg

4

David

Choffnes

5

Sue

Jones

Figura 20.21 | La tabla Autores, después de una operación UPDATE.

IDAutor

PrimerNombre

ApellidoPaterno

1

Harvey

Deitel

2

Paul

Deitel

3

Andrew

Goldberg

4

David

Choffnes

Figura 20.22 | La tabla Autores, después de una operación DELETE.

20.5 Modelo de objetos ADO.NET

739

Conclusión sobre SQL Esto concluye nuestra introducción a SQL. Demostramos varias palabras clave de SQL que tienen uso común, formamos consultas de SQL para recuperar datos de las bases de datos y formamos otras instrucciones de SQL para manipular datos en una base de datos. Ahora vamos a presentar el modelo de objetos ADO.NET, el cual permite que las aplicaciones en C# interactúen con las bases de datos. Como veremos pronto, los objetos de ADO. NET manipulan bases de datos mediante instrucciones de SQL como las que presentamos aquí.

20.5 Modelo de objetos ADO.NET

El modelo de objetos ADO.NET proporciona una API para acceder a los sistemas de bases de datos mediante la programación. ADO.NET se creó para que el .NET Framework sustituyera a la tecnología ActiveX Data Objects™ (ADO) de Microsoft. Como veremos en la siguiente sección, el IDE cuenta con herramientas de programación visual que simplifican el proceso de utilizar una base de datos en sus proyectos. Aunque tal vez no necesite trabajar directamente con muchos objetos ADO.NET para desarrollar aplicaciones simples, es importante tener un conocimiento básico acerca de cómo funciona el modelo de objetos ADO.NET para comprender el acceso a los datos en C#.

Espacios de nombres System.Data, System.Data.OleDb y System.Data.SqlClient El espacio de nombres System.Data es la raíz de la API de ADO.NET. Los otros espacios de nombres importantes de ADO.NET, System.Data.OleDb y System.Data.SqlClient, contienen clases que permiten a los programas conectarse con orígenes de datos y manipularlos; los orígenes de datos son ubicaciones que contienen datos, como una base de datos o un archivo XML. El espacio de nombres System.Data.OleDb contiene clases diseñadas para trabajar con cualquier origen de datos, mientras que System.Data.SqlClient cuenta con clases optimizadas para trabajar con bases de datos de Microsoft SQL Server. En este capítulo los ejemplos manipulan bases de datos de SQL Server 2005 Express, por lo que utilizamos las clases del espacio de nombres System.Data.SqlClient. SQL Server Express 2005 se incluye con Visual C# 2005 Express. También puede descargarlo de lab.msdn.microsoft.com/express/sql/default.aspx (en inglés), y de www.microsoft. com/spanish/msdn/vstudio/express/SQL/default.mspx (en español). Un objeto de la clase SqlConnection (espacio de nombres System.Data.SqlClient) representa una conexión a un origen de datos; en específico, una base de datos de SQL Server. Un objeto SqlConnection lleva el registro de la ubicación del origen de datos y de cualquier configuración que especifique cómo se va a acceder a ese origen de datos. Una conexión puede estar activa (es decir, abierta y que permita presentar los datos a, y recuperarlos del origen de datos) o cerrada. Un objeto de la clase SqlCommand (espacio de nombres System.Data.SqlClient) representa a un comando de SQL que un DBMS puede ejecutar en una base de datos. Un programa puede utilizar objetos SqlCommand para manipular un origen de datos a través de un objeto SqlConnection. El programa debe abrir la conexión con el origen de datos antes de poder ejecutar uno o más objetos SqlCommand, y debe cerrar la conexión una vez que ya no se requiera el acceso al origen de datos. Una conexión que permanece activa durante cierto tiempo para permitir múltiples operaciones de datos se conoce como conexión persistente. La clase DataTable (espacio de nombres System.Data) representa a una tabla de datos. Un objeto DataTable contiene una colección de objetos DataRow, que representan a los datos de la tabla. Un objeto DataTable también tiene una colección de objetos DataColumn, los cuales describen las columnas en una tabla. Tanto DataRow como DataColumn se encuentran en el espacio de nombres System.Data. Un objeto de la clase System. Data.DataSet, que consta de un conjunto de objetos DataTable y de las relaciones entre ellos, representa a una caché de datos: los datos que un programa almacena en forma temporal en la memoria local. La estructura de un objeto DataSet imita a la estructura de una base de datos relacional.

Modelo sin conexión de ADO.NET

Una ventaja de utilizar la clase DataSet es que trabaja sin conexión; el programa no necesita una conexión persistente al origen de datos para trabajar con los datos en un objeto DataSet. En vez de ello, se conecta al origen de datos para poblar el objeto DataSet (es decir, llenar los objetos DataTable de DataSet con datos), pero se desconecta justo después de recuperar los datos deseados. Después, accede a, y potencialmente manipula, los datos almacenados en el objeto DataSet. El programa opera con esta caché local de datos, en vez de hacerlo con

740

Capítulo 20 Bases de datos: SQL y ADO.NET

los datos originales en el origen de datos. Si el programa realiza modificaciones en los datos en el objeto DataSet que necesiten guardarse de manera permanente en el origen de datos, vuelve a conectarse al origen de datos para realizar una actualización, y después se desconecta lo más pronto posible. Por ende, el programa no requiere de una conexión activa y persistente con el origen de datos. Un objeto de la clase SqlDataAdapter (espacio de nombres System.Data.SqlClient) se conecta a un origen de datos de SQL Server y ejecuta instrucciones de SQL, tanto para poblar un objeto DataSet como para actualizar el origen de datos con base en el contenido actual de un objeto DataSet. Un objeto SqlDataAdapter mantiene un objeto SqlConnection que abre y cierra según sea necesario, para realizar estas operaciones mediante el uso de objetos SqlCommand. Más adelante en este capítulo mostraremos cómo poblar objetos DataSet y actualizar orígenes de datos.

20.6 Programación con ADO.NET: extraer información de una base de datos En esta sección mostraremos cómo conectarnos a una base de datos, realizar una consulta y mostrar el resultado. En esta sección podrá observar que hay muy poco código. El IDE cuenta con herramientas de programación visual y asistentes que simplifican el acceso a los datos en sus proyectos. Estas herramientas establecen las conexiones a las bases de datos y crean los objetos ADO.NET necesarios para ver y manipular los datos a través de controles de la GUI. El ejemplo en esta sección se conecta a la base de datos Libros de SQL Server, de la cual hemos hablado a lo largo de este capítulo. Encontrará el archivo Libros.mdf que contiene la base de datos junto con los ejemplos de este capítulo en www.deitel.com/books/csharpforprogrammers2.

20.6.1 Mostrar una tabla de base de datos en un control DataGridView Este ejemplo realiza una consulta simple en la base de datos Libros; recupera toda la tabla Autores y muestra los datos en un control DataGridView (un control del espacio de nombres System.Windows.Forms que puede mostrar un origen de datos en una GUI; en la figura 20.32 podrá ver la salida, más adelante en esta sección). Primero le mostraremos cómo conectarse a la base de datos Libros e incluirla como un origen de datos en su proyecto. Una vez establecida la base de datos Libros como un origen de datos, podrá mostrar los datos de la tabla Autores en un control DataViewGrid, con sólo arrastrar y colocar elementos en la vista de Diseño del proyecto.

Paso 1: crear el proyecto Cree una nueva Aplicación para Windows llamada MostrarTabla. Cambie el nombre del formulario (Form) a MostrarTablaForm y cambie el nombre del archivo de código fuente a MostrarTabla.cs. Después establezca la propiedad Text del formulario a Mostrar Tabla.

Paso 2: agregar un origen de datos al proyecto Para interactuar con un origen de datos (por ejemplo, una base de datos), debe agregarlo al proyecto usando la ventana Orígenes de datos, que lista los datos a los que su proyecto puede acceder. Para abrir la ventana Orígenes de datos (figura 20.23), seleccione Datos > Mostrar orígenes de datos o haga clic en la ficha que se encuentra a la derecha del Explorador de soluciones. En la ventana Orígenes de datos, haga clic en Agregar nuevo origen de datos… para abrir el Asistente para la configuración de orígenes de datos (figura 20.24). Este asistente lo guiará a través del proceso para conectarse a una base de datos y seleccionar a qué partes de la base de datos desee acceder en su proyecto.

Paso 3: seleccionar el tipo de origen de datos que se va a agregar al proyecto La primera pantalla del Asistente para la configuración de orígenes de datos (figura 20.24) le pide que seleccione el tipo de origen de datos que desea incluir en el proyecto. Seleccione Base de datos y haga clic en Siguiente >.

Paso 4: agregar una nueva conexión de base de datos Ahora debe seleccionar la conexión que utilizará para conectarse a la base de datos (es decir, el origen actual de los datos). Haga clic en Nueva conexión… para abrir el cuadro de diálogo Agregar conexión (figura 20.25). Si el Origen de datos no es Archivo de base de datos de Microsoft SQL Server (SqlClient), haga clic en Cambiar…, seleccione Archivo de base de datos de Microsoft SQL Server y haga clic en Aceptar. En el cuadro de diálogo Agregar conexión,

20.6 Programación con ADO.NET: extraer información de una base de datos

Ventana Orígenes de datos

741

Menú Datos

Figura 20.23 | Agregar un origen de datos a un proyecto.

Figura 20.24 | Seleccionar el tipo del origen de datos en el Asistente para la configuración de orígenes de datos. haga clic en Examinar…, localice el archivo de base de datos Libro.mdf en su computadora, selecciónelo y haga clic en Abrir. Puede hacer clic en Probar conexión para verificar que el IDE pueda conectarse a la base de datos a través de SQL Server. Haga clic en Aceptar para crear la conexión.

Paso 5: seleccionar la conexión de datos Libros.mdf Ahora que ha creado una conexión a la base de datos Libros.mdf, puede seleccionar y usar esta conexión para acceder a la base de datos. Haga clic en Siguiente > para establecer la conexión y después haga clic en Sí cuando se le pregunte si desea mover el archivo de base de datos a su proyecto (figura 20.26).

742

Capítulo 20 Bases de datos: SQL y ADO.NET

Figura 20.25 | Agregar una nueva conexión de datos.

Figura 20.26 | Seleccionar la conexión de datos Libros.mdf.

Paso 6: guardar la cadena de conexión La siguiente pantalla (figura 20.27) le pregunta si desea guardar la cadena de conexión en el archivo de configuración de la aplicación. Una cadena de conexión especifica la ruta a un archivo de base de datos en el disco, así como algunas configuraciones adicionales que determinan cómo acceder a la base de datos. Si guarda la cadena de conexión en un archivo de configuración, será más sencillo cambiar las configuraciones de conexión más adelante. Deje las selecciones predeterminadas y haga clic en Siguiente > para proceder.

20.6 Programación con ADO.NET: extraer información de una base de datos

743

Figura 20.27 | Guardar la cadena de conexión en el archivo de configuración de la aplicación.

Paso 7: elegir los objetos de base de datos que va a incluir en su conjunto de datos (DataSet) El IDE recupera información acerca de la base de datos que usted seleccionó y le pide que elija los objetos de base de datos (es decir, las partes de la base de datos) a los que desea que su proyecto pueda acceder (figura 20.28). Recuerde que, por lo general, los programas acceden al contenido de una base de datos a través de una caché de datos, la cual se almacena en un objeto DataSet. En respuesta a lo que usted elija en esta pantalla, el IDE generará una clase derivada de System.Data.DataSet que está diseñada específicamente para almacenar datos de la

Figura 20.28 | Elección de los objetos de base de datos a incluir en el DataSet.

744

Capítulo 20 Bases de datos: SQL y ADO.NET

base de datos Libros. Haga clic en la casilla de verificación a la izquierda de Tablas para indicar que el objeto DataSet personalizado debe almacenar en la caché (es decir, almacenar localmente) los datos de todas las tablas en la base de datos Libros: Autores, ISBNAutor y Titulos. [Nota: También puede expandir el nodo Tablas para seleccionar tablas específicas. Los demás objetos de base de datos que se listan no contienen datos en nuestra base de datos Libros, y están más allá del alcance del libro.] De manera predeterminada, el IDE nombra al objeto DataSet LibrosDataSet, aunque es posible especificar un nombre distinto en esta pantalla. Por último, haga clic en Finalizar para completar el proceso de agregar un origen de datos al proyecto.

Paso 8: ver el origen de datos en la ventana Orígenes de datos Observe que ahora aparece un nodo LibrosDataSet en la ventana Orígenes de datos (figura 20.29), con nodos hijos para cada tabla en la base de datos Libros; estos nodos representan los objetos DataTable del conjunto de datos LibrosDataSet. Expanda el nodo Autores y podrá ver las columnas de la tabla; la estructura del DataSet imita la de la verdadera base de datos Libros.

Paso 9: ver la base de datos en el Explorador de soluciones Libros.mdf se lista ahora como un nodo en el Explorador de soluciones (figura 20.30), lo cual indica que ahora la base de datos forma parte de este proyecto. Además, el Explorador de soluciones lista ahora un nuevo nodo llamado LibrosDataSet.xsd. En el capítulo 19 aprendió que un archivo con la extensión .xsd es un documento de XML Schema, el cual especifica la estructura de un conjunto de documentos de XML. El IDE utiliza un documento de XML Schema para representar la estructura de un objeto DataSet, incluyendo las tablas que conforman el objeto DataSet y las relaciones entre ellos. Cuando agregó la base de datos Libros como un origen de datos, el IDE creó el archivo LibrosDataSet.xsd con base en la estructura de la base de datos Libros. Después, el IDE generó la clase LibrosDataSet del esquema (es decir, la estructura) descrito por el archivo .xsd.

Mostrar la tabla Autores Ahora que agregó la base de datos Libros como un origen de datos, puede mostrar los datos de la tabla Autores de la base de datos en su programa. El IDE cuenta con herramientas de diseño que le permiten mostrar datos de un

Ventana Orígenes de datos

Figura 20.29 | Ver un origen de datos listado en la ventana Orígenes de datos.

20.6 Programación con ADO.NET: extraer información de una base de datos

745

Figura 20.30 | Ver una base de datos listada en el Explorador de soluciones. origen de datos en un formulario, sin necesidad de escribir código. Sólo tiene que arrastrar y colocar elementos de la ventana Orígenes de datos en un formulario, y el IDE genera los controles de la GUI y el código necesarios para mostrar el contenido del origen de datos seleccionado. Para mostrar la tabla Autores de la base de datos Libros, arrastre el nodo Autores de la ventana Orígenes de datos hacia el formulario. La figura 20.31 presenta la vista de Diseño después de realizar esta acción y de cambiar el tamaño a los controles. El IDE genera dos controles de la GUI que aparecen en MostrarTablaForm: autoresBindingNavigator y autoresDataGridView. El IDE también genera varios componentes no visuales adicionales, que aparecen en la bandeja de componentes: la región gris debajo del formulario en vista de Diseño. Utilizamos los nombres predeterminados del IDE para estos componentes generados en forma automática (y otros a lo largo del capítulo) para mostrar exactamente qué es lo que crea el IDE. Aquí hablaremos brevemente sobre los controles autoresBindingNavigator y autoresDataGridView. La siguiente sección habla con detalle sobre todos los componentes generados en forma automática, y explica cómo el IDE utiliza esos componentes para conectar los controles de la GUI con la tabla Autores de la base de datos Libros. Un control DataGridView muestra los datos organizados en filas y columnas, que corresponden a las filas y columnas del origen de datos subyacente. En este caso, el control DataGridView muestra los datos de la tabla Autores, por lo que sus columnas se llaman IDAutor, PrimerNombre y ApellidoPaterno. En la vista de Diseño, el control no muestra las filas de los datos actuales debajo de los encabezados de las columnas. Los datos se recuperan de la base de datos y se muestran en el control DataGridView sólo en tiempo de ejecución. Ejecute el programa. Cuando se carga el formulario, el control DataGridView contiene cuatro filas de datos; una para cada fila de la tabla Autores (figura 20.32). La tira de botones debajo de la barra de título de la ventana es un control BindingNavigator, el cual permite a los usuarios explorar y manipular los datos mostrados por otro control de la GUI (en este caso, un control DataGridView) en el formulario. Los botones de un control BindingNavigator se asemejan a los controles en un reproductor de CD o DVD, y le permiten avanzar hacia la primera fila de datos, a la fila anterior, a la siguiente y a la última. El control también muestra el número de fila actual seleccionado en un cuadro de texto. Puede usar este cuadro de texto para introducir el número de una fila que desee seleccionar. En este ejemplo, el control autoresBindingNavigator le permite “navegar” por la tabla Autores que se muestra en el control autoresDataGridView. Al hacer clic en los botones o al introducir un valor en el cuadro de texto, el control DataGridView

746

Capítulo 20 Bases de datos: SQL y ADO.NET

autoresBindingNavigator

autoresDataGridView

Bandeja de componentes

Figura 20.31 | Vista de Diseño después de arrastrar el nodo de origen de datos Autores al formulario.

Elimina la fila actual Agrega una nueva fila Guarda los cambios

Avanza a la primera fila Avanza a la siguiente fila Avanza a la fila anterior Avanza a la última fila a)

b)

Fila actual seleccionada

Figura 20.32 | Mostrar la tabla Autores en un control DataGridView. selecciona la fila apropiada. Una flecha en la columna más a la izquierda del control DataGridView indica la fila actual seleccionada. Un control BindingNavigator también tiene botones que le permiten agregar o eliminar una fila, y guardar los cambios de vuelta en el origen de datos subyacente (en este caso, la tabla Autores de la base de datos Libros). Al hacer clic en el botón con el icono de suma amarillo ( ) se agrega una nueva fila al control DataGridView. Sin embargo, con sólo escribir valores en las columnas PrimerNombre y ApellidoPaterno

20.6 Programación con ADO.NET: extraer información de una base de datos

747

no se inserta una nueva fila en la tabla Autores. Para agregar la nueva fila a la base de datos en el disco, haga clic en el botón Guardar ( ). Al hacer clic en el botón con la X de color rojo ( ) elimina la fila actual seleccionada del control DataGridView. De nuevo, debe hacer clic en el botón Guardar para realizar el cambio en la base de datos. Pruebe estos botones. Ejecute el programa y agregue una nueva fila, después guarde los cambios y cierre el programa. Cuando reinicie el programa, deberá ver que la nueva fila se guardó en la base de datos y aparece en el control DataGridView. Ahora elimine la nueva fila y haga clic en el botón Guardar. Cierre y reinicie el programa para ver que la nueva fila ya no existe en la base de datos.

20.6.2 Cómo funciona el enlace de datos

La técnica a través de la que se conectan los controles de la GUI con los orígenes de datos se conoce como enlace de datos. El IDE permite que los controles tales como DataGridView se enlacen a un origen de datos, como un objeto DataSet que represente a una tabla en una base de datos. Cualquier modificación que usted realice a través de la aplicación al origen de datos subyacente, se reflejará de manera automática en la forma en que se presentan los datos en el control enlazado a datos (por ejemplo, el control DataGridView). De igual forma, si se modifican los datos en el control enlazado a datos y se guardan los cambios, se actualiza el origen de datos subyacente. En el ejemplo actual, el control DataGridView se enlaza al objeto DataTable del conjunto de datos LibrosDataSet que representa a la tabla Autores en la base de datos. Si se arrastra el nodo Autores de la ventana Orígenes de datos al formulario, el IDE crea este enlace de datos por usted, usando varios componentes generados en forma automática (es decir, objetos) en la bandeja de componentes. La figura 20.33 modela estos objetos y sus asociaciones, que las siguientes secciones analizan con detalle para explicar cómo funciona el enlace de datos. LibrosDataSet

Como vimos en la sección 20.6.1, al agregar la base de datos Libros al proyecto, permitimos que el IDE generara el conjunto de datos LibrosDataSet. Recuerde que un objeto DataSet representa a una caché de datos, la cual

7VhZYZYVidhYZ HFAHZgkZg Libros.mdf

:_ZXjiV^chigjXX^dcZhYZHFAZc AUTORES4ABLE!DAPTER 6XijVa^oVaVWVhZYZYVidhXdcadhYVidhVXijVaZh

8dadXVadhgZhjaiVYdhYZaVXdchjaiVYZaVWVhZYZYVidhZc 8dci^ZcZ

LIBROS$ATA3ET

!UTORES

:cXVehjaV AUTORES"INDING3OURCE 6ea^XVadhXVbW^dhVadg^\ZcYZYVidhXdc WVhZZcaVZcigVYVYZajhjVg^dVigVk‚hYZ AUTORES$ATA'RID6IEW

BjZhigV YVidhYZ

CVkZ\Vedg$bVc^ejaV Zadg^\ZcYZYVidhVigVk‚hYZ AUTORES"INDING.AVIGATOR

Figura 20.33 | Arquitectura de enlace de datos que se utiliza para mostrar la tabla Autores de la base de datos Libros en una GUI.

748

Capítulo 20 Bases de datos: SQL y ADO.NET

imita la estructura de una base de datos relacional. Puede explorar la estructura de conjunto de datos LibrosDataSet en la ventana Orígenes de datos. La estructura de un objeto DataSet puede determinarse en tiempo de ejecución o en tiempo de diseño. La estructura de un objeto Dataset sin tipo (es decir, las tablas que lo componen y las relaciones entre ellas) se determina en tiempo de ejecución, con base en el resultado de una consulta específica. Para acceder a las tablas y los valores de las columnas se utilizan índices en colecciones de objetos DataTable y DataRow, respectivamente. El tipo de cada pieza de datos en un objeto DataSet sin tipo se desconoce en tiempo de diseño. No obstante, el IDE crea el conjunto de datos LibrosDataSet en tiempo de diseño, como un objeto DataSet fuertemente tipificado. El conjunto LibrosDataSet (una clase derivada de DataSet) contiene objetos de clases derivadas de DataTable, que representan las tablas en la base de datos Libros. LibrosDataSet proporciona propiedades que corresponden a los objetos cuyos nombres coinciden con los de las tablas subyacentes. Por ejemplo, librosDataSet.Autores representa una caché de los datos en la tabla Autores. Cada objeto DataTable contiene una colección de objetos DataRow. Cada objeto DataRow contiene miembros cuyos nombres y tipos corresponden a los de las columnas de la tabla de la base de datos subyacente. Por ende, librosDataSet.Autores[ 0 ].IDAutor se refiere al IDAutor de la primera fila de la tabla Autores en la base de datos Libros. Observe que se utilizan índices con base cero para acceder a los objetos DataRow en un objeto DataTable. El objeto librosDataSet en la bandeja de componentes es un objeto de la clase LibrosDataSet. Cuando usted indica que desea mostrar el contenido de la tabla Autores en el formulario, el IDE genera un objeto LibrosDataSet para almacenar los datos que mostrará el formulario. Éstos son los datos con los que se enlazará el control DataGridView. Este control no muestra datos de la base de datos en forma directa, sino que muestra el contenido de un objeto LibrosDataSet. Como veremos en breve, el objeto AutoresTableAdapter llena el objeto LibrosDataSet con datos recuperados de la base de datos, mediante la ejecución de una consulta de SQL. AutoresTableAdapter

El objeto AutoresTableAdapter es el componente que interactúa con la base de datos Libros en el disco (es decir, el archivo Libros.mdf). Cuando otros componentes necesitan recuperar datos de la base de datos, o escribir en ella, invocan a los métodos del objeto AutoresTableAdapter. El IDE genera la clase AutoresTableAdapter cuando usted arrastra una tabla de la base de datos Libros hacia el formulario. El objeto autoresTableAdapter en la bandeja de componentes es un objeto de esta clase. AutoresTableAdapter es responsable de llenar el objeto LibrosDataSet con los datos de Autores de la base de datos; almacena una copia de la tabla Autores en la memoria local. Como veremos pronto, esta copia en la caché puede modificarse durante la ejecución del programa. Por ende, AutoresTableAdapter es también responsable de actualizar la base de datos, cuando cambian los datos en LibrosDataSet. La clase AutoresTableAdapter encapsula a un objeto SqlDataAdapter, el cual contiene objetos SqlCommand que especifican la manera en que el objeto SqlDataAdapter selecciona, inserta, actualiza y elimina datos en la base de datos. En la sección 20.5 vimos que un objeto SqlCommand debe tener un objeto SqlConnection, a través del cual el objeto SqlCommand puede comunicarse con una base de datos. En este ejemplo, el objeto AutoresTableAdapter establece la propiedad Connection de cada uno de los objetos SqlCommand de SqlDataAdapter, con base en la cadena de conexión que hace referencia a la base de datos Libros. Para interactuar con la base de datos, el objeto AutoresTableAdapter invoca a los métodos de su objeto SqlDataAdapter, cada uno de los cuales ejecuta el objeto SqlCommand apropiado. Por ejemplo, para llenar la tabla Autores de LibrosDataSet, el método Fill de AutoresTableAdapter invoca al método Fill de su objeto SqlDataAdapter, el cual ejecuta un objeto SqlCommand que representa la siguiente consulta SELECT: SELECT IDAutor, PrimerNombre, ApellidoPaterno FROM Autores

Esta consulta selecciona todas las filas y columnas de la tabla Autores y las coloca en librosDataSet.Autores. En breve, verá un ejemplo de la invocación del método Fill de autoresTableAdapter. autoresBindingSource

y autoresDataGridView

El objeto autoresBindingSource (un objeto de la clase BindingSource) identifica a un origen de datos que un programa puede enlazar a un control, y sirve como intermediario entre un control enlazado a datos de la GUI y su origen de datos. En este ejemplo, el IDE utiliza un objeto BindingSource para conectar el objeto autoresDataGridView con librosDataSet.Autores. Para lograr este enlace de datos, el IDE primero establece la propiedad DataSource de autoresBindingSource a LibrosDataSet. Esta propiedad especifica el objeto DataSet

20.6 Programación con ADO.NET: extraer información de una base de datos

749

que contiene los datos que se van a enlazar. Después, el IDE establece la propiedad DataMember a Autores. Esta propiedad identifica a una tabla específica dentro del objeto DataSource. Después de configurar el objeto autoresBindingSource, el IDE asigna este objeto a la propiedad DataSource de autoresDataGridView para indicar qué es lo que va a mostrar el control DataGridView. Un objeto BindingSource también administra la interacción entre un control enlazado a datos de la GUI y su origen de datos subyacente. Si usted edita los datos mostrados en un control DataGridView y desea guardar los cambios en el origen de datos, su código debe invocar el método EndEdit del objeto BindingSource. Este método aplica los cambios realizados en los datos a través del control de la GUI (es decir, los cambios pendientes) al origen de datos enlazado a ese control. Observe que esto sólo actualiza al objeto DataSet; se requiere un paso adicional para actualizar en forma permanente a la base de datos. En breve veremos un ejemplo de esto, cuando presentemos el código generado por el IDE en el archivo MostrarTabla.cs. autoresBindingNavigator

Recuerde que un objeto BindingNavigator le permite recorrer (navegar) y manipular (agregar o eliminar filas) los datos enlazados a un control en un formulario. Un objeto BindingNavigator se comunica con un objeto BindingSource (especificado en la propiedad BindingSource de BindingNavigator) para llevar a cabo estas acciones en el origen de datos subyacente (es decir, el objeto DataSet). El objeto BindingNavigator no interactúa con el control enlazado a datos, sino que invoca a los métodos de BindingSource que hacen que el control enlazado a datos actualice su presentación de los datos. Por ejemplo, cuando usted hace clic en el botón de BindingNavigator para agregar una nueva fila, el objeto BindingNavigator invoca a un método del objeto BindingSource. Después, el objeto BindingSource agrega una nueva fila a su objeto DataSet asociado. Una vez que se modifica este objeto DataSet, el control DataGridView muestra la nueva fila, ya que DataGridView y BindingNavigator están enlazados al mismo objeto BindingSource (y por ende, al mismo DataSet).

Examinar el código generado en forma automática para MostrarTablaForm La figura 20.34 presenta el código para MostrarTablaForm. Observe que no necesita escribir este código; el IDE lo genera cuando usted arrastra y suelta la tabla Autores de la ventana Orígenes de datos hacia el formulario. Nosotros modificamos el código generado en forma automática para agregar comentarios, dividir las líneas extensas para fines de visualización y eliminar las declaraciones using innecesarias. El IDE también genera una cantidad considerable de código adicional, como el código que define a las clases LibrosDataSet y AutoresTableAdapter, así como el código del diseñador que declara los objetos generados en forma automática en la bandeja de componentes. El código adicional generado por el IDE reside en archivos que pueden verse en el Explorador de soluciones al seleccionar Mostrar todos los archivos. Nosotros sólo presentamos el código en MostrarTabla.cs, ya que es el único archivo que necesitará modificar.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 20.34: MostrarTabla.cs // Muestra los datos de la tabla de una base de datos en un control DataGridView. using System; using System.Windows.Forms; namespace MostrarTabla { public partial class MostrarTablaForm : Form { public MostrarTablaForm() { InitializeComponent(); } // fin del constructor // El manejador del evento Click para el botón Guardar en el objeto // BindingNavigator guarda los cambios realizados a los datos

Figura 20.34 | Código generado en forma automática para mostrar datos de una tabla de la base de datos en un control DataGridView. (Parte 1 de 2).

750

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

Capítulo 20 Bases de datos: SQL y ADO.NET

private void autoresBindingNavigatorSaveItem_Click( object sender, EventArgs e ) { this.Validate(); this.autoresBindingSource.EndEdit(); this.autoresTableAdapter.Update( this.librosDataSet.Autores ); } // fin del método autoresBindingNavigatorSaveItem_Click // carga los datos en la tabla librosDataSet.Autores, // que después se muestra en el control DataGridView private void MostrarTablaForm_Load( object sender, EventArgs e ) { // TODO: esta línea de código carga datos en la // tabla 'librosDataSet.Autores'. Puede moverla o // quitarla según sea necesario. this.autoresTableAdapter.Fill( this.librosDataSet.Autores ); } // fin del método MostrarTablaForm_Load } // fin de la clase MostrarTablaForm } // fin del espacio de nombres MostrarTabla a)

b)

Figura 20.34 | Código generado en forma automática para mostrar datos de una tabla de la base de datos en un control DataGridView. (Parte 2 de 2).

Las líneas 17-23 contienen el manejador del evento Click para el botón Guardar en el objeto AutoresBindingNavigator. Recuerde que debe hacer clic en este botón para guardar en el origen de datos subyacente (es decir, la tabla Autores de la base de datos Libros) los cambios realizados a los datos en el control DataGridView. El proceso de guardar los cambios es de dos pasos: 1. El objeto DataSet asociado con el control DataGridView (indicado por su objeto debe actualizarse para incluir cualquier cambio realizado por el usuario.

BindingSource)

2. La base de datos en el disco debe actualizarse para que coincida con el nuevo contenido del objeto DataSet. Antes de que el manejador de eventos guarde cualquier cambio, la línea 21 invoca a this.Validate() para validar los controles en el formulario. Si usted implementa eventos Validating o Validated para cualquiera de los controles del formulario, estos eventos le permitirán validar la entrada del usuario y posiblemente indicar errores cuando los datos son inválidos. La línea 21 invoca al método EndEdit de autoresBindingSource para asegurar que el origen de datos asociado con el objeto (librosDataSet.Autores) se actualice con cualquier cambio que realice el usuario a la fila actual seleccionada en el control DataGridView (por ejemplo, agregar una fila, cambiar el valor de una columna). Cualquier cambio a las otras filas se aplica al objeto DataSet cuando usted selecciona otra fila. La línea 22 invoca al método Update de autoresTableAdapter para escribir la versión modificada de la tabla

20.7 Consulta de la base de datos Libros

751

Autores (en memoria) a la base de datos de SQL Server en el disco. El método Update ejecuta las instrucciones de SQL (encapsuladas en objetos SqlCommand) necesarias para hacer que la tabla Autores de la base de datos coincida con librosDataSet.Autores. El manejador del evento Load para MostrarTablaForm (líneas 27-33) se ejecuta cuando se carga el programa. Este manejador de eventos llena el objeto DataSet en memoria con datos de la base de datos de SQL Server en el disco. Una vez que se llena el objeto DataSet, el control de la GUI enlazado a este objeto puede mostrar sus datos. La línea 32 llama al método Fill de autoresTableAdapter para recuperar información de la base de datos, colocando esta información en el miembro de DataSet que se proporciona como argumento. Recuerde que el IDE generó a autoresTableAdapter para ejecutar objetos SqlCommand a través de la conexión que creamos dentro del Asistente para la configuración de orígenes de datos. Por ende, el método Fill aquí ejecuta una instrucción SELECT para recuperar todas las filas de la tabla Autores de la base de datos Libros, y después coloca el resultado de esta consulta en librosDataSet.Autores. Recuerde que la propiedad DataSource de autoresDataGridView se establece a autoresBindingSource (que hace referencia a librosDataSet.Autores). Por lo tanto, después de que se carga este origen de datos, el control autoresDataGridView muestra automáticamente los datos recuperados de la base de datos.

20.7 Consulta de la base de datos Libros Ahora que ha visto cómo mostrar toda una tabla completa de una base de datos en un control DataGridView, le demostraremos cómo ejecutar consultas SELECT de SQL específicas en una base de datos y mostrar los resultados. Aunque este ejemplo sólo consulta los datos, la aplicación podría modificarse fácilmente para ejecutar otras instrucciones de SQL. Realice los siguientes pasos para crear la aplicación de ejemplo, que ejecuta consultas personalizadas en la tabla Titulos de la base de datos Libros.

Paso 1: crear el proyecto Cree una nueva Aplicación para Windows llamada MostrarResultadoConsulta. Cambie el nombre del formulario a MostrarResultadoConsultaForm y el nombre de su archivo de código fuente a MostrarResultadoConsulta.cs; después establezca la propiedad Text del formulario a Mostrar resultado de consulta.

Paso 2: agregar un origen de datos al proyecto Realice los pasos en la sección 20.6.1 para incluir la base de datos Libros como un origen de datos en el proyecto.

Paso 3: crear un control DataGridView para mostrar la tabla Titulos Arrastre el nodo Titulos de la ventana Orígenes de datos en el formulario para crear un control DataGridView que muestre todo el contenido de la tabla Titulos.

Paso 4: agregar consultas personalizadas al objeto TitulosTableAdapter Recuerde que al invocar al método Fill de TableAdapter se llena el objeto DataSet que recibe como argumento con todo el contenido de la tabla de la base de datos que corresponde a ese objeto TableAdapter. Para poblar un miembro de DataSet (es decir, un objeto DataTable) con sólo una porción de una tabla (por ejemplo, con fechas de copyright del 2006), debe agregar un método al objeto TableAdapter que llene el objeto DataTable especificado con los resultados de una consulta personalizada. El IDE proporciona el Asistente para la configuración de consultas de TableAdapter para realizar esta tarea. Para abrir este asistente, primero haga clic con el botón derecho del ratón en el nodo LibrosDataSet.xsd del Explorador de soluciones y seleccione la opción Ver diseñador. También puede hacer clic en el icono Editar DataSet con el Diseñador ( ). Cualquiera de estas acciones abre el Diseñador de DataSet (figura 20.35), que muestra una representación visual del objeto LibrosDataSet (es decir, las tablas ISBNAutor, Autores y Titulos, y las relaciones entre ellas). El Diseñador de DataSet lista las columnas de cada tabla y el objeto TableAdapter generado en forma automática que accede a la tabla. Para seleccionar el objeto TitulosTableAdapter, haga clic en su nombre, después haga clic con el botón derecho sobre el nombre y seleccione Agregar Query… para iniciar el Asistente para la configuración de consultas de TableAdapter (figura 20.36).

752

Capítulo 20 Bases de datos: SQL y ADO.NET

Diseñador de DataSet

TitulosTableAdapter

Figura 20.35 | Vista del conjunto de datos LibrosDataSet en el Diseñador de DataSet.

Paso 5: elegir la forma en que el objeto TableAdapter debe acceder a la base de datos En la primera pantalla del asistente (figura 20.36), mantenga la opción predeterminada Usar instrucciones SQL y haga clic en Siguiente.

Figura 20.36 | Asistente para la configuración de consultas de TableAdapter para agregar una consulta a un objeto TableAdapter.

20.7 Consulta de la base de datos Libros

753

Paso 6: elegir un tipo de consulta En la siguiente pantalla del asistente (figura 20.37), mantenga la opción predeterminada y haga clic en Siguiente.

SELECT que devuelve

filas

Paso 7: especificar una instrucción SELECT para la consulta La siguiente pantalla (figura 20.38) le pide que escriba una consulta que se utilizará para recuperar datos de la base de datos Libros. Observe que la instrucción SELECT predeterminada antepone el prefijo “dbo.” a Titulos. Este prefijo significa “propietario de la base de datos”, e indica que la tabla Titulos pertenece al propietario de la base de datos (es decir, usted). En casos en los que se necesita hacer referencia a una tabla cuyo propietario es otro usuario del sistema, este prefijo se sustituye por el nombre de usuario del propietario. Usted puede modificar la instrucción de SQL en el cuadro de texto aquí (usando la sintaxis de SQL que vimos en la sección 20.4), o puede hacer clic en Generador de consultas… para diseñar y probar la consulta utilizando una herramienta visual.

Figura 20.37 | Elegir el tipo de consulta que se va a generar para el objeto TableAdapter.

Figura 20.38 | Especificar una instrucción SELECT para la consulta.

754

Capítulo 20 Bases de datos: SQL y ADO.NET

Paso 8: crear una consulta con el Generador de consultas Haga clic en el botón Generador de consultas… para abrir el Generador de consultas (figura 20.39). La porción superior de la ventana Generador de consultas contiene un cuadro que lista las columnas de la tabla Titulos. Todas las columnas están seleccionadas de manera predeterminada (figura 20.39a ), lo cual indica que la consulta debe devolver todas las columnas. La porción intermedia de la ventana contiene una tabla en la que cada fila corresponde a una columna en la tabla Titulos. A la derecha de los nombres de las columnas, hay columnas en las que usted puede escribir valores o hacer selecciones para modificar la consulta. Por ejemplo, para crear una consulta que seleccione sólo libros que tengan fecha de copyright del 2006, escriba el valor 2006 en la columna Filtro de la fila Copyright. Observe que el Generador de consultas modifica su entrada para que sea “= ‘2006’” y agrega una

a)

b)

Figura 20.39 | El Generador de consultas después de agregar una cláusula WHERE al escribir un valor en la columna Filtro.

20.7 Consulta de la base de datos Libros

755

cláusula WHERE apropiada a la instrucción SELECT que se muestra en la parte intermedia de la figura 20.39b. Haga clic en el botón Ejecutar consulta para probar la consulta y mostrar los resultados en la parte inferior de la ventana del Generador de consultas. Para obtener más información sobre el Generador de consultas, vea los sitios msdn2. microsoft.com/library/ms172013.aspx (en inglés) y msdn2.microsoft.com/es-es/library/ms141087. aspx (en español).

Paso 9: cerrar el Generador de consultas Haga clic en Aceptar para cerrar el Generador de consultas y regresar al Asistente para la configuración de consultas de TableAdapter (figura 20.40), el cual ahora muestra la consulta de SQL creada en el paso anterior. Haga clic en Siguiente para continuar.

Paso 10: establecer los nombres de los métodos generados en forma automática que realizan la consulta Después de especificar la consulta de SQL, debe nombrar los métodos que el IDE generará para realizar la consulta (figura 20.41). Se generan dos métodos de manera predeterminada: un “método Fill” que llena un parámetro DataTable con el resultado de la consulta, y un “método Get” que devuelve un nuevo objeto DataTable lleno con el resultado de la consulta. Los cuadros de texto para escribir los nombres para estos métodos se llenan previamente con FillBy y GetDataBy, respectivamente. Modifique estos nombres por LlenarConCopyright2006 y ObtenerDatosConCopyright2006, como se muestra en la figura 20.41. Por último, haga clic en Finalizar para completar el asistente y regresar al Diseñador de DataSet (figura 20.42). Observe que ahora estos métodos se listan en la sección TitulosTableAdapter del cuadro que representa a la tabla Titulos.

Paso 11: agregar una consulta adicional Repita los pasos 4-10 para agregar otra consulta que seleccione todos los libros cuyos títulos terminen con el texto “How to Program”, y que almacene los resultados por título en orden ascendente (vea la sección 20.4.3). En el Generador de consultas, escriba LIKE ‘%How to Program’ en la columna Filtro de la fila Titulo. Para especificar el orden, seleccione Ascendente en la columna Tipo de orden de la fila Titulo. En el paso final del Asistente para la configuración de consultas de TableAdapter, cambie los nombres de los métodos Fill y Get por LlenarConLibrosHowToProgram y ObtenerDatosParaLibrosHowToProgram, respectivamente.

Paso 12: agregar un control ComboBox al formulario Regrese a la vista de Diseño y agregue debajo del control DataGridView un control ComboBox llamado consultasComboBox al formulario. Los usuarios utilizarán este control para elegir una consulta SELECT a ejecutar,

Figura 20.40 | La instrucción SELECT creada con el Generador de consultas.

756

Capítulo 20 Bases de datos: SQL y ADO.NET

Figura 20.41 | Especificar nombres para los métodos que se van a agregar a TitulosTableAdapter.

Figura 20.42 | El Diseñador de DataSet, después de agregar métodos Fill y Get a TitulosTableAdapter.

cuyo resultado se mostrará en el control DataGridView. Agregue tres elementos a consultasComboBox: uno que coincida con cada una de las tres consultas que ahora puede realizar el objeto TitulosTableAdapter: SELECT ISBN, Titulo, NumeroEdicion, Copyright FROM Titulos SELECT ISBN, Titulo, NumeroEdicion, Copyright FROM Titulos WHERE ( Copyright = '2006' ) SELECT ISBN, Titulo, NumeroEdicion, Copyright FROM Titulos WHERE ( Titulo LIKE '%How to Program' ) ORDER BY Titulo

20.7 Consulta de la base de datos Libros

757

Paso 13: personalizar el manejador de eventos Load del formulario Agregue una línea de código al manejador de eventos MostrarResultadoConsultaForm_Load generado en forma automática, para establecer la propiedad SelectedIndex inicial de consultasComboBox en 0. Recuerde que el manejador del evento Load llama al método Fill de manera predeterminada, que ejecuta la primera consulta (el elemento en el índice 0). Por lo tanto, al establecer la propiedad SelectedIndex el control ComboBox muestra la consulta que se realiza en un principio, cuando se carga MostrarResultadoConsultaForm por primera vez.

Paso 14: programar un manejador de eventos para el control ComboBox Ahora debe escribir código que ejecute la consulta apropiada, cada vez que el usuario seleccione un elemento distinto de consultasComboBox. Haga doble clic en el control consultasComboBox en vista de Diseño para generar un manejador de eventos consultasComboBox_SelectedIndexChanged (líneas 42-61) en el archivo MostrarResultadoConsulta.cs (figura 20.43). Después agregue al manejador de eventos una instrucción switch que invoque al método de titulosTableAdapter que ejecute la consulta asociada con la selección actual del control ComboBox (líneas 47-60). Recuerde que el método Fill (línea 50) ejecuta una consulta SELECT que selecciona todas las filas, el método LlenarConCopyright2006 (líneas 53-54) ejecuta una consulta SELECT que selecciona todas las filas en las que el año de copyright es 2006 y el método LlenarConLibrosHowToProgram (líneas 57-58) ejecuta una consulta que selecciona todas las filas que tienen “How to Program” al final de sus títulos, y las ordena en forma ascendente, por título. Cada método llena librosDataSet.Titulos sólo con las filas devueltas en la columna correspondiente. Gracias a las relaciones de enlace de datos creadas por el IDE, al volver a llenar librosDataSet. Titulos el control TitulosDataGridView muestra el resultado de la consulta seleccionada, sin necesidad de código adicional. La figura 20.43 también muestra la salida para MostrarResultadoConsultaForm. La figura 20.43a muestra el resultado de recuperar todas las filas de la tabla Titulos. La figura 20.43(b) demuestra la segunda consulta, que recupera sólo las filas para los libros con copyright del 2006. Por último, la figura 20.43(c) demuestra la tercera consulta, que selecciona filas para los libros How to Program, y las ordena en forma ascendente por título.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// Fig. 20.43: MostrarResultadoConsulta.cs // Muestra el resultado de una consulta seleccionada por el usuario en un control DataGridView. using System; using System.Windows.Forms; namespace MostrarResultadoConsulta { public partial class MostrarResultadoConsultaForm : Form { public MostrarResultadoConsultaForm() { InitializeComponent(); } // fin del constructor de MostrarResultadoConsultaForm // el manejador del evento Click para el botón Guardar en el objeto // BindingNavigator guarda los cambios realizados en los datos private void titulosBindingNavigatorSaveItem_Click( object sender, EventArgs e ) { this.Validate(); this.titulosBindingSource.EndEdit(); this.titulosTableAdapter.Update( this.librosDataSet.Titulos ); } // fin del método titulosBindingNavigatorSaveItem_Click

Figura 20.43 | Muestra el resultado de una consulta seleccionada por el usuario en un control DataGridView. (Parte 1 de 3).

758

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

Capítulo 20 Bases de datos: SQL y ADO.NET

// carga datos en la tabla librosDataSet.Titulos, // que después se muestra en el control DataGridView private void MostrarResultadoConsultaForm_Load( object sender, EventArgs e ) { // TODO: esta línea de código carga datos en la // tabla 'librosDataSet.Titulos' Puede moverla o // quitarla según sea necesario. this.titulosTableAdapter.Fill( this.librosDataSet.Titulos ); // establece el control ComboBox para que muestre la consulta // predeterminada que selecciona todos los libros de la tabla Titulos consultasComboBox.SelectedIndex = 0; } // fin del método MostrarResultadoConsultaForm_Load // carga datos en la tabla librosDataSet.Titulos, con base // en la consulta seleccionada por el usuario private void consultasComboBox_SelectedIndexChanged( object sender, EventArgs e ) { // llena el objeto DataTable de Titulos con // el resultado de la consulta seleccionada switch ( consultasComboBox.SelectedIndex ) { case 0: // todos los libros titulosTableAdapter.Fill( librosDataSet.Titulos ); break; case 1: // libros con año de copyright 2006 titulosTableAdapter.LlenarConCopyright2006( librosDataSet.Titulos ); break; case 2: // libros How to Program, ordenados por Titulo titulosTableAdapter.LlenarConLibrosHowToProgram( librosDataSet.Titulos ); break; } // fin de switch } // fin del método consultasComboBox_SelectedIndexChanged } // fin de la clase MostrarResultadoConsultaForm } // fin del espacio de nombres MostrarResultadoConsulta a)

Figura 20.43 | Mostrar el resultado de una consulta seleccionada por el usuario en un control DataGridView. (Parte 2 de 3).

20.8 Programación con ADO.NET: caso de estudio de libreta de direcciones

759

b)

c)

Figura 20.43 | Mostrar el resultado de una consulta seleccionada por el usuario en un control DataGridView. (Parte 3 de 3).

20.8 Programación con ADO.NET: caso de estudio de libreta de direcciones Nuestro siguiente ejemplo implementa una aplicación simple de libreta de direcciones, la cual permite a los usuarios insertar filas, localizar filas y actualizar la base de datos LibretaDirecciones.mdf de SQL Server. La aplicación LibretaDirecciones (figura 20.44) cuenta con una GUI, a través de la cual los usuarios pueden ejecutar instrucciones de SQL en la base de datos. No obstante, en vez de mostrar una tabla de la base

1 2 3 4 5 6 7 8 9 10 11

// Fig. 20.44: LibretaDirecciones.cs // Permite a los usuarios manipular una libreta de direcciones. using System; using System.Windows.Forms; namespace LibretaDirecciones { public partial class LibretaDireccionesForm : Form { public LibretaDireccionesForm() {

Figura 20.44 | Aplicación LibretaDirecciones le permite manipular entradas en una base de datos de libreta de direcciones. (Parte 1 de 3).

760

12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55

Capítulo 20 Bases de datos: SQL y ADO.NET

InitializeComponent(); } // fin del constructor de LibretaDireccionesForm // el manejador del evento Click para el botón Guardar en el objeto // BindingNavigator guarda los cambios realizados a los datos private void direccionesBindingNavigatorSaveItem_Click( object sender, EventArgs e ) { this.Validate(); this.direccionesBindingSource.EndEdit(); this.direccionesTableAdapter.Update( this.libretaDireccionesDataSet.Direcciones ); } // fin del método direccionesBindingNavigatorSaveItem_Click // carga datos en la tabla libretaDireccionesDataSet.Direcciones private void LibretaDireccionesForm_Load( object sender, EventArgs e ) { // TODO: esta línea de código carga datos en la // tabla 'libretaDireccionesDataSet.Direcciones'. // Puede moverla o quitarla según sea necesario. this.direccionesTableAdapter.Fill( this.libretaDireccionesDataSet.Direcciones ); } // fin del método LibretaDireccionesForm_Load // carga los datos para las filas con el apellido paterno especificado // en la tabla libretaDireccionesDataSet.Direcciones private void buscarButton_Click( object sender, EventArgs e ) { // llena el objeto DataTable de DataSet sólo con filas // que contengan el apellido paterno especificado por el usuario direccionesTableAdapter.LlenarPorApellidoPaterno( libretaDireccionesDataSet.Direcciones, buscarTextBox.Text ); } // fin del método buscarButton_Click // vuelve a cargar libretaDireccionesDataSet.Direcciones con todas las filas private void explorarTodoButton_Click( object sender, EventArgs e ) { // llena el objeto DataTable de DataSet con todas las filas en la base de datos direccionesTableAdapter.Fill( libretaDireccionesDataSet.Direcciones ); buscarTextBox.Text = ""; // borra el control buscarTextBox } // fin del método explorarTodoButton_Click } // fin de la clase LibretaDireccionesForm } // fin del espacio de nombres LibretaDirecciones a)

b)

Figura 20.44 | Aplicación LibretaDirecciones le permite manipular entradas en una base de datos de libreta de direcciones. (Parte 2 de 3).

20.8 Programación con ADO.NET: caso de estudio de libreta de direcciones

761

c)

Figura 20.44 | Aplicación LibretaDirecciones le permite manipular entradas en una base de datos de libreta de direcciones. (Parte 3 de 3). de datos en un control DataGridView, este ejemplo presenta los datos de una tabla, una fila a la vez, usando un conjunto de controles TextBox que muestran los valores de cada una de las columnas de la fila. Un control BindingNavigator le permite controlar cuál fila de la tabla se puede ver en cualquier momento dado. El control BindingNavigator también le permite agregar nuevas filas, eliminar filas y guardar cambios a los datos visualizados. Observe que las líneas 17-34 en la figura 20.44 son similares a las correspondientes líneas del código en los ejemplos anteriores de este capítulo. En breve hablaremos sobre la funcionalidad adicional de la aplicación y el código en las líneas 38-53 que soporta esta funcionalidad. Empezaremos por mostrarle los pasos requeridos para crear esta aplicación.

Paso 1: agregar la base de datos al proyecto Como en los ejemplos anteriores, debe empezar por agregar la base de datos al proyecto. Después de agregar el archivo LibretaDirecciones.mdf como origen de datos, la ventana Orígenes de datos listará el objeto LibretaDireccionesDataSet, que contiene una tabla llamada Direcciones.

Paso 2: indicar que el IDE debe crear un conjunto de controles Label y TextBox para mostrar cada fila de datos En las secciones anteriores, usted arrastraba un nodo de la ventana Orígenes de datos hacia el formulario para crear un control DataGridView enlazado al miembro del origen de datos representado por ese nodo. El IDE le permite especificar el tipo de control(es) que va a crear cuando usted arrastra y suelta un miembro de origen de datos en un formulario. En vista de Diseño, haga clic en el nodo Direcciones en la ventana Orígenes de datos (figura 20.45). Observe que este nodo se convierte en una lista desplegable cuando lo selecciona. Haga clic en la flecha hacia abajo para ver los elementos en la lista. Al principio, el icono a la izquierda de DataGridView estará resaltado en color azul, ya que el control predeterminado que se enlaza a una tabla es DataGridView (como vimos en los ejemplos anteriores). Seleccione la opción Detalles en la lista desplegable, para indicar que el IDE debe crear un conjunto de pares de controles Label–TextBox para cada par de valores nombre-columna, cuando arrastre y suelte la tabla Direcciones en el formulario. (En la figura 20.46 puede ver los resultados de esta operación.) La lista desplegable contiene sugerencias para los controles que van a mostrar los datos de la tabla, pero también puede elegir la opción Personalizar… para seleccionar otros controles que sean capaces de enlazarse con los datos de una tabla.

Paso 3: arrastrar el nodo de origen de datos Direcciones hacia el formulario Arrastre el nodo Direcciones de la ventana Orígenes de datos hacia el formulario (figura 20.46). El IDE creará una serie de controles Label y TextBox, ya que usted seleccionó Detalles en el paso anterior. Como en los ejemplos anteriores, el IDE también crea un objeto BindingNavigator y los demás componentes en la bandeja de componentes. El IDE establece el texto de cada control Label con base en su correspondiente nombre de columna en la tabla de la base de datos, y utiliza expresiones regulares para insertar espacios en nombres de columnas con varias palabras, para que los controles Label sean más legibles.

762

Capítulo 20 Bases de datos: SQL y ADO.NET

Paso 4: hacer que el cuadro de texto IDDireccion sea de Sólo lectura La columna IDDireccion de la tabla Direcciones es una columna de identidad autoincremental, por lo que no se debe permitir que los usuarios editen los valores en esta columna. Seleccione el control TextBox para IDDireccion y establezca su propiedad ReadOnly a true, usando la ventana Propiedades. Tal vez necesite hacer clic en una parte vacía del formulario para deseleccionar los demás controles Label y TextBox y poder seleccionar el control TextBox IDDireccion.

Paso 5: ejecutar la aplicación Ejecute la aplicación y experimente con los controles en el objeto BindingNavigator que se encuentra en la parte superior de la ventana. Al igual que los ejemplos anteriores, este ejemplo llena un objeto DataSet (específicamente, un objeto LibretaDireccionesDataSet) con todas las filas de una tabla de la base de datos (es decir, Direcciones). Sin embargo, sólo aparece una sola fila del objeto DataSet en cualquier momento dado. Los botones del objeto BindingNavigator similares a los del reproductor de CD o DVD le permiten cambiar la fila que se muestra en un momento dado (es decir, cambian los valores en cada uno de los controles TextBox). Los botones para agregar una fila, eliminar una fila y guardar cambios también realizan sus tareas designadas. Al agregar una fila se borran los controles TextBox y aparece un nuevo ID autoincrementado (por ejemplo, 5) en el control TextBox a la derecha de ID Dirección. Después de escribir datos, haga clic en el botón Guardar para registrar la nueva fila en la base de datos. Después de cerrar y reiniciar la aplicación, debe haber todavía cinco filas. Elimine la nueva fila haciendo clic en el botón apropiado y después guarde los cambios.

Paso 6: agregar una consulta al objeto DireccionesTableAdapter Aunque el objeto BindingNavigator le permite explorar la libreta de direcciones, sería más conveniente poder buscar una entrada específica con base en el apellido paterno. Para agregar esta funcionalidad a la aplicación, debe agregar una nueva consulta al objeto AddressesTableAdapter, usando el Asistente para la configuración de consultas de TableAdapter. Haga clic en el icono Editar DataSet con el Diseñador ( ) en la ventana Orígenes de datos. Seleccione el cuadro que representa al objeto DireccionesTableAdapter. Haga clic con el botón derecho en el nombre del objeto TableAdapter y seleccione Agregar Query…. En el Asistente para la configuración de consultas de TableAdapter, mantenga la opción predeterminada Usar instrucciones SQL y haga clic en Siguiente. En la siguiente pantalla, mantenga la opción predeterminada SELECT que devuelve filas y haga clic en Siguiente. En vez de usar

Figura 20.45 | Seleccionar el (los) control(es) que se va(n) a crear cuando se arrastra y suelta un miembro de origen de datos en el formulario.

20.8 Programación con ADO.NET: caso de estudio de libreta de direcciones

763

Figura 20.46 | Mostrar una tabla en un formulario, usando una serie de controles Label y TextBox. el Generador de consultas para formar su consulta (como hicimos en el ejemplo anterior), modifique la consulta directamente en el cuadro de texto del asistente. Adjunte la cláusula “WHERE ApellidoPaterno = @apellidoPaterno” al final de la consulta predeterminada. Observe que @apellidoPaterno es un parámetro que se sustituirá con un valor cuando se ejecute la consulta. Haga clic en Siguiente, después escriba LlenarPorApellidoPaterno y ObtenerDatosPorApellidoPaterno como los nombres de los dos métodos que generará el asistente. La consulta contiene un parámetro, por lo que cada uno de estos métodos recibirá un parámetro para establecer el valor de @apellidoPaterno en la consulta. Haga clic en Finalizar para completar el asistente y regresar al Diseñador de DataSet (figura 20.47). Observe que los métodos Fill y Get recién creados aparecen bajo el objeto DireccionesTableAdapter y que el parámetro @apellidoPaterno está listado a la derecha de los nombres de los métodos.

Figura 20.47 | El Diseñador de DataSet para LibretaDireccionesDataSet, después de agregar una consulta a DireccionesTableAdapter.

764

Capítulo 20 Bases de datos: SQL y ADO.NET

Paso 7: agregar controles para permitir a los usuarios especificar un apellido paterno para localizarlo Ahora que ha creado una consulta para localizar filas con un apellido paterno específico, agregue controles para permitir a los usuarios escribir un apellido paterno y ejecutar esta consulta. Vaya a la vista de Diseño (figura 20.48) y agregue al formulario un control Label llamado buscarLabel, un control TextBox llamado buscarTextBox y un control Button llamado buscarButton. Coloque estos controles dentro de un control GroupBox llamado buscarGroupBox y después establezca su propiedad Text a Buscar una entrada por apellido paterno. Establezca las propiedades de los controles Label y Button según se muestra en la figura 20.48.

Paso 8: programar un manejador de eventos que localice el apellido paterno especificado por el usuario Haga doble clic en buscarButton para agregar un manejador del evento Click para este objeto Button. En el manejador de eventos, escriba las siguientes líneas de código (líneas 42-43 de la figura 20.44): direccionesTableAdapter.LlenarPorApellidoPaterno( libretaDireccionesDataSet.Direcciones, buscarTextBox.Text );

El método LlenarPorApellidoPaterno sustituye los datos actuales en libretaDireccionesDataSet.Direcciones con datos sólo para aquellas filas que tengan el apellido paterno que se escribió en buscarTextBox. Observe que, al invocar a LlenarPorApellidoPaterno, debe pasar el objeto DataTable a llenar, así como un argumento que especifique el apellido paterno a buscar. Este argumento se convierte en el valor del parámetro @apellidoPaterno en la instrucción SELECT creada en el paso 6. Inicie la aplicación para probar la nueva funcionalidad. Observe que cuando busca una entrada específica (es decir, escriba un apellido paterno y haga clic en Buscar), el objeto BindingNavigator permite al usuario explorar sólo las filas que contienen el apellido paterno especificado. Esto se debe a que el origen de datos enlazado a los controles del formulario (es decir, libretaDireccionesDataSet.Direcciones) ha cambiado, y ahora contiene sólo un número limitado de filas.

Figura 20.48 | Vista Diseño después de agregar controles para localizar un apellido paterno en la libreta de direcciones.

20.9 Uso de un objeto DataSet para leer y escribir XML

765

Paso 9: permitir que el usuario regrese a explorar todas las filas en la base de datos Para permitir que los usuarios regresen a explorar todas las filas después de buscar filas específicas, agregue un control Button llamado explorarTodoButton debajo del control buscarGroupBox. Haga doble clic en explorarTodoButton para agregar un manejador del evento Click al código. Establezca la propiedad Text de explorarTodoButton a Explorar todas las entradas en la ventana Propiedades. Agregue una línea de código que llame a direccionesTableAdapter.Fill( libretaDireccionesDataSet.Direcciones ) para volver a llenar el objeto DireccionesDataTable con todas las filas de la tabla en la base de datos (línea 50 de la figura 20.44). Agregue también una línea de código que borre la propiedad Text de buscarTextBox (línea 52). Inicie la aplicación. Busque un apellido paterno específico como en el paso anterior, y después haga clic en el botón explorarTodoButton para probar la nueva funcionalidad.

Enlace de datos en la aplicación LibretaDirecciones El proceso de arrastrar y soltar el nodo Direcciones de la ventana Orígenes de datos en el formulario Libretaeste ejemplo hizo que el IDE generara varios componentes en la bandeja de componentes. Éstos sirven para los mismos fines que los componentes generados para los ejemplos anteriores, en los que se utilizó la base de datos Libros. En este caso, libretaDireccionesDataSet es un objeto de un DataSet fuertemente tipificado, LibretaDireccionesDataSet, cuya estructura imita a la de la base de datos LibretaDirecciones. direccionesBindingSource es un objeto BindingSource que hace referencia a la tabla Direcciones del conjunto de datos LibretaDireccionesDataSet. direccionesTableAdapter encapsula a un objeto SqlDataAdapter configurado con objetos SqlCommand que ejecutan instrucciones de SQL en la base de datos LibretaDirecciones. Por último, direccionesBindingNavigator está enlazado al objeto direccionesBindingSource, con lo cual usted puede manipular en forma indirecta la tabla Direcciones del conjunto de datos LibretaDireccionesDataSet. En cada uno de los ejemplos anteriores, en los que se utilizó un control DataGridView para mostrar todas las filas de una tabla de la base de datos, se estableció la propiedad BindingSource del control DataGridView al correspondiente objeto BindingSource. En este ejemplo, usted seleccionó Detalles de la lista desplegable para la tabla Direcciones en la ventana Orígenes de datos, por lo que los valores de una sola fila de la tabla aparecen en el formulario, en un conjunto de controles TextBox. El IDE configura el enlace de datos en este ejemplo, enlazando cada uno de los controles TextBox con una columna específica del objeto DireccionesDataTable en el conjunto de datos LibretaDireccionesDataSet. Para ello, el IDE establece la propiedad DataBindings.Text del control TextBox. Usted puede ver esta propiedad haciendo doble clic en el signo + que está enseguida de (DataBindings) en la ventana Propiedades (figura 20.49). Si hace clic en la lista desplegable para esta propiedad, podrá elegir un objeto BindingSource y una propiedad (es decir, columna) dentro del origen de datos asociado, para enlazarlos con el control TextBox. Considere el control TextBox que muestra el valor PrimerNombre; el IDE lo nombró primerNombreTextBox. La propiedad DataBindings.Text de este control tiene asignada la propiedad PrimerNombre de DireccionesBindingSource (que hace referencia a LibretaDireccionesDataSet.Direcciones). Por ende, primerNombreTextBox siempre muestra el valor de la columna PrimerNombre en la fila actual seleccionada de libretaDireccionesDataSet.Direcciones. Cada control TextBox creado por el IDE en el formulario se configura de manera similar. Al explorar la libreta de direcciones con el objeto DireccionesBindingNavigator, cambia la posición actual en libretaDireccionesDataSet.Direcciones y, por consiguiente, cambian los valores que se muestran en cada control TextBox. Sin importar qué cambios se hagan al contenido de libretaDireccionesDataSet.Addresses, los controles TextBox permanecen enlazados a las mismas propiedades del objeto DataTable y siempre muestran los datos apropiados. Observe que los controles TextBox no muestran ningún valor si la versión en caché de Direcciones está vacía (es decir, si no hay filas en el objeto DataTable, debido a que la consulta que lo llenó no devolvió filas). DireccionesForm de

20.9 Uso de un objeto DataSet para leer y escribir XML Una poderosa característica de ADO.NET es su habilidad para convertir en XML los datos almacenados en un origen de datos, para intercambiar datos entre aplicaciones, en un formato que sea portable. La clase DataSet del espacio de nombres System.Data proporciona los métodos WriteXML, ReadXml y GetXml, los cuales permiten a los desarrolladores crear documentos de XML a partir de orígenes de datos, y convertir datos de XML en orígenes de datos.

766

Capítulo 20 Bases de datos: SQL y ADO.NET

Figura 20.49 | Vista de la propiedad DataBindings.Text de un control TextBox en la ventana Propiedades.

Escribir datos de un origen de datos a un documento de XML La aplicación de la figura 20.50 llena un objeto DataSet con estadísticas acerca de jugadores de béisbol, y después escribe los datos en un documento de XML. La aplicación también muestra el XML en un control TextBox. Para crear esta GUI, primero agregamos la base de datos Beisbol.mdf (que se encuentra en el directorio de ejemplos de este capítulo) al proyecto, y después arrastramos el nodo Jugadores de la ventana Orígenes de datos hacia el formulario. Esta acción creó los controles BindingNavigator y DataGridView que aparecen en los resultados de la figura 20.50. Después agregamos el botón escribirButton y el cuadro de texto salidaTextBox. La representación en XML de la tabla Jugadores no debe editarse y abarca más líneas de las que el control TextBox puede mostrar a la vez, por lo que establecimos las propiedades ReadOnly y MultiLine del control TextBox a true, y su propiedad ScrollBars a Vertical. Para crear el manejador de eventos de escribirButton, haga doble clic sobre este control en la vista de Diseño. El manejador de eventos XMLWriterForm_Load generado en forma automática (líneas 26-32) llama al método Fill de la clase JugadoresTableAdapter para llenar el conjunto de datos beisbolDataSet con datos provenientes de la tabla Jugadores en la base de datos Béisbol. Observe que el IDE enlaza el control DataGridView a beisbolDataSet.Jugadores (a través del objeto JugadoresBindingSource) para mostrar la información al usuario. 1 2 3 4 5 6 7 8 9

// Fig. 20.50: XMLWriter.cs // Demuestra la generación de XML a partir de un DataSet de ADO.NET. using System; using System.Windows.Forms; namespace XMLWriter { public partial class XMLWriterForm : Form {

Figura 20.50 | Escribir la representación en XML de un objeto DataSet a un archivo. (Parte 1 de 2).

20.9 Uso de un objeto DataSet para leer y escribir XML

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

767

public XMLWriterForm() { InitializeComponent(); } // fin del constructor de XMLWriterForm // el manejador del evento Click para el botón Guardar en el objeto // BindingNavigator guarda los cambios realizados a los datos private void jugadoresBindingNavigatorSaveItem_Click( object sender, EventArgs e ) { this.Validate(); this.jugadoresBindingSource.EndEdit(); this.jugadoresTableAdapter.Update( this.beisbolDataSet.Jugadores ); } // fin del método jugadoresBindingNavigatorSaveItem_Click // carga los datos en la tabla beisbolDataSet.Jugadores private void XMLWriterForm_Load( object sender, EventArgs e ) { // TODO: esta línea de código carga datos en la // tabla 'beisbolDataSet.Jugadores'. // Puede moverla o quitarla según sea necesario. this.jugadoresTableAdapter.Fill( this.beisbolDataSet.Jugadores ); } // escribe la representación en XML de DataSet cuando se hace clic en el botón private void escribirButton_Click( object sender, EventArgs e ) { // establece el espacio de nombres para este objeto DataSet // y el documento de XML resultante beisbolDataSet.Namespace = "http://www.deitel.com/beisbol"; // escribe la representación en XML de DataSet a un archivo beisbolDataSet.WriteXml( "Jugadores.xml" ); // muestra la representación en XML en un cuadro de texto salidaTextBox.Text += "Se escribió el siguiente XML:\r\n" + beisbolDataSet.GetXml() + "\r\n"; } // fin del método escribirButton_Click } // fin de la clase XMLWriterForm } // fin del espacio de nombres XMLWriter

Figura 20.50 | Escribir la representación en XML de un objeto DataSet a un archivo. (Parte 2 de 2).

768

Capítulo 20 Bases de datos: SQL y ADO.NET

Las líneas 35-47 definen el manejador de eventos para el botón Escribir a XML. Cuando el usuario hace clic en este botón, la línea 39 establece la propiedad Namespace de beisbolDataSet para especificar un nombre de espacio para el objeto DataSet y cualquier documento de XML basado en ese objeto DataSet (en la sección 19.4 aprenderá acerca de los espacios de nombres de XML). La línea 42 invoca al método WriteXml de DataSet, el cual genera una representación en XML de los datos contenidos en el objeto DataSet, y después escribe el XML en el archivo especificado. Este archivo se crea en el directorio bin/Debug o bin/Release del proyecto, dependiendo de cómo haya ejecutado el programa. A continuación, las líneas 45-46 muestran esta representación en XML, que se obtiene mediante la invocación del método GetXml de DataSet, el cual devuelve un objeto string que contiene el XML.

Examinar un documento de XML generado por el método WriteXml de DataSet La figura 20.51 presenta el documento Jugadores.xml generado por el método WriteXml de DataSet en la figura 20.50. Observe que el elemento raíz de BeisbolDataSet (línea 2) declara el espacio de nombres predeterminado del documento para que sea el espacio de nombres especificado en la línea 39 de la figura 20.50. Cada elemento de Jugadores representa un registro en la tabla Jugadores. Los elementos IDJugador, PrimerNombre, ApellidoPaterno y PromedioBateo corresponden a las columnas con esos nombres en la tabla Jugadores de la base de datos.

20.10 Conclusión En este capítulo se introdujeron las bases de datos relacionales, SQL, ADO.NET y las herramientas de programación visual del IDE para trabajar con bases de datos. Usted analizó el contenido de una base de datos simple llamada Libros, y aprendió acerca de las relaciones entre las tablas de la base de datos. Después aprendió SQL básico para recuperar datos de, agregar nuevos datos a, y actualizar datos en, una base de datos. Aprendió acerca de las clases de los espacios de nombres System.Data y System.Data.SqlClient, que permiten a los programas conectarse a una base de datos, para después acceder a esa base de datos y manipular sus datos. En este capítulo también se explicó el modelo sin conexión de ADO.NET, el cual permite a un programa almacenar de manera temporal en la memoria local los datos de una base de datos, como un objeto DataSet. La segunda parte del capítulo se enfocó en el uso de las herramientas y asistentes del IDE para acceder a los orígenes de datos y manipularlos como una base de datos en las aplicaciones de GUI en C#. Usted aprendió a agregar orígenes de datos a los proyectos, y a utilizar las herramientas de “arrastrar y colocar” del IDE para mostrar

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21



1 John Doe 0.375

2 Jack Smith 0.223

3 George O'Malley 0.344

Figura 20.51 | Documento de XML generado a partir de BeisbolDataSet en XMLWriter.

20.11 Recursos Web

769

tablas de bases de datos en las aplicaciones. Le mostramos cómo el IDE oculta de usted el SQL que se utiliza para interactuar con la base de datos. También demostramos cómo agregar consultas personalizadas a las aplicaciones de GUI, de manera que se muestren sólo las filas de datos que cumplan con criterios específicos. Por último, aprendió a escribir datos de un origen de datos hacia un archivo XML. En el siguiente capítulo, demostraremos cómo crear aplicaciones Web mediante el uso de la tecnología ASP. NET de Microsoft. También presentaremos el concepto de una aplicación de tres niveles, en donde una aplicación se divide en tres piezas que pueden residir en el mismo equipo, o pueden distribuirse entre equipos separados a través de una red como Internet. Como veremos, uno de estos niveles (el nivel de información) por lo general almacena datos en un RDBMS como SQL Server.

20.11 Recursos Web www.microsoft.com/spain/sql/default.mspx

Este sitio ofrece información en español acerca de SQL Server 2005, incluyendo tutoriales, artículos, noticias, actualizaciones y descargas. www.microsoft.com/spanish/msdn/centro_recursos/sql2005/default.mspx

El sitio del Centro de desarrollo de Microsoft SQL Server 2005 ofrece información en español acerca de SQL Server 2005 Express, incluyendo una descripción general, tutoriales y artículos. msdn.microsoft.com/sql/

El Centro para desarrolladores de SQL Server ofrece información actualizada sobre productos, descargas, artículos y foros comunitarios. lab.msdn.microsoft.com/express/sql

La página inicial para SQL Server 2005 Express ofrece artículos con ejemplos, blogs, grupos de noticias y demás recursos valiosos. msdn2.microsoft.com/library/system.data.aspx

La documentación de Microsoft acerca del espacio de nombres System.Data. msdn.microsoft.com/SQL/sqlreldata/TSQL/default.aspx

La guía de referencia del lenguaje SQL de Microsoft. msdn2.microsoft.com/library/ms172013.aspx La documentación de Microsoft para el Generador de consultas y otras herramientas visuales de bases de datos. www.w3schools.com/sql/default.asp

El tutorial de SQL del W3C presenta las características básicas y avanzadas de SQL con ejemplos. www.sql.org

El portal SQL ofrece vínculos a muchos recursos, incluyendo la sintaxis de SQL, tips, tutoriales, libros, revistas, grupos de discusión, compañías con servicios de SQL, consultores de SQL y software gratuito. www.oracle.com/database/index.html

La página inicial para los sistemas de administración de bases de datos de Oracle. www.sybase.com

La página inicial para el sistema de administración de bases de datos de Sybase. www-306.ibm.com/software/data/db2/

La página inicial para el sistema de administración de bases de datos DB2 de IBM. www.postgresql.org

La página inicial para el sistema de administración de bases de datos PostgreSQL. www.mysql.com

La página inicial para el servidor de bases de datos MySQL.

21

ASP.NET 2.0, formularios Web Forms y controles Web

OBJETIVOS En este capítulo aprenderá lo siguiente: Q

Desarrollar aplicaciones Web mediante el uso de ASP. NET.

Si cualquier hombre prepara su caso y coloca su nombre al pie de la primera página, yo le daré una respuesta inmediata. Si me obliga a dar vuelta a la hoja, deberá esperar a mi conveniencia. —Lord Sandwich

Regla uno: nuestro cliente siempre tiene la razón. Regla dos: si piensas que nuestro cliente está mal, consulta la Regla uno.

Q

Crear formularios Web Forms.

Q

Crear aplicaciones ASP.NET que consistan en varios formularios Web Forms.

Q

Mantener la información de estado acerca de un usuario, con rastreo de sesión y cookies.

—Anónimo

Q

Usar la Herramienta Administración de sitios Web para modificar las opciones de configuración de una aplicación Web.

Una pregunta justa debe ir seguida de un acto en silencio.

Q

Controlar el acceso de los usuarios a las aplicaciones Web, usando autenticación de formularios y controles de inicio de sesión de ASP.NET.

Q

Utilizar bases de datos en aplicaciones ASP.NET.

Q

Diseñar una página principal y páginas de contenido para crear un sitio Web con apariencia visual uniforme.

—Dante Alighieri

Vendrás aquí y obtendrás libros que abrirán tus ojos, oídos, y tu curiosidad; y sacarán tu interior, o meterán tu exterior. —Ralph Waldo Emerson

Pla n g e ne r a l

772

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

21.1 21.2 21.3 21.4

21.5

21.6

21.7

21.8

21.9 21.10

Introducción Transacciones HTTP simples Arquitectura de aplicaciones multinivel Creación y ejecución de un ejemplo de formulario Web Forms simple 21.4.1 Análisis de un archivo ASPX 21.4.2 Análisis de un archivo de código subyacente (code-behind) 21.4.3 Relación entre un archivo ASPX y un archivo de código subyacente 21.4.4 Cómo se ejecuta el código en una página Web ASP.NET 21.4.5 Análisis del XHTML generado por una aplicación ASP.NET 21.4.6 Creación de una aplicación Web ASP.NET Controles Web 21.5.1 Controles de texto y gráficos 21.5.2 Control AdRotator 21.5.3 Controles de validación Rastreo de sesiones 21.6.1 Cookies 21.6.2 Rastreo de sesiones con HttpSessionState Caso de estudio: conexión a una base de datos en ASP.NET 21.7.1 Creación de un formulario Web Forms que muestra datos de una base de datos 21.7.2 Modificación del archivo de código subyacente para la aplicación Libro de visitantes Caso de estudio: aplicación segura de la base de datos Libros 21.8.1 Análisis de la aplicación segura de la base de datos Libros completa 21.8.2 Creación de la aplicación segura de la base de datos Libros Conclusión Recursos Web

21.1 Introducción En capítulos anteriores, utilizamos formularios Windows Forms y controles de Windows para desarrollar aplicaciones para Windows. En este capítulo presentaremos el desarrollo de aplicaciones Web con la tecnología ASP.NET 2.0 de Microsoft. Las aplicaciones basadas en Web crean contenido Web para los exploradores Web clientes. Este contenido Web incluye Lenguaje de marcado de hipertexto extensible (XHTML), secuencias de comandos del lado cliente, imágenes y datos binarios. Es conveniente que si usted no está familiarizado con XHTML lea primero el apéndice F, Introducción a XHTML: Parte 1, y el apéndice G, Introducción a XHTML: Parte 2, antes de estudiar este capítulo. Presentaremos varios ejemplos que demuestran el desarrollo de aplicaciones Web mediante el uso de formularios Web Forms, controles Web (también llamados controles de servidor ASP.NET ) y programación en C#. Los archivos de los formularios Web Forms tienen la extensión de archivo .aspx y contienen la GUI de la página Web. Usted personaliza los formularios Web Forms agregando controles Web, incluyendo etiquetas, cuadros de texto, imágenes, botones y demás componentes de la GUI. El archivo de formulario Web Forms representa la página Web que se envía al explorador cliente. De aquí en adelante, nos referiremos a los archivos de formularios Web Forms como archivos ASPX. Todo archivo ASPX creado en Visual Studio tiene su correspondiente clase escrita en un lenguaje .NET, como C#. Esta clase contiene los manejadores de eventos, el código de inicialización, los métodos utilitarios y demás código de soporte. El archivo que contiene esta clase se llama archivo de código subyacente (code-behind ), y es el que proporciona la implementación programática del archivo ASPX. Para desarrollar el código y las GUIs en este capítulo, utilizamos Microsoft Visual Web Developer 2005 Express: un IDE diseñado para desarrollar aplicaciones Web de ASP.NET. Visual Web Developer y Visual C# 2005

21.2 Transacciones HTTP simples

773

Express comparten muchas características y herramientas de programación visual comunes, que simplifican la creación de aplicaciones complejas, como las que acceden a una base de datos (que presentaremos en las secciones 21.7-21.8). La versión completa de Visual Studio 2005 incluye la funcionalidad de Visual Web Developer, por lo que las introducciones que presentaremos para Visual WEB Developer se aplican también para Visual Studio 2005. Tenga en cuenta que debe instalar Visual Web Developer 2005 Express (disponible en inglés: lab.msdn. microsoft.com/express/vwd/default.aspx, o en español: www.microsoft.com/spanish/msdn/vstudio/ express/VWD/default.mspx) o una versión completa de Visual Studio 2005 para implementar los programas en este capítulo y en el capítulo 22, Servicios Web. El sitio www.deitel.com/books/csharpforprogrammers2 proporciona instrucciones para ejecutar los ejemplos de ASP.NET 2.0 que se presentan en este capítulo, en caso de que no desee recrearlos.

21.2 Transacciones HTTP simples El desarrollo de aplicaciones Web requiere una comprensión básica de las redes y World Wide Web. En esta sección veremos el Protocolo de transferencia de hipertexto (HTTP ) y lo que ocurre detrás de las cámaras cuando un explorador visualiza una página Web. HTTP especifica un conjunto de métodos y encabezados que permiten a los clientes y servidores interactuar e intercambiar información, de una manera uniforme y predecible. En su forma más simple, una página Web no es más que un documento XHTML: un archivo de texto simple que contiene marcado (es decir, etiquetas) para describir a un explorador Web cómo visualizar y dar formato a la información del documento. Por ejemplo, el siguiente marcado de XHTML: Mi página Web

indica que el explorador debe mostrar el texto entre la etiqueta inicial y la etiqueta final en la barra de título. Los documentos de XHTML también pueden contener datos de hipertexto (que por lo general se conocen como hipervínculos) con vínculos hacia distintas páginas, o a otras partes de la misma página. Cuando el usuario activa un hipervínculo (por lo general, haciendo clic sobre él con el ratón), la página Web solicitada se carga en la ventana del explorador del usuario. Cualquier documento de XHTML disponible para verlo a través de la Web tiene su correspondiente Localizador de recursos uniforme (URL). Un URL es una dirección que indica la ubicación de un recurso de Internet, como un documento de XHTML. El URL contiene información que dirige a un explorador al recurso que el usuario desea acceder. Las computadoras que ejecutan software de servidor Web hacen que dichos recursos estén disponibles. Cuando se realizan peticiones de aplicaciones Web ASP.NET, por lo general el servidor Web es Microsoft Internet Information Services (IIS ). Como veremos en breve, también es posible probar aplicaciones ASP.NET mediante el uso del Servidor de desarrollo ASP.NET integrado en Visual Web Developer. Examinaremos los componentes del URL http://www.deitel.com/books/downloads.html

El

http:// indica que el recurso se debe obtener utilizando el protocolo HTTP. La porción intermedia, www. deitel.com, es el nombre de host completamente calificado del servidor: el nombre de la computadora en la que

reside el recurso. Por lo general, a esta computadora se le denomina host, ya que aloja los recursos y los mantiene. El nombre de host www.deitel.com se traduce en una dirección IP (68.236.123.125), que identifica al servidor en forma similar al número telefónico que identifica en forma única a una línea telefónica específica. El nombre de host se traduce en una dirección IP mediante un servidor del sistema de nombre de dominios (DNS ): una computadora que mantiene una base de datos de nombres de hosts y sus correspondientes direcciones IP. Esta operación de traducción se conoce como búsqueda en el DNS. El resto del URL (es decir, /books/downloads.html) especifica el nombre del recurso solicitado (el documento de XHTML downloads.html) y su ruta, o ubicación (/books) en el servidor Web. La ruta podría especificar la ubicación de un directorio real en el sistema de archivos del servidor Web. No obstante, por razones de seguridad la ruta a menudo especifica la ubicación de un directorio virtual. En dichos sistemas, el servidor traduce el directorio virtual en una ubicación real en el servidor (o en otra computadora en la red del servidor), con lo cual se oculta la verdadera ubicación del recurso. Algunos recursos se crean en forma dinámica y, por lo tanto, no residen en ninguna parte del equipo servidor. El nombre de host en el URL para un recurso de este tipo especifica el servidor correcto, y la información de la ruta y del recurso identifican la ubicación del recurso con el cual se responderá a la solicitud del cliente.

774

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Cuando el explorador Web recibe un URL, realiza una transacción HTTP simple para recuperar y visualizar la página Web que se encuentra en esa dirección. La figura 21.1 ilustra la transacción con detalle. Esta transacción consiste en la interacción entre el explorador Web (el lado cliente) y la aplicación del servidor Web (el lado servidor). En la figura 21.1, el explorador Web envía una solicitud HTTP al servidor. La solicitud (en su forma más simple) es: GET /books/downloads.html HTTP/1.1

La palabra GET es un método de HTTP, el cual indica que el cliente desea obtener un recurso del servidor. El resto de la solicitud proporciona el nombre de la ruta del recurso (un documento de XHTML), junto con el nombre del protocolo y el número de versión (HTTP/1.1). Cualquier servidor que entienda HTTP (versión 1.1) puede traducir esta solicitud y responder en forma apropiada. La figura 21.2 ilustra el resultado de una solicitud exitosa. El servidor primero responde enviando una línea de texto que indica la versión de HTTP, seguida de un código numérico y una frase que describe el estado de la transacción. Por ejemplo, HTTP/1.1 200 OK

indica éxito, mientras que HTTP/1.1 404 Not found

informa al cliente que el servidor Web no pudo localizar el recurso solicitado. Después, el servidor envía uno o más encabezados HTTP, que proporcionan información adicional acerca de los datos que se van a enviar. En este caso, el servidor envía un documento de texto de XHTML, por lo que el encabezado HTTP para este ejemplo sería: Content-type: text/html

La información que se proporciona en este encabezado especifica el tipo de Extensiones multipropósito de correo Internet (MIME ) del contenido que el servidor transmitirá al explorador. MIME es un estándar de Internet que especifica formatos de datos, de manera que los programas puedan interpretar los datos en forma correcta. Por ejemplo, el tipo MIME text/plain indica que la información que se envía es texto que puede visualizarse de manera directa, sin necesidad de interpretar el contenido como marcado de XHTML. De manera similar, el tipo MIME image/jpeg indica que el contenido es una imagen JPEG. Cuando el explorador recibe este tipo MIME, trata de visualizar la imagen. El encabezado, o conjunto de encabezados, va seguido de una línea en blanco, la cual indica al cliente que el servidor terminó de enviar encabezados HTTP. Después, el servidor envía el contenido del documento de XHTML solicitado (downloads.html). El servidor termina la conexión cuando se completa la transferencia

V AVhda^X^ijYGEThZ Zck†VYZaXa^ZciZ VahZgk^YdgLZW#

HZgk^YdgLZW W 9Zhej‚hYZ gZX^W^gaVhda^X^ijY! ZahZgk^YdgLZW WjhXVZa gZXjghdZchj h^hiZbV#

8a^ZciZ >ciZgcZi

Figura 21.1 | Un cliente interactuando con el servidor Web. Paso 1: la solicitud GET.

21.3 Arquitectura de aplicaciones multinivel

775

HZgk^YdgLZW :ahZgk^YdggZhedcYZ VaVhda^X^ijY XdcjcbZchV_Z Vegde^VYd nZaXdciZc^Yd YZagZXjghd# 8a^ZciZ >ciZgcZi

Figura 21.2 | Un cliente interactuando con el servidor Web. Paso 2: la respuesta HTTP. del recurso. En este punto, el explorador del lado cliente analiza el marcado de XHTML que recibió y representa (o visualiza) los resultados.

21.3 Arquitectura de aplicaciones multinivel

Las aplicaciones basadas en Web son aplicaciones multinivel (a las que algunas veces se les conoce como aplicaciones de n niveles). Las aplicaciones multinivel dividen su funcionalidad en niveles separados (es decir, agrupamientos lógicos de funcionalidad). Aunque los niveles pueden ubicarse en la misma computadora, por lo general los niveles de las aplicaciones basadas en Web residen en computadoras separadas. La figura 21.3 presenta la estructura básica de una aplicación basada en Web de tres niveles. El nivel de información (también conocido como nivel de datos o nivel inferior) mantiene los datos que pertenecen a la aplicación. Por lo común, este nivel almacena los datos en un sistema de administración de bases de datos relacionales (RDBMS). En el capítulo 20 hablamos sobre los sistemas RDBMS. Por ejemplo, una tienda de ventas al detalle podría tener una base de datos para almacenar la información de sus productos, como las descripciones, los precios y las cantidades en existencia. La misma base de datos también podría contener información sobre los clientes, como los nombres de usuario, las direcciones de facturación y los números de tarjetas de crédito. Este nivel puede contener varias bases de datos, que en conjunto conforman los datos necesarios para nuestra aplicación. El nivel intermedio implementa la lógica comercial, la lógica de control y la lógica de presentación, para controlar las interacciones entre los clientes de la aplicación y los datos de la misma. El nivel intermedio actúa como un intermediario entre los datos en el nivel de información y los clientes de la aplicación. La lógica de control del nivel intermedio procesa las peticiones de los clientes (como las peticiones para ver un catálogo de productos) y recupera datos de la base de datos. Después, la lógica de presentación del nivel intermedio procesa los datos del nivel de información y presenta el contenido al cliente. Por lo general, las aplicaciones Web presentan datos a los clientes en forma de documentos de XHTML.

C^kZahjeZg^dg

C^kZa^ciZgbZY^d

C^kZa^c[Zg^dg

>ciZg[VoYZjhjVg^d

A‹\^XVXdbZgX^Va

9Vidh

8a^ZciZ

6HE#C:I

>c[dgbVX^‹c

:meadgVYdg

M=IBA

69D#C:I HZgk^YdgLZW

Figura 21.3 | Arquitectura de tres niveles.

7VhZYZ YVidh

776

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

La lógica comercial en el nivel intermedio hace valer las reglas comerciales y asegura que los datos sean confiables antes de que la aplicación servidor actualice la base de datos o presente los datos a los usuarios. Las reglas comerciales dictan la forma en que los clientes pueden o no tener acceso a los datos de la aplicación, y cómo las aplicaciones procesan los datos. Por ejemplo, una regla comercial en el nivel intermedio de la aplicación basada en Web de una tienda de ventas al detalle podría asegurar que las cantidades de todos los productos permanezcan siempre positivas. La lógica comercial del nivel intermedio rechazaría la solicitud de un cliente de establecer una cantidad negativa en la base de datos de información de productos del nivel inferior. El nivel cliente, o nivel superior, es la interfaz de usuario de la aplicación, la cual recopila los datos de entrada y visualiza los resultados. Los usuarios interactúan en forma directa con la aplicación a través de la interfaz de usuario, que por lo general es un explorador Web, un teclado y un ratón. En respuesta a las acciones del usuario (por ejemplo, hacer clic en un hipervínculo), el nivel cliente interactúa con el nivel intermedio para realizar peticiones y para recuperar datos del nivel de información. Después, el nivel cliente muestra al usuario los datos recuperados del nivel intermedio. El nivel cliente nunca interactúa en forma directa con el nivel de información.

21.4 Creación y ejecución de un ejemplo de formulario Web Forms simple Nuestro primer ejemplo muestra la hora del día del servidor Web en una ventana del explorador Web. Al ejecutarse, este programa muestra el texto Un ejemplo simple de formularios Web Forms, seguido de la hora del servidor Web. Como dijimos antes, el programa consiste en dos archivos relacionados: un archivo ASPX (figura 21.4) y un archivo de código subyacente en C# (figura 21.5). Primero le mostraremos el marcado, el código y los resultados; después lo guiaremos cuidadosamente a través del proceso paso a paso para crear este programa. [Nota: el marcado en la figura 21.4 y en los demás listados de archivos ASPX en este capítulo es el mismo que el marcado que aparece en Visual Web Developer, pero nosotros cambiamos el formato del marcado para fines de presentación y para mejorar la legibilidad del código.] Visual Web Developer genera todo el marcado que se muestra en la figura 21.4 cuando usted establece el título de la página Web, escribe texto en el formulario Web Forms, arrastra un control Label hacia el formulario Web y establece las propiedades del texto de la página y del control Label. En breve le mostraremos estos pasos. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25





Un ejemplo simple de un formulario Web Forms



Hora actual en el servidor Web:







Figura 21.4 | Archivo ASPX que muestra la hora del servidor Web.

21.4 Creación y ejecución de un ejemplo de formulario Web Forms simple

21.4.1 Análisis de un archivo ASPX

777

El archivo ASPX contiene otra información además de XHTML. Las líneas 1-2 son comentarios de ASP.NET que indican el número de figura, el nombre del archivo y el propósito del mismo. Los comentarios de ASP.NET empiezan con . Nosotros agregamos estos comentarios al archivo. Las líneas 3-4 utilizan una directiva Page (en un archivo ASPX, una directiva se delimita con ) para especificar información que ASP. NET necesita para procesar este archivo. El atributo Language de la directiva Page especifica el lenguaje del archivo de código subyacente (code-behind) como C#; el archivo de código subyacente (es decir, CodeFile) es HoraWeb. aspx.cs. Observe que el nombre de un archivo de código subyacente por lo general consiste en el nombre completo del archivo ASPX (por ejemplo, HoraWeb.aspx), seguido de la extensión .cs. El atributo AutoEventWireup (línea 3) determina cómo se manejan los eventos de los formularios Web Forms. Cuando AutoEventWireup se establece en true, ASP.NET determina cuáles métodos de la clase se van a llamar, en respuesta a un evento generado por la directiva Page. Por ejemplo, ASP.NET llama a los métodos Page_Load y Page_Init en el archivo de código subyacente para manejar los eventos Load e Init de Page, respectivamente. (Más adelante en el capítulo hablaremos sobre estos eventos.) El atributo Inherits (línea 4) especifica la clase en el archivo de código subyacente de la que esta clase ASP. NET hereda; en este caso, HoraWeb. En unos momentos hablaremos más sobre Inherits. [Nota: establecimos de manera explícita el atributo EnableSessionState (línea 4) a False. Más adelante en el capítulo explicaremos el significado de este atributo. Algunas veces el IDE genera valores para los atributos (por ejemplo, True y False) y nombres para los controles (como verá más adelante en el capítulo) que no se adhieren a nuestras convenciones estándar de capitalización de código (es decir, true y false). No obstante, y a diferencia del código de C#, el marcado de ASP.NET no es sensible al uso de mayúsculas o minúsculas, por lo que no hay ningún problema si utilizamos de manera distinta las mayúsculas y las minúsculas. Para permanecer consistentes con el código generado por el IDE, no modificamos estos valores en nuestros listados de código ni en nuestras discusiones complementarias.] Para este primer archivo ASPX, proporcionamos una breve discusión acerca del marcado de XHTML. No hablaremos sobre la mayoría del XHTML contenido en los siguientes archivos ASPX. Las líneas 6-7 contienen la declaración de tipo de documento, que especifica el nombre del elemento de documento (HTML) y el Identificador de recursos uniforme (URI) PUBLIC para la DTD que define el vocabulario XHTML. Las líneas 9-10 contienen las etiquetas iniciales y , respectivamente. Los documentos de XHTML tienen el elemento raíz html y la información de marcado sobre el documento en el elemento head. Observe además que el elemento html especifica el espacio de nombres XML del documento, usando el atributo xmlns (vea la sección 19.4). La línea 11 establece el título de esta página Web. En breve demostraremos cómo establecer el título a través de una propiedad en el IDE. Observe el atributo runat en la línea 10, que se estableció a "server". Este atributo indica que, cuando un cliente solicita este archivo ASPX, ASP.NET procesa el elemento head y sus elementos anidados en el servidor, y genera el XHTML correspondiente, el cual se envía posteriormente al cliente. En este caso, el XHTML que se envía al cliente será idéntico al marcado en el archivo ASPX. Sin embargo, y como veremos pronto, ASP.NET puede generar marcado de XHTML complejo a partir de elementos simples en un archivo ASPX. La línea 13 contiene la etiqueta inicial , la cual empieza el cuerpo del documento de XHTML; el cuerpo tiene el contenido principal que el explorador visualiza. El formulario que contiene nuestro texto XHTML y los controles se define en las líneas 14-23. De nuevo, el atributo runat en el elemento form indica que este elemento se ejecuta en el servidor, el cual genera el XHTML equivalente y lo envía al cliente. Las líneas 15-22 contienen un elemento div, que agrupa los elementos del formulario en un bloque de marcado. La línea 16 es un elemento de encabezado h2, el cual contiene texto que indica el propósito de la página Web. Como demostraremos en breve, el IDE genera este elemento en respuesta a la acción de escribir texto directamente en el formulario Web Forms y seleccionar el texto como encabezado de segundo nivel. Las líneas 17-21 contienen un elemento p para marcar el contenido y que se visualice como un párrafo en el explorador. Las líneas 18-20 marcan un control Web tipo etiqueta. Las propiedades que establecemos en la ventana Propiedades, como Font-Size y BackColor (es decir, el color de fondo), son atributos aquí. El atributo ID (línea 18) asigna un nombre al control, de manera que pueda manipularse mediante programación en el archivo de código subyacente. Establecimos el atributo EnableViewState del control (línea 20) a False. Más adelante en el capítulo explicaremos el significado de este atributo.

778

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

El prefijo de etiqueta asp: en la declaración de la etiqueta Label (línea 18) indica que la etiqueta es un control Web de ASP.NET, no un elemento de XHTML. Cada control Web se asigna a un elemento correspondiente de XHTML (o grupo de elementos); cuando procesa un control Web en el servidor, ASP.NET genera marcado de XHTML que se enviará al cliente para representar a ese control en un explorador Web.

Tip de portabilidad 21.1 El mismo control Web puede asignarse a distintos elementos de XHTML, dependiendo del explorador cliente y de las configuraciones de las propiedades del control Web.

En este ejemplo, el control asp:Label se asigna al elemento span de XHTML (es decir, ASP.NET crea un elemento span para representar a este control en el explorador Web del cliente). Un elemento span contiene texto que se visualiza en una página Web. Este elemento específico se utiliza debido a que los elementos span permiten aplicar estilos de formato al texto. Varios de los valores de las propiedades que se aplicaron a nuestra etiqueta se representan como parte del atributo style del elemento span. Pronto verá cuál es la apariencia del marcado del elemento span generado. El control Web en este ejemplo contiene el par atributo-valor runat="server" (línea 18), ya que este control debe procesarse en el servidor, de manera que éste pueda traducir el control en XHTML que pueda representarse en el explorador cliente. Si este par de atributos no está presente, el elemento asp:Label se escribe como texto en el cliente (es decir, el control no se convierte en un elemento span y no se representa en forma apropiada).

21.4.2 Análisis de un archivo de código subyacente (code-behind) La figura 21.5 presenta el archivo de código subyacente. Recuerde que el archivo ASPX en la figura 21.4 hace referencia a este archivo en la línea 3. La línea 13 empieza la declaración de la clase WebTime. En el capítulo 9 vimos que la declaración de una clase puede abarcar varios archivos de código fuente, y que las porciones separadas de la declaración de la clase en cada archivo se conocen como clases parciales. El modificador partial en la línea 13 de la figura 21.5 indica que el archivo de código subyacente en realidad es una clase parcial. En breve hablaremos sobre el resto de esta clase. La línea 13 indica que HoraWeb hereda de la clase Page en el espacio de nombres System.Web.UI. Este espacio de nombres contiene clases y controles que ayudan a crear aplicaciones basadas en Web. La clase Page proporciona los manejadores de eventos y los objetos necesarios para crear aplicaciones basadas en Web. Además de la clase Page (de la que todas las aplicaciones Web heredan, ya sea en forma directa o indirecta), System. Web.UI incluye también la clase Control: la clase base que proporciona una funcionalidad común para todos los controles Web.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

// Fig. 21.5: HoraWeb.aspx.cs // Archivo de código subyacente para una página que muestra la hora actual. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class HoraWeb : System.Web.UI.Page { // Inicializa el contenido de la página protected void Page_Init( object sender, EventArgs e ) {

Figura 21.5 | Archivo de código subyacente para una página que muestra la hora del servidor Web. (Parte 1 de 2).

21.4 Creación y ejecución de un ejemplo de formulario Web Forms simple

18 19 20 21 22

779

// muestra la hora actual del servidor en horaLabel horaLabel.Text = string.Format( "{0:D2}:{1:D2}:{2:D2}", DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second ); } // fin del método Page_Init } // fin de la clase HoraWeb

Figura 21.5 | Archivo de código subyacente para una página que muestra la hora del servidor Web. (Parte 2 de 2). Las líneas 16-21 definen el método Page_Init, el cual maneja el evento Init de la página. Este evento (el primero que se genera después de que se solicita una página) indica que la página está lista para inicializarse. La única inicialización requerida para esta página es establecer la propiedad Text de horaLabel con la hora del servidor (es decir, la computadora en la que se ejecuta este código). La instrucción en las líneas 19-20 recupera la hora actual y le aplica el formato HH:MM:SS. Por ejemplo, a las 9 AM se les aplica el formato 09:00:00 y a las 2:30 PM se les aplica el formato 14:30:00. Observe que el archivo de código subyacente puede acceder a horaLabel (el ID del control Label en el archivo ASPX) mediante programación, aun cuando el archivo no contiene una declaración para una variable llamada horaLabel. En unos momentos veremos por qué.

21.4.3 Relación entre un archivo ASPX y un archivo de código subyacente ¿Cómo se utilizan los archivos ASPX y de código subyacente para crear la página Web que se envía al cliente? En primer lugar, recuerde que la clase HoraWeb es la clase base especificada en la línea 3 del archivo ASPX (figura 21.4). Esta clase (declarada parcialmente en el archivo de código subyacente) hereda de Page, la cual define la funcionalidad general de una página Web. La clase parcial HoraWeb hereda esta funcionalidad y define cierta funcionalidad propia (es decir, mostrar la hora actual). El archivo de código subyacente contiene el código para mostrar la hora, mientras que el archivo ASPX contiene el código para definir la GUI. Cuando un cliente solicita un archivo ASPX, ASP.NET crea dos clases detrás de las cámaras. Recuerde que el archivo de código subyacente contiene una clase parcial llamada HoraWeb. El primer archivo que ASP.NET genera es otra clase parcial, la cual contiene el resto de la clase HoraWeb, con base en el marcado que contiene el archivo ASPX. Por ejemplo, HoraWeb.aspx contiene un control Web Label con un ID de horaLabel, por lo que la clase parcial generada contendría una declaración para una variable Label llamada horaLabel. Esta clase parcial podría verse así: public partial class HoraWeb { protected System.Web.UI.WebControls.Label horaLabel; }

Observe que Label es un control Web definido en el espacio de nombres System.Web.UI.WebControls, el cual contiene controles Web para diseñar la interfaz de usuario de una página. Los controles Web en este espacio de nombres se derivan de la clase WebControl. Al compilarse, la declaración de la clase parcial anterior que contiene declaraciones de controles Web se combina con la declaración de la clase parcial del archivo de código subyacente, para formar la clase HoraWeb completa. Esto explica por qué la línea 19 en el método Page_Init de HoraWeb. aspx.cs (figura 21.5) puede acceder a horaLabel, la cual se crea en las líneas 18-20 de HoraWeb.aspx (figura 21.4); el método Page_Init y el control horaLabel son en realidad miembros de la misma clase, pero están definidos en clases parciales separadas.

780

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

La segunda clase generada por ASP.NET se basa en el archivo ASPX que define la representación visual de la página. Esta nueva clase hereda de la clase HoraWeb, la cual define la lógica de la página. La primera vez que se solicita la página Web, esta clase se compila y se crea una instancia. Esta instancia representa a nuestra página; crea el XHTML que se envía al cliente. El ensamblado creado a partir de nuestras clases compiladas se coloca dentro de un subdirectorio de C:\Windows\Microsoft.NET\Framework\NumeroVersión\ Temporary ASP.NET Files\HoraWeb

en donde NúmeroVersión es el número de versión del .NET Framework (por ejemplo, instalado en su computadora.

v2.0.50215)

que está

Tip de rendimiento 21.1 Una vez que se ha creado una instancia de la página Web, varios clientes pueden usarla para acceder a la página; no es necesario volver a compilar. El proyecto se volverá a compilar sólo cuando usted modifique la aplicación; el entorno en tiempo de ejecución detecta los cambios y el proyecto se recompila para reflejar el contenido alterado.

21.4.4 Cómo se ejecuta el código en una página Web ASP.NET Veamos brevemente cómo se ejecuta el código para nuestra página Web. Cuando se crea una instancia de la página, el evento Init ocurre primero, invocando al método Page_Init. Este método puede contener el código necesario para inicializar objetos y otros aspectos de la página. Una vez que se ejecuta Page_Init, ocurre el evento Load y se ejecuta el manejador de eventos Page_Load. Aunque no está presente en este ejemplo, este evento se hereda de la clase Page. Más adelante en el capítulo verá ejemplos del manejador de eventos Page_Load. Cuando este manejador de eventos termina su ejecución, la página procesa eventos que generan los controles de la página, como las interacciones del usuario con la GUI. Cuando el objeto Web Forms está listo para la recolección de basura se produce un evento Unload, el cual llama al manejador de eventos Page_Unload. Este evento también se hereda de la clase Page. Por lo general, Page_Unload contiene código que libera los recursos utilizados por la página.

21.4.5 Análisis del XHTML generado por una aplicación ASP.NET La figura 21.6 muestra el XHTML generado por ASP.NET cuando un explorador Web cliente solicita el archivo HoraWeb.aspx (figura 21.4). Para ver este XHTML, seleccione Ver > Código fuente en Internet Explorer. [Nota: agregamos los comentarios de XHTML en las líneas 1-2 y cambiamos el formato de XHTML para que se conforme a nuestras convenciones de codificación.] El contenido de esta página es similar al del archivo ASPX. Las líneas 7-9 definen un encabezado de documento comparable al de la figura 21.4. Las líneas 10-28 definen el cuerpo del documento. La línea 11 empieza el formulario, un mecanismo para recolectar la información del usuario y enviarlo al servidor Web. En este programa específico, el usuario no envía datos al servidor Web para que los procese; sin embargo, el procesamiento de los datos del usuario es una parte crucial de muchas aplicaciones, y el formulario lo facilita. En ejemplos posteriores le demostraremos cómo enviar datos al servidor. Los formularios de XHTML pueden contener componentes visuales y no visuales. Los componentes visuales incluyen botones en los que se puede hacer clic, y otros componentes de la GUI con los que interactúan los usuarios. Los componentes no visuales, llamados entradas ocultas, almacenan datos tales como direcciones de correo

1 2 3 4 5 6 7 8



Un ejemplo simple de un formulario Web Forms

Figura 21.6 | Respuesta de XHTML cuando el explorador solicita HoraWeb.aspx. (Parte 1 de 2).

21.4 Creación y ejecución de un ejemplo de formulario Web Forms simple

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

781





Hora actual en el servidor Web:

21:23:28





Figura 21.6 | Respuesta de XHTML cuando el explorador solicita HoraWeb.aspx. (Parte 2 de 2).

electrónico, que el autor del documento especifica. Una de estas entradas ocultas se define en las líneas 13-15. Más adelante en el capítulo, hablaremos sobre el significado preciso de esta entrada oculta. El atributo method del elemento form (línea 11) especifica el método mediante el cual el explorador Web envía el formulario al servidor. El atributo action identifica el nombre y la ubicación del recurso que se solicitará cuando se envíe este formulario; en este caso, HoraWeb.aspx. Recuerde que el elemento form del archivo ASPX contiene el par atributo-valor runat ="server" (línea 14 de la figura 21.4). Cuando el elemento form se procesa en el servidor, se elimina el atributo runat. Se agregan los atributos method y action, y el elemento form de XHTML resultante se envía al explorador cliente. En el archivo ASPX, el control Label del formulario (es decir, horaLabel) es un control Web. Aquí estamos viendo el XHTML creado por nuestra aplicación, por lo que el elemento form contiene un elemento span (líneas 21-24 de la figura 21.6) para representar el texto en la etiqueta. En este caso específico, ASP.NET asigna el control Web Label a un elemento span de XHTML. Las opciones de formato que se especificaron como propiedades de horaLabel, tales como el tamaño de fuente y el color del texto en el control Label, ahora se especifican en el atributo style del elemento span. Observe que sólo los elementos en el archivo ASPX que están marcados con el par atributo-valor runat= "server", o que se especifican como controles Web, se modifican o sustituyen cuando el servidor procesa el archivo. Los elementos de XHTML puro, como el h2 en la línea 19, se envían al explorador exactamente como aparecen en el archivo ASPX.

21.4.6 Creación de una aplicación Web ASP.NET Ahora que hemos presentado el archivo ASPX, el archivo de código subyacente y la página Web resultante que se envía al explorador Web, describiremos el proceso mediante el cual creamos esta aplicación. Para crear la aplicación HoraWeb, realice los siguientes pasos en Visual Web Developer:

Paso 1: crear el proyecto de aplicación Web Seleccione Archivo > Nuevo sitio Web… para mostrar el cuadro de diálogo Nuevo sitio Web (figura 21.7). En este cuadro de diálogo, seleccione Sitio Web ASP.NET en el panel Plantillas. Debajo de este panel, el cuadro de diálogo Nuevo sitio Web contiene dos campos, con los cuales usted puede especificar el tipo y la ubicación de la aplicación Web que creará. Si no está seleccionado de antemano, seleccione HTTP de la lista desplegable más cercana a Ubicación. Esto indica que la aplicación Web debe configurarse para ejecutarse como una aplicación IIS

782

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Figura 21.7 | Creación de un Sitio Web ASP.NET en Visual Web Developer. usando HTTP (ya sea en su computadora, o en una computadora remota). Queremos que nuestro proyecto se ubique en http://localhost, que es el URL del directorio raíz de IIS (este URL corresponde al directorio C:\InetPub\wwwroot en su equipo). El nombre localhost indica que el cliente y el servidor residen en el mismo equipo. Si el servidor Web se encontrara en un equipo distinto, localhost se sustituiría con la dirección IP o el nombre de host apropiados. De manera predeterminada, Visual Web Developer establece la ubicación en la que se creará el sitio Web a http://localhost/WebSite, que cambiaremos por http://localhost/HoraWeb. Si no tiene acceso a IIS, puede seleccionar Sistema de archivos de la lista desplegable que está enseguida de Ubicación, para crear la aplicación Web en una carpeta en su computadora. Podrá probar la aplicación utilizando el Servidor de desarrollo ASP.NET interno de Visual Web Developer, pero no podrá acceder a la aplicación en forma remota, a través de Internet. La lista desplegable Lenguaje en el cuadro de diálogo Nuevo sitio Web le permite especificar el lenguaje (es decir, Visual Basic, Visual C# o Visual J#) en el que escribirá el (los) archivo(s) de código subyacente para la aplicación Web. Cambie la opción a Visual C#. Haga clic en Aceptar para crear el proyecto de la aplicación Web. Esta acción crea el directorio C:\Inetpub\wwwroot\HoraWeb y lo hace accesible a través del URL http//localhost/ HoraWeb. Esta acción también crea un directorio HoraWeb dentro del directorio Visual Studio 2005/Projects, del directorio Mis documentos de su usuario de Windows, para almacenar los archivos de solución del proyecto (por ejemplo, HoraWeb.sln).

Paso 2: examinar el Explorador de soluciones del proyecto recién creado Las siguientes figuras describen el contenido del nuevo proyecto, empezando con el Explorador de soluciones que se muestra en la figura 21.8. Al igual que Visual C# 2005 Express, Visual Web Developer crea varios archivos cuando se crea un nuevo proyecto. Se crea un archivo ASPX (es decir, formulario Web Forms) llamado Default.aspx para cada nuevo proyecto Sitio Web ASP.NET. Este archivo se abre de manera predeterminada en el Diseñador de formularios Web Forms, en modo Source cuando se carga el proyecto por primera vez (en breve hablaremos sobre esto). Como dijimos antes, se incluye un archivo de código subyacente como parte del proyecto. Visual Web Developer crea un archivo de código subyacente llamado default.aspx.cs. Para abrir el archivo de código subyacente del archivo ASPX, haga clic con el botón derecho del ratón en el archivo ASPX y seleccione la opción Ver código o haga clic en el botón Ver código en la parte superior del Explorador de soluciones. De manera alternativa, puede expandir el nodo para que el archivo ASPX revele el nodo del archivo de código subyacente (vea la figura 21.8). También puede elegir listar todos los archivos en el proyecto de manera individual (en vez de anidados), haciendo clic en el botón Anidar archivos relacionados; esta opción está activada de manera predeterminada, por lo que si hace clic en este botón se desactiva la opción.

21.4 Creación y ejecución de un ejemplo de formulario Web Forms simple

783

Ver código Anidar archivos relacionados

Ver Diseñador

Copiar sitio Web

Actualizar Propiedades

Configuración de ASP.NET

Archivo ASPX Archivo de código subyacente

Figura 21.8 | Ventana del Explorador de soluciones para el proyecto HoraWeb. Los botones Propiedades y Actualizar en el Explorador de soluciones de Visual Web Developer se comportan en forma idéntica a los de Visual C# 2005 Express. El Explorador de soluciones de Visual Web Developer también contiene tres botones adicionales: Ver diseñador, Copiar sitio Web y Configuración de ASP.NET. El botón Ver Diseñador le permite abrir el formulario Web en modo de Diseño, el cual veremos en breve. El botón Copiar sitio Web abre un cuadro de diálogo que le permite desplazar los archivos en este proyecto hacia otra ubicación, como un servidor Web remoto. Esto es útil si está desarrollando la aplicación en su equipo local, pero desea que esté disponible al público desde una ubicación distinta. Por último, el botón Configuración de ASP.NET lo lleva a una página Web llamada Herramienta Administración de sitios Web, en donde usted podrá manipular varias configuraciones y opciones de seguridad para su aplicación. En la sección 21.8 veremos esta herramienta con mucho más detalle.

Paso 3: examinar el Cuadro de herramientas en Visual Web Developer La figura 21.9 muestra el Cuadro de herramientas que aparece en el IDE cuando se carga el proyecto. La figura 21.9(a) muestra el inicio de la lista Estándar de controles Web, y la figura 21.9(b) muestra el resto de los controles Web, así como la lista de controles de Datos utilizados en ASP.NET. Hablaremos sobre ciertos controles específicos de la figura 21.9 a medida que los utilicemos en el capítulo. Observe que algunos controles en el Cuadro de herramientas son similares a los controles de Windows que presentamos antes en este libro.

Paso 4: examinar el Diseñador de Web Forms La figura 21.10 muestra el Diseñador de Web Forms en modo Código, que aparece en el centro del IDE. Cuando el proyecto se carga por primera vez, el Diseñador de Web Forms muestra el archivo ASPX generado en forma automática (es decir, Default.aspx) en modo Código, lo cual nos permite ver y editar el marcado que compone la página Web. El IDE creó el marcado que se lista en la figura 21.10, y sirve como una plantilla que modificaremos en unos momentos. Si hacemos clic en el botón Diseño que está en la esquina inferior izquierda del Diseñador de Web Forms, el IDE cambia al modo Diseño (figura 21.11), que le permite arrastrar y soltar controles del Cuadro de herramientas en el formulario Web Forms. También puede escribir en la posición actual del cursor para agregar texto a la página Web. Demostraremos esto en breve. En respuesta a dichas acciones, el IDE genera el marcado apropiado en el archivo ASPX. Observe que el modo Diseño indica el elemento de XHTML en el que se encuentra actualmente el cursor. Al hacer clic en el botón Código, el Diseñador de Web Forms regresa al modo Código, en donde se puede ver el marcado generado.

Paso 5: examinar el archivo de código subyacente en el IDE La siguiente figura (figura 21.12) muestra a Default.aspx.cs, el archivo de código subyacente que Visual Web Developer genera para Default.aspx. Haga clic con el botón derecho del ratón en el archivo ASPX, dentro del Explorador de soluciones, y seleccione la opción Ver código para abrir el archivo de código subyacente. Cuando se crea por primera vez, este archivo no contiene nada más que una declaración de clase parcial con un manejador de eventos Page_Load vacío. En unos momentos agregaremos el manejador de eventos Page_Init a este código.

784

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

a)

b)

Figura 21.9 | El Cuadro de herramientas en Visual Web Developer.

Botón del modo Código

Botón del modo Diseño

Figura 21.10 | El modo Código del Diseñador de Web Forms.

Paso 6: cambiar el nombre del archivo ASPX Ya mostramos el contenido de los archivos ASPX predeterminado y de código subyacente. Ahora cambiaremos el nombre de estos archivos. Haga clic con el botón derecho del ratón en el archivo ASPX dentro del Explorador de soluciones, y seleccione la opción Cambiar nombre. Escriba el nuevo nombre de archivo HoraWeb.aspx y oprima Intro. Esta operación actualiza el nombre del archivo ASPX y del archivo de código subyacente. Observe que el IDE también actualiza el atributo CodeFile de la directiva Page de HoraWeb.aspx.

21.4 Creación y ejecución de un ejemplo de formulario Web Forms simple

785

Cursor

Ubicación actual del cursor

Figura 21.11 | El modo Diseño del Diseñador de Web Forms.

Figura 21.12 | Archivo de código subyacente para Default.aspx, generado por Visual Web Developer.

Paso 7: cambiar el nombre de la clase en el archivo de código subyacente y actualizar el archivo ASPX Aunque al cambiar el nombre del archivo ASPX, el archivo de código subyacente también cambia su nombre, esta acción no afecta al nombre de la clase parcial declarada en el archivo de código subyacente. Abra el archivo de código subyacente y cambie el nombre de la clase, de _Default (línea 11 en la figura 21.12) a HoraWeb, de manera que la declaración parcial de la clase aparezca como en la línea 13 de la figura 21.5. Recuerde que la directiva Page del archivo ASPX también hace referencia a esta clase. Utilice el modo Código del Diseñador de Web Forms para modificar el atributo Inherits de la directiva Page en HoraWeb.aspx, para que aparezca como en la línea 4 de la figura 21.4. El valor del atributo Inherits y el nombre de la clase en el archivo de código subyacente deben ser idénticos; de lo contrario, obtendrá errores cuando genere la aplicación Web.

Paso 8: cambiar el título de la página Antes de diseñar el contenido del formulario Web Forms, hay que cambiar su título del valor predeterminado Página sin título (línea 9 de la figura 21.10) por Un ejemplo simple de un formulario Web Forms. Para ello, abra el archivo ASPX en modo Código y modifique el texto entre las etiquetas inicial y final. De manera alternativa, puede abrir el archivo ASPX en modo Diseño y modificar la propiedad Title en la ventana Propiedades. Para ver las propiedades del formulario Web Forms, seleccione DOCUMENT de la lista desplegable en la ventana Propiedades; DOCUMENT es el nombre que se utiliza para representar el formulario Web Forms en la ventana Propiedades.

786

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Paso 9: diseñar la página Diseñar un formulario Web Forms es tan simple como diseñar un formulario Windows. Para agregar controles a la página, puede arrastrarlos y soltarlos desde el Cuadro de herramientas, hacia el formulario Web Forms en modo Diseño. Al igual que el formulario Web Forms en sí, cada control es un objeto que tiene propiedades, métodos y eventos. Usted puede establecer en forma visual esas propiedades y eventos mediante el uso de la ventana Propiedades, o mediante programación en el archivo de código subyacente. No obstante, a diferencia de trabajar con un formulario Windows, puede escribir texto directamente en un formulario Web Forms en la posición del cursor, o insertar elementos de XHTML mediante comandos de menú. Los controles y otros elementos se colocan en forma secuencial en un formulario Web Forms, de manera muy similar a la forma en que se colocan el texto y las imágenes en un documento, usando software de procesamiento de palabras como Microsoft Word. Los controles se colocan uno después de otro, en el orden en el que usted los arrastre y suelte en el formulario Web Forms. El cursor indica el punto en el cual se van a insertar los elementos de texto y de XHTML. Si desea posicionar un control entre el texto o los controles ya existentes, puede soltarlo en una posición específica dentro de los elementos existentes. También puede reordenar los controles existentes, usando acciones de arrastrar y colocar. Las posiciones de los controles y demás elementos son relativas a la esquina superior izquierda del formulario Web Forms. A este tipo de distribución se le conoce como posicionamiento relativo. Hay un tipo alternativo de distribución conocido como posicionamiento absoluto, en el cual los controles se posicionan exactamente en donde se sueltan en el formulario Web Forms. Puede habilitar el posicionamiento absoluto en modo Diseño seleccionando Diseño > Posición > Opciones de autoposición…, y después haciendo clic en la primer casilla de verificación en el panel Opciones de posición del cuadro de diálogo Opciones que aparezca.

Tip de portabilidad 21.2 No se recomienda usar el posicionamiento absoluto, ya que las páginas diseñadas de esta forma tal vez no se representen en forma correcta en computadoras con distintas resoluciones de pantalla y tamaños de fuente. Esto podría hacer que los elementos con posicionamiento absoluto se traslaparan entre sí, o que se mostraran fuera de la pantalla, y el cliente tendría que desplazarse por la pantalla para ver el contenido de la página completa.

En este ejemplo, utilizamos una pieza de texto y un control Label. Para agregar el texto al formulario Web Forms, haga clic en este formulario en modo Diseño y escriba Hora actual en el servidor Web:. Visual Web Developer es un editor WYSIWYG (Lo que se ve es lo que se obtiene); cada vez que usted realiza una modificación a un formulario Web Forms en modo de Diseño, el IDE crea el marcado (visible en modo Código) necesario para lograr los efectos visuales deseados que se ven en modo Diseño. Después de agregar el texto al formulario Web Forms, cambie a modo Código. En este modo podrá ver que el IDE agregó este texto al elemento div que aparece en el archivo ASPX de manera predeterminada. Regrese al modo Diseño y resalte el texto que agregó. En la lista desplegable Formato del bloque (vea la figura 21.13), seleccione Heading 2 para dar formato a este texto como un encabezado, que aparecerá en negritas, en una fuente un poco más grande que la predeterminada. Esta acción hace que el IDE encierre el texto recién escrito en un elemento h2. Por último, haga clic a la derecha del texto y oprima Intro para desplazar el cursor a un nuevo párrafo. Esta acción genera un elemento p vacío en el marcado del archivo ASPX. Ahora, el IDE deberá verse como la figura 21.13. Para colocar un control Label en un formulario Web Forms, puede arrastrar y soltar el control Label sobre el formulario, o hacer doble clic en este control dentro del Cuadro de herramientas. Asegúrese que el cursor se encuentre en el párrafo recién creado y después agregue un control Label, el cual se utilizará para mostrar la hora. Use la ventana Propiedades para establecer la propiedad (ID) del control Label a horaLabel. Eliminamos el texto de horaLabel, ya que este texto se establecerá mediante programación en el archivo de código subyacente. Cuando un control Label no contiene texto, su nombre aparece entre corchetes en el Diseñador de Web Forms (figura 21.14), pero no se muestra en tiempo de ejecución. El nombre de la etiqueta es un receptáculo para fines de diseño y distribución. Establezca las propiedades BackColor, ForeColor y Font-Size de horaLabel a Black, Yellow y XX-Large, respectivamente. Para cambiar las propiedades de la fuente, expanda el nodo Font en la ventana Propiedades y después modifique cada una de las propiedades relevantes de manera individual. Una vez que se establezcan las propiedades del control Label en la ventana Propiedades, Visual Web Developer actualizará el contenido del archivo ASPX. La figura 21.14 muestra el IDE, una vez que se establecen estas propiedades.

21.4 Creación y ejecución de un ejemplo de formulario Web Forms simple

787

Lista desplegable Formato del bloque

Posición del cursor después de insertar texto y un nuevo párrafo

Figura 21.13 |

HoraWeb.aspx

después de insertar texto y un nuevo párrafo.

Label

Figura 21.14 |

HoraWeb.aspx

después de agregar un control Label y establecer sus propiedades.

A continuación, hay que establecer la propiedad EnableViewState del control Label a False. Por último, seleccione DOCUMENT de la lista desplegable en la ventana Propiedades y establezca la propiedad EnableSessionState del formulario Web Forms a False. Más adelante en este capítulo hablaremos sobre estas dos propiedades.

Paso 10: agregar la lógica de la página Una vez diseñada la interfaz de usuario, hay que agregar código en C# al archivo de código subyacente. Abra el archivo HoraWeb.aspx.cs, haciendo doble clic sobre su nodo en el Explorador de soluciones. En este ejemplo,

788

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

agregaremos un manejador de eventos Page_Init (líneas 16-21 de la figura 21.5) al archivo de código subyacente. Recuerde que Page_Init maneja el evento Init y contiene código para inicializar la página. La instrucción en las líneas 19-20 de la figura 21.5 establece mediante programación el texto de horaLabel con la hora actual en el servidor.

Paso 11: ejecutar el programa Una vez creado el formulario Web Forms, puede verlo de varias formas. Primero, puede seleccionar Depurar > Iniciar sin depurar, con lo cual se abre una ventana del explorador para ejecutar la aplicación. Si creó la aplicación en su servidor IIS local (como hicimos en este ejemplo), el URL que aparezca en el explorador será http://localhost/ HoraWeb/HoraWeb.aspx (figura 21.5), lo cual indica que la página Web (el archivo ASPX) se encuentra dentro del directorio virtual HoraWeb en el servidor Web IIS local. Para probar el sitio Web en un explorador, IIS debe estar ejecutándose. Para iniciar IIS, ejecute el archivo inetmgr.exe desde Inicio > Ejecutar…, haga clic con el botón derecho del ratón en Sitio Web predeterminado y seleccione Start. [Nota: Tal vez necesite expandir el nodo que representa a su equipo para visualizar el Sitio Web predeterminado.] Tenga en cuenta que, si creó la aplicación ASP.NET en el sistema de archivos local, el URL que aparecerá en el explorador será http://localhost:NumeroPuerto/HoraWeb/HoraWeb.aspx, en donde NumeroPuerto es el número del puerto asignado al azar en el que se ejecuta el servidor de prueba integrado de Visual Web Developer. El IDE asigna el número de puerto con base en cada solución. Este URL indica que se está accediendo a la carpeta del proyecto HoraWeb a través del directorio raíz del servidor de prueba que se ejecuta en localhost: NúmeroPuerto. Cuando seleccione Depurar > Iniciar sin depurar, aparecerá un icono en la bandeja de notificación, cerca de la parte inferior derecha de su pantalla, enseguida de la fecha y hora de la computadora, para mostrar que el Servidor de desarrollo ASP.NET se está ejecutando. El servidor se detiene cuando usted sale de Visual Web Developer. También puede seleccionar Depurar > Iniciar depuración para ver la página Web en un explorador Web con depuración habilitada. Tenga en cuenta que no puede depurar un sitio Web a menos que se habilite explícitamente la depuración mediante el archivo Web.config; un archivo que almacena opciones de configuración para una aplicación Web de ASP.NET. Muy raras veces tendrá que crear o modificar el archivo Web.config en forma manual. La primera vez que selecciona Depurar > Iniciar depuración en un proyecto, aparece un cuadro de diálogo y le pregunta si desea que el IDE genere el archivo Web.config necesario y lo agregue al proyecto; después el IDE entra en modo Ejecución. Para salir del modo Ejecución, seleccione Depurar > Detener depuración en Visual Web Developer, o cierre la ventana del explorador que muestra el sitio Web. También puede hacer clic con el botón derecho del ratón en el Diseñador de Web Forms o en el nombre del archivo ASPX (en el Explorador de soluciones) y seleccionar la opción Ver en el explorador, para abrir una ventana del explorador y cargar una página Web. Si hace clic, con el botón derecho del ratón, en el archivo ASPX dentro del Explorador de soluciones y selecciona Explorar con… también se abre la página en un explorador, pero primero nos permite especificar el explorador Web que debe abrir la página, junto con su resolución de pantalla. Por último, puede ejecutar su aplicación abriendo una ventana del explorador y escribiendo el URL de la página Web en el campo Dirección. Si está probando una aplicación ASP.NET en la misma computadora que ejecuta IIS, escriba http://localhost/CarpetaProyecto/NombrePágina.aspx, en donde CarpetaProyecto es la carpeta en la que reside la página (por lo general, el nombre del proyecto), y NombrePágina es el nombre de la página ASP. NET. Si su aplicación reside en el sistema de archivos local, primero debe iniciar el Servidor de desarrollo ASP.NET ejecutando la aplicación mediante uno de los métodos antes descritos. Después puede escribir el URL (incluyendo el NúmeroPuerto que aparezca en el icono del servidor de prueba en la bandeja de notificación) en el explorador para ejecutar la aplicación. Observe que todos estos métodos para ejecutar la aplicación compilan el proyecto por usted. De hecho, ASP. NET compila su página Web cada vez que ésta cambia entre las peticiones HTTP. Por ejemplo, suponga que explora la página y después modifica el archivo ASPX, o agrega código al archivo de código subyacente. Cuando vuelva a cargar la página, ASP.NET la recompilará en el servidor antes de devolver la respuesta HTTP al usuario. Este nuevo e importante comportamiento de ASP.NET 2.0 asegura que el cliente que solicita la página siempre reciba la versión más reciente de la misma. No obstante, usted puede compilar una página Web, o todo un sitio Web completo, seleccionando Generar página o Generar sitio Web, respectivamente, en el menú General de Visual Web Developer.

21.5 Controles Web

789

Si desea probar su aplicación Web a través de una red, tal vez necesite modificar su configuración de Firewall de Windows. Por cuestiones de seguridad, Firewall de Windows no permite el acceso remoto a un servidor Web en su equipo local de manera predeterminada. Para modificar esta configuración, abra la herramienta Firewall de Windows en el Panel de control de Windows. Haga clic en la ficha Opciones avanzadas y seleccione su conexión de red de la lista Configuración de conexión de red. Después haga clic en el botón Configuración…. En la ficha Servicios del cuadro de diálogo Configuración avanzada, asegúrese que esté seleccionada la opción Servidor Web (HTTP).

21.5 Controles Web Esta sección presenta algunos de los controles Web ubicados en la sección Estándar del Cuadro de herramientas (figura 21.9). La figura 21.15 muestra un resumen de algunos de los controles Web utilizados en los ejemplos de este capítulo.

21.5.1 Controles de texto y gráficos La figura 21.16 ilustra un formulario simple para recopilar la entrada del usuario. Este ejemplo utiliza todos los controles listados en la figura 21.15 excepto el control Label, que utilizamos en la sección 21.4. Observe que todo el código de la figura 21.16 lo generó Visual Web Developer, en respuesta a las acciones realizadas en el modo Diseño. [Nota: este ejemplo no contiene ninguna funcionalidad; es decir, no ocurre acción alguna cuando el usuario hace clic en Registrar. Le pediremos a usted que proporcione la funcionalidad como un ejercicio. En los ejemplos subsiguientes, demostraremos cómo agregar funcionalidad a muchos de estos controles Web.] Antes de hablar sobre los controles Web utilizados en este archivo ASPX, explicaremos el XHTML que crea la distribución que podemos ver en la figura 21.16. La página contiene un elemento de encabezado h3 (línea 17), seguido de una serie de bloques de XHTML adicionales. Colocamos la mayor parte de los controles Web dentro de elementos p (es decir, párrafos), pero usamos un elemento table de XHTML (líneas 26-60) para organizar los controles Image y TextBox en la sección de la página que trata acerca de la información sobre el usuario. En la sección anterior describimos cómo agregar elementos de encabezado y párrafos en forma visual, sin manipular XHTML en el archivo ASPX directamente. Visual Web Developer nos permite agregar una tabla de una manera similar.

Agregar una tabla de XHTML a un formulario Web Forms Para crear una tabla con dos filas y dos columnas en modo Diseño, seleccione el comando Insertar tabla del menú Diseño. En el cuadro de diálogo Insertar tabla que aparezca, asegúrese que el botón de opción Personalizada esté seleccionado. En el cuadro de grupo Diseño, modifique los valores de los cuadros combinados Filas y Columnas a 2. De manera predeterminada, el contenido de la celda de una tabla se alinea en forma vertical con la parte media de la celda. Para modificar la alineación vertical de todas las celdas en la tabla, haga clic en el botón Propiedades de celda… y después seleccione la opción top del cuadro combinado Alineación vertical en el cuadro de diálogo que

Control Web

Descripción

Label

Muestra texto que el usuario no puede editar. Recopila la entrada del usuario y muestra texto. Activa un evento cuando se oprime. Muestra un hipervínculo. Muestra una lista desplegable de opciones, de donde el usuario puede seleccionar un elemento. Agrupa botones de opción. Muestra imágenes (por ejemplo, GIF y JPG).

TextBox Button HyperLink DropDownList RadioButtonList Image

Figura 21.15 | Controles Web de uso frecuente.

790

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

aparezca. Esto hace que el contenido de cada celda de la tabla se alinee con la parte superior de la celda. Haga clic en Aceptar para cerrar el cuadro de diálogo Propiedades de celda y después haga clic en Aceptar para cerrar el cuadro de diálogo Insertar tabla y crear la tabla. Una vez creada, podrá agregar controles y texto a celdas específicas, para crear un diseño muy bien organizado.

Seleccionar el color del texto en un formulario Web Forms Observe que algunas de las instrucciones para el usuario en el formulario aparecen en color verde azulado. Para establecer el color de una pieza específica de texto, resalte el texto y seleccione la opción Formato > Color de primer plano…. En el cuadro de diálogo Selector de color, haga clic en la ficha Colores con nombre y seleccione un color de la paleta que se muestra. Haga clic en Aceptar para aplicar el color. Observe que el IDE coloca el texto de color en un elemento span de XHTML (por ejemplo, líneas 23-24) y aplica el color usando el atributo style de span. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43





Demostración de los controles Web



Éste es un formulario de registro simple.

Por favor llene todos los campos y haga clic en Registrar.



  Por favor llene los siguientes campos.



Figura 21.16 | Formulario Web que demuestra el uso de los controles Web. (Parte 1 de 3).

21.5 Controles Web

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102

791









Debe tener el formato (555) 555-5555.



  ¿De cuál libro desea información?



Visual Basic 2005 How to Program 3e

Visual C# 2005 How to Program 2e

Java How to Program 6e C++ How to Program 5e XML How to Program 1e



Haga clic aquí para ver más información acerca de nuestros libros



  ¿Qué sistema operativo utiliza?



Windows XP Windows 2000 Windows NT Linux Otro

Figura 21.16 | Formulario Web que demuestra el uso de los controles Web. (Parte 2 de 3).

792

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

103

104

105

106

108

109

110

111

112

Control Image

Control TextBox

Control DropDownList Control HyperLink

Control RadioButtonList

Control Button

Figura 21.16 | Formulario Web que demuestra el uso de los controles Web. (Parte 3 de 3).

Examinar los controles Web en un ejemplo de formulario de registro Las líneas 20-21 de la figura 21.16 definen un control Image, el cual inserta una imagen en una página Web. Las imágenes que utilizamos en este ejemplo se encuentran en el directorio de ejemplos de este capítulo. Puede descargar los ejemplos de www.deitel.com/books/csharpforprogrammers2. Antes de poder mostrar una imagen en una página Web mediante el uso de un control Web Image, hay que agregar la imagen al proyecto. Nosotros agregamos una carpeta llamada Imagenes a este proyecto (y a cada uno de los proyectos de ejemplo de este capítulo que utilizan imágenes); para ello, hicimos clic con el botón derecho en la ubicación del proyecto dentro del Explorador de soluciones, seleccionamos la opción Nueva carpeta y escribimos el nombre Imagenes para la carpeta. Después agregamos a esta carpeta cada una de las imágenes utilizadas en el ejemplo; para ello, hicimos clic con el botón derecho del ratón sobre la carpeta, seleccionamos Agregar elemento existente… y exploramos en busca de los archivos de imagen para agregarlos.

21.5 Controles Web

793

La propiedad ImageUrl (línea 21) especifica la ubicación de la imagen que se mostrará en el control Image. Para seleccionar una imagen, haga clic en la elipsis que está enseguida de la propiedad ImageUrl en la ventana Propiedades, y utilice el cuadro de diálogo Seleccionar imagen para explorar en busca de la imagen deseada en la carpeta Imagenes del proyecto. Cuando el IDE llena la propiedad ImageUrl con base en su selección, incluye una tilde y una barra diagonal (~/) al principio de ImageUrl; esto indica que la carpeta Imágenes se encuentra en el directorio raíz del proyecto (es decir, http://localhost/ControlesWeb, cuya ruta física es C:\Inetpub\ wwwroot\ControlesWeb). Las líneas 26-60 contienen el elemento table creado por los pasos que vimos antes. Cada elemento td contiene un control Image y un control TextBox, el cual le permite obtener texto del usuario y mostrar texto al usuario. Por ejemplo, las líneas 32-33 definen un control TextBox que se utiliza para recolectar el primer nombre del usuario. Las líneas 70-79 definen un control DropDownList. Este control es similar al control ComboBox de Windows. Cuando un usuario hace clic en la lista desplegable, ésta se expande y muestra una lista para que el usuario pueda realizar una selección. Cada elemento en la lista desplegable se define mediante un elemento ListItem (líneas 72-78). Después de arrastrar un control DropDownList hacia un formulario Web Forms, puede agregar elementos en él usando el Editor de la colección ListItem. Este proceso es similar al de personalizar un control ListBox en una aplicación para Windows. En Visual Web Developer, puede acceder al Editor de la colección ListItem haciendo clic en la elipsis que está a un lado de la propiedad Items del control DropDownList. También puede acceder a este editor usando el menú Tareas de DropDownList, el cual se abre al hacer clic en la pequeña punta de flecha que aparece en la esquina superior derecha del control en el modo Diseño (figura 21.17). A este menú se le conoce como menú de etiquetas inteligentes. Visual Web Developer muestra menús de etiquetas inteligentes para muchos controles ASP.NET, para facilitar la realización de tareas comunes. Al hacer clic en la opción Editar elementos… del menú Tareas de DropDownList se abre el Editor de la colección ListItem, el cual le permite agregar elementos ListItem al control DropDownList. El control HyperLink (líneas 82-86 de la figura 21.16) agrega un hipervínculo a una página Web. La propiedad NavigateUrl (línea 83) de este control especifica el recurso (es decir, http://www.deitel.com) que se solicita cuando un usuario hace clic en el hipervínculo. Al establecer la propiedad Target a _blank se especifica que la página Web solicitada debe abrirse en una nueva ventana del explorador. De manera predeterminada, los controles HyperLink hacen que se abran páginas en la misma ventana del explorador. Las líneas 96-103 definen un control RadioButtonList, el cual proporciona una serie de botones de opción, de los que el usuario sólo puede seleccionar uno. Al igual que las opciones en un control DropDownList, los botones de opción individuales se definen mediante elementos ListItem. Observe que, al igual que el menú de etiquetas inteligentes Tareas de DropDownList, el menú de etiquetas inteligentes Tareas de RadioButtonList también cuenta con un vínculo Editar elementos… para abrir el Editor de la colección ListItem. El último control Web en la figura 21.16 es Button (líneas 106-107). Al igual que un control Button de Windows, el control Web Button representa a un botón que desencadena una acción cuando se hace clic sobre él. Por lo general, un control Web Button se asigna a un elemento input de XHTML, cuyo atributo type se establece a "button". Como dijimos antes, al hacer clic en el botón Registrar en el ejemplo no pasa nada.

21.5.2 Control AdRotator A menudo, las páginas Web contienen anuncios de productos o servicios, los que por lo general consisten en imágenes. Aunque los autores de sitios Web desean incluir todos los patrocinadores que sea posible, las páginas Web sólo pueden mostrar un número limitado de anuncios. Para tratar este problema, ASP.NET cuenta con el control Web AdRotator para mostrar anuncios. Usando los datos sobre los anuncios ubicados en un archivo de

Figura 21.17 | Menú de etiquetas inteligentes Tareas de DropDownList.

794

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

XML, el control AdRotator selecciona una imagen al azar para mostrarla en la página, y genera un hipervínculo a la página Web asociada con esa imagen. Los exploradores que no soportan imágenes muestran texto alternativo, el cual se especifica en el documento XML. Si un usuario hace clic en la imagen o en el texto que la sustituye, el explorador carga la página Web asociada con esa imagen.

Demostración del control Web AdRotator La figura 21.18 demuestra el control Web AdRotator. En este ejemplo, los “anuncios” que rotamos son las banderas de 10 países. Cuando un usuario hace clic en la imagen de la bandera mostrada, el explorador se redirecciona a una página Web que contiene información acerca del país que representa la bandera. Si un usuario actualiza el explorador o solicita la página de nuevo, se vuelve a elegir al azar una de las 11 banderas y se muestra en la pantalla. El archivo ASPX en la figura 21.18 es similar al de la figura 21.4. Sin embargo, en vez de texto de XHTML y un control Label, esta página contiene texto de XHTML (es decir, el elemento h3 en la línea 17) y un control AdRotator llamado paisRotator (líneas 19-21). Esta página también contiene un control XmlDataSource (líneas 22-24), el cual provee los datos al control AdRotator. El atributo background del elemento body de la página (línea 14) se establece para mostrar la imagen fondo.png, ubicada en la carpeta Imagenes del proyecto. Para especificar este archivo, haga clic en la elipsis que se encuentra a un lado de la propiedad Background de DOCUMENT en la ventana Propiedades, y utilice el cuadro de diálogo que aparezca para explorar en busca de fondo.png. No necesita agregar código al archivo de código subyacente, debido a que el control AdRotator realiza “todo el trabajo”. Las ventanas de resultados muestran dos peticiones distintas. La figura 21.18(a) muestra la primera vez que se solicita la página, cuando se muestra la bandera estadounidense. En la segunda solicitud, como se muestra en la figura 21.18(b), se muestra la bandera francesa. La figura 21.18(c) muestra la página Web que se carga al hacer clic en la bandera francesa.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29





Rotador de banderas



Ejemplo de AdRotator







Figura 21.18 | Formulario Web Forms que demuestra el uso de un control Web AdRotator. (Parte 1 de 2).

21.5 Controles Web

795

b)

a)

Imagen AdRotator Elemento AlternateText que se despliega en un cuadro informativo c)

Figura 21.18 | Formulario Web Forms que demuestra el uso de un control Web AdRotator. (Parte 2 de 2).

Conectar datos a un control AdRotator Un control AdRotator accede a un archivo de XML (que presentaremos en breve) para determinar de cuál anuncio se mostrará la imagen (es decir, bandera), el URL de hipervínculo y el texto alternativo, e incluirlos en la página. Para conectar el control AdRotator con el archivo de XML, creamos un control XmlDataSource; uno de varios controles de datos de ASP.NET (se encuentra en la sección Datos del Cuadro de herramientas) que encapsula orígenes de datos y hace que dichos datos estén disponibles para los controles Web. Un control XmlDataSource hace referencia a un archivo de XML, el cual contiene los datos que se utilizarán en una aplicación ASP.NET. Más adelante en este capítulo aprenderá más acerca de los controles Web enlazados a datos, así como sobre el control SqlDataSource, que recupera datos de una base de datos de SQL Server, y del control ObjectDataSource, que encapsula a un objeto que hace que los datos estén disponibles. Para crear este ejemplo, primero hay que agregar el archivo de XML InformacionAdRotator.xml al proyecto. Cada proyecto que se crea en Visual Web Developer contiene una carpeta App_Data, cuya función es almacenar todos los datos utilizados por el proyecto. Haga clic con el botón derecho del ratón sobre esta carpeta en el Explorador de soluciones y seleccione la opción Agregar elemento existente…, después explore su computadora para buscar el archivo InformacionAdRotator.xml. (Nosotros incluimos este archivo en el directorio de ejemplos de este capítulo.) Después de agregar el archivo de XML al proyecto, arrastre un control AdRotator del Cuadro de herramientas al formulario Web Forms. El menú de etiquetas inteligentes Tareas de AdRotator se abrirá en forma automática. De este menú, seleccione de la lista desplegable Elegir origen de datos para iniciar el Asistente para la configuración de orígenes de datos. Seleccione Archivo XML como el tipo de origen de datos. Esto hace que el asistente cree un control XmlDataSource con el ID especificado en la mitad inferior del cuadro de diálogo del asistente. Cambie el nombre del ID del control a adXmlDataSource. Haga clic en el botón

796

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

del cuadro de diálogo Asistente para la configuración de orígenes de datos. A continuación aparecerá el cuadro de diálogo Configurar origen de datos – adXmlDataSource. En la sección Archivo de datos de este cuadro de diálogo, haga clic en Examinar… y, en el cuadro de diálogo Seleccionar archivo XML, localice el archivo de XML que agregó a la carpeta App_Data. Haga clic en Aceptar para salir de este cuadro de diálogo y después haga clic en Aceptar para salir del cuadro de diálogo Configurar origen de datos – adXmlDataSource. Al completar estos pasos, el control AdRotator estará configurado para usar el archivo XML y determinar cuáles anuncios debe mostrar. Aceptar

Examinar un archivo de XML que contiene información sobre anuncios El documento de XML InformacionAdRotator.xml (figura 21.19) (o cualquier documento de XML que se utilice con un control AdRotator) debe contener un elemento raíz llamado Advertisements (líneas 4-94). Dentro de ese elemento puede haber varios elementos Ad (por ejemplo, las líneas 5-12), cada uno de los cuales proporciona información acerca de un anuncio distinto. El elemento ImageUrl (línea 6) especifica la ruta (ubicación) de la imagen del anuncio, y el elemento NavigateUrl (líneas 7-9) especifica el URL para la página Web que se carga cuando un usuario hace clic sobre el anuncio. Observe que cambiamos el formato de este archivo para fines de presentación. El archivo XML real no puede contener espacio en blanco antes o después del URL en el elemento NavigateUrl, ya que se consideraría parte del URL, y la página no se cargaría en forma apropiada.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36



Imagenes/francia.png

http://www.cia.gov/cia/publications/factbook/geos/fr.html

Información de Francia 1

Imagenes/alemania.png

http://www.cia.gov/cia/publications/factbook/geos/gm.html

Información de Alemania 1

Imagenes/italia.png

http://www.cia.gov/cia/publications/factbook/geos/it.html

Información de Italia 1

Imagenes/espania.png

http://www.cia.gov/cia/publications/factbook/geos/sp.html

Figura 21.19 | Archivo que contiene información sobre anuncios, para utilizarla en un ejemplo del control AdRotator. (Parte 1 de 2).

21.5 Controles Web

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94

Información de España 1

Imagenes/latvia.png

http://www.cia.gov/cia/publications/factbook/geos/lg.html

Información de Latvia 1

Imagenes/peru.png

http://www.cia.gov/cia/publications/factbook/geos/pe.html

Información de Perú 1

Imagenes/senegal.png

http://www.cia.gov/cia/publications/factbook/geos/sg.html

Información de Senegal 1

Imagenes/suecia.png

http://www.cia.gov/cia/publications/factbook/geos/sw.html

Información de Suecia 1

Imagenes/tailandia.png

http://www.cia.gov/cia/publications/factbook/geos/th.html

Información de Tailandia 1

Imagenes/estadosunidos.png

http://www.cia.gov/cia/publications/factbook/geos/us.html

Información de Estados Unidos 1

Figura 21.19 | Archivo que contiene información sobre anuncios, para utilizarla en un ejemplo del control AdRotator. (Parte 2 de 2).

797

798

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

El elemento AlternateText (línea 10) anidado en cada elemento Ad contiene texto que se muestra en lugar de la imagen, cuando el explorador no puede localizar o representar la imagen por alguna razón (por ejemplo, si el archivo no existe, o si el explorador no es capaz de mostrarla). El texto del elemento AlternateText es también un cuadro de información sobre la herramienta (tooltip), que Internet Explorer muestra cuando un usuario coloca el puntero del ratón sobre la imagen (figura 21.18). El elemento Impressions (línea 56) especifica con qué frecuencia debe aparecer una imagen específica, en relación con las demás imágenes. Un anuncio que tenga un valor más alto en Impressions se mostrará con más frecuencia que un anuncio con un valor menor. En nuestro ejemplo, los anuncios se muestran con la misma probabilidad, ya que el valor de cada elemento Impressions es 1.

21.5.3 Controles de validación

Esta sección introduce un tipo distinto de control Web, conocido como control de validación (o validador), el cual determina si los datos en otro control Web tienen el formato apropiado. Por ejemplo, los validadores podrían determinar si un usuario ha proporcionado información en un campo requerido, o si un campo de código postal contiene exactamente cinco dígitos. Los validadores proporcionan un mecanismo para validar la entrada del usuario en el explorador cliente. Cuando se crea el XHTML para nuestra página, el validador se convierte en ECMAScript,1 que realiza la validación. ECMAScript es un lenguaje de secuencias de comandos que mejora la funcionalidad y apariencia de las páginas Web. Por lo general, ECMAScript se ejecuta en el cliente. Algunos clientes no tienen soporte para secuencias de comandos o lo deshabilitan. No obstante, por cuestiones de seguridad, siempre es preferible realizar la validación en el servidor, se ejecute o no la secuencia de comandos en el cliente.

Validar la entrada en un formulario Web Forms El ejemplo en esta sección pide al usuario que introduzca un nombre, dirección de correo electrónico y número telefónico. Un sitio Web podría utilizar un formulario como este para recolectar la información de contacto de sus visitantes. Después de que el usuario escribe datos, pero antes de que éstos se envíen al servidor Web, los validadores se aseguran que el usuario haya introducido un valor en cada campo, y que los valores de dirección de correo electrónico y número telefónico tengan un formato aceptable. En este ejemplo, (555) 123-4567, 555-123-4567 y 123-4567 se consideran todos números telefónicos válidos. Una vez que se envían los datos, el servidor Web responde mostrando un mensaje apropiado y una tabla de XHTML en la que se repite la información enviada. Por lo general, una aplicación comercial real almacena los datos enviados en una base de datos, o en un archivo en el servidor. Nosotros sólo enviaremos los datos de vuelta al formulario, para demostrar que el servidor los recibió. La figura 21.20 presenta el archivo ASPX. Al igual que el formulario Web Forms en la figura 21.16, este formulario Web Forms utiliza un elemento table para organizar el contenido de la página. Las líneas 24-25, 36-37 y 56-57 definen controles TextBox para recuperar el nombre del usuario, la dirección de correo electrónico y el número telefónico, respectivamente, y la línea 75 define un botón Enviar. Las líneas 77-79 crean un control Label llamado salidaLabel, que muestra la respuesta del servidor cuando el usuario envía con éxito el formulario. Observe 1 2 3 4 5 6





Demostración de los controles de validación



Por favor llene el siguiente formulario.
Todos los campos son obligatorios y deben contener información válida.

Nombre:




Dirección de E-mail:

 ejemplo: [email protected]


Número telefónico:

 ejemplo: (555) 555-1234











a)

b)

Figura 21.20 | Validadores utilizados en un formulario Web Forms que recupera la información de contacto de un usuario. (Parte 3 de 4).

21.5 Controles Web

801

c)

d)

Figura 21.20 | Validadores utilizados en un formulario Web Forms que recupera la información de contacto de un usuario. (Parte 4 de 4). que en un principio la propiedad Visible de salidaLabel se establece en False, para que el control Label no aparezca en el explorador del cliente cuando la página se carga por primera vez.

Uso de los controles RequiredFieldValidator En este ejemplo utilizamos tres controles RequiredFieldValidator (se encuentran en la sección Validación del Cuadro de herramientas) para asegurar que los controles TextBox del nombre, la dirección de correo electrónico y el número telefónico no estén vacíos cuando se envíe el formulario. Un control RequiredFieldValidator convierte un control de entrada en un campo requerido. Si dicho campo está vacío, la validación falla. Por ejemplo, las líneas 26-29 definen el control RequiredFieldValidator llamado nombreInputValidator, el cual confirma que nombreTextBox no esté vacío. La línea 27 asocia a nombreTextBox con nombreInputValidator estableciendo la propiedad ControlToValidate del validador a nombreTextBox. Esto indica que nombreInputValidator verifica el contenido de nombreTextBox. El texto de la propiedad ErrorMessage (línea 28) se muestra en el formulario Web

802

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Forms si falla la validación. Si el usuario no introduce datos en nombreTextBox y trata de enviar el formulario, el texto de ErrorMessage se muestra en rojo. Como establecimos la propiedad Display del control en Dynamic (línea 29), el validador ocupa espacio en el formulario Web Forms sólo cuando falla la validación; el espacio se asigna en forma dinámica cuando falla la validación, lo cual provoca que los controles que están debajo del validador se desplacen hacia abajo para dar cabida a ErrorMessage, como se puede ver en las figuras de la 21.20(a) a la 21.20(c).

Uso de controles RegularExpresionValidator Este ejemplo utiliza también controles RegularExpressionValidator para comparar la dirección de correo electrónico y el número telefónico escritos por el usuario con expresiones regulares. (En el capítulo 16 se introdujeron las expresiones regulares.) Estos controles determinan si la dirección de correo electrónico y el número telefónico se introdujeron en un formato válido. Por ejemplo, las líneas 43-50 crean un control RegularExpressionValidator llamado emailFormatValidator. La línea 45 establece la propiedad ControlToValidate a emailTextBox, para indicar que emailFormatValidator verifica el contenido de emailTextBox. La propiedad ValidationExpression de un control RegularExpressionValidator especifica la expresión regular que valida el contenido de ControlToValidate. Al hacer clic en la elipsis que está a un lado de la propiedad ValidationExpression en la ventana Propiedades se muestra el cuadro de diálogo Editor de expresiones regulares, el cual contiene una lista de Expresiones estándar para números telefónicos, códigos postales y otra información con formato. Usted también puede escribir su propia expresión personalizada. Para el control emailFormatValidator, elegimos la expresión estándar Dirección de correo electrónico de Internet, que utiliza la siguiente expresión de validación: \w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*

Esta expresión regular indica que una dirección de correo electrónico es válida si la parte de la dirección antes del símbolo @ contiene uno o más caracteres de palabra (es decir, caracteres alfanuméricos o guiones bajos), seguida de cero o más cadenas compuestas por un guión corto, signo de suma, punto o apóstrofe, y caracteres de palabra adicionales. Después del símbolo @, una dirección de correo electrónico válida debe contener uno o más grupos de caracteres de palabra, posiblemente separados por guiones cortos o puntos, seguidos de un punto obligatorio y otro grupo de uno o más caracteres de palabra, posiblemente separados por guiones cortos o puntos. Por ejemplo, [email protected], [email protected] y [email protected] son todas direcciones de correo electrónico válidas. Si el usuario introduce texto en el control emailTextBox que no tenga el formato correcto, y hace clic en un cuadro de texto distinto o trata de enviar el formulario, el texto de ErrorMessage se mostrará en rojo. También usamos el control RegularExpressionValidator llamado telefonoFormatValidator (líneas 63-70) para asegurarnos que el control telefonoTextBox contenga un número telefónico válido, antes de enviar el formulario. En el cuadro de diálogo Editor de expresiones regulares, seleccionamos Número de teléfono en EE.UU., que asigna la expresión de validación ((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}

a la propiedad ValidationExpression. Esta expresión indica que un número telefónico puede contener un código de área de tres dígitos, ya sea entre paréntesis y seguido por un espacio opcional, o sin paréntesis y seguido por un guión corto requerido. Después de un código de área opcional, un teléfono debe contener tres dígitos, un guión corto y otros cuatro dígitos. Por ejemplo, (555) 123-4567, 555-123-4567 y 123-4567 son todos números telefónicos válidos. Si los cinco validadores tienen éxito (es decir, que se llene cada uno de los controles TextBox, y que la dirección de correo electrónico y el número telefónico que se proporcionan sean válidos), al hacer clic en el botón Enviar se envían los datos del formulario al servidor. Como se muestra en la figura 21.20(d), el servidor responde mostrando los datos enviados en el control salidaLabel (líneas 77-79).

Análisis del archivo de código subyacente para un formulario Web Forms que recibe entrada del usuario La figura 21.21 muestra el archivo de código subyacente para el archivo ASPX en la figura 21.20. Observe que este archivo de código subyacente no contiene ninguna implementación relacionada con los validadores. Pronto hablaremos más sobre esto.

21.5 Controles Web

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

803

// Fig. 21.21: Validacion.aspx.cs // Archivo de código subyacente para el formulario que demuestra los controles de validación. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class Validacion : System.Web.UI.Page { // el manejador de eventos Page_Load se ejecuta cuando se carga la página protected void Page_Load( object sender, EventArgs e ) { // si no es la primera vez que se carga la página // (es decir, si el usuario ya envió los datos del formulario) if ( IsPostBack ) { // recupera los valores enviados por el usuario string nombre = Request.Form[ "nombreTextBox" ]; string email = Request.Form[ "emailTextBox" ]; string telefono = Request.Form[ "telefonoTextBox" ]; // crea una tabla, indicando los valores enviados salidaLabel.Text += "
Recibimos la siguiente información:" + "" + "" + "" + "" + "
Nombre: " + nombre + "
Dirección de e-mail: " + email + "
Número telefónico: " + telefono + "
"; salidaLabel.Visible = true; // muestra el mensaje de salida } // fin de if } // fin del método Page_Load } // fin de la clase Validation

Figura 21.21 | Código subyacente para un formulario Web Forms que obtiene la información de contacto de un usuario. A menudo, los programadores de sitios Web que utilizan ASP.NET diseñan sus páginas Web de tal forma que la página actual se actualice cuando el usuario envía el formulario; esto permite al programa recibir la entrada, procesarla según sea necesario y mostrar los resultados en la misma página, cuando se carga la segunda vez. Por lo general, estas páginas contienen un formulario que, cuando se envía, envía los valores de todos los controles al servidor y hace que la página actual se solicite de nuevo. Este evento se conoce como postback. La línea 20 utiliza la propiedad IsPostBack de la clase Page para determinar si la página se cargará debido a un postback. La primera vez que se solicita la página Web, IsPostBack es false y la página sólo muestra el formulario para que el usuario introduzca datos. Cuando ocurre el postback (debido a que el usuario hizo clic en Enviar), IsPostBack cambia a true. Las líneas 23-25 utilizan el objeto Request para recuperar los valores de nombreTextBox, emailTextBox y telefonoTextBox del objeto NameValueCollection Form. Cuando se publican datos al servidor Web, los datos del formulario de XHTML están accesibles para la aplicación Web a través del arreglo Form del objeto Request. Las líneas 28-34 adjuntan al valor de Text de salidaLabel un salto de línea, un mensaje adicional y una tabla de XHTML que contiene los datos enviados, de manera que el usuario sepa que el servidor recibió los datos en forma correcta. En una aplicación empresarial real, los datos se almacenarían en una base de datos o en un archivo, en

804

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

este punto de la aplicación. La línea 36 establece la propiedad Visible de salidaLabel a true, de manera que el usuario pueda ver el mensaje de agradecimiento y los datos que envió.

Análisis del XHTML del lado cliente para un formulario Web Forms con validación La figura 21.22 muestra el XHTML y el ECMAScript que se envían al explorador Web cuando se carga Validacion.aspx después del evento postback. Para ver este código, seleccione Ver > Código fuente en Internet Explorer. Las líneas 25-36, 100-171 y 180-218 contienen el ECMAScript que proporciona la implementación para los controles de validación y para realizar el postback. ASP.NET genera este código ECMAScript. Usted no necesita crear ni tampoco entender el código ECMAScript; la funcionalidad definida para los controles en nuestra aplicación se convierte en ECMAScript funcional para nosotros. En los archivos ASPX anteriores, establecimos en forma explícita el atributo EnableViewState de cada control Web a false. Este atributo determina si el valor de un control Web persiste (es decir, se retiene) cuando ocurre un postback. Este atributo es true de manera predeterminada, lo cual indica que el valor del control persiste. Observe en la figura 21.20(d) que los valores introducidos por el usuario siguen apareciendo en los cuadros de texto una vez que ocurre el postback. Una entrada hidden en el documento de XHTML (líneas 14-22 de la figura 21.22) contiene los datos de los controles en esta página. Este elemento siempre se llama __VIEWSTATE y almacena los datos de los controles en forma de una cadena codificada.

Tip de rendimiento 21.2 Al establecer EnableViewState a false se reduce la cantidad de datos que se pasan al servidor Web en cada solicitud.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31



Demostración de los controles de validación





Por favor llene el siguiente formulario.
Todos los campos son obligatorios y deben contener información válida.

Nombre:


 ejemplo: [email protected]
Número telefónico:

 ejemplo: (555) 555-1234





Gracias por llenar el formulario y enviarlo.
Recibimos la siguiente información:
Nombre: Bob
Dirección de e-mail: [email protected]
Número telefónico: (555) 555-1234








219

220

221

Figura 21.22 | XHTML y ECMAScript generados por ASP.NET, que se envían al explorador cuando se solicita el archivo Validacion.aspx. (Parte 5 de 5).

21.6 Rastreo de sesiones En un principio, los críticos acusaron a Internet y al comercio electrónico de no proporcionar el tipo de servicio personalizado que se experimenta por lo general en las tiendas convencionales. Para tratar este problema, los comercios electrónicos empezaron a establecer mecanismos mediante los cuales pudieran personalizar las experiencias de navegación de los usuarios, ajustando el contenido para cada usuario y permitiéndoles, al mismo tiempo, evitar información irrelevante. Las empresas logran este nivel de servicio mediante el rastreo de los movimientos de cada cliente a través de Internet, y combinan los datos recolectados con la información que proporciona el consumidor, incluyendo información de facturación, preferencias personales, intereses y pasatiempos.

Personalización

La personalización hace posible para los comercios electrónicos comunicarse en forma efectiva con sus clientes, y también mejora la habilidad del usuario para localizar los productos y servicios que desea. Las compañías que proporcionan contenido de interés particular para los usuarios pueden establecer relaciones con los clientes y mejorar esas relaciones con el tiempo. Lo que es más, al dirigirse a los clientes con ofertas, recomendaciones, anuncios, promociones y servicios personales, los comercios electrónicos crean la lealtad del cliente. Los sitios Web pueden utilizar tecnología sofisticada para permitir a los visitantes personalizar sus páginas iniciales, para que se adapten a sus necesidades y preferencias individuales. De manera similar, los negocios de compras en línea a menudo almacenan información personal de los clientes, y con base en ello ajustan las notificaciones y ofertas especiales a sus intereses. Dichos servicios alientan a los clientes a visitar los sitios con más frecuencia, y a realizar compras con más regularidad.

Privacidad No obstante, existe un sacrificio entre el servicio personalizado de los comercios electrónicos y la protección de la privacidad. Algunos clientes adoptan la idea del contenido personalizado, pero otros temen a las posibles consecuencias adversas si la información que proporcionan a los comercios electrónicos se libera o recolecta mediante tecnologías de rastreo. Los consumidores y los defensores de la privacidad se preguntan: ¿qué pasa si el comercio electrónico al que le proporcionamos datos personales vende o da esa información a otra organización, sin nuestro conocimiento? ¿Qué tal si no queremos que partes desconocidas rastreen nuestras acciones en Internet (un medio supuestamente anónimo)? ¿Qué pasa si partes no autorizadas obtienen acceso a datos privados delicados,

21.6 Rastreo de sesiones

809

como los números de tarjetas de crédito o los historiales médicos? Los programadores, consumidores, comercios electrónicos y legisladores deben, por igual, debatir y tratar todas estas preguntas.

Reconocer clientes Para proporcionar servicios personalizados a los consumidores, los comercios electrónicos deben tener la capacidad de reconocer a los clientes, cuando soliciten información de un sitio. Como hemos visto, HTTP facilita el sistema de solicitud/respuesta en el que opera la Web. Por desgracia, HTTP es un protocolo que carece de estado; no soporta conexiones persistentes que permitan a los servidores Web mantener información de estado en relación con clientes específicos. Esto significa que los servidores Web no pueden determinar si una solicitud viene de un cliente específico, o si el mismo o varios clientes generan una serie de solicitudes. Para sortear este problema, los sitios pueden proporcionar mecanismos mediante los cuales puedan identificar a los clientes individuales. Una sesión representa a un único cliente en un sitio Web. Si el cliente sale del sitio y regresa después, de todas formas se le reconocerá como el mismo usuario. Para ayudar al servidor a diferenciar los diversos clientes, cada cliente debe identificarse con el servidor. El rastreo de clientes individuales, conocido como rastreo de sesiones, puede realizarse de varias formas. Una técnica popular utiliza cookies (sección 21.6.1); otra utiliza el objeto HttpSessionState de ASP.NET (sección 21.6.2). Las demás técnicas de rastreo de sesiones incluyen el uso de elementos input de tipo oculto ("hidden") en el formulario, y reescritura de URLs. Mediante el uso de elementos "hidden", un formulario Web Forms puede escribir datos de rastreo de sesiones en un elemento form de la página Web que devuelve al cliente, en respuesta a una solicitud previa. Cuando el usuario envía el formulario en la nueva página Web, todos los datos (incluyendo los campos "hidden") se envían al manejador de formularios en el servidor Web. Cuando un sitio Web lleva a cabo la reescritura de URLs, el formulario Web Forms incrusta la información de rastreo de sesiones directamente en los URLs de los hipervínculos en los que el usuario hace clic, para enviar las solicitudes subsiguientes al servidor Web. Observe que en nuestros ejemplos anteriores se estableció la propiedad EnableSessionState del formu− lario Web Forms a false. No obstante, y como deseamos utilizar el rastreo de sesiones en los siguientes ejemplos, mantendremos la configuración predeterminada de esta propiedad: True.

21.6.1 Cookies

Las cookies proporcionan a los desarrolladores Web una herramienta para personalizar páginas Web. Una cookie es una pieza de datos almacenados en un pequeño archivo, en la computadora del usuario. Una cookie mantiene información acerca del cliente durante, y entre, las sesiones en el explorador. La primera vez que un usuario visita un sitio Web, su computadora podría recibir una cookie; después, esta cookie se reactiva cada vez que el usuario vuelve a visitar ese sitio. La información recolectada tiene el propósito de ser un registro anónimo con datos que se utilizan para personalizar las futuras visitas que haga el usuario al sitio. Cuando un usuario agrega artículos a un carrito de compras en línea, o cuando realiza otra tarea que resulta en una solicitud al servidor Web, éste recibe una cookie que contiene el identificador único del usuario. Luego, el servidor utiliza ese identificador único para localizar el carrito de compras y realiza cualquier procesamiento requerido. Además de identificar a los usuarios, las cookies también pueden indicar las preferencias de compra de los clientes. Cuando un formulario Web Forms recibe una solicitud de un cliente, puede examinar la(s) cookie(s) que envió al cliente durante las sesiones de comunicación anteriores, para identificar las preferencias del cliente y mostrar de inmediato productos de su interés. Cada interacción basada en HTTP entre un cliente y un servidor incluye un encabezado, el cual contiene información acerca de la solicitud (cuando la comunicación es del cliente al servidor) o acerca de la respuesta (cuando la comunicación es del servidor al cliente). Cuando un formulario Web Forms recibe una solicitud, el encabezado incluye información como el tipo de solicitud (por ejemplo Get) y cualquier cookie que se haya enviado previamente del servidor para almacenarla en el equipo cliente. Cuando el servidor formula su respuesta, la información del encabezado contiene todas las cookies que el servidor desea almacenar en el equipo cliente y demás información, como el tipo MIME de la respuesta. La fecha de expiración de una cookie determina cuánto tiempo permanece en la computadora del cliente. Si no se establece una fecha de expiración para una cookie, el explorador Web mantiene esa cookie el tiempo que dure la sesión de navegación. En caso contrario, el explorador Web mantiene la cookie hasta que llega la fecha de expiración. Cuando el explorador solicita un recurso de un servidor Web, las cookies que envió el servidor previamente al cliente se devuelven al servidor Web como parte de la solicitud formulada por el explorador. Las cookies se eliminan cuando expiran.

810

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Tip de portabilidad 21.3 Los clientes pueden deshabilitar las cookies en sus exploradores Web, para asegurar que se proteja su privacidad. Dichos clientes tendrán dificultades al utilizar aplicaciones Web que dependan de las cookies para mantener la información de estado.

Uso de cookies para proporcionar recomendaciones de libros La siguiente aplicación Web demuestra el uso de las cookies. El ejemplo contiene dos páginas. En la primera página (figuras 21.23-21.24), los usuarios elijen su lenguaje de programación favorito de un grupo de botones de opción, y envían el formulario de XHTML al servidor Web para procesarlo. El servidor Web responde creando una cookie que almacena un registro del lenguaje seleccionado, así como el número ISBN para un libro acerca de ese tema. Después, el servidor devuelve un documento de XHTML al explorador Web, con lo cual permite al usuario elegir otro lenguaje de programación favorito, o ver la segunda página en nuestra aplicación (figuras 21.25-21.26), que muestra una lista de libros recomendados, relacionados con el lenguaje de programación que el usuario seleccionó antes. Cuando el usuario hace clic en el hipervínculo, se leen las cookies que se habían almacenado antes en el cliente, y se utilizan para formar la lista de recomendaciones de libros. El archivo ASPX en la figura 21.23 contiene cinco botones de opción (líneas 21-27) con los valores Visual Basic 2005, Visual C# 2005, C, C++ y Java. Recuerde que puede establecer los valores de los botones de opción a través del Editor de la colección ListItem, que se abre haciendo clic en la propiedad Items de RadioButtonList en la ventana Propiedades, o haciendo clic en el vínculo Editar elementos… del menú de etiquetas inteligentes Tareas de RadioButtonList. Para seleccionar un lenguaje de programación, el usuario hace clic en uno de los botones de opción. La página contiene un botón Enviar, y cuando se hace clic en este botón se crea una cookie, la cual con tiene un registro del lenguaje seleccionado. Una vez creada, esa cookie se agrega al encabezado de respuesta HTTP, y se produce un evento postback. Cada vez que el usuario selecciona un lenguaje y hace clic en Enviar, se escribe una cookie en el cliente. Cuando ocurre el evento postback, ciertos controles se ocultan y otros se muestran. Los controles Label, RadioButtonList y Button que se utilizan para seleccionar un lenguaje, se ocultan. Cerca de la parte inferior de la página, se muestra un control Label y dos controles HyperLink. Uno de los vínculos solicita esta página (líneas 36-38) y el otro solicita Recomendaciones.aspx (líneas 41-43). Observe que al hacer clic en el primer hipervínculo (el que solicita la página actual) no se produce un postback. El archivo Opciones.aspx se especifica en la propiedad NavigateUrl del hipervínculo. Cuando se hace clic en el hipervínculo, esta página se solicita como una solicitud completamente nueva. Anteriormente en el capítulo establecimos NavigateUrl a un sitio

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20





Cookies





Figura 21.23 | Archivo ASPX que presenta una lista de lenguajes de programación. (Parte 1 de 3).

21.6 Rastreo de sesiones

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

811

Visual Basic 2005 Visual C# 2005 C C++ Java





Haga clic aquí para elegir otro lenguaje



Haga clic aquí para obtener las recomendaciones de libros



a)

b)

Figura 21.23 | Archivo ASPX que presenta una lista de lenguajes de programación. (Parte 2 de 3).

812

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

c)

d)

Figura 21.23 | Archivo ASPX que presenta una lista de lenguajes de programación. (Parte 3 de 3).

Web remoto (http://www.deitel.com). Para establecer esta propiedad a una página dentro de la misma aplicación ASP.NET, haga clic en el botón de elipsis cerca de la propiedad NavigateUrl en la ventana Propiedades para abrir el cuadro de diálogo Seleccionar dirección URL. Use este cuadro de diálogo para seleccionar una página dentro de su proyecto como el destino del control HyperLink.

Agregar y crear un vínculo hacia un nuevo formulario Web Forms Para establecer la propiedad NavigateUrl a una página en la aplicación actual se requiere que la página de destino exista de antemano. Por ende, para establecer la propiedad NavigateUrl del segundo vínculo (el que solicita la página con las recomendaciones de libros) a Recomendaciones.aspx, primero debe crear este archivo; para ello, haga clic con el botón derecho en la ubicación del proyecto dentro del Explorador de soluciones y seleccione Agregar nuevo elemento… del menú que aparezca. En el cuadro de diálogo Agregar nuevo elemento, seleccione Web Forms del panel Plantillas y cambie el nombre del archivo a Recomendaciones.aspx. Por último, seleccione la casilla de verificación Colocar el código en un archivo independiente para indicar que el IDE debe crear un archivo de código subyacente para este archivo ASPX. Haga clic en Agregar para crear el archivo. (En breve hablaremos sobre el contenido de este archivo ASPX y del archivo de código subyacente.) Una vez que exista el archivo Recomendaciones.aspx, podrá seleccionarlo como valor para la propiedad NavigateUrl de un control HyperLink en el cuadro de diálogo Seleccionar dirección URL.

Escribir cookies en un archivo de código subyacente La figura 21.24 presenta el archivo de código subyacente para Opciones.aspx (figura 21.23). Este archivo contiene el código que escribe una cookie en el equipo cliente cuando el usuario selecciona un lenguaje de programación. El archivo de código subyacente también modifica la apariencia de la página, en respuesta a un evento postback.

21.6 Rastreo de sesiones

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

813

// Fig. 21.24: Opciones.aspx.cs // Procesa la elección que hizo el usuario sobre un lenguaje de programación, // mostrando vínculos y escribiendo una cookie en el equipo del usuario. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class Opciones : System.Web.UI.Page { // almacena valores para representar los libros como cookies private System.Collections.Hashtable libros = new System.Collections.Hashtable(); // inicializa la tabla Hash de los valores que se van a almacenar como cookies protected void Page_Init( object sender, EventArgs e ) { libros.Add( "Visual Basic 2005", "0-13-186900-0" ); libros.Add( "Visual C# 2005", "0-13-152523-9" ); libros.Add( "C", "0-13-142644-3" ); libros.Add( "C++", "0-13-185757-6" ); libros.Add( "Java", "0-13-148398-6" ); } // fin del método Page_Init // si ocurrió un postback, oculta el formulario y muestra los vínculos para realizar // selecciones adicionales o ver las recomendaciones protected void Page_Load( object sender, EventArgs e ) { if ( IsPostBack ) { // el usuario envió información, por lo que se muestra el mensaje // y los hipervínculos apropiados respuestaLabel.Visible = true; lenguajeLink.Visible = true; recomendacionesLink.Visible = true; // oculta los demás controles que se usan para seleccionar lenguajes indicadorLabel.Visible = false; lenguajesList.Visible = false; enviarButton.Visible = false; // si el usuario hizo una selección, la muestra en respuestaLabel if ( lenguajesList.SelectedItem != null ) respuestaLabel.Text += " Usted seleccionó " + lenguajesList.SelectedItem.Text.ToString(); else respuestaLabel.Text += " No seleccionó un lenguaje."; } // fin de if } // fin del método Page_Load // escribe una cookie para registrar la selección del usuario protected void enviarButton_Click( object sender, EventArgs e ) { // si el usuario hizo una selección if ( lenguajesList.SelectedItem != null )

Figura 21.24 | Archivo de código subyacente que escribe una cookie en el cliente. (Parte 1 de 2).

814

60 61 62 63 64 65 66 67 68 69 70 71 72 73

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

{ string lenguaje = lenguajesList.SelectedItem.ToString(); // obtiene el número ISBN del libro sobre el lenguaje dado string ISBN = libros[ lenguaje ].ToString(); // crea cookie usando el par nombre-valor lenguaje-ISBN HttpCookie cookie = new HttpCookie( lenguaje, ISBN ); // agrega cookie a la respuesta, para colocarla en el equipo del usuario Response.Cookies.Add( cookie ); } // fin de if } // fin del método submitButton_Click } // fin de la clase Opciones

Figura 21.24 | Archivo de código subyacente que escribe una cookie en el cliente. (Parte 2 de 2). Las líneas 16-17 crean libros como un objeto Hashtable (espacio de nombres System.Collections): una estructura de datos que almacena pares clave-valor. Un programa utiliza la clave para almacenar y recuperar el valor asociado en el objeto Hashtable. En este ejemplo, las claves son objetos string que contienen los nombres de los lenguajes de programación, y los valores son objetos string que contienen los números ISBN para los libros recomendados. La clase Hashtable proporciona el método Add, que recibe una clave y un valor como argumentos. Un valor que se agrega mediante el método Add se coloca en el objeto Hashtable, en una ubicación determinada por la clave. El valor para una entrada específica en el objeto Hashtable puede determinarse mediante la indexación del objeto Hashtable con la clave de ese valor. La expresión NombreHashtable[ nombreClave ]

devuelve el valor en el par clave-valor, en el que nombreClave es la clave. Por ejemplo, la expresión libros[ lenguaje ] en la línea 64 devuelve el valor que corresponde a la clave contenida en lenguaje. En el capítulo 24, Estructuras de datos, hablaremos con detalle sobre la clase Hashtable. Al hacer clic en el botón Enviar se crea una cookie si hay un lenguaje seleccionado, y se produce un postback. En el manejador de eventos enviarButton_Click (líneas 56-72), se crea un nuevo objeto cookie (de tipo HttpCookie) para almacenar el lenguaje y su correspondiente número ISBN (línea 67). Después, esta cookie se agrega (mediante Add) a la colección Cookies que se envía como parte del encabezado de respuesta HTTP (línea 70). El postback hace que la condición en la instrucción if de Page_Load (línea 33) se evalúe a true, y las líneas 37-51 se ejecutan. Las líneas 37-39 revelan los controles respuestaLabel, lenguajeLink y recomendacionesList, que en un principio estaban ocultos. Las líneas 42-44 ocultan los controles usados para obtener el lenguaje que seleccionó el usuario. La línea 47 determina si el usuario seleccionó un lenguaje. De ser así, el lenguaje se muestra en respuestaLabel (líneas 48-49). En caso contrario, se muestra texto en respuestaLabel indicando que no se seleccionó un lenguaje (línea 51).

Mostrar recomendaciones de libros con base en los valores de una cookie Después del evento postback de Opciones.aspx, el usuario puede solicitar una recomendación de un libro. El hipervínculo de recomendación de libros lleva al usuario a la página Recomendaciones.aspx (figura 21.25) para mostrar las recomendaciones, con base en los lenguajes que seleccionó el usuario.

1 2 3 4 5



Figura 21.25 | Archivo ASPX que muestra las recomendaciones de libros, con base en las cookies. (Parte 1 de 2).

21.6 Rastreo de sesiones

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

815



Recomendaciones de libros









Haga clic aquí para elegir otro lenguaje



Figura 21.25 | Archivo ASPX que muestra las recomendaciones de libros, con base en las cookies. (Parte 2 de 2). Recomendaciones.aspx contiene un control Label (líneas 16-19), un control ListBox (líneas 21-22) y un control HyperLink (líneas 24-27). El control Label muestra el texto Recomendaciones si el usuario seleccionó uno o más lenguajes; en caso contrario, muestra No hay recomendaciones. El control ListBox muestra la recomendación creada por el archivo de código subyacente, que se muestra en la figura 21.26. El control HyperLink permite al usuario regresar a Opciones.aspx para seleccionar lenguajes adicionales.

Archivo de código subyacente que crea las recomendaciones a partir de cookies En el archivo de código subyacente Recomendaciones.aspx.cs (figura 21.26), el método Page_Init (líneas 17-40) recupera las cookies del cliente, usando la propiedad Cookies del objeto Request (línea 20). Esto devuelve una colección de tipo HttpCookieCollection, que contiene las cookies que se escribieron previamente en el cliente. Una aplicación puede leer cookies sólo si éstas se crearon en el dominio en el que se ejecuta la aplicación; un servidor Web nunca podrá acceder a las cookies creadas fuera del dominio asociado con ese servidor. Por ejemplo,

816

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

// Fig. 21.26: Recomendaciones.aspx.cs // Crea recomendaciones de libros, con base en las cookies. using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class Recomendaciones : System.Web.UI.Page { // lee cookies y llena el control ListBox con recomendaciones de libros protected void Page_Init( object sender, EventArgs e ) { // recupera las cookies del cliente HttpCookieCollection cookies = Request.Cookies; // si hay cookies, lista los libros apropiados, junto con los números ISBN if ( cookies.Count != 0 ) { for ( int i = 0; i < cookies.Count; i++ ) librosListBox.Items.Add( cookies[ i ].Name + " How to Program. ISBN#: " + cookies[ i ].Value ); } // fin de if else { // si no hay cookies, significa que no se eligió un lenguaje, por // lo que se muestra el mensaje apropiado y se oculta librosListBox recomendacionesLabel.Text = "No hay recomendaciones"; librosListBox.Items.Clear(); librosListBox.Visible = false; // modifica lenguajeLink porque no se seleccionó un lenguaje lenguajeLink.Text = "Haga clic aquí para elegir un lenguaje"; } // fin de else } // fin de Page_Init } // fin de la clase Recomendaciones

Figura 21.26 | Leer cookies de un cliente para determinar las recomendaciones de libros. una cookie creada por un servidor Web en el dominio deitel.com no puede ser leída por un servidor Web en cualquier otro dominio. La línea 23 determina si existe cuando menos una cookie. Las líneas 25-27 agregan la información en la(s) cookie(s) al objeto librosListBox. La instrucción for recupera el nombre y el valor de cada cookie usando i, la variable de control de la instrucción, para determinar el valor actual en la colección de cookies. Las propiedades Name y Value de la clase HttpCookie, que contienen el lenguaje y el correspondiente ISBN, respectivamente, se concatenan con " How to Program. ISBN# " y se agregan al control ListBox. Las líneas 33-38 se ejecutan si no se seleccionó un lenguaje. En la figura 21.27 se muestra un resumen de algunas propiedades de HttpCookie de uso común.

21.6.2 Rastreo de sesiones con HttpSessionState C# proporciona herramientas para rastreo de sesiones en la clase HttpSessionState de la Biblioteca de clases del .NET Framework. Para demostrar las técnicas básicas de rastreo de sesiones, modificamos la figura 21.26 de manera que utilice objetos HttpSessionState. La figura 21.28 presenta el archivo ASPX, y la figura 21.29 presenta el archivo de código subyacente. El archivo ASPX es similar al que presentamos en la figura 21.23, sólo que la figura 21.28 contiene dos controles Label adicionales (líneas 35-36 y 38-39), que veremos en breve.

21.6 Rastreo de sesiones

817

Todo formulario Web Forms incluye un objeto HttpSessionState, el cual es accesible a través de la propiedad Session de la clase Page. A lo largo de esta sección utilizaremos la propiedad Session para manipular el objeto HttpSessionState de nuestra página. Cuando se solicita la página Web, se crea un objeto HttpSessionState y se asigna a la propiedad Session del objeto Page. Como resultado, a menudo haremos referencia a la propiedad Session como el objeto Session.

Propiedades

Descripción

Domain

Devuelve un objeto string que contiene el dominio de la cookie (es decir, el dominio del servidor Web que ejecuta la aplicación que escribió la cookie). Esto determina cuáles servidores Web pueden recibir la cookie. De manera predeterminada, las cookies se envían al servidor Web que envió originalmente la cookie al cliente. Si se modifica la propiedad Domain, la cookie se devuelve a un servidor Web distinto del que la escribió originalmente. Devuelve un objeto DateTime que indica cuándo puede el explorador eliminar la cookie. Devuelve un objeto string que contiene el nombre de la cookie. Devuelve un objeto string que contiene la ruta hacia un directorio en el servidor (es decir, Domain) en el que se aplica la cookie. Las cookies pueden “dirigirse” a directorios específicos en el servidor Web. De manera predeterminada, una cookie se devuelve sólo a las aplicaciones que operan en el mismo directorio que la aplicación que envió la cookie, o a un subdirectorio de ese directorio. Si se modifica la propiedad Path, la cookie se devuelve a un directorio distinto del directorio en el que se escribió originalmente. Devuelve un valor bool que indica si la cookie debe transmitirse a través de un protocolo seguro. El valor true hace que se utilice un protocolo seguro. Devuelve un objeto string que contiene el valor de la cookie.

Expires Name Path

Secure Value

Figura 21.27 | Propiedades de HttpCookie. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25





Sessions





Visual Basic 2005 Visual C# 2005 C C++

Figura 21.28 | Archivo de ASPX que presenta una lista de lenguajes de programación. (Parte 1 de 3).

818

26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Java













Haga clic aquí para elegir otro lenguaje



Haga clic aquí para obtener recomendaciones de libros



a)

b)

Figura 21.28 | Archivo de ASPX que presenta una lista de lenguajes de programación. (Parte 2 de 3).

21.6 Rastreo de sesiones

819

c)

d)

Figura 21.28 | Archivo de ASPX que presenta una lista de lenguajes de programación. (Parte 3 de 3).

Agregar elementos de sesión Cuando el usuario oprime el botón Enviar en el formulario Web Forms, se invoca el método enviarButton_ Click en el archivo de código subyacente (figura 21.29). El método enviarButton_Click responde agregando un par clave-valor a nuestro objeto Session, especificando el lenguaje elegido y el número ISBN para un libro sobre ese lenguaje. Estos pares clave-valor se conocen comúnmente como elementos de sesión. A continuación, se produce un evento postback. Cada vez que el usuario hace clic en Enviar, enviarButton_Click agrega un nuevo elemento de sesión al objeto HttpSessionState. Como la mayor parte de este ejemplo es idéntica al ejemplo anterior, nos concentraremos en las nuevas características.

Observación de ingeniería de software 21.1 Un formulario Web Forms no debe utilizar variables de instancia para mantener la información de estado del cliente, ya que cada nueva solicitud o postback se maneja mediante una nueva instancia de la página. Los formularios Web Forms deben mantener la información de estado del cliente en objetos HttpSessionState, ya que dichos objetos son específicos para cada cliente. 1 2 3 4 5 6

// Fig. 21.29: Opciones.aspx.cs // Procesa el lenguaje de programación seleccionado por el usuario, // mostrando vínculos y escribiendo una cookie en el equipo del usuario. using System; using System.Data; using System.Configuration;

Figura 21.29 | Crea un elemento de sesión para cada lenguaje de programación seleccionado por el usuario en la página ASPX. (Parte 1 de 3).

820

7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class Opciones : System.Web.UI.Page { // almacena valores para representar los libros como cookies private System.Collections.Hashtable libros = new System.Collections.Hashtable(); //inicializa el objeto Hashtable de valores para almacenarlos como cookies protected void Page_Init( object sender, EventArgs e ) { libros.Add( "Visual Basic 2005", "0-13-186900-0" ); libros.Add( "Visual C# 2005", "0-13-152523-9" ); libros.Add( "C", "0-13-142644-3" ); libros.Add( "C++", "0-13-185757-6" ); libros.Add( "Java", "0-13-148398-6" ); } // fin del método Page_Init // si ocurre el evento postback, oculta el formulario y muestra vínculos // para realizar selecciones adicionales o ver recomendaciones protected void Page_Load( object sender, EventArgs e ) { if ( IsPostBack ) { // el usuario envió información, por lo que muestra las etiquetas // e hipervínculos apropiados respuestaLabel.Visible = true; idLabel.Visible = true; tiempolimiteLabel.Visible = true; lenguajeLink.Visible = true; recomendacionesLink.Visible = true; // oculta los otros controles que se usan para seleccionar los lenguajes indicadorLabel.Visible = false; lenguajesList.Visible = false; enviarButton.Visible = false; // si el usuario hizo una selección, la muestra en respuestaLabel if ( lenguajesList.SelectedItem != null ) respuestaLabel.Text += " Usted seleccionó " + lenguajesList.SelectedItem.Text.ToString(); else respuestaLabel.Text += " No seleccionó un lenguaje."; // muestra el ID de sesión idLabel.Text = "Su ID de sesión único es: " + Session.SessionID; // muestra el tiempo límite tiempolimiteLabel.Text = "Tiempo límite: " + Session.Timeout + " minutos."; } // fin de if } // fin del método Page_Load // escribe una cookie para registrar la selección del usuario protected void enviarButton_Click( object sender, EventArgs e )

Figura 21.29 | Crea un elemento de sesión para cada lenguaje de programación seleccionado por el usuario en la página ASPX. (Parte 2 de 3).

21.6 Rastreo de sesiones

65 66 67 68 69 70 71 72 73 74 75 76 77

821

{ // si el usuario hizo una selección if ( lenguajesList.SelectedItem != null ) { string lenguaje = lenguajesList.SelectedItem.ToString(); // obtiene el número ISBN del libro para el lenguaje elegido string ISBN = libros[ lenguaje ].ToString(); Session.Add( lenguaje, ISBN ); // agrega el par nombre/valor a Session } // fin de if } // fin del método enviarButton_Click } // fin de la clase Opciones

Figura 21.29 | Crea un elemento de sesión para cada lenguaje de programación seleccionado por el usuario en la página ASPX. (Parte 3 de 3). Al igual que una cookie, un objeto HttpSessionState puede almacenar pares nombre-valor. Estos elementos de sesión se colocan en un objeto HttpSessionState mediante una llamada al método Add. La línea 74 llama a Add para colocar el lenguaje y su correspondiente número ISBN del libro recomendado en el objeto HttpSessionState. Si la aplicación llama al método Add para agregar un atributo que tenga el mismo nombre que un atributo guardado en una sesión previa, se sustituye el objeto asociado con el atributo.

Observación de ingeniería de software 21.2 Uno de los principales beneficios del uso de objetos HttpSessionState (en vez de cookies) es que los objetos HttpSessionState pueden almacenar cualquier tipo de objeto (no sólo objetos string) como valores de los atributos. Esto nos proporciona una mayor flexibilidad para determinar el tipo de información de estado que se debe mantener para los clientes.

La aplicación maneja el evento postback (líneas 33-60) en el método Page_Load. Aquí recuperamos de las propiedades del objeto Session la información acerca de la sesión actual del cliente, y mostramos esta información en la página Web. La aplicación ASP.NET contiene información acerca del objeto HttpSessionState para el cliente actual. La propiedad SessionID (línea 56) contiene el ID de sesión único: una secuencia de letras y números al azar. La primera vez que se conecta un cliente al servidor Web, se crea un ID de sesión único para ese cliente. Cuando el cliente realiza solicitudes adicionales, el ID de sesión del cliente se compara para ese cliente. Cuando el cliente realiza solicitudes adicionales, el ID de sesión del cliente se compara con los IDs de sesión almacenados en la memoria del servidor Web, para recuperar el objeto HttpSessionState para ese cliente. La propiedad Timeout (línea 59) especifica la máxima cantidad de tiempo que un objeto HttpSessionState puede estar inactivo antes de descartarlo. La figura 21.30 lista algunas propiedades comunes de HttpSessionState.

Propiedades

Descripción

Count

Especifica el número de pares clave-valor en el objeto Session. Indica si ésta es una nueva sesión (es decir, si la sesión se creó al momento de cargar esta página). Indica si el objeto Session es de sólo lectura. Devuelve una colección que contiene las claves del objeto Session. Devuelve el ID de sesión único. Especifica el número máximo de minutos durante los que una sesión puede estar inactiva (es decir, que no se realicen solicitudes) antes de que expire. Esta propiedad se establece a 20 minutos de manera predeterminada.

IsNewSession IsReadOnly Keys SessionID Timeout

Figura 21.30 | Propiedades de HttpSessionState.

822

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Mostrar las recomendaciones con base en los valores de la sesión Al igual que en el ejemplo de las cookies, esta aplicación proporciona un vínculo a Recomendaciones.aspx (figura 21.31), que muestra una lista de recomendaciones de libros con base en los lenguajes que seleccionó el usuario. Las líneas 21-22 definen un control Web ListBox que se utiliza para presentar las recomendaciones al usuario.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31





Recomendaciones de libros









Haga clic aquí para elegir otro lenguaje



Figura 21.31 | Las recomendaciones de libros basadas en sesiones se muestran en un control ListBox.

21.6 Rastreo de sesiones

823

Archivo de código subyacente que crea recomendaciones de libros a partir de una sesión La figura 21.32 presenta el archivo de código subyacente para Recomendaciones.aspx. El manejador de eventos Page_Init (líneas 17-47) recupera la información de la sesión. Si un usuario no ha seleccionado un lenguaje en Opciones.aspx, la propiedad Count de nuestro objeto Session será 0. Esta propiedad proporciona el número de elementos de sesión contenidos en un objeto Session. Si la propiedad Count de un objeto Session es 0 (es decir, no se seleccionó ningún lenguaje), entonces mostramos el texto No hay recomendaciones y actualizamos la propiedad Text del control HyperLink de vuelta a Opciones.aspx.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

// Fig. 21.32: Recomendaciones.aspx.cs // Crea recomendaciones de libros con base en un objeto session. using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class Recomendaciones : System.Web.UI.Page { // lee las cookies y llena el control ListBox con cualquier recomendación de libro protected void Page_Init( object sender, EventArgs e ) { // almacena el nombre de una clave que se encuentre en el objeto Session string nombreClave; // determina si Session contiene información if ( Session.Count != 0 ) { for ( int i = 0; i < Session.Count; i++ ) { nombreClave = Session.Keys[ i ]; // almacena el nombre de la clave actual // usa la clave actual para mostar uno // de los pares nombre-valor de la sesión librosListBox.Items.Add( nombreClave + " How to Program. ISBN#: " + Session[ nombreClave ].ToString() ); } // fin de for } // fin de if else { // si no hay elementos de sesión, no se eligió ningún lenguaje, por lo // que se muestra el mensaje apropiado y se borra y oculta librosListBox recomendacionesLabel.Text = "No hay recomendaciones"; librosListBox.Items.Clear(); librosListBox.Visible = false; // modifica lenguajeLink, ya que no se seleccionó un lenguaje lenguajeLink.Text = "Haga clic aquí para elegir un lenguaje"; } // fin de else } // fin del método Page_Init } // fin de la clase Recomendaciones

Figura 21.32 | Datos de sesión utilizados para proporcionar recomendaciones de libros al usuario.

824

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Si el usuario eligió un lenguaje, la instrucción for (líneas 25-34) itera a través de los elementos de sesión de nuestro objeto Session y almacena en forma temporal el nombre de cada clave (línea 27). El valor en un par clave-valor se recupera del objeto Session mediante una indexación del objeto Session con el nombre de la clave, usando el mismo proceso mediante el cual recuperamos un valor de nuestro objeto Hashtable en la sección anterior. La línea 27 accede a la propiedad Keys de la clase HttpSessionState, la cual devuelve una colección que contiene todas las claves en la sesión. La línea 27 indexa esta colección para recuperar la clave actual. Las líneas 31-33 concatenan el valor de nombreClave con el objeto string " How to Program. ISBN#: " y el valor del objeto Session para el que nombreClave es la clave. Este objeto string es la recomendación que aparece en el control ListBox.

21.7 Caso de estudio: conexión a una base de datos en ASP.NET

Muchos sitios Web permiten a los usuarios proporcionar retroalimentación acerca del sitio Web en un libro de visitantes. Por lo general, los usuarios hacen clic en un vínculo en la página inicial del sitio Web para solicitar la página del libro de visitantes. Esta página consiste comúnmente en un formulario de XHTML que contiene campos para el nombre del usuario, la dirección de correo electrónico, el mensaje/retroalimentación y así, en lo sucesivo. Después, los datos que se envían en el formulario del libro de visitantes se almacenan en una base de datos ubicada en el equipo servidor Web. En esta sección creamos una aplicación Web Forms de libro de visitantes. La GUI de este ejemplo es un poco más compleja que la de los ejemplos anteriores. Contiene un control de datos de ASP.NET llamado GridView, como se muestra en la figura 21.33, el cual muestra todas las entradas en el libro de visitantes, en formato tabular. En breve le explicaremos cómo crear y configurar este control de datos. Observe que el control GridView muestra abc en modo Diseño para indicar los datos tipo string que recuperará de un origen de datos en tiempo de ejecución. El formulario de XHTML que se presenta al usuario consiste en un campo para el nombre, un campo para la dirección de correo electrónico y un campo para el mensaje. El formulario contiene también un botón Enviar, para enviar los datos al servidor, y un botón Borrar para restablecer cada uno de los campos en el formulario. La aplicación almacena la información del libro de visitantes en una base de datos de SQL Server llamada LibroVisitantes.mdf, que se encuentra en el servidor Web. (En el directorio de ejemplos de este capítulo incluimos esta base de datos. Puede descargar los ejemplos de www.deitel.com/books/csharpforprogrammers2.) Debajo del formulario de XHTML, el control GridView muestra los datos (es decir, las entradas en el libro de visitantes) en la tabla Mensajes de la base de datos.

Control GridView

Figura 21.33 | GUI de la aplicación Libro de visitantes en modo Diseño.

21.7 Caso de estudio: conexión a una base de datos en ASP.NET

825

21.7.1 Creación de un formulario Web Forms que muestra datos de una base de datos Ahora le explicaremos cómo crear esta GUI y establecer el enlace de datos entre el control GridView y la base de datos. Muchos de estos pasos son similares a los que realizamos en el capítulo 20 para acceder a una base de datos e interactuar con ella en una aplicación para Windows. Más adelante en esta sección presentaremos el archivo ASPX generado mediante la GUI, y hablaremos sobe el archivo de código subyacente relacionado en la siguiente sección. Para crear la aplicación de libro de visitantes, realice los siguientes pasos:

Paso 1: crear el proyecto Cree un Sitio Web ASP.NET llamado LibroVisitantes y cambie el nombre del archivo ASPX a LibroVisitantes.aspx. Cambie el nombre de la clase en el archivo de código subyacente LibroVisitantes, y actualice la directiva Page en el archivo ASPX de manera acorde.

Paso 2: crear el formulario para la entrada del usuario En modo

Diseño para el archivo ASPX, agregue el texto Por favor deje un mensaje en nuestro libro de visitantes: y aplique el formato de encabezado h2, color azul marino. Como vimos en la sección 21.5.1,

inserte una tabla de XHTML con dos columnas y cuatro filas, configurada de manera que el texto en cada celda se alinee con la parte superior de la celda. Coloque el texto apropiado (vea la figura 21.33) en las tres celdas superiores de la columna izquierda de la tabla. Después, coloque los controles TextBox llamados nombreTextBox, emailTextBox y mensajeTextBox en las tres celdas superiores de la columna derecha. Establezca mensajeTextBox de manera que sea un control TextBox multilínea. Por último, agregue los controles Button llamados enviarButton y borrarButton a la celda inferior derecha de la tabla. Establezca las leyendas de los botones a Enviar y Borrar, respectivamente. Hablaremos sobre los manejadores de eventos para estos botones cuando presentemos el archivo de código subyacente.

Paso 3: agregar un control GridView al formulario Web Forms Agregue un control GridView llamado mensajesGridView para mostrar las entradas en el libro de visitantes. Este control aparece en la sección Datos del Cuadro de herramientas. Los colores para el control GridView se especifican a través del vínculo Formato automático… en el menú de etiquetas inteligentes Tareas de GridView, que aparece cuando se coloca el control GridView en la página. Al hacer clic en este vínculo se abre un cuadro de diálogo llamado Formato automático con varias opciones. En este ejemplo, elegimos Simple. En unos instantes, le mostraremos cómo establecer el origen de datos del control GridView (es decir, de donde obtiene los datos que mostrará en sus filas y columnas).

Paso 4: agregar una base de datos a una aplicación Web ASP.NET Para usar una base de datos en una aplicación Web ASP.NET, primero debe agregarla a la carpeta App_Data del proyecto. Haga clic con el botón derecho del ratón sobre esta carpeta en el Explorador de soluciones y seleccione Agregar elemento existente…. Localice el archivo LibroVisitantes.mdf en el directorio de ejemplos del capítulo, y después haga clic en Agregar.

Paso 5: enlazar el control GridView a la tabla Mensajes de la base de datos LibroVisitantes Ahora que la base de datos forma parte del proyecto, podemos configurar el control GridView para mostrar sus datos. Abra el menú de etiquetas inteligentes Tareas de GridView y seleccione de la lista desplegable Elegir origen de datos. En el Asistente para la configuración de orígenes de datos que aparezca, seleccione Base de datos. En este ejemplo, utilizamos un control SqlDataSource, que permite a la aplicación interactuar con la base de datos LibroVisitantes. Escriba mensajesSqlDataSource para el ID del origen de datos y haga clic en Aceptar para iniciar el asistente Configurar origen de datos. En la pantalla Elegir la conexión de datos, seleccione LibroVisitantes.mdf de la lista desplegable (figura 21.34) y después haga clic en Siguiente > dos veces, para continuar con la pantalla Configurar la instrucción Select. La pantalla Configurar la instrucción Select (figura 21.35) le permite especificar cuáles datos debe recuperar el objeto SqlDataSource de la base de datos. Sus elecciones en esta página diseñan una instrucción SELECT, que se muestra en el panel inferior del cuadro de diálogo. La lista desplegable Nombre identifica a una tabla en la base de datos. La base de datos LibroVisitantes sólo contiene una tabla llamada Mensajes, que se selecciona de manera predeterminada. En el panel Columnas, haga clic en la casilla de verificación marcada con un

826

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Figura 21.34 | El cuadro de diálogo Configurar origen de datos en Visual Web Developer.

SQL

Figura 21.35 | Configuración de la instrucción SELECT utilizada por el control SqlDataSource para obtener datos. asterisco (*), para indicar que desea recuperar los datos de todas las columnas en la tabla Mensajes. Haga clic en el botón Avanzadas…, y después seleccione la casilla de verificación que está a un lado de Generar instrucciones Insert, Update y Delete. Esto configura el control SqlDataSource para permitirnos insertar nuevos datos en la base de datos. En breve hablaremos sobre cómo insertar nuevas entradas en el libro de visitantes, con base en los datos que envíe el usuario mediante el formulario. Haga clic en Aceptar y después en Siguiente > para continuar con el asistente Configurar origen de datos. La siguiente pantalla del asistente le permite probar la consulta que acaba de diseñar. Haga clic en Consulta de prueba para obtener la vista previa de los datos que devolverá el control SqlDataSource (mostrado en la figura 21.36). Por último, haga clic en Finalizar para completar el asistente. Observe que ahora aparece un control llamado mensajesSqlDataSource en el formulario Web Forms, justo debajo del control GridView (figura 21.37).

21.7 Caso de estudio: conexión a una base de datos en ASP.NET

827

SQL

Figura 21.36 | Vista previa de los datos obtenidos por el control SqlDataSource.

Control SqlDataSource

Figura 21.37 | El modo Diseño muestra un control SqlDataSource para un control GridView. Este control está representado en el modo Diseño como una caja gris que contiene su tipo y su nombre. Este control no aparecerá en la página Web; el cuadro gris sólo proporciona un medio para manipular el control en forma visual, a través del modo Diseño. Observe además que el control GridView ahora tiene encabezados de columna que corresponden a las columnas en la tabla Mensajes, y que cada fila contiene un número (que representa a una columna con autoincremento) o abc (que indica datos tipo cadena de caracteres). Los verdaderos datos del archivo de base de datos LibroVisitantes aparecerán en estas filas cuando se ejecute el archivo ASPX y se vea en un explorador Web.

828

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Paso 6: modificar las columnas del origen de datos mostrado en el control GridView No es necesario que los visitantes a los sitios Web vean la columna IDMensaje cuando estén viendo las entradas anteriores en el libro de visitantes; esta columna es tan sólo una clave primaria única, requerida por la tabla Mensajes dentro de la base de datos. Por lo tanto, modificaremos el control GridView para que esta columna no aparezca en el formulario Web Forms. En el menú de etiquetas inteligentes Tareas de GridView, haga clic en Editar columnas. En el cuadro de diálogo Campos que aparezca (figura 21.38), seleccione IDMensaje en el panel Campos seleccionados y haga clic en la X. Esto eliminará la columna IDMensaje del control GridView. Haga clic en Aceptar para regresar a la ventana principal del IDE. Ahora, el control GridView deberá aparecer como en la figura 21.33.

Paso 7: modificar la forma en que el control SqlDataSource inserta los datos Cuando se crea un control SqlDataSource de la manera aquí descrita, se configura para permitir operaciones SQL INSERT en la tabla de la base de datos de la que recopila datos. Hay que especificar los valores a insertar, ya sea mediante programación o a través de otros controles en el formulario Web Forms. En este ejemplo, deseamos insertar los datos introducidos por el usuario en los controles nombreTextBox, emailTextBox y mensajeTextBox. También queremos insertar la fecha actual; en el archivo de código subyacente especificaremos la fecha a insertar mediante programación, lo cual presentaremos más adelante. Para configurar el control SqlDataSource de manera que permita una inserción de ese tipo, haga clic en el botón de elipsis que está a un lado de la propiedad InsertQuery del control mensajesSqlDataSource en la ventana Propiedades. A continuación aparecerá el Editor de parámetros y comandos (figura 21.39), el cual muestra el comando INSERT utilizado por el control SqlDataSource. Este comando contiene los parámetros @Fecha, @Nombre, @Email y @Mensaje. Usted debe proporcionar valores para estos parámetros, antes de insertarlos en la base de datos. Cada parámetro se lista en la sección Parámetros del Editor de parámetros y comandos. Como estableceremos el parámetro Fecha mediante programación, no lo modificaremos aquí. Para cada uno de los tres parámetros restantes, seleccione el parámetro y después seleccione Control en la lista desplegable Origen del parámetro. Esto indica que el valor del parámetro debe tomarse de un control. La lista desplegable ControlID contiene todos los controles en el formulario Web Forms. Seleccione el control apropiado para cada parámetro y después haga clic en Aceptar. Ahora el control SqlDataSource estará configurado para insertar el nombre del usuario, su dirección de correo electrónico y el mensaje en la tabla Mensajes de la base de datos LibroVisitantes. En breve le mostraremos cómo establecer el parámetro de la fecha, e iniciar la operación de inserción cuando el usuario haga clic en Enviar.

Archivo ASPX para un formulario Web Forms que interactúa con una base de datos El archivo ASPX generado por la GUI del libro de visitantes (y por el control mensajesSqlDataSource) se muestra en la figura 21.40. Este archivo contiene una gran cantidad de marcado generado. Sólo hablaremos de las

Figura 21.38 | Eliminar la columna ID Mensaje del control GridView

21.7 Caso de estudio: conexión a una base de datos en ASP.NET

829

Figura 21.39 | Establecer los parámetros de INSERT con base en valores de control. partes que son nuevas, o que tienen importancia para el ejemplo actual. Las líneas 20-58 contienen los elementos de XHTML y ASP.NET que componen el formulario para recopilar la entrada del usuario. El control GridView aparece en las líneas 61-87. La etiqueta inicial (líneas 61-65) contiene propiedades que establecen varios aspectos de la apariencia y comportamiento del control GridView; por ejemplo, si deben mostrarse líneas de cuadrícula entre las filas y columnas. La propiedad DataSourceID identifica al origen de datos que se utiliza para llenar el control GridView con datos en tiempo de ejecución. Las líneas 66-76 contienen elementos anidados que definen los estilos utilizados para dar formato a las filas del control GridView. El IDE configuró estos estilos, con base en el estilo Simple que usted seleccionó en el cuadro de diálogo Formato automático para el control GridView. Las líneas 77-86 definen las columnas que aparecen en el control GridView. Cada columna se representa como un objeto BoundField, ya que los valores en las columnas están enlazados a los valores que se obtienen del origen de datos (es decir, la tabla Mensajes de la base de datos LibroVisitantes). La propiedad DataField de cada objeto BoundField identifica la columna en el origen de datos con la que se enlaza la columna en el control GridView. La propiedad HeaderText indica el texto que aparece como encabezado para la columna. De manera predeterminada, éste es el nombre de la columna en el origen de datos, pero puede modificar esta propiedad según lo desee. El control mensajesSqlDataSource se define mediante el marcado en las líneas 89-120 de la figura 21.40. Las líneas 90-91 contienen una propiedad ConnectionString, la cual indica la conexión a través de la cual el control SqlDataSource interactúa con la base de datos. El valor de esta propiedad utiliza una expresión ASP.NET,

1 2 3 4 5 6 7 8





Figura 21.40 | Archivo ASPX para la aplicación libro de visitantes. (Parte 1 de 4).

830

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Libro de visitantes



Por favor deje un mensaje en nuestro libro de visitantes:
Su nombre:


Su dirección de e-mail:
Dígalo a todo el mundo:







68

69

71

73

75

76

77

78

80

82

84

86

87

88 89

101

102

103

104

105

106

107

108

109

110

111

112

113

115

117

119

120

121

122

123

124

Figura 21.40 | Archivo ASPX para la aplicación libro de visitantes. (Parte 3 de 4).

831

832

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

a)

b)

Figura 21.40 | Archivo ASPX para la aplicación libro de visitantes. (Parte 4 de 4). delimitada por , para acceder a la conexión LibroVisitantesConnectionString almacenada en la sección ConnectionStrings del archivo de configuración Web.config. Recuerde que creamos esta cadena de conexión anteriormente en esta sección, usando el asistente Configurar origen de datos. La línea 92 define la propiedad SelectCommand de SqlDataSource, la cual contiene la instrucción SQL SELECT que se utiliza para obtener los datos de la base de datos. Con base en nuestras acciones en el asistente Configurar origen de datos, esta instrucción obtiene los datos en todas las columnas de todas las filas de la tabla Mensajes. Las líneas 93-100 definen las propiedades DeleteCommand, InsertCommand y UpdateCommand, que

21.7 Caso de estudio: conexión a una base de datos en ASP.NET

833

contienen las instrucciones SQL DELETE, INSERT y UPDATE, respectivamente. Estas propiedades también las generó el asistente Configurar origen de datos. En este ejemplo sólo utilizamos InsertCommand. Más adelante hablaremos sobre cómo invocar este comando. Observe que los comandos SQL utilizados por el control SqlDataSource contienen varios parámetros (con el prefijo @). Las líneas 101-119 contienen elementos que definen el nombre, el tipo y, para algunos parámetros, el origen. Los parámetros que se establecen mediante programación se definen mediante elementos Parameter que contienen propiedades Name y Type. Por ejemplo, la línea 112 define el parámetro Fecha de tipo String. Éste corresponde al parámetro @Fecha en InsertCommand (línea 97). Los parámetros que obtienen sus valores de los controles se definen mediante elementos ControlParameter. Las líneas 113-118 contienen marcado que establece las relaciones entre los parámetros de INSERT y los controles TextBox del formulario Web Forms. En el Editor de parámetros y comandos (figura 21.39) establecimos estas relaciones. Cada elemento ControlParameter contiene una propiedad ControlID, la cual indica el control a partir del cual obtiene el parámetro su valor. PropertyName especifica la propiedad que contiene el valor real que se usará como valor para el parámetro. El IDE establece PropertyName con base en el tipo de control especificado por ControlID (de manera indirecta, a través del Editor de parámetros y comandos). En este caso sólo utilizamos controles TextBox, por lo que el valor de PropertyName para cada ControlParameter es Text (por ejemplo, el valor del parámetro @Nombre viene de nombreTextBox.Text). No obstante, si utilizáramos un control DropDownList, por ejemplo, el valor de PropertyName sería SelectedValue.

21.7.2 Modificación del archivo de código subyacente para la aplicación Libro de visitantes Después de crear el formulario Web Forms y configurar los controles de datos utilizados en este ejemplo, haga doble clic en los botones Enviar y Borrar para crear los correspondientes manejadores de los eventos Click en el archivo de código subyacente LibroVisitantes.aspx.cs (figura 21.41). El IDE genera manejadores de eventos vacíos, por lo que debemos agregar el código apropiado para hacer que estos botones funcionen en forma apropiada. El manejador de eventos para borrarButton (líneas 43-48) borra cada uno de los controles TextBox, estableciendo su propiedad Text a una cadena vacía. Esto restablece el formulario para un nuevo envío de datos al libro de visitantes. Las líneas 17-40 contienen el código para manejo de eventos de enviarButton, que agrega la información del usuario a la tabla Mensajes de la base de datos LibroVisitantes. Recuerde que configuramos el comando INSERT de mensajesSqlDataSource para utilizar los valores de los controles TextBox en el formulario Web Forms como los valores de los parámetros que se insertan en la base de datos. Aún no hemos especificado el valor de fecha a insertar. La línea 20-22 asigna una representación string de la fecha actual (por ejemplo, "5/27/05") a un nuevo

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 21.41: LibroVisitantes.aspx.cs // Archivo de código subyacente que define los manejadores de eventos para el libro de visitantes. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class LibroVisitantes : System.Web.UI.Page { // El botón Enviar agrega una nueva entrada del libro de visitantes a la base de datos, // borra el formulario y muestra la lista actualizada de entradas en el libro de visitantes

Figura 21.41 | Archivo de código subyacente para la aplicación del libro de visitantes. (Parte 1 de 2).

834

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

protected void enviarButton_Click( object sender, EventArgs e ) { // crea un parámetro de fecha para almacenar la fecha actual System.Web.UI.WebControls.Parameter fecha = new System.Web.UI.WebControls.Parameter( "Fecha", TypeCode.String, DateTime.Now.ToShortDateString() ); // establece el parámetro @Fecha con el parámetro de fecha mensajesSqlDataSource.InsertParameters.RemoveAt( 0 ); mensajesSqlDataSource.InsertParameters.Add( fecha ); // ejecuta una instrucción SQL INSERT para agregar una nueva fila a la // tabla Mensajes en la base de datos Libro de visitantes que contiene // la fecha actual y el nombre, e-mail y mensaje del usuario mensajesSqlDataSource.Insert(); // borra los controles TextBox nombreTextBox.Text = ""; emailTextBox.Text = ""; mensajeTextBox.Text = ""; // actualiza el control GridView con el nuevo contenido de la tabla de la base de datos mensajesGridView.DataBind(); } // fin del método enviarButton_Click // El botón Borrar borra los controles TextBox del formulario Web Forms protected void borrarButton_Click( object sender, EventArgs e ) { nombreTextBox.Text = ""; emailTextBox.Text = ""; mensajeTextBox.Text = ""; } // fin del método borrarButton_Click } // fin de la clase LibroVisitantes

Figura 21.41 | Archivo de código subyacente para la aplicación del libro de visitantes. (Parte 2 de 2). objeto de tipo Parameter. Este objeto Parameter se identifica como "Fecha" y recibe la fecha actual como valor predeterminado. La colección InsertParameters de SqlDataSource contiene un elemento llamado Fecha, el cual eliminamos (Remove) en la línea 25 y sustituimos en la línea 26, agregando (Add) nuestro parámetro fecha. Al invocar al método Insert de SqlDataSource en la línea 31 se ejecuta el comando INSERT en la base de datos, con lo cual se agrega una fila a la tabla Mensajes. Después de insertar los datos en la base de datos, las líneas 34-36 borran los controles TextBox y la línea 39 invoca al método DataBind de mensajesGridView para actualizar los datos que muestra el control GridView. Esto hace que mensajesSqlDataSource (el origen de datos de GridView) ejecute su comando SELECT para obtener los datos recién actualizados de la tabla Mensajes.

21.8 Caso de estudio: aplicación segura de la base de datos Libros Este caso de estudio presenta una aplicación Web en la que un usuario inicia sesión en un sitio Web, para ver una lista de publicaciones de un autor que el usuario seleccione. La aplicación consiste en varios archivos ASPX. La sección 21.8.1 presenta la aplicación funcional y explica el propósito de cada una de sus páginas Web. La sección 21.8.2 proporciona instrucciones paso a paso para guiarlo a través del proceso de crear la aplicación, y presenta el marcado en los archivos ASPX a medida que se van creando.

21.8.1 Análisis de la aplicación segura de la base de datos Libros completa

Este ejemplo utiliza una técnica conocida como autenticación de formularios para proteger una página, de manera que sólo los usuarios que el sito Web reconozca puedan acceder a él. A dichos usuarios se les conoce como miembros

21.8 Caso de estudio: aplicación segura de la base de datos Libros

835

del sitio Web. La autenticación es una herramienta imprescindible para los sitios que sólo permiten que los miembros entren al sitio completo, o a una parte de éste. En esta aplicación, los visitantes del sitio Web deben iniciar sesión antes de que puedan ver las publicaciones en la base de datos Libros. Por lo general, la primera página que solicita un usuario es Login.aspx (figura 21.42). Pronto aprenderá a crear esta página utilizando un control Login, uno de varios controles ASP.NET de inicio de sesión que ayudan a crear aplicaciones seguras, mediante el uso de la autenticación. Estos controles se encuentran en la sección Inicio de sesión del Cuadro de herramientas. La página Login.aspx permite que un visitante del sitio introduzca un nombre de usuario y contraseñas existentes, para iniciar sesión en el sitio Web. Un usuario que visite el sitio por primera vez debe hacer clic en el vínculo debajo del botón Iniciar sesión para crear un nuevo usuario, antes de intentar iniciar sesión. Así el visitante será redirigido a la página CrearNuevoUsuario.aspx (figura 21.43), la cual contiene un control CreateUserWizard que presenta al visitante un formulario de registro de usuarios. En la sección 21.8.2 hablaremos con detalle sobre el control

Figura 21.42 | La página Login.aspx, de la aplicación segura de la base de datos Libros.

Figura 21.43 | La página CrearNuevoUsuario.aspx de la aplicación segura de base de datos de libros.

836

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

CreateUserWizard. En la figura 21.43, utilizamos la contraseña pa$$word para fines de prueba; como aprenderá, el control CreateUserWizard requiere que la contraseña contenga caracteres especiales por cuestiones de seguridad. Al hacer clic en Crear usuario se establece una nueva cuenta de usuario. Una vez creada su cuenta, el usuario inicia sesión en forma automática y aparece un mensaje indicando que la cuenta se creó con éxito (figura 21.44). Al hacer clic en el botón Continuar en la página de confirmación, el usuario es enviado a la página Libros. aspx (figura 21.45), la cual proporciona una lista desplegable de autores y una tabla que contiene los números ISBN, títulos, números de edición y años de copyright de los libros en la base de datos. De manera predeterminada se muestran todos los libros de Harvey Deitel. Aparecen vínculos en la parte inferior de la tabla, que permiten al usuario acceder a páginas adicionales de datos. Cuando el usuario elije un autor, se produce un evento postback

Figura 21.44 | Mensaje que se muestra para indicar que se creó una cuenta de usuario con éxito.

Figura 21.45 | La página Libros.aspx muestra los libros escritos por Harvey Deitel (de manera predeterminada).

21.8 Caso de estudio: aplicación segura de la base de datos Libros

837

y la página se actualiza para mostrar información acerca de los libros escritos por el autor seleccionado (figura 21.46). Observe que, una vez que el usuario crea una cuenta e inicia sesión, Libros.aspx muestra un mensaje de bienvenida personalizado para cada usuario específico que inicia sesión. Como veremos pronto, hay un control llamado LoginName que proporciona esta funcionalidad. Una vez que usted agrega el control a la página, ASP. NET se encarga de los detalles acerca de cómo determinar el nombre del usuario. Si el usuario hace clic en el vínculo Haga clic para cerrar sesión, se cierra su sesión y después regresa a la página Login.aspx (figura 21.47). Como aprenderá, este vínculo lo crea un control LoginStatus, el cual se encarga de los detalles relacionados con el cierre de sesión de un usuario. Para ver el listado de libros otra vez, el usuario debe iniciar sesión a través de Login.aspx. El control Login en esta página recibe el nombre de usuario y la contraseña escritas por un visitante. Después, ASP.NET compara estos valores con los nombres de usuarios y contraseñas almacenados

Figura 21.46 | La página Libros.aspx muestra los libros escritos por Andrew Goldberg.

Figura 21.47 | Inicio de sesión mediante el uso del control Login.

838

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

en una base de datos en el servidor. Si hay una coincidencia, el visitante es autentificado (es decir, se confirma la identidad del usuario). En la sección 21.8.2 explicaremos con detalle el proceso de autentificación. Cuando un usuario existente se autentica con éxito, Login.aspx redirige al usuario a Libros.aspx (figura 21.45). Si falla el intento de iniciar sesión del usuario, se muestra un mensaje de error apropiado (figura 21.48). Observe que Login.aspx, CrearNuevoUsuario.aspx y Libros.aspx comparten el mismo encabezado de página, que contiene la imagen del logotipo Bug2Bug. En vez de colocar esta imagen en la parte superior de cada página, utilizamos una página principal para lograr esto. Como demostraremos en breve, una página principal define los elementos comunes de la GUI que hereda cada página en un conjunto de páginas de contenido. Así como las clases en C# pueden heredar variables de instancia y métodos de las clases existentes, las páginas de contenido heredan elementos de las páginas principales; a esto se le conoce como herencia visual.

21.8.2 Creación de la aplicación segura de la base de datos Libros Ahora que está familiarizado en cuanto al comportamiento de esta aplicación, le demostraremos cómo crearla desde cero. Gracias al extenso conjunto de controles de inicio de sesión y de datos que proporciona ASP.NET, no tendrá que escribir nada de código para crear esta aplicación. De hecho, la aplicación no contiene archivos de código subyacente. Toda la funcionalidad se especifica a través de las propiedades de los controles, muchas de las cuales se establecen mediante asistentes y demás herramientas de programación visual. ASP.NET oculta los detalles de autenticar a los usuarios en una base de datos de nombres de usuarios y contraseñas; muestra mensajes de éxito o de error apropiados y redirige al usuario a la página correcta, con base en los resultados de la autenticación. Ahora veremos los pasos que debe realizar para crear la aplicación segura de base de datos de libros.

Paso 1: crear el sitio Web Cree un Sitio Web ASP.NET en http://localhost/Bug2Bug, como en los ejemplos anteriores. Crearemos de manera explícita cada uno de los archivos ASPX que necesitamos en esta aplicación, así que puede eliminar el archivo Default.aspx generado por el IDE (y su correspondiente archivo de código subyacente); para ello seleccione la página Default.aspx en el Explorador de soluciones y oprima Supr. Haga clic en el botón Aceptar del cuadro de diálogo de confirmación para eliminar estos archivos.

Paso 2: establecer las carpetas del sitio Web Antes de crear cualquiera de las páginas en el sitio Web, crearemos carpetas para organizar su contenido. Primero cree una carpeta llamada Imagenes y agregue el archivo bug2bug.png a esta carpeta. Encontrará esta imagen en el directorio de ejemplos para este capítulo. A continuación, agregue el archivo de base de datos Libros.mdf (que también encontrará en el directorio de ejemplos) a la carpeta App_Data del proyecto. Más adelante en esta sección le mostraremos cómo obtener datos de esta base de datos.

Figura 21.48 | Mensaje de error que se muestra para un intento de inicio de sesión sin éxito, usando control Login.

21.8 Caso de estudio: aplicación segura de la base de datos Libros

839

Paso 3: configurar las opciones de seguridad de la aplicación En esta aplicación, queremos asegurarnos que sólo los usuarios autenticados puedan tener acceso a Libros.aspx (que crearemos en los pasos 9 y 10 ) para ver la información en la base de datos. Anteriormente, creamos todas nuestras páginas ASPX en el directorio raíz de la aplicación Web (por ejemplo, http://localhost/NombreProyecto). De manera predeterminada, cualquier visitante del sitio Web (sin importar que esté autenticado o no) puede ver las páginas del directorio raíz. ASP.NET nos permite restringir el acceso a determinadas carpetas de un sitio Web. No queremos restringir el acceso a la raíz del sitio Web, ya que todos los usuarios deben poder ver Login.aspx y CrearNuevoUsuario.aspx para iniciar sesión y crear cuentas de usuario, respectivamente. Por lo tanto, si queremos restringir el acceso a Libros.aspx, debe residir en un directorio que no sea el directorio raíz. Cree una carpeta llamada Segura. Más adelante en esta sección, crearemos la página Libros.aspx en esta carpeta. Primero, habilitaremos la autenticación de formularios en nuestra aplicación y configurar la carpeta Segura para restringir el acceso sólo a usuarios autenticados. Seleccione Sitio Web > Configuración de ASP.NET para abrir la Herramienta Administración de sitios Web en un explorador Web (figura 21.49). Esta herramienta le permite configurar varias opciones que determinan la manera en que se comportará la aplicación. Haga clic en el vínculo Seguridad o en la ficha Seguridad para abrir una página Web en la que puede establecer las opciones de seguridad (figura 21.50), como el tipo de autenticación que debe utilizar la aplicación. En la columna Usuarios, haga clic en Seleccionar tipo de autenticación. En la página resultante (figura 21.51), seleccione el botón de opción que está a un lado de Desde Internet para indicar que los usuarios iniciarán sesión a través de un formulario en el sitio Web, en el que el usuario puede escribir un nombre de usuario y contraseña (es decir, la aplicación utilizará autenticación de formularios). La opción predeterminada (Desde una red local) se basa en los nombres y contraseñas de usuarios de Windows para fines de autenticación. Haga clic en el botón Listo para guardar este cambio. Ahora que está habilitada la autenticación de formularios, la columna Usuarios en la página principal de la Herramienta Administración de sitios Web (figura 21.52) proporciona vínculos para crear y administrar usuarios. Como vio en la sección 21.8.1, nuestra aplicación incluye la página CrearNuevoUsuario.aspx, en la que los usuarios pueden crear sus propias cuentas. Por lo tanto, aunque es posible crear usuarios a través de la Herramienta Administración de sitios Web, no lo haremos aquí. Aun cuando no existen usuarios en este momento, configuramos la carpeta Segura para otorgar el acceso sólo a los usuarios autenticados (es decir, denegar el acceso a todos los usuarios no autenticados). Haga clic en el vínculo Crear reglas de acceso que está en la columna Reglas de acceso de la Herramienta Administración de sitios

Figura 21.49 | La Herramienta Administración de sitios Web para configurar una aplicación Web.

840

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Figura 21.50 | La página Seguridad de la Herramienta Administración de sitios Web.

Figura 21.51 | Elegir el tipo de autenticación usada por una aplicación Web ASP.NET. Web (figura 21.52) para ver la página Agregar nueva regla de acceso (figura 21.53). Esta página se utiliza para crear una regla de acceso: una regla que otorga o deniega el acceso a un directorio específico de la aplicación Web, para un usuario o grupo de usuarios específico. Haga clic en el directorio Segura en la columna izquierda de la

21.8 Caso de estudio: aplicación segura de la base de datos Libros

841

Figura 21.52 | Página principal de la Herramienta Administración de sitios Web, después de habilitar la autenticación de formularios.

Figura 21.53 | La página Agregar nueva regla de acceso se utiliza para configurar el acceso a los directorios.

842

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

página para identificar el directorio al que se aplica nuestra regla de acceso. En la columna de en medio, seleccione el botón de opción identificado como Usuarios anónimos para especificar que la regla se aplica a los usuarios que no se han autenticado. Por último, seleccione Denegar en la columna derecha etiquetada como Permiso y después haga clic en Aceptar. Esta regla indica que se debe denegar el acceso a los usuarios anónimos (es decir, los usuarios que no se hayan identificado a sí mismos mediante el inicio de sesión) a cualquier página dentro del directorio Segura (por ejemplo, Libros.aspx). De manera predeterminada, los usuarios anónimos que intenten cargar una página en el directorio Segura se redirigen a la página Login.aspx, para que puedan identificarse a sí mismos. Observe que, como no establecimos reglas de acceso para el directorio raíz Bug2Bug, los usuarios anónimos pueden acceder a las páginas de ese directorio (por ejemplo, Login.aspx, CrearNuevoUsuario.aspx). En unos momentos crearemos estas páginas.

Paso 4: examinar los archivos Web.config generados en forma automática Ahora hemos configurado la aplicación para usar la autenticación de formularios, y creamos una regla de acceso para asegurar que sólo los usuarios autenticados puedan acceder a la carpeta Segura. Antes de crear el contenido del sitio Web, vamos a examinar cómo aparecen los cambios realizados a través de la Herramienta Administración de sitios Web en el IDE. Recuerde que Web.config es un archivo de XML que se utiliza para la configuración de aplicaciones, como habilitar la depuración o almacenar cadenas de conexión de bases de datos. Visual Web Developer genera dos archivos Web.config en respuesta a nuestras acciones al usar la Herramienta Administración de sitios Web; uno en el directorio raíz de la aplicación y otro en la carpeta Segura. [Nota: Tal vez necesite hacer clic en el botón Actualizar del Explorador de soluciones para ver estos archivos.] En una aplicación ASP.NET, las opciones de configuración de una página se determinan con base en el archivo Web.config del directorio actual. Las opciones en este archivo tienen precedencia sobre las opciones en el archivo Web.config del directorio raíz. Después de establecer el tipo de autenticación para la aplicación Web, el IDE genera un archivo Web.config en http://localhost/Bug2Bug/Web.config, el cual contiene un elemento authentication:

Este elemento aparece en el archivo Web.config del directorio raíz, por lo que la opción se aplica a todo el sitio Web. El valor "Forms" del atributo mode especifica que deseamos utilizar la autenticación de formularios. Si hubiéramos dejado el tipo de autenticación establecido en Desde una red local en la Herramienta Administración de sitios Web, el atributo mode se establecería a "Windows". Hay que tener en cuenta que "Forms" es el modo predeterminado en un archivo Web.config generado para otro fin, como para guardar una cadena de conexión. Después de crear la regla de acceso para la carpeta Segura, el IDE genera un segundo archivo Web.config en esa carpeta. Este archivo contiene un elemento authorization que indica quién (y quién no) está autorizado para acceder a esta carpeta a través de la Web. En esta aplicación, sólo queremos permitir acceso a los usuarios autenticados al contenido de la carpeta Segura, por lo que el elemento authorization aparece de la siguiente manera:



En vez de otorgar permiso a cada usuario autenticado por separado, denegamos el acceso a todos los que no estén autenticados (es decir, los que no hayan iniciado sesión). El elemento deny dentro del elemento authorization especifica los usuarios a los que deseamos denegar el acceso. Cuando el valor del atributo users se establece en "?", se deniega el acceso a la carpeta a todos los usuarios anónimos (es decir, no autenticados). Por ende, un usuario no autenticado no podrá cargar http://localhost/Bug2Bug/Segura/Libros.aspx, sino que será redirigido a la página Login.aspx; cuando a un usuario se le deniega el acceso a una parte de un sitio, ASP.NET envía de manera predeterminada al usuario a una página llamada Login.aspx en el directorio raíz de la aplicación.

Paso 5: creación de una página principal Ahora que ha establecido las opciones de seguridad de la aplicación, puede crear las páginas Web de la misma. Comenzaremos con la página principal, que define los elementos que queremos que aparezcan en cada página. Una página principal es como una clase base en una jerarquía de herencia visual, y las páginas de contenido son

21.8 Caso de estudio: aplicación segura de la base de datos Libros

843

como clases derivadas. La página principal contiene receptáculos para el contenido personalizado creado en cada página de contenido. Las páginas de contenido heredan en forma visual el contenido de la página principal y después agregan contenido en lugar de los receptáculos de la página principal. Por ejemplo, tal vez sea conveniente incluir una barra de navegación (es decir, una serie de botones para navegar por un sitio Web) en todas las páginas de un sitio. Si el sitio comprende un número extenso de páginas, agregar marcado para crear la barra de navegación para cada página puede ser un proceso que ocupe mucho tiempo. Lo que es más, si modifica después la barra de navegación, todas las páginas del sitio que la utilicen deben actualizarse. Al crear una página principal, puede especificar el marcado de la barra de navegación en un archivo y hacer que aparezca en todas las páginas de contenido, con sólo unas cuantas líneas de marcado. Si la barra de navegación cambia, sólo cambia la página principal; cualquier página de contenido que la utilice se actualiza la próxima vez que se solicite. En este ejemplo, queremos que el logotipo Bug2Bug aparezca como encabezado en la parte superior de todas las páginas, por lo que colocaremos un control Image en la página principal. Cada página subsiguiente que creemos será una página de contenido basada en la página principal y, por lo tanto, incluirá el encabezado. Para crear una página principal, haga clic con el botón derecho en la ubicación del sitio Web en el Explorador de soluciones y seleccione la opción Agregar nuevo elemento…. En el cuadro de diálogo Agregar nuevo elemento, seleccione Página principal de la lista de plantillas y especifique Bug2Bug.master como el nombre de archivo. Las páginas principales tienen la extensión de archivo .master y, al igual que los formularios Web Forms, pueden utilizar de manera opcional un archivo de código subyacente para definir una funcionalidad adicional. En este ejemplo no necesitamos especificar código para la página principal, por lo que deje la casilla de verificación Colocar el código en un archivo independiente en blanco. Haga clic en Agregar para crear la página. El IDE abrirá la página principal en modo Código (figura 21.54) al momento de crear el archivo. [Nota: Agregamos un retorno de línea en el elemento DOCTYPE para fines de presentación.] El marcado para una página principal es casi idéntico al de un formulario Web Forms. Una diferencia es que la página principal contiene una directiva Master (línea 1 en la figura 21.54), la cual especifica que este archivo define a una página maestra, usando el lenguaje (Language) indicado para cualquier código. Como elegimos no utilizar un archivo de código subyacente, la página maestra también contiene un elemento script (líneas 6-8). El código que se coloca comúnmente en un archivo de código subyacente puede colocarse en un elemento script. No obstante, eliminaremos el elemento script de esta página, ya que no necesitamos escribir código adicional. Después de eliminar este bloque de marcado, establezca el título (title) de la página a Bug2Bug. Por último, observe que la página principal contiene un

Figura 21.54 | Página principal en modo Código.

844

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

control ContentPlaceHolder en las líneas 17-18. Este control sirve como un receptáculo para el contenido que se definirá mediante una página de contenido. En breve veremos cómo definir contenido para sustituir el control ContentPlaceHolder. En este punto, puede editar la página principal en modo Diseño (figura 21.55) como si fuera un archivo ASPX. Observe que el control ContentPlaceHolder aparece como un rectángulo grande, con una barra color gris que indica el tipo y el ID del control. Utilice la ventana Propiedades para cambiar el ID de este control a cuerpoContent. Para crear un encabezado en la página principal que aparezca en la parte superior de todas las páginas de contenido, insertaremos una tabla en la página principal. Coloque el cursor a la izquierda del control ContentPlaceHolder y seleccione Diseño > Insertar tabla. En el cuadro de diálogo Insertar tabla, haga clic en el botón de opción Plantilla y seleccione Encabezado de la lista desplegable de plantillas de tabla disponibles. Haga clic en Aceptar para crear una tabla que llene la página y contenga dos filas. Arrastre y suelte el control ContentPlaceHolder en la celda inferior de la tabla. Cambie la propiedad valign de esta celda a top, para que el control ContentPlaceHolder se alinee en forma vertical con la parte superior de la celda. A continuación, establezca la propiedad Height de la celda superior a 130. Agregue a esta celda un control Image llamado encabezadoImage, y establezca su propiedad ImageUrl al archivo bug2bug.png que se encuentra en la carpeta Imagenes del proyecto. (También puede arrastrar la imagen del Explorador de soluciones hacia la celda superior). La figura 21.56 muestra el marcado y la vista Diseño de la página principal completa. Como verá en el paso 6, una página de contenido basada en esta página principal muestra la imagen del logotipo que se define aquí, así como el contenido diseñado para esa página específica (en lugar del control ContentPlaceHolder).

Figura 21.55 | Página principal en modo Diseño. 1 2 3 4 5 6 7 8 9 10 11 12 13





Bug2Bug

Figura 21.56 | La página Bug2Bug.master define un encabezado con una imagen de logotipo para todas las páginas en la aplicación segura de la base de datos Libros. (Parte 1 de 2).

21.8 Caso de estudio: aplicación segura de la base de datos Libros

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

845









Figura 21.56 | La página Bug2Bug.master define un encabezado con una imagen de logotipo para todas las páginas en la aplicación segura de la base de datos Libros. (Parte 2 de 2).

Paso 6: crear una página de contenido Ahora crearemos una página de contenido basada en Bug2Bug.master. Empezaremos por crear la página CrearNuevoUsuario.aspx. Para crear este archivo, haga clic con el botón derecho del ratón en la página maestra dentro del Explorador de soluciones, y seleccione Agregar página de contenido. Esta acción hace que se agregue al proyecto un archivo Default.aspx, configurado para usar la página principal. Cambie el nombre de este archivo a CrearNuevoUsuario.aspx y después ábralo en modo Código (figura 21.57). Observe que este archivo contiene una directiva Page con una propiedad Language, una propiedad MasterPageFile y una propiedad Title. La directiva Page indica el archivo de página maestra (MasterPageFile) en el que se basa la página de contenido. En este caso, la propiedad MasterPageFile es “~\Bug2Bug.master” para indicar que el archivo actual se basa en la página maestra que acabamos de crear. La propiedad Title especifica el título que aparecerá en la barra de título del explorador Web cuando se cargue la página de contenido. Este valor, que establecimos a Crear un nuevo usuario, sustituye el valor (es decir, Bug2Bug) establecido en el elemento title de la página principal.

846

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Figura 21.57 | La página de contenido CrearNuevoUsuario.aspx en modo Código. Como la directiva Page de CrearNuevoUsuario.aspx especifica a Bug2Bug.master como el archivo de página principal (MasterPageFile) de la página, la página de contenido contiene implícitamente el contenido de la página principal, como los elementos DOCTYPE, html y body. El archivo de la página de contenido no duplica los elementos de XHTML que se encuentran en la página principal. En vez de ello, la página de contenido contiene un control Content (líneas 4-6 en la figura 21.57), en el que colocaremos el contenido específico de la página que sustituirá al control ContentPlaceHolder de la página principal cuando se solicite la página de contenido. La propiedad ContentPlaceHolderID del control Control identifica el elemento ContentPlaceHolder en la página principal que el control debe sustituir; en este caso, cuerpoContent. La relación entre una página de contenido y su página principal es más evidente en el modo Diseño (figura 21.58). La región sombreada guarda el contenido de la página principal Bug2Bug.master que aparecerá en CrearNuevoUsuario.aspx cuando se represente en un explorador Web. La única parte modificable de esta página es el control Content, que aparece en lugar del elemento ContentPlaceHolder de la página principal.

Paso 7: agregar un control CreateUserWizard a una página de contenido En la sección 21.8.1 vimos que CrearNuevoUsuario.aspx es la página en nuestro sitio Web que permite a los usuarios que nos visitan por primera vez crear cuentas de usuario. Para proporcionar esta funcionalidad, utilizamos un control CreateUserWizard. Coloque el cursor dentro del control Content en modo Diseño y haga doble clic en el control CrearUserWizard que está en la sección Inicio de sesión del Cuadro de herramientas para

Figura 21.58 | La página de contenido CrearNuevoUsuario.aspx en modo Diseño.

21.8 Caso de estudio: aplicación segura de la base de datos Libros

847

agregarlo a la página, en la posición actual del cursor. También puede arrastrar y soltar el control en la página. Para modificar la apariencia de CreateUserWizard, abra el menú de etiquetas inteligentes Tareas de CreateUserWizard y haga clic en Formato automático. Seleccione el esquema de color Profesional. Como vimos antes, un control CreateUserWizard proporciona un formulario de registro que los visitantes del sitio pueden usar para crear una cuenta de usuario. ASP.NET maneja los detalles de crear una base de datos de SQL Server (llamada ASPNETDB.MDF y ubicada en la carpeta App_Data) para almacenar los nombres de usuario, contraseñas y demás información de los usuarios de la aplicación. ASP.NET también impone un conjunto predeterminado de requerimientos para llenar el formulario. Cada campo en el formulario es requerido, la contraseña debe contener por lo menos siete caracteres, incluyendo al menos un carácter no alfanumérico, y las dos contraseñas introducidas deben coincidir. El formulario también pide una pregunta de seguridad y una respuesta, que pueden usarse para identificar a un usuario, en caso de que éste necesite restablecer o recuperar su contraseña. Una vez que el usuario llena los campos del formulario y hace clic en el botón Crear usuario para enviar la información de la cuenta, ASP.NET verifica que se hayan cumplido todos los requerimientos del formulario e intenta crear la cuenta del usuario. Si ocurre un error (por ejemplo, si ya existe el nombre del usuario), el control CreateUserWizard muestra un mensaje debajo del formulario. Si la cuenta se crea con éxito, el formulario se sustituye por un mensaje de confirmación y un botón que permite al usuario continuar. Para ver este mensaje de confirmación en modo Diseño, seleccione la opción Completar de la lista desplegable Paso en el menú de etiquetas inteligente Tareas de CreateUserWizard. Cuando se crea una cuenta de usuario, ASP.NET inicia la sesión del usuario en el sito, de forma automática (en breve hablaremos más acerca del proceso de inicio de sesión). En este punto, el usuario es autenticado y se le permite acceder a la carpeta Segura. Después de crear la página Libros.aspx más adelante en esta sección, estableceremos la propiedad ContinueDestinationPageUrl de CreateUserWizard a ~/Segura/Libros.aspx para indicar que el usuario debe ser redirigido a Libros.aspx después de hacer clic en el botón Continuar en la página de confirmación. La figura 21.59 presenta el archivo CrearNuevoUsuario.aspx completo (se cambió su formato para mejorar la legibilidad). Dentro del control Content, el control CreateUserWizard se define mediante el marcado en las líneas 9-40. La etiqueta inicial (líneas 9-12) contiene varias propiedades que especifican estilos de formato para el control, así como la propiedad ContinueDestinationPageUrl, que estableceremos más adelante en este capítulo. Las líneas 14-32 contienen elementos que definen los estilos adicionales utilizados para dar formato a partes específicas del control. Por último, las líneas 34-39 especifican los dos pasos del asistente (CreateUserWizardStep y CompleteWizardStep) en un elemento WizardSteps. CreateUserWizardStep y CompleteWizardStep son clases que encapsulan los detalles de crear un usuario y emitir un mensaje de confirmación. Los resultados de ejemplo en las figuras 21.59(a) y 21.59(b) muestran la creación exitosa de una cuenta de usuario con CrearNuevoUsuario.aspx. Utilizamos la contraseña pa$$word para fines de prueba. Esta contraseña cumple con los requerimientos de longitud mínima y caracteres especiales impuestos por ASP.NET, pero en una aplicación real es conveniente que utilice una contraseña que sea más difícil de adivinar. La figura 21.59(c) muestra el mensaje de error que aparece si intenta crear una segunda cuenta de usuario con el mismo nombre; ASP.NET requiere que cada nombre de usuario sea único.

1 2 3 4 5 6 7 8 9

Fig. 21.59: CrearNuevoUsuario.aspx --%>













a)

b)

Figura 21.59 | La página de contenido CrearNuevoUsuario.aspx proporciona un formulario de registro de usuarios. (Parte 2 de 3).

21.8 Caso de estudio: aplicación segura de la base de datos Libros

849

c)

Figura 21.59 | La página de contenido CrearNuevoUsuario.aspx proporciona un formulario de registro de usuarios. (Parte 3 de 3).

Paso 8: crear una página de inicio de sesión En la sección 21.8.1 vimos que Login.aspx es la página en nuestro sitio Web que permite que los visitantes que regresan puedan iniciar sesión en sus cuentas de usuario. Para crear esta funcionalidad, agregue otra página de contenido llamada Login.aspx y establezca su título a Inicio de sesión. En modo Diseño, arrastre un control Login (ubicado en la sección Inicio de sesión del Cuadro de herramientas) al control Content de la página. Abra el cuadro de diálogo Formato automático del menú de etiquetas inteligentes Tareas de Login y establezca el esquema de colores del control a Profesional. A continuación, configure el control Login para mostrar un vínculo a la página para crear nuevos usuarios. Establezca la propiedad CreateUserUrl del control Login a CrearNuevoUsuario.aspx, haciendo clic en el botón de elipsis a la derecha de esta propiedad y seleccionando el archivo CrearNuevoUsuario.aspx en el cuadro de diálogo resultante. Después, establezca la propiedad CreateUserText a Haga clic aquí para crear un nuevo usuario. Estos valores de las propiedades hacen que aparezca un vínculo en el control Login. Por último, cambiaremos el valor de la propiedad DisplayRememberMe del control Login a False. De manera predeterminada, el control muestra una casilla de verificación con el texto Recordármelo la próxima vez. Esta casilla puede usarse para permitir que un usuario permanezca autenticado más allá de una sola sesión del explorador, en la computadora actual del usuario. No obstante, deseamos requerir que los usuarios inicien sesión cada vez que visiten el sitio, por lo que deshabilitaremos esta opción. El control Login encapsula los detalles relacionados con el inicio de sesión de un usuario en una aplicación Web (es decir, la autenticación de un usuario). Cuando un usuario introduce un nombre de usuario y contraseña, y después hace clic en el botón Inicio de sesión, ASP.NET determina si la información proporcionada coincide con la de una cuenta en la base de datos de los miembros (es decir, el archivo ASPNETDB.MDF creado por ASP.NET). Si coinciden, el usuario es autenticado (es decir, se confirma la identidad del usuario) y el explorador se redirige a la página especificada por la propiedad DestinationPageUrl del control Login. En la siguiente sección estableceremos esta propiedad a la página Libros.aspx, después de crearla. Si no se puede confirmar la identidad del usuario (es decir, si el usuario no está autenticado), el control Login muestra un mensaje de error (vea la figura 21.60) y el usuario puede intentar iniciar sesión de nuevo. La figura 21.60 presenta el archivo Login.aspx completo. Observe que, al igual que en CrearNuevoUsuario.aspx, la directiva Page indica que esta página hereda contenido de Bug2Bug.master. En el control Content

850

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web







a) b)

Figura 21.60 | La página de contenido Login.aspx utiliza un control Login. que sustituye al control ContentPlaceHolder de la página inicial con el ID cuerpoContenido, las líneas 8-22 crean un control Login. Observe las propiedades CreateUserText y CreateUserUrl (líneas 10-11) que establecimos mediante la ventana Propiedades. La línea 12 en la etiqueta inicial para el control Login contiene las propiedades DestinationPageUrl (en el siguiente paso establecerá esta propiedad) y DisplayRememberMe, que establecimos en False. Los elementos en las líneas 15-21 definen varios estilos de formato que se aplican a ciertas partes del control. Observe que toda la funcionalidad relacionada con el verdadero inicio de sesión del usuario, o con el proceso de mostrar errores, se oculta completamente de usted. Cuando un usuario introduce el nombre de usuario y contraseña de una cuenta de usuario existente, ASP. NET autentica al usuario y escribe en el cliente una cookie cifrada, que contiene información acerca del usuario autenticado. Los datos cifrados se traducen en un código que sólo el emisor y el receptor pueden entender; por lo tanto, se mantiene privado. La cookie cifrada contiene un nombre de usuario string y un valor bool que especifica si esta cookie debe persistir (es decir, permanecer en la computadora del cliente) más allá de la sesión actual. Nuestra aplicación autentica al usuario sólo para la sesión actual.

21.8 Caso de estudio: aplicación segura de la base de datos Libros

851

Paso 9: crear una página de contenido a la cual sólo pueden acceder los usuarios autenticados Un usuario que haya sido autenticado se redirigirá a Libros.aspx. Ahora crearemos el archivo Libros.aspx en la carpeta Segura, para la cual establecimos una regla de acceso, que deniega el acceso a los usuarios anónimos. Si un usuario no autenticado solicita este archivo, será redirigido a Login.aspx. De aquí, el usuario puede iniciar sesión o crear una nueva cuenta; cualquiera de estas dos acciones autenticarán al usuario, con lo que se le permitirá regresar a Libros.aspx. Para crear la página Libros.aspx, haga clic con el botón derecho del ratón en la carpeta Segura dentro del Explorador de soluciones, y seleccione la opción Agregar nuevo elemento…. En el cuadro de diálogo que aparezca, seleccione Web Forms y especifique el nombre de archivo Libros.aspx. Seleccione la casilla de verificación Seleccionar la página principal para indicar que este formulario Web Forms debe crearse como una página de contenido que hace referencia a una página principal, y después haga clic en Agregar. En el cuadro de diálogo Seleccionar una página principal, seleccione Bug2Bug.master y haga clic en Aceptar. El IDE creará el archivo y lo abrirá en modo Código. Cambie la propiedad Title de la directiva Page a Información sobre libros.

Paso 10: personalizar la página segura Para personalizar la página Libros.aspx para un usuario específico, agregaremos un mensaje de bienvenida que contenga un control LoginName, el cual muestra el nombre del usuario autenticado actual. Abra Libros.aspx en modo Diseño. En el control Content, escriba la palabra ¡Bienvenido, seguida de una coma y un espacio. Después arrastre un control LoginName del Cuadro de herramientas hacia la página. Cuando esta página se ejecute en el servidor, el texto (Nombre de usuario) que aparece en este control en modo Diseño se sustituirá por el nombre del usuario actual. En modo Código, escriba un signo de admiración (!) justo después del control LoginName (sin espacios entre ellos). [Nota: Si agrega el signo de admiración en modo Diseño, el IDE tal vez inserte espacios adicionales o un retorno de línea entre este carácter y el control que lo antecede. Al escribir el ! en modo Código nos aseguramos que aparezca adyacente al nombre del usuario.] A continuación agregaremos un control LoginStatus, el cual permitirá al usuario cerrar la sesión en el sitio Web cuando termine de ver la lista de libros en la base de datos. Un control LoginStatus se representa en una página Web de dos maneras: de manera predeterminada, si el usuario no está autenticado, el control muestra un hipervínculo con el texto Iniciar sesión; si el usuario está autenticado, el control muestra un hipervínculo con el texto Cerrar sesión. Cada vínculo realiza la acción indicada. Agregue un control LoginStatus a la página, arrastrándolo del Cuadro de herramientas hacia la página. En este ejemplo, cualquier usuario que llegue a esta página ya debe estar autenticado, por lo que el control siempre se representará como un vínculo Cerrar sesión. El menú de etiquetas inteligentes Tareas de LoginStatus le permite alternar entre las Vistas del control. Seleccione la vista Sesión iniciada para ver el vínculo Cerrar sesión. Para cambiar el texto de este vínculo, modifique la propiedad LogoutText a Haga clic aquí para cerrar sesión. Después, establezca la propiedad LogoutAction a RedirectToLoginPage.

Paso 11: conectar los controles CreateUserWizard y Login a la página segura Ahora que hemos creado la página Libros.aspx, podemos especificar que ésta es la página a la que los controles CreateUserWizard y Login redirigirán a los usuarios, una vez que se autentiquen. Abra la página CrearNuevoUsuario.aspx en modo Diseño y establezca la propiedad ContinueDestinationPageUrl del control CreateUserWizard a Libros.aspx. Ahora abra Login.aspx y seleccione Libros.aspx como el valor para la propiedad DestinationPageUrl del control Login. En este punto, puede ejecutar la aplicación Web seleccionando Depurar > Iniciar sin depuración. Primero, cree una cuenta de usuario en CrearNuevoUsuario.aspx, y después observe cómo aparecen los controles LoginName y LoginStatus en Libros.aspx. Después, cierre la sesión del sitio y vuelva a iniciarla usando Login.aspx.

Paso 12: generar un DataSet basado en la base de datos Libros.mdf Ahora agregaremos el contenido (es decir, la información sobre los libros) a la página segura Libros.aspx. Esta página debe tener un control DropDownList con los nombres de los autores, y un control GridView que muestre información acerca de los libros escritos por el autor seleccionado en el control DropDownList. Un usuario seleccionará un autor del control DropDownList para hacer que el control GridView muestre sólo información acerca de los libros escritos por el autor seleccionado. Como verá a continuación, creamos esta función únicamente en modo Diseño, sin escribir código.

852

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Para trabajar con la base de datos Libros, usaremos un enfoque algo distinto al del caso de estudio anterior, en el que accedimos a la base de datos LibroVisitantes mediante el uso de un control SqlDataSource. Aquí utilizaremos un control ObjectDataSource, el cual encapsula a un objeto que proporciona acceso a un origen de datos. Como recordará, en el capítulo 20 accedimos a la base de datos Libros en una aplicación para Windows, usando objetos TableAdapter configurados para comunicarse con el archivo de la base de datos. Estos objetos TableAdapter colocaban una copia en caché de los datos de la base de datos en un objeto DataSet, que la aplicación usaba para acceder a esos datos. En este ejemplo utilizamos un enfoque similar. Un objeto ObjectDataSource puede encapsular a un objeto TableAdapter, y utilizar sus métodos para acceder a los datos en la base de datos. Esto ayuda a separar la lógica de acceso a los datos de la lógica de presentación. Como veremos en breve, las instrucciones de SQL que se utilizan para obtener datos no aparecen en la página ASPX cuando se utiliza un ObjectDataSource. El primer paso para acceder a los datos mediante un ObjectDataSource es crear un DataSet que contenga los datos de la base de datos Libros que requiera la aplicación. En Visual C# 2005 Express, esto ocurre de manera automática cuando agregamos un origen de datos a un proyecto. Sin embargo, en Visual Web Developer hay que generar el objeto DataSet de manera explícita. Haga clic con el botón derecho del ratón en la ubicación del proyecto dentro del Explorador de soluciones y seleccione la opción Agregar nuevo elemento…. En el cuadro de diálogo que aparezca, seleccione DataSet y especifique LibrosDataSet.xsd como el nombre de archivo; después haga clic en Agregar. A continuación aparecerá un cuadro de diálogo que le preguntará si el objeto DataSet debe colocarse en la carpeta App_Code: una carpeta cuyo contenido se compila y está disponible para todas las partes del proyecto. Haga clic en Sí para que el IDE cree esta carpeta y almacene LibrosDataSet.xsd ahí.

Paso 13: crear y configurar un objeto AuthorsTableAdapter Una vez que se agregue el objeto DataSet, aparecerá el Diseñador de DataSet y se abrirá el Asistente para la configuración de TableAdapter. En el capítulo 20 vimos que este asistente le permite configurar un objeto TableAdapter para llenar un objeto DataTable en un conjunto DataSet, con los datos provenientes de una base de datos. La página Libros.aspx requiere dos conjuntos de datos: una lista de autores que se mostrarán en el control DropDownList de la página (que crearemos en breve) y una lista de libros escritos por un autor específico. Aquí nos enfocaremos en el primer conjunto de datos: los autores. Por ende, utilizaremos primero el Asistente para la configuración de TableAdapter para configurar un objeto AutoresTableAdapter. En el siguiente paso configuraremos un objeto TitlesTableAdapter. En el Asistente para la configuración de TableAdapter, seleccione Libros.mdf de la lista desplegable. Después haga clic en Siguiente > dos veces para guardar la cadena de conexión en el archivo Web.config de la aplicación y avanzar a la pantalla Elija un tipo de comando. En la pantalla Elija un tipo de comando del asistente, seleccione Usar instrucciones SQL y haga clic en Siguiente >. La siguiente pantalla le permite escribir una instrucción SELECT para recuperar datos de la base de datos, que después se colocarán en un objeto DataTable llamado Autores, dentro de LibrosDataSet. Escriba la siguiente instrucción SQL: SELECT IDAutor, PrimerNombre + ' ' + ApellidoPaterno AS Nombre FROM Autores

en el cuadro de texto de la pantalla Escriba una instrucción SQL. Esta consulta selecciona el IDAutor de cada fila. El resultado de esta consulta también debe contener una columna llamada Nombre, que se crea mediante la concatenación del PrimerNombre y el ApellidoPaterno de cada fila, separados por un espacio. La palabra clave AS de SQL nos permite generar una columna en el resultado de una consulta (lo que se conoce como alias) que contiene el resultado de una expresión SQL (por ejemplo, PrimerNombre + ' ' + ApellidoPaterno). Pronto veremos cómo utilizar el resultado de esta consulta para llenar el control DropDownList con elementos que contengan los nombres completos de los autores. Después de escribir la instrucción SQL, haga clic en el botón Opciones avanzadas… y desactive la opción Generar instrucciones Insert, Update y Delete, ya que esta aplicación no necesita modificar el contenido de la base de datos. Haga clic en Aceptar para cerrar el cuadro de diálogo Opciones avanzadas. Haga clic en Siguiente > para avanzar a la pantalla Elija los métodos que se van a generar. Deje los nombres predeterminados y haga clic en Finalizar. Observe que ahora el Diseñador de DataSet (figura 21.61) muestra un objeto DataTable llamado Autores con los miembros IDAutor y Nombre, y los métodos Fill y GetData.

21.8 Caso de estudio: aplicación segura de la base de datos Libros

Figura 21.61 | El objeto DataTable

Autores

853

en el Diseñador de DataSet.

Paso 14: crear y configurar un objeto TitulosTableAdapter necesita acceder a una lista de libros escritos por un autor específico, y a una lista de autores. Por ende, debemos crear un objeto TitulosTableAdapter que recupere la información deseada de la tabla Titulos de la base de datos. Haga clic con el botón derecho del ratón en el Diseñador de DataSet y en el menú que aparezca, seleccione Agregar > TableAdapter… para iniciar el Asistente para la configuración de TableAdapter. Asegúrese que la cadena LibrosConnectionString esté seleccionada como la conexión en la primera pantalla del asistente, y después haga clic en Siguiente >. Elija la opción Usar instrucciones SQL y haga clic en Siguiente >. En la pantalla Escriba una instrucción SQL, abra el cuadro de diálogo Opciones avanzadas y desactive la opción Generar instrucciones Insert, Update y Delete; después haga clic en Aceptar. Nuestra aplicación permite a los usuarios filtrar los libros mostrados por el nombre del autor, por lo que necesitamos crear una consulta que reciba un IDAutor como parámetro y devuelva las filas en la tabla Titulos para los libros escritos por ese autor. Para crear esta consulta compleja, haga clic en el botón Generador de consultas…. En el cuadro de diálogo Agregar tabla que aparezca, seleccione ISBNAutor y haga clic en Agregar. Después agregue también la tabla Titulos. Nuestra consulta requerirá acceso a los datos en ambas tablas. Haga clic en Cerrar para salir del cuadro de diálogo Agregar tabla. En el panel superior de la ventana Generador de consultas (figura 21.62), seleccione la casilla marcada como * (Todas las columnas) en la tabla Titulos. A continuación, en el panel de en medio agregue una fila con la propiedad Columna establecida en ISBNAutor.IDAutor. Desactive la casilla Resultados, ya que no queremos que IDAutor aparezca en el resultado de nuestra consulta. Agregue un parámetro @IDAutor en la columna Filtro de la fila recién agregada. La instrucción SQL generada por estas acciones obtendrá Libros.aspx

Figura 21.62 | El Generador de consultas para diseñar una consulta que seleccione los libros escritos por un autor específico.

854

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

información acerca de todos los libros escritos por el autor especificado mediante el parámetro @IDautor. La instrucción primero mezcla los datos de las tablas ISBNAutor y Titulos. La cláusula INNER JOIN especifica que se deben comparar las columnas ISBN de cada tabla, para determinar cuáles filas se van a mezclar. La operación INNER JOIN produce una tabla temporal que contiene las columnas de ambas tablas. La porción exterior de la instrucción SQL selecciona la información de los libros de esta tabla temporal, para un autor específico (es decir, todas las filas en las que la columna IDAutor es igual a @IDautor). Haga clic en Aceptar para salir del Generador de consultas y después haga clic en el botón Siguiente > del Asistente para la configuración de TableAdapter. En la pantalla Elija los métodos que se van a generar, escriba LlenarPorIDAutor y ObtenerDatosPorIDAutor como los nombres de los dos métodos que se van a generar para TitulosTableAdapter. Haga clic en Finalizar para salir del asistente. Ahora deberá ver un objeto DataTables llamado Titulo en el Diseñador de DataSet (figura 21.63).

Paso 15: agregar un control DropDownList que contenga el primer nombre y el apellido de los autores Ahora que hemos creado un objeto LibrosDataSet y que configuramos los objetos TableAdapter necesarios, agregaremos controles a Libros.aspx para mostrar los datos en la página Web. Primero agregaremos el control DropDownListBox, del cual los usuarios pueden seleccionar un autor. Abra Libros.aspx en modo Diseño y después agregue el texto Autor: y un control DropDownList llamado autoresDropDownList en el control Content de la página, debajo del contenido existente. Al principio, el control DropDownList muestra el texto [Sin enlazar]. Ahora enlazaremos la lista a un origen de datos, por lo que ésta mostrará la información del autor, que el objeto AutoresTableAdapter colocó en el objeto LibrosDataSet. En el menú de etiquetas inteligentes Tareas de DropDownList, haga clic en Elegir origen de datos… para iniciar el Asistente para la configuración de orígenes de datos. Seleccione de la lista desplegable Seleccionar un origen de datos en la primera pantalla del asistente. Al hacer esto, se abre la pantalla Elija un tipo de origen de datos. Seleccione Objeto y establezca la ID a autoresObjectDataSource; después haga clic en Aceptar. Un objeto ObjectDataSource accede a los datos a través de otro objeto, que a menudo se le conoce como objeto comercial. En la sección 21.3 vimos que el nivel intermedio de una aplicación de tres niveles contiene la lógica comercial que controla la forma en que la interfaz de usuario del nivel superior de una aplicación (en este caso, Libros.aspx) accede a los datos del nivel inferior (en este caso, el archivo de base de datos Libros.mdf). Por lo tanto, un objeto comercial representa el nivel medio de una aplicación, y media las interacciones entre los otros dos niveles. En una aplicación Web ASP.NET, es común que un objeto TableAdapter sirva como el objeto comercial que obtiene los datos de la base de datos de nivel inferior, y los pone a disposición de la interfaz de usuario de nivel superior, a través de un objeto DataSet. En la pantalla Elegir un objeto comercial del asistente Configurar origen de datos (figura 21.64), seleccione LibrosDataSetTableAdapters.AutoresTableAdapter. [Nota: Tal vez necesite guardar el proyecto para ver el objeto AutoresTableAdapter.] LibrosDataSetTableAdapters es un espacio de nombres que declara el IDE cuando usted crea a LibrosDataSet. Haga clic en Siguiente > para continuar. La pantalla Definir métodos de datos (figura 21.65) nos permite especificar cuál método del objeto comercial (en este caso, AutoresTableAdapter) debe utilizarse para obtener los datos a los que se accede a través del objeto ObjectDataSource. Sólo se pueden elegir métodos que devuelvan datos, por lo que la única opción que se proporciona es el método GetData, que devuelve un objeto AutoresDataTable. Haga clic en Finalizar para

Figura 21.63 | El Diseñador de DataSet, después de agregar el objeto TitulosTableAdapter.

21.8 Caso de estudio: aplicación segura de la base de datos Libros

855

Figura 21.64 | Elegir un objeto comercial para un ObjectDataSource.

Figura 21.65 | Elegir un método de datos de un objeto comercial, para usarlo con un ObjectDataSource.

cerrar el asistente Configurar origen de datos y regresar al Asistente para la configuración de orígenes de datos para el control DropDownList (figura 21.66). El origen de datos recién creado (es decir, autoresObjectDataSource) deberá estar seleccionado en la lista desplegable superior. Las otras dos listas desplegables en esta pantalla le permiten configurar la forma en que el control DropDownList utiliza los datos del origen de datos. Establezca Nombre como el campo de datos para mostrar, e IDAutor como el campo de datos para usarlo como valor. Así, cuando se represente autoresDropDownList en un explorador Web, los elementos de la lista mostrarán los nombres de los autores, pero los valores subyacentes asociados con cada elemento serán los campos IDAutor de los autores. Por último, haga clic en Aceptar para enlazar el control DropDownList con los datos especificados. El último paso para configurar el control DropDownList en Libros.aspx es establecer la propiedad AutoPostBack del control a True. Esta propiedad indica que se produce un postback cada vez que el usuario selecciona un elemento en el control DropDownList. Como veremos en breve, esto hace que el control GridView (que crearemos en el siguiente paso) de la página muestre nuevos datos.

856

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Figura 21.66 | Elegir un origen de datos para un control DropDownList.

Paso 16: crear un control GridView para mostrar los libros de los autores seleccionados Ahora agregaremos un control GridView a Libros.aspx, para mostrar la información de los libros según el autor seleccionado en el control autoresDropDownList. Agregue un control GridView llamado titulosGridView debajo de los demás controles que se encuentran en el control Content de la página. Para enlazar el control GridView a los datos de la base de datos Libros, seleccione de la lista desplegable Elegir origen de datos en el menú de etiquetas inteligentes Tareas de GridView. Cuando se abra el Asistente para la configuración de orígenes de datos, seleccione Objeto y establezca el ID del origen de datos a titulosObjectDataSource; después haga clic en Aceptar. En la pantalla Elija un objeto comercial, seleccione LibrosDataSetTableAdapters.TitulosTableAdapter de la lista desplegable, para indicar el objeto que se utilizará para acceder a los datos. Haga clic en Siguiente >. En la pantalla Definir métodos de datos, deje la selección predeterminada de ObtenerDatosPorIDAutor como el método que se invocará para obtener los datos y mostrarlos en el control GridView. Haga clic en Siguiente >. Recuerde que el método ObtenerDatosPorIDAutor de TitulosTableAdapter requiere un parámetro para indicar el IDAutor para el que deben obtenerse los datos. La pantalla Definir parámetros (figura 21.67) le permite especificar en dónde obtener el valor del parámetro @IDautor en la instrucción SQL ejecutada por ObtenerDatosPorAutorID. Seleccione Control de la lista desplegable Origen del parámetro. Seleccione autoresDropDownList como el IDControl (es decir, el ID del control de origen del parámetro). A continuación, escriba 1 en DefaultValue para que se muestren los libros escritos por Harvey Deitel (que tiene el IDAutor 1 en la base de datos) la primera vez que se cargue la página (es decir, antes de que el usuario haya realizado alguna selección usando el control autoresDropDownList). Por último, haga clic en Finalizar para salir del asistente. Ahora el control GridView está configurado para mostrar los datos obtenidos por TitulosTableAdapter. ObtenerDatosPorIDAutorID, usando el valor de la selección actual en autoresDropDownList como parámetro. Así, cuando el usuario seleccione un nuevo autor y ocurra un postback, el control GridView mostrará un nuevo conjunto de datos. Ahora que el control GridView está enlazado a un origen de datos, modificaremos varias de sus propiedades para ajustar su apariencia y comportamiento. Establezca la propiedad CellPadding del control GridView a 5, establezca la propiedad BackColor de AlternatingRowStyle a LightYellow y establezca la propiedad BackColor de HeaderStyle a LightGreen. Cambie la anchura (Width) del control a 600px, para poder alojar valores de datos extensos. A continuación, en el menú de etiquetas inteligentes Tareas de GridView, seleccione la opción Habilitar ordenación. Esto hace que los encabezados de las columnas en el control GridView se conviertan en hipervínculos,

21.8 Caso de estudio: aplicación segura de la base de datos Libros

857

Figura 21.67 | Elegir el origen de datos para un parámetro en el método de datos del objeto comercial. para permitir a los usuarios ordenar los datos. Por ejemplo, al hacer clic en el encabezado Titles en el explorador Web, los datos mostrados aparecerán ordenados alfabéticamente. Si hacemos clic en este encabezado por segunda vez, los datos se mostrarán en orden alfabético inverso. ASP.NET oculta los detalles requeridos para lograr esta funcionalidad. Por último, en el menú de etiquetas inteligentes Tareas de GridView, seleccione la opción Habilitar paginación. Esto hace que el control GridView se divida en varias páginas. El usuario puede hacer clic en los vínculos numerados en la parte inferior del control GridView para mostrar una página distinta de datos. La propiedad PageSize de GridView determina el número de entradas por página. Establezca la propiedad PageSize a 4, usando la ventana Propiedades, de manera que el control GridView sólo muestre cuatro libros por página. Esta técnica para mostrar los datos aumenta la legibilidad del sitio y permite que las páginas se carguen más rápido (debido a que se muestran menos datos a la vez). Observe que, al igual que con el proceso de ordenarlos datos en un control GridView, no necesitamos agregar código para lograr la funcionalidad de la paginación. La figura 21.68 muestra el archivo Libros.aspx completo en modo Diseño.

Paso 17: examinar el marcado en Libros.aspx La figura 21.69 presenta el marcado en Libros.aspx (cambiamos el formato por cuestión de legibilidad). Aparte del signo de admiración en la línea 9, que agregamos de forma manual en modo Código, el resto del marcado lo generó el IDE, en respuesta a las acciones que realizamos en modo Diseño. El control Content (líneas 6-55) define el contenido específico para la página que sustituirá al control ContentPlaceHolder llamado cuerpoContent. Recuerde que este control se encuentra en la página maestra que especificamos en la línea 3. La línea 9 crea el control LoginName, que muestra el nombre del usuario autenticado cuando solicitamos y vemos la página desde un explorador. Las líneas 10-12 crean el control LoginStatus. Recuerde que este control está configurado para redirigir al usuario a la página de inicio de sesión después de cerrar la sesión (es decir, hacer clic en el hipervínculo con la propiedad LogoutText). Las líneas 15-18 definen el control DropDownList que muestra los nombres de los autores en la base de datos Libros. La línea 16 contiene la propiedad AutoPostBack del control, la cual indica que si se cambia el elemento seleccionado en la lista, se produce un postback. La propiedad DataSourceID en la línea 16 especifica que los elementos del control DropDownList se crean con base en los datos que se obtuvieron a través del objeto autoresObjectDataSource (definido en las líneas 20-23). La línea 21 especifica que este objeto ObjectDataSource accede a la base de datos Libros mediante una llamada al método GetData del objeto AutoresTableAdapter de LibrosDataSet (línea 22).

858

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

Figura 21.68 | La página Libros.aspx completa, en modo Diseño. Las líneas 25-43 crean el control GridView que muestra información acerca de los libros escritos por el autor seleccionado. La etiqueta inicial (líneas 25-28) indica que el control GridView tiene habilitadas la paginación (con un tamaño de página de 4) y la ordenación. La propiedad AutoGenerateColumns indica si las columnas en el control GridView se generan en tiempo de ejecución, con base en los campos en el origen de 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22



¡Bienvenido, !

Autor:



Figura 21.69 | Marcado para el archivo Libros.aspx completo. (Parte 1 de 3).

21.8 Caso de estudio: aplicación segura de la base de datos Libros

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55















a)

Figura 21.69 | Marcado para el archivo Libros.aspx completo. (Parte 2 de 3).

859

860

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

b)

c)

Figura 21.69 | Marcado para el archivo Libros.aspx completo. (Parte 3 de 3).

datos. Esta propiedad se estableció en False, debido a que el elemento Columns generado por el IDE (líneas 3039) ya especifica las columnas para el control GridView, mediante el uso de elementos BoundField. Las líneas 45-54 definen el objeto ObjectDataSource que se utilizará para llenar el control GridView de datos. Recuerde que configuramos a titulosObjectDataSource para que utilizara el método ObtenerDatosPorIDAutor del objeto TitlesTableAdapter de LibrosDataSet para este fin. El elemento ControlParameter en las líneas 50-52 especifica que el valor del parámetro del método ObtenerDatosPorIDAutor proviene de la propiedad SelectedValue de autoresDropDownList. La figura 21.69(a) ilustra la apariencia predeterminada de Libros.aspx en un explorador Web. Como la propiedad DefaultValue (línea 51) del elemento ControlParameter para el objeto titulosObjectDataSource es 1, cuando se cargue la página por primera vez, se mostrarán los libros escritos por el autor con el IDAutor de 1 (es decir, Harvey Deitel). Observe que el control GridView muestra los vínculos de paginación

21.10 Recursos Web

861

debajo de los datos, ya que el número de filas de datos devueltas por ObtenerDatosPorIDAutor es mayor que el tamaño de la página. La figura 21.69(b) muestra el control GridView después de hacer clic en el vínculo 2 para ver la segunda página de datos. La figura 21.69(c) presenta a Libros.aspx después de que el usuario selecciona un autor distinto de autoresDropDownList. Los datos se ajustan a una página, por lo que el control GridView no muestra los vínculos de paginación.

21.9 Conclusión En este capítulo presentamos el desarrollo de aplicaciones Web mediante ASP.NET y Visual Web Developer 2005 Express. Primero hablamos sobre las transacciones HTTP simples que se llevan a cabo al solicitar y recibir una página Web a través de un explorador Web. Después aprendió acerca de los tres niveles (es decir, el cliente o nivel superior, la lógica comercial o nivel medio y la información o nivel inferior) que componen la mayoría de las aplicaciones Web. Después explicamos la función de los archivos ASPX (es decir, archivos Web Forms) y los archivos de código subyacente, y la relación entre ellos. Hablamos sobre cómo ASP.NET compila y ejecuta aplicaciones Web, de manera que se puedan mostrar como XHTML en un explorador Web. También aprendió a crear una aplicación Web ASP.NET, usando el IDE de Visual Web Developer. En este capítulo se demostraron varios controles Web comunes de ASP.NET, que se utilizan para mostrar texto e imágenes en un formulario Web Forms. Aprendió a utilizar un control AdRotator para mostrar imágenes seleccionadas al azar. También hablamos sobre los controles de validación, que le permiten asegurar que la entrada del usuario en una página Web cumpla con ciertos requerimientos. Hablamos sobre los beneficios de mantener la información de estado acerca de un usuario, a través de varias páginas de un sitio Web. Después le mostramos cómo puede incluir dicha funcionalidad en una aplicación Web, usando cookies o rastreo de sesiones con objetos HttpSessionState. Por último, el capítulo presentó dos casos de estudio acerca de la creación de aplicaciones ASP.NET que interactúan con bases de datos. Primero le mostramos cómo crear una aplicación de libro de visitantes, la cual permite a los usuarios enviar comentarios acerca de un sitio Web. Aprendió a guardar la entrada del usuario en una base de datos SQL y cómo mostrar la información de usuarios anteriores en la página Web. El segundo caso de estudio demostró cómo crear una aplicación que requiere que los usuarios inicien sesión antes de acceder a la información de la base de datos Libros, que vimos en el capítulo 20. Utilizó la Herramienta Administración de sitios Web para configurar la aplicación y utilizar autenticación de formularios, para evitar que los usuarios anónimos accedan a la información sobre los libros. Este caso de estudio le explicó cómo utilizar los controles Login, CreateUserWizard, LoginName y LoginStatus para simplificar la autenticación de usuarios. También aprendió a crear una apariencia visual uniforme para un sitio Web, usando una página principal y varias páginas de contenido. En el siguiente capítulo continuaremos nuestra cobertura sobre la tecnología ASP.NET, con una introducción a los servicios Web, que permiten que los métodos en un equipo llamen a los métodos en otro equipo a través de formatos de datos y protocolos comunes, tales como XML y HTTP. Aprenderá cómo los servicios Web promueven la reutilización de software y la interoperabilidad a través de varias computadoras en una red como Internet.

21.10 Recursos Web beta.asp.net

Este sitio oficial de Microsoft presenta las generalidades acerca de ASP.NET y proporciona un vínculo para descargar Visual Web Developer. Este sitio incluye artículos sobre ASP.NET, vínculos a recursos útiles de ASP.NET y listas de libros acerca del desarrollo Web mediante ASP.NET. beta.asp.net/QuickStartv20/aspnet/

El Tutorial de inicio rápido sobre ASP.NET de Microsoft proporciona ejemplos de código y discusiones sobre los temas fundamentales de ASP.NET. beta.asp.net/guidedtour2/

Este paseo con guía sobre Visual Web Developer 2005 Express introduce las características clave en el IDE que se utiliza para desarrollar aplicaciones Web ASP.NET.

862

Capítulo 21 ASP.NET 2.0, formularios Web Forms y controles Web

www.15seconds.com

Este sitio ofrece noticias, artículos, ejemplos de código, FAQs y vínculos a valiosos recursos comunitarios sobre ASP. NET, como un foro de mensajes ASP.NET y una lista de correo. aspalliance.com

Este sitio comunitario contiene artículos, tutoriales y ejemplos acerca de ASP.NET. aspadvice.com

Este sitio proporciona acceso a muchas listas de correo electrónico, en las que cualquiera puede hacer y responder preguntas acerca de ASP.NET y las tecnologías relacionadas. www.asp101.com/aspdotnet/

Este sitio presenta las generalidades acerca de ASP.NET, e incluye artículos, ejemplos de código, un foro de discusión y vínculos a recursos sobre ASP.NET. Los ejemplos de código se basan en muchas de las técnicas que presentamos en este capítulo, como el rastreo de sesiones y la conexión a una base de datos. www.411asp.net

Este sitio de recursos ofrece a los programadores tutoriales y ejemplos de código sobre ASP.NET. Las páginas comunitarias permiten a los programadores hacer preguntas, responder preguntas y publicar mensajes. www.123aspx.com

Este sitio ofrece un directorio de vínculos a recursos sobre ASP.NET. El sitio incluye también boletines de noticias diarios y semanales.

22

Servicios Web

Un cliente para mí es una simple unidad, un factor en un problema. —Sir Arthur Conan Doyle

OBJETIVOS En este capítulo aprenderá lo siguiente:

…si las cosas más simples de la naturaleza tienen un mensaje que comprendas, regocíjate, por que tu alma está viva.

Q

Qué es un servicio Web.

Q

Cómo crear servicios Web.

Q

La parte importante que juegan XML y el Protocolo simple de acceso a objetos basado en XML para habilitar los servicios Web.

Q

Los elementos que conforman los servicios Web, como las descripciones del servicio y los archivos de descubrimiento.

Q

Cómo crear un cliente que utilice un servicio Web.

Ellos también sirven a quien sólo está esperando.

Q

Cómo utilizar servicios Web con aplicaciones para Windows y aplicaciones Web.

—John Milton

Q

Cómo utilizar el rastreo de sesiones en los servicios Web, para mantener la información de estado para el cliente.

Q

Cómo pasar tipos definidos por el usuario a un servicio Web.

—Eleonora Duse

El protocolo es todo. —Francoise Giuliani

Pla n g e ne r a l

864

Capítulo 22 Servicios Web

22.1 Introducción 22.2 Fundamentos de los servicios Web .NET 22.2.1 Creación de un servicio Web en Visual Web Developer 22.2.2 Descubrimiento de servicios Web 22.2.3 Determinación de la funcionalidad de un servicio Web 22.2.4 Prueba de los métodos de un servicio Web 22.2.5 Creación de un cliente para utilizar un servicio Web 22.3 Protocolo simple de acceso a objetos (SOAP) 22.4 Publicación y consumo de los servicios Web 22.4.1 Definición del servicio Web EnteroEnorme 22.4.2 Creación de un servicio Web en Visual Web Developer 22.4.3 Despliegue del servicio Web EnteroEnorme 22.4.4 Creación de un cliente para consumir el servicio Web EnteroEnorme 22.4.5 Consumo del servicio Web EnteroEnorme 22.5 Rastreo de sesiones en los servicios Web 22.5.1 Creación de un servicio Web de Blackjack 22.5.2 Consumo del servicio Web de Blackjack 22.6 Uso de formularios Web Forms y servicios Web 22.6.1 Agregar componentes de datos a un servicio Web 22.6.2 Creación de un formulario Web Forms para interactuar con el servicio Web de reservaciones de una aerolínea 22.7 Tipos definidos por el usuario en los servicios Web 22.8 Conclusión 22.9 Recursos Web

22.1 Introducción En este capítulo se introducen los servicios Web, que promueven la reutilización de software en sistemas distribuidos, en los que las aplicaciones se ejecutan a través de varias computadoras en una red. Un servicio Web es una clase que permite que sus métodos sean llamados por otros métodos en otros equipos, a través de formatos de datos y protocolos comunes, como XML (vea el capítulo 19) y HTTP. En .NET, las llamadas a los métodos a través de la red se implementan comúnmente a través del Protocolo simple de acceso a objetos (SOAP ), un protocolo basado en XML que describe cómo marcar solicitudes y respuestas, de manera que puedan transferirse a través de protocolos como HTTP. Mediante el uso de SOAP, las aplicaciones representan y transmiten datos en un formato estandarizado, basado en XML. Microsoft está alentando a los distribuidores de software y comercios electrónicos a que desplieguen servicios Web. Como cada vez hay más organizaciones a nivel mundial que se conectan a Internet, el concepto de aplicaciones que llaman a métodos a través de una red se ha vuelto más práctico. Los servicios Web representan el siguiente paso en la programación orientada a objetos; en vez de desarrollar software a partir de un pequeño número de bibliotecas de clases que se proporcionan en una ubicación, los programadores pueden acceder a las bibliotecas de clases de un servicio Web que estén distribuidas en todo el mundo.

Tip de rendimiento 22.1 Los servicios Web no son la mejor solución para ciertas aplicaciones de rendimiento intensivo, ya que las aplicaciones que invocan a los servicios Web experimentan retrasos en la red. Además, las transferencias de datos son por lo general más extensas, ya que los datos se transmiten en formatos XML basados en texto.

22.2 Fundamentos de los servicios Web .NET

865

Los servicios Web facilitan la colaboración y permiten que los comercios crezcan. Al comprar servicios Web y utilizar algunos gratuitos en la red que son relevantes para sus comercios, las compañías pueden invertir menos tiempo desarrollando nuevas aplicaciones. Los comercios electrónicos pueden usar servicio Web para proporcionar a sus clientes una mejor experiencia de compra. Por ejemplo, considere una tienda de música en línea. El sitio Web de la tienda proporciona vínculos a información acerca de varios CDs, para permitir al usuario comprar los CDs o ver información acerca de los artistas. Otra compañía que vende boletos para los conciertos proporciona un servicio Web que muestra las próximas fechas de conciertos de varios artistas, para permitir a los usuarios comprar los boletos. Al consumir el servicio Web de boletos de conciertos en su sitio, la tienda de música en línea puede ofrecer un servicio adicional a sus clientes e incrementar el tráfico de su sitio. La compañía que vende boletos de conciertos también se beneficia de la relación comercial, al vender más boletos y posiblemente recibir ingresos de la tienda de música en línea, por el uso de su servicio Web. Muchos servicios Web se proporcionan sin costo. Por ejemplo, Amazon y Google ofrecen servicios Web gratuitos, que usted puede utilizar en sus propias aplicaciones para acceder a la información que proporcionan. Visual Web Developer y el .NET Framework proporcionan una manera simple y amigable para el usuario, para crear servicios Web. En este capítulo le mostraremos cómo emplear estas herramientas para crear, desplegar y utilizar servicios Web. Para cada ejemplo, proporcionamos el código para el servicio Web y después presentamos una aplicación que utiliza este servicio Web. Nuestros primeros ejemplos analizan los servicios Web y cómo funcionan en Visual Web Developer. Después mostramos servicios Web que utilizan características más sofisticadas, como el rastreo de sesiones (que vimos en el capítulo 21) y la manipulación de objetos de tipos definidos por el usuario. Al igual que en el capítulo 21, en este capítulo haremos distinciones entre Visual C# 2005 Express y Visual Web Developer 2005. Crearemos los servicios Web en Visual Web Developer y crearemos aplicaciones cliente que utilicen estos servicios Web mediante el uso de Visual C# 2005 y de Visual Web Developer 2005. La versión completa de Visual Studio 2005 incluye la funcionalidad de ambas ediciones Express.

22.2 Fundamentos de los servicios Web .NET Un servicio Web es un componente de software que se almacena en un equipo, al que una aplicación (o cualquier otro componente de software) puede acceder desde otro equipo, a través de una red. El equipo en el que reside el servicio Web se conoce como equipo remoto. La aplicación (es decir, el cliente) que accede al servicio Web envía la llamada a un método a través de una red hasta el equipo remoto, el cual procesa la llamada y devuelve una respuesta a la aplicación, a través de la red. Este tipo de computación distribuida beneficia a varios sistemas. Por ejemplo, una aplicación sin acceso directo a ciertos datos en otro sistema podría obtener estos datos a través de un servicio Web. De manera similar, una aplicación que carezca del poder de procesamiento necesario para realizar cálculos específicos podría utilizar un servicio Web para aprovechar los recursos superiores de otro sistema. Por lo general, un servicio Web se implementa como una clase. En capítulos anteriores incluimos una clase en un proyecto, ya sea definiendo la clase en el proyecto, o agregando una referencia a una DLL compilada. Todas las piezas de una aplicación residen en un equipo. Cuando un cliente utiliza un servicio Web, la clase (y su DLL compilada) se almacena en un equipo remoto; una versión compilada de la clase del servicio Web no se coloca en el directorio de la aplicación actual. En breve hablaremos sobre lo que ocurre. Las solicitudes y respuestas relacionadas con los servicios Web que se crean mediante Visual Web Developer se transmiten, por lo general, a través de SOAP. Así, cualquier cliente capaz de generar y procesar mensajes SOAP puede interactuar con un servicio Web, sin importar el lenguaje en el que esté escrito. En la sección 22.3 hablaremos más acerca de SOAP. Es posible para los servicios Web limitar el acceso a los clientes autorizados. Al final del capítulo encontrará los Recursos Web, en donde podrá ver vínculos a información acerca de los mecanismos y protocolos estándar que tratan con las cuestiones de seguridad de los servicios Web. Los servicios Web tienen implicaciones importantes para las transacciones de negocio a negocio (B2B ). Estos servicios permiten a los comercios realizar transacciones a través de servicios Web estandarizados y con una disponibilidad amplia, en vez de depender de aplicaciones propietarias. Los servicios Web y SOAP son independientes de la plataforma y el lenguaje, por lo que las compañías pueden colaborar mediante los servicios Web, sin preocuparse acerca de la compatibilidad de su hardware, software y tecnologías de comunicaciones. Las compañías como Amazon, Google, eBay y muchas otras están utilizando servicios Web para su provecho. Para leer casos de estudio acerca del uso de los servicios Web en las empresas, visite msdn.microsoft.com/ webservices/understanding/ casestudies/default.aspx.

866

Capítulo 22 Servicios Web

22.2.1 Creación de un servicio Web en Visual Web Developer Para crear un servicio Web en Visual Web Developer, primero debe crear un proyecto tipo Servicio Después, Visual Web Developer genera lo siguiente:

Web ASP.NET.

• archivos para contener el código del servicio Web (que implementa el servicio Web). • un archivo ASMX (que proporciona acceso al servicio Web). • un archivo DISCO (que utilizan los clientes potenciales para descubrir el servicio Web). La figura 22.1 muestra los archivos que conforman un servicio Web. Al crear una aplicación Servicio Web ASP.NET en Visual Web Developer, el IDE por lo común genera varios archivos adicionales. Sólo mostramos los archivos específicos para las aplicaciones de servicios Web. En la sección 22.2.2 hablaremos sobre estos archivos. Visual Web Developer genera archivos de código para la clase del servicio Web y cualquier otro código que forma parte de la implementación del servicio Web. En la clase del servicio Web se definen los métodos que estarán disponibles para las aplicaciones cliente. Al igual que las aplicaciones Web ASP.NET, los servicios Web ASP. NET pueden probarse con el servidor integrado de Visual Web Developer. No obstante, para que un servicio Web ASP.NET pueda accederse públicamente desde clientes que estén fuera de Visual Web Developer, debe desplegar el servicio Web en un servidor Web como el servidor Web Internet Information Services (IIS). En un servicio Web, los métodos se invocan a través de una Llamada a procedimiento remoto (RPC ). Estos métodos, que se marcan con el atributo WebMethod, a menudo se conocen como métodos del servicio Web, o simplemente como métodos Web; de aquí en adelante nos referiremos a ellos como métodos Web. Al declarar un método con el atributo WebMethod, otras clases pueden acceder a él mediante RPCs, lo cual se conoce como exponer un método Web. En la sección 22.4 hablaremos sobre los detalles acerca de cómo exponer los métodos Web.

22.2.2 Descubrimiento de servicios Web Una vez que se implementa un servicio Web, se compila y despliega en un servidor Web (lo cual veremos en la sección 22.4), una aplicación cliente puede consumir (es decir, usar) el servicio Web. No obstante, los clientes deben poder encontrar el servicio Web y aprender acerca de sus herramientas. Descubrimiento de servicios Web (DISCO) es una tecnología específica de Microsoft, que se utiliza para localizar los servicios Web en un servidor. Cuatro tipos de archivos DISCO facilitan el proceso de descubrimiento: archivos .disco, .vsdisco, .discomap y .map.

8a^ZciZhediZcX^VaZh

HZgk^X^dLZW

6gX]^kd6HBM egdedgX^dcV VXXZhdVLH9A nVadhVgX]^kdh .disco#

6gX]^kd9>H8D

8‹Y^\dYZahZgk^X^dLZW

Figura 22.1 | Componentes de un servicio Web.

22.2 Fundamentos de los servicios Web .NET

867

Los archivos DISCO consisten en marcado XML que describe para los clientes la ubicación de los servicios Web. Para acceder a un archivo .disco se utiliza una página ASMX del servicio Web, la cual contiene marcado que especifica referencias a los documentos que definen varios servicios Web. Los datos resultantes que se devuelven del acceso a un archivo .disco se colocan en el archivo .discomap. Un archivo .vsdisco se coloca en el directorio de la aplicación de un servicio Web, y se comporta de una manera un poco distinta. Cuando un cliente potencial solicita un archivo .vsdisco, se genera en forma dinámica el marcado XML que describe las ubicaciones de los servicios Web, y después se devuelve al cliente. Primero, el .NET Framework busca los servicios Web en el directorio en el que se encuentra el archivo .vsdisco, así como en los subdirectorios de ese directorio. Después, el .NET Framework genera XML (usando la misma sintaxis que la de un archivo .disco) que contiene referencias a todos los servicios Web que encontró en esta búsqueda. Debemos tener en cuenta que un archivo .vsdisco no almacena el marcado que se genera en respuesta a una solicitud, sino que el archivo .vsdisco en el disco contiene opciones de configuración que especifican su comportamiento. Por ejemplo, los desarrolladores pueden especificar en el archivo .vsdisco ciertos directorios en los que no debe buscarse cuando un cliente solicita un archivo .vsdisco. Aunque un desarrollador puede abrir un archivo .vsdisco en un editor de texto y examinar su contenido, esto muy raras veces es necesario; el propósito de un archivo .vsdisco es que lo soliciten (es decir, lo vean en un explorador) los clientes a través de la Web. Cada vez que esto ocurre, se genera y se visualiza nuevo marcado. El uso de archivos .vsdisco beneficia a los desarrolladores de diversas formas. Estos archivos sólo contienen una pequeña cantidad de datos y proporcionan información actualizada acerca de los servicios Web disponibles en un servidor. No obstante, los archivos .vsdisco generan más sobrecarga (es decir, requieren más procesamiento) que los archivos .disco, ya que debe realizarse una búsqueda cada vez que se accede a un archivo .vsdisco. Por lo tanto, algunos desarrolladores consideran más benéfico actualizar los archivos .disco en forma manual. Muchos sistemas utilizan ambos tipos de archivos. Como veremos en breve, los servicios Web que se crean mediante ASP.NET contienen la funcionalidad para generar un archivo .disco, cuando éste se solicita. Este archivo .disco contiene referencias sólo a archivos en el servicio Web actual. Por ende, es común que un desarrollador coloque un archivo .vsdisco en el directorio raíz de un servidor; al acceder a este archivo, localiza los archivos .disco para los servicios Web en cualquier parte del sistema, y utiliza el marcado que encuentra en estos archivos .disco para devolver información acerca de todo el sistema completo.

22.2.3 Determinación de la funcionalidad de un servicio Web Después de localizar un servicio Web, el cliente debe determinar la funcionalidad de éste y cómo utilizarlo. Por lo general, y para este fin, los servicios Web contienen una descripción del servicio. Éste es un documento XML que se conforma al Lenguaje de descripción de servicios Web (WS-DL): un vocabulario de XML que define los métodos que un servicio Web hace disponibles, y la manera en que los clientes interactúan con estos métodos. El documento WSDL también especifica información de bajo nivel que podrían necesitar los clientes, como los formatos requeridos para las solicitudes y respuestas. El propósito de los documentos WSDL no es que los desarrolladores puedan leerlos, sino las aplicaciones, para que éstas sepan cómo interactuar con los servicios Web que se describen en los documentos. Visual Web Developer genera un archivo ASMX cuando se construye un servicio Web. Los archivos con la extensión de archivo .asmx son archivos de servicio Web ASP.NET, y se ejecutan en un servidor Web (por ejemplo, IIS). Al mostrarse en un explorador Web, un archivo ASMX presenta las descripciones de los métodos Web y vínculos a páginas de prueba que permiten a los usuarios ejecutar llamadas de ejemplo a estos métodos. En esta sección explicaremos estas páginas de prueba con mayor detalle. El archivo ASMX también especifica la clase de implementación del servicio Web, y opcionalmente el archivo de código de subyacente en el que se definen el servicio Web y los ensamblados a los que éste hace referencia. Cuando el servidor Web recibe una solicitud en relación con el servicio Web, accede al archivo ASMX que, a su vez, invoca a la implementación del servicio Web. Para ver información más técnica acerca del servicio Web, los desarrolladores pueden acceder al archivo WSDL (que se genera mediante ASP.NET). En breve le mostraremos cómo hacer esto. La página ASMX en la figura 22.2 muestra información acerca del servicio Web EnteroEnorme que crearemos en la sección 22.4. Este servicio Web está diseñado para realizar cálculos con enteros que contengan un máximo de 100 dígitos. La mayoría de los lenguajes de programación no pueden realizar cálculos fácilmente al usar enteros tan grandes. El servicio Web proporciona a las aplicaciones cliente métodos que reciben dos “enteros enormes” y determinan su suma, su diferencia, cuál es mayor o menor y si los dos números son iguales. Observe que la parte

868

Capítulo 22 Servicios Web

superior de la página contiene un vínculo a la Descripción del servicio Web. ASP.NET genera la descripción del servicio WSDL del código que usted escribe para definir el servicio Web. Los programas cliente utilizan la descripción de un servicio Web para validar las llamadas a los métodos Web cuando se compilan los programas cliente. ASP.NET genera la información WSDL en forma dinámica, en vez de crear un archivo WSDL. Si un cliente solicita la descripción WSDL del servicio Web (ya sea adjuntando ?WSDL al URL del archivo ASMX, o haciendo clic en el vínculo Descripción del servicio), ASP.NET genera la descripción WSDL y después la devuelve al cliente para mostrarla en el explorador Web. Al generar la descripción WSDL en forma dinámica, nos aseguramos que los clientes reciban la información más actual acerca del servicio Web. Es común que un documento XML (por ejemplo, una descripción WSDL) se cree en forma dinámica y no se guarde en disco. Cuando un usuario hace clic en el vínculo Descripción del servicio en la parte superior de la página ASMX en la figura 22.2, el explorador muestra el documento WSDL generado que contiene la descripción para nuestro servicio Web EnteroEnorme (figura 22.3).

22.2.4 Prueba de los métodos de un servicio Web Debajo del vínculo Descripción del servicio, la página ASMX que se muestra en la figura 22.2 lista los métodos que ofrece el servicio Web. Al hacer clic en el nombre de cualquier método se solicita una página de prueba que describe a ese método (figura 22.4). La página de prueba permite a los usuarios probar el método, escribiendo valores para los parámetros y haciendo clic en el botón Invocar. (En breve hablaremos sobre el proceso de probar un método Web.) Debajo del botón Invocar, la página muestra mensajes de solicitud y respuesta de ejemplo, en los que se utilizan SOAP y HTTP POST. Estos protocolos son dos opciones para enviar y recibir mensajes en los servicios Web. El protocolo que transmite mensajes de solicitud y respuesta también se conoce como el formato de alambre o protocolo de alambre del servicio Web, ya que define la manera en que se va a enviar la información “a través del alambre”. SOAP es el formato de alambre que se utiliza con más frecuencia, ya que los mensajes SOAP pueden enviarse mediante el uso de varios protocolos de transporte, mientras que HTTP POST debe utilizar HTTP. Cuando se prueba un servicio Web a través de una página ASMX (como en la figura 22.4), la página ASMX utiliza HTTP POST para probar los métodos del servicio Web. Más adelante en este capítulo, cuando utilicemos servicios Web en nuestros programas en C#, emplearemos SOAP: el protocolo predeterminado para los servicios Web .NET. La figura 22.4 muestra la página de prueba para el método Web MasGrande de EnteroEnorme. De esta página, los usuarios pueden probar el método escribiendo valores en los campos primero: y segundo:, y después haciendo clic en Invocar. El método se ejecuta y aparece una nueva ventana del explorador Web, en la que se muestra un documento que contiene el resultado (figura 22.5).

Vínculo a la descripción del servicio

Vínculos a los métodos del servicio Web

Figura 22.2 | Archivo ASMX representado en un explorador Web.

22.2 Fundamentos de los servicios Web .NET

Figura 22.3 | Descripción de nuestro servicio Web EnteroEnorme.

Figura 22.4 | Invocación de un método desde un explorador Web.

869

870

Capítulo 22 Servicios Web

Figura 22.5 | Resultados de la invocación de un método Web desde un explorador Web.

Tip de prevención de errores 22.1 Usar la página ASMX de un servicio Web para probar y depurar los métodos puede ser útil para hacer que el servicio Web sea más confiable y robusto.

22.2.5 Creación de un cliente para utilizar un servicio Web Ahora que hemos hablado acerca de los distintos archivos que conforman un servicio Web .NET, examinaremos las partes de un cliente del servicio Web .NET (figura 22.6). Un cliente .NET puede ser cualquier tipo de aplicación .NET, como una aplicación Windows, una aplicación de consola o una aplicación Web. Para habilitar una aplicación cliente, de manera que consuma un servicio Web, es necesario agregar una referencia Web al cliente. Este proceso agrega, a la aplicación cliente, archivos que permiten que el cliente acceda al servicio Web. Esta sección habla sobre Visual C# 2005, pero la discusión también se aplica a Visual Web Developer. Para agregar una referencia Web en Visual C# 2005, haga clic con el botón derecho en el nombre del proyecto dentro del Explorador de soluciones, y seleccione la opción Agregar referencia Web…. En el cuadro de diálogo que aparezca, especifique el servicio Web que se va a consumir. A continuación, Visual C# 2005 agrega una referencia Web apropiada a la aplicación cliente. En la sección 22.4 demostraremos en detalle cómo agregar referencias Web. Cuando especificamos el servicio Web que deseamos consumir, Visual C# 2005 accede a la información WSDL del servicio Web y la copia en un archivo WSDL que se almacena en la carpeta Web References del proyecto del cliente. Este archivo está visible cuando la opción Mostrar todos los archivos está seleccionada en Visual C# 2005. [Nota: una copia del archivo WSDL proporciona a la aplicación cliente acceso local a la descripción del servicio Web. Para asegurar que el archivo WSDL esté actualizado, Visual C# 2005 proporciona una opción Actualizar referencia Web (disponible haciendo clic con el botón derecho en la referencia Web, dentro del

8a^ZciZ 8‹Y^\dXa^ZciZ Xdchjb^YdgYZa hZgk^X^dLZW

GZ[ZgZcX^VLZW

8aVhZ egdmn

8de^V LH9A

Figura 22.6 | Cliente de un servicio Web .NET, después de agregar una referencia Web.

22.3 Protocolo simple de acceso a objetos (SOAP)

871

Explorador de Soluciones),

la cual actualiza los archivos en la carpeta Web References.] La información WSDL se utiliza para crear una clase proxy, la cual se encarga de todo el trabajo técnico requerido para las llamadas a los métodos Web (es decir, los detalles sobre el trabajo en red y la formación de los mensajes SOAP). Cada vez que la aplicación cliente llama a un método Web, en realidad llama a su correspondiente método en la clase proxy. Este método tiene el mismo nombre y parámetros que el método Web al que se está llamando, pero da formato a la llamada para que se envíe como una solicitud en un mensaje SOAP. El servicio Web recibe esta solicitud como un mensaje SOAP, ejecuta la llamada al método y devuelve el resultado como otro mensaje SOAP. Cuando la aplicación cliente recibe el mensaje SOAP que contiene la respuesta, la clase proxy lo deserializa y devuelve los resultados como el valor de retorno del método Web que se llamó. La figura 22.7 muestra las interacciones entre el código cliente, la clase proxy y el servicio Web. El entorno .NET oculta la mayoría de estos detalles a usted como programador. Muchos aspectos de la creación y consumo de servicios Web (como la generación de archivos WSDL, clases proxy y archivos DISCO) los manejan Visual Web Developer, Visual C# 2005 y ASP.NET. Aunque los desarrolladores quedan liberados del tedioso proceso de crear estos archivos, de todas formas pueden modificar los archivos, en caso de ser necesario. Esto se requiere sólo cuando se desarrollan servicios Web avanzados; ninguno de nuestros ejemplos requiere modificaciones a estos archivos.

22.3 Protocolo simple de acceso a objetos (SOAP) El Protocolo simple de acceso a objetos (SOAP) es independiente de la plataforma, utiliza XML para hacer llamadas a procedimientos remotos, por lo general a través de HTTP. Cada solicitud y respuesta se encapsula en un mensaje SOAP : un mensaje XML que contiene la información que requiere un servicio Web para procesar el mensaje. Los mensajes SOAP se escriben en XML, para ser legibles por los humanos e independientes de la plataforma. La mayoría de los firewalls (barreras de seguridad que restringen la comunicación entre redes) no restringen el tráfico HTTP. Por lo tanto, XML y HTTP permiten que computadoras en distintas plataformas envíen y reciban mensajes SOAP con pocas limitaciones. Los servicios Web también utilizan SOAP para el extenso conjunto de tipos que soporta. El formato de alambre que se utiliza para transmitir solicitudes y respuestas debe soportar todos los tipos que se pasan entre las aplicaciones. Los tipos SOAP incluyen los tipos primitivos (por ejemplo, Integer), así como DateTime, XmlNode y otros. SOAP también puede transmitir arreglos de todos estos tipos. Además, los objetos DataSet pueden serializarse en SOAP. En la sección 22.7 podrá ver que puede transmitir tipos definidos por el usuario en mensajes SOAP. Cuando un programa invoca a un método Web, la solicitud y toda la información relevante se encapsulan en un mensaje SOAP y se envían al servidor en el que reside el servicio Web. Cuando el servicio Web recibe este mensaje SOAP, empieza a procesar el contenido (que está dentro de una envoltura SOAP ) que especifica el método que el cliente desea ejecutar, junto con cualquier argumento que el cliente pase a ese método. A este proceso de interpretar el contenido de un mensaje SOAP se le conoce como analizar un mensaje SOAP. Una vez que el servicio Web recibe y analiza una solicitud, se hace una llamada al método apropiado con los argumentos especificados (si los hay), y la respuesta se envía de vuelta al cliente en otro mensaje SOAP. El cliente analiza la respuesta para obtener el resultado de la llamada al método. La solicitud SOAP en la figura 22.8 se obtuvo de la página de prueba para el método MasGrande del servicio Web EnteroEnorme (figura 22.4). Visual C# 2005 crea dicho mensaje cuando un cliente desea ejecutar el método MasGrande del servicio Web EnteroEnorme. Si el cliente es una aplicación Web, Visual Web Developer crea el mensaje SOAP. El mensaje en la figura 22.8 contiene receptáculos (length en la línea 4 y string en las líneas 16-17)

8a^ZciZ

8‹Y^\d Xa^ZciZ

8aVhZ egdmn

>ciZgcZi

HZgk^X^d LZW

Figura 22.7 | Interacción entre el cliente de un servicio Web y el servicio Web.

872

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

Capítulo 22 Servicios Web

POST /EnteroEnorme/EnteroEnorme.asmx HTTP/1.1 Host: localhost Content-Type: text/xml; charset=utf-8 Content-Length: length SOAPAction: "http://www.deitel.com/MasGrande"



string string



Figura 22.8 | Mensaje de la solicitud SOAP para el servicio Web EnteroEnorme. que representan los valores específicos para una llamada específica a MasGrande. Si ésta fuera una verdadera solicitud SOAP, los elementos primero y segundo (líneas 16-17) contendrían un valor real que el cliente pasa al servicio Web, en vez del receptáculo string. Por ejemplo, si esta envoltura transmitiera la solicitud de la figura 22.4, el elemento primero y el elemento segundo contendrían los números que se muestran en la figura, y el receptáculo length (línea 4) contendría la longitud del mensaje SOAP. La mayoría de los programadores no manipulan los mensajes SOAP directamente, sino que permiten al .NET Framework encargarse de los detalles de la transmisión.

22.4 Publicación y consumo de los servicios Web

En esta sección presentamos varios ejemplos de la creación (que también se conoce como publicación) y el uso (que también se conoce como consumo) de servicios Web. Recuerde que una aplicación que consume un servicio Web, en realidad consiste de dos partes: una clase proxy que representa al servicio Web y una aplicación cliente que accede al servicio Web a través de una instancia de la clase proxy. Esa instancia de la clase proxy pasa los argumentos de un método Web de la aplicación cliente al servicio Web. Cuando el método Web termina su tarea, la instancia de la clase proxy recibe el resultado y la analiza para la aplicación cliente. Visual C# 2005 y Visual Web Developer crean estas clases proxy por usted. En unos momentos demostraremos esto.

22.4.1 Definición del servicio Web EnteroEnorme La figura 22.9 presenta el archivo de código subyacente para el servicio Web que crearemos en la sección 22.4.2. Cuando se crean servicios Web en Visual Web Developer, es necesario trabajar casi exclusivamente en el archivo

1 2 3 4 5 6 7 8 9 10

// Fig. 22.9: EnteroEnorme.cs // El servicio Web EnteroEnorme realiza operaciones con enteros grandes. using System; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; [ WebService( Namespace = "http://www.deitel.com/", Description = "Un servicio Web que proporciona métodos para" + " manipular valores enteros grandes" ) ]

Figura 22.9 | El servicio Web EnteroEnorme. (Parte 1 de 4).

22.4 Publicación y consumo de los servicios Web

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69

873

[ WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 ) ] public class EnteroEnorme : System.Web.Services.WebService { private const int MAXIMO = 100; // número máximo de dígitos public int[] numero; // arreglo que representa el entero enorme // constructor predeterminado public EnteroEnorme() { numero = new int[ MAXIMO ]; } // fin del constructor predeterminado // indexador que acepta un parámetro entero public int this[ int indice ] { get { return numero[ indice ]; } // fin de get set { numero[ indice ] = value; } // fin de set } // fin del indexador // devuelve representación de cadena de EnteroEnorme public override string ToString() { string returnString = ""; foreach ( int i in numero ) returnString = i + returnString; return returnString; } // fin del método ToString // crea EnteroEnorme con base en el argumento public static EnteroEnorme DeString( string valor ) { // crea EnteroEnorme temporal, que el método va a devolver EnteroEnorme enteroAnalizado = new EnteroEnorme(); for ( int i = 0; i < valor.Length; i++ ) enteroAnalizado[ i ] = Int32.Parse( valor[ valor.Length - i - 1 ].ToString() ); return enteroAnalizado; } // fin del método DeString // Método Web que suma los enteros representados por los argumentos String [ WebMethod( Description = "Suma dos enteros enormes." ) ] public string Sumar( string primero, string segundo ) { int acarreo = 0; EnteroEnorme operando1 = EnteroEnorme.DeString( primero ); EnteroEnorme operando2 = EnteroEnorme.DeString( segundo ); EnteroEnorme resultado = new EnteroEnorme(); // almacena el resultado de la suma

Figura 22.9 | El servicio Web EnteroEnorme. (Parte 2 de 4).

874

70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125

Capítulo 22 Servicios Web

// realiza el algoritmo de suma para cada dígito for ( int i = 0; i < MAXIMO; i++ ) { // suma dos dígitos en la misma columna, // el resultado es su suma más el acarreo de // la operación anterior, módulo 10 resultado[ i ] = ( operando1[ i ] + operando2[ i ] + acarreo ) % 10; // asigna al acarreo el residuo de la división de las sumas de dos dígitos entre 10 acarreo = ( operando1[ i ] + operando2[ i ] + acarreo ) / 10; } // fin de for return resultado.ToString(); } // fin del método Sumar // Método Web que resta enteros // representados por los argumentos string [ WebMethod( Description = "Resta dos enteros enormes." ) ] public string Restar( string primero, string segundo ) { EnteroEnorme operando1 = EnteroEnorme.DeString( primero ); EnteroEnorme operando2 = EnteroEnorme.DeString( segundo ); EnteroEnorme resultado = new EnteroEnorme(); // resta el dígito inferior del dígito superior for ( int i = 0; i < MAXIMO; i++ ) { // si el dígito superior es menor que el dígito inferior, necesitamos pedir prestado if ( operando1[ i ] < operando2[ i ] ) PedirPrestado( operando1, i ); // resta inferior de superior resultado[ i ] = operando1[ i ] - operando2[ i ]; } // fin de for return resultado.ToString(); } // fin del método Restar // pide prestado 1 al siguiente dígito private void PedirPrestado( EnteroEnorme enteroEnorme, int place) { // si no hay lugar de dónde pedir prestado, indica el problema if ( lugar >= MAXIMO - 1 ) throw new ArgumentException(); // en caso contrario, si el siguiente dígito es cero, pide prestado de la columna a la izquierda else if ( enteroEnorme[ lugar + 1 ] == 0 ) PedirPrestado( enteroEnorme, lugar + 1 ); // suma diez al lugar actual, ya que pedimos prestado y restamos // uno del dígito anterior; éste es el dígito al que pedimos prestado enteroEnorme[ lugar ] += 10; enteroEnorme[ lugar + 1 ]--; } // fin del método PedirPrestado

Figura 22.9 | El servicio Web EnteroEnorme. (Parte 3 de 4).

22.4 Publicación y consumo de los servicios Web

126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156

875

// Método Web que devuelve verdadero si el primer entero es mayor que el segundo [ WebMethod( Description = "Determina si el primer entero es " + "más grande que el segundo entero." ) ] public bool MasGrande( string primero, string segundo ) { char[] ceros = { '0' }; try { // // // if

si la eliminación de todos los ceros del resultado de la resta es una cadena vacía, los números son iguales, por lo que se devuelve falso, en caso contrario se devuelve verdadero ( Restar( primero, segundo ).Trim( ceros ) == "" ) return false; else return true; } // fin de try // si ocurre una excepción ArgumentException, el primer // número era más pequeño, por lo que se devuelve falso catch ( ArgumentException exception ) { return false; } // fin de catch } // fin del método MasGrande // Método Web que devuelve verdadero si el primer entero es más pequeño que el segundo [ WebMethod( Description = "Determina si el primer entero " + "es más pequeño que el segundo." ) ] public bool MasPequenio( string primero, string segundo ) { // si segundo es más grande que primero, entonces primero es más pequeño que segundo return MasGrande( segundo, primero ); } // fin del método MasPequenio

157 158 159 160 // Método Web que devuelve verdadero si dos enteros son iguales 161 [ WebMethod( Description = "Determina si el primer entero " + 162 "es igual al segundo." ) ] 163 public bool IgualA( string primero, string segundo ) 164 { 165 // si el primero es más grande que el segundo, o el primero es 166 // más pequeño que el segundo, no son iguales 167 if ( MasGrande( primero, segundo ) || MasPequenio( primero, segundo ) ) 168 return false; 169 else 170 return true; 171 } // fin del método IgualA 172 } // fin de la clase EnteroEnorme

Figura 22.9 | El servicio Web EnteroEnorme. (Parte 4 de 4).

de código subyacente. Como dijimos antes, este servicio Web está diseñado para realizar cálculos con enteros que tengan un máximo de 100 dígitos. Las variables long no pueden manejar enteros de este tamaño (es decir, se produce un desbordamiento). El servicio Web proporciona métodos que reciben dos “enteros enormes” (representados como objetos string) y determinan su suma, su diferencia, cuál es más grande o más pequeño, y si los dos números son iguales. Puede considerar estos métodos como servicios disponibles para los programadores de otras aplicaciones a través de la Web (de aquí que se utilice el término servicios Web). Cualquier programador puede acceder a este servicio Web, utilizar los métodos y por consecuencia evitar escribir 172 líneas de código.

876

Capítulo 22 Servicios Web

Las líneas 8-10 contienen un atributo WebService. Si adjuntamos este atributo a la declaración de la clase de un servicio Web, podemos especificar el espacio de nombres y la descripción del servicio Web. Al igual que un espacio de nombres XML (vea la sección 19.4), las aplicaciones cliente utilizan el espacio de nombres de un servicio Web para diferenciar a ese servicio Web de los otros servicios disponibles en la Web. La línea 8 asigna http://www.deitel.com como el espacio de nombres del servicio Web, usando la propiedad Namespace del atributo WebService. Las líneas 9-10 utilizan la propiedad Description del atributo WebService para describir el propósito del servicio Web; éste aparece en la página ASMX (figura 22.2). Visual Web Developer coloca la línea 11 en todos los servicios Web recién creados. Esta línea indica que el servicio Web se conforma al Perfil básico 1.1 (BP 1.1) desarrollado por la Organización de interoperabilidad de servicios Web (WS-I ), un grupo dedicado a promover la interoperabilidad entre los servicios Web desarrollados en distintas plataformas, con distintos lenguajes de programación. BP 1.1 es un documento que define las mejores prácticas para diversos aspectos de la creación y consumo de servicios Web (www.WS-I.org). Como vimos en la sección 22.2, el entorno .NET oculta muchos de estos detalles a los programadores. Al establecer la propiedad ConformsTo del atributo WebServiceBinding a WsiProfiles.BasicProfile1_1, indicamos a Visual Web Developer que debe realizar su trabajo “detrás de las cámaras”, como la generación de archivos WSDL y ASMX, en conformidad con los lineamientos establecidos en BP 1.1. Para obtener más información acerca de la interoperabilidad de los servicios Web y el Perfil básico 1.1, visite el sitio Web de WS-I en www.ws-i.org. De manera predeterminada, cada nuevo servicio Web que se crea en Visual Web Developer hereda de la clase System.Web.Services.WebService (línea 12). Aunque un servicio Web no necesita derivarse de la clase WebService, esta clase proporciona miembros que son útiles para determinar información acerca del cliente y del servicio Web en sí. Varios métodos en la clase EnteroEnorme están etiquetados con el atributo WebMethod (líneas 62, 88, 127, 152 y 161), el cual expone a un método de tal forma que pueda llamarse por vía remota. Cuando este atributo está ausente, el método no está accesible para los clientes que consumen el servicio Web. Al igual que el atributo WebService, este atributo contiene una propiedad Description, la cual permite que la página ASMX muestre información acerca del método (en la figura 22.2 se muestran estas descripciones).

Error común de programación 22.1 Si no se expone un método como método Web, declarándolo con el atributo WebMethod, los clientes del servicio Web no podrán acceder a ese método.

Tip de portabilidad 22.1 Especifique un espacio de nombres para cada servicio Web, de manera que los clientes puedan identificarlo en forma única. En general, debe utilizar el nombre de dominio de su compañía como el espacio de nombres del servicio Web, ya que se garantiza que los nombres de dominio de las compañías sean únicos.

Tip de portabilidad 22.2 Especifique las descripciones para un servicio Web y sus métodos Web, de tal forma que los clientes del servicio Web puedan ver información acerca del servicio, en la página ASMX del mismo.

Error común de programación 22.2 Ningún método con el atributo WebMethod puede declararse static; para que un cliente pueda acceder a un método Web, debe existir una instancia de ese servicio Web.

Las líneas 24-35 definen un indexador, el cual nos permite acceder a cualquier dígito en un EnteroEnorme. Las líneas 62-84 y 88-107 definen los métodos Web Sumar y Restar, que realizan las operaciones de suma y resta, respectivamente. El método PedirPrestado (líneas 110-124) maneja el caso en el cual el dígito que estamos examinando en el operando izquierdo es menor que el dígito correspondiente en el operando derecho. Por ejemplo, cuando restamos 19 de 32, por lo general examinamos los números en los operandos dígito por dígito, empezando desde la derecha. El número 2 es más pequeño que 9, por lo que sumamos 10 al 2 (cuyo resultado es 12). Después de pedir prestado, podemos restar 9 de 12, lo que produce un 3 para el dígito de más a la derecha en la solución. Después restamos 1 del 3 en 32; el siguiente dígito a la izquierda (es decir, el dígito al que le pedimos prestado). Esto deja un 2 en el lugar de las decenas. El dígito correspondiente en el otro operando es ahora el

22.4 Publicación y consumo de los servicios Web

877

1 del 19. Al restar 1 de 2 se produce 1, con lo cual el dígito correspondiente en el resultado es 1. El resultado final, cuando se juntan todos los dígitos, es 13. El método PedirPrestado es el método que suma 10 a los dígitos apropiados y resta 1 de los dígitos a la izquierda. Éste es un método utilitario que no está diseñado para que se llame en forma remota, por lo que no se califica con el atributo WebMethod. En la figura 22.2 presentamos una captura de pantalla de la página ASMX EnteroEnorme.asmx, para la que el archivo EnteroEnorme.cs (figura 22.9) de código subyacente define los métodos Web. Una aplicación cliente puede invocar sólo a los cinco métodos que se listan en la captura de pantalla de la figura 22.2 (es decir, los métodos calificados con el atributo WebMethod en la figura 22.9).

22.4.2 Creación de un servicio Web en Visual Web Developer Ahora le mostraremos cómo crear el servicio Web EnteroEnorme. En los siguientes pasos creará un proyecto Servicio Web ASP.NET que se ejecuta en el servidor Web IIS local de su computadora. Para crear el servicio Web EnteroEnorme en Visual Web Developer, realice los siguientes pasos:

Paso 1: crear el proyecto Para empezar, debemos crear un proyecto de tipo Servicio Web ASP.NET. Seleccione Archivo > Nuevo sitio Web… para mostrar el cuadro de diálogo Nuevo sitio Web (figura 22.10). Seleccione Servicio Web ASP.NET en el panel Plantillas. Seleccione HTTP en la lista desplegable Ubicación para indicar que los archivos deben colocarse en un servidor Web. De manera predeterminada, Visual Web Developer indica que colocará los archivos en el servidor Web IIS del equipo local, en un directorio virtual llamado WebSite (http://localhost/WebSite). Sustituya el nombre WebSite con EnteroEnorme para este ejemplo. A continuación, seleccione Visual C# de la lista desplegable Lenguaje para indicar que utilizará Visual C# para generar este servicio Web. Visual Web Developer coloca el archivo de solución (.sln) del proyecto del servicio Web en la subcarpeta Projects dentro de la carpeta Mis documentos\Visual Studio 2005 del usuario de Windows actual. Si no tiene acceso a un servidor Web IIS para generar y probar los ejemplos en este capítulo, puede elegir Sistema de archivos de la lista desplegable Ubicación. En este caso, Visual Web Developer colocará los archivos de su servicio Web en su disco duro local. Después podrá probar el servicio Web con el servidor Web integrado de Visual Web Developer.

Paso 2: examinar el proyecto recién creado Cuando se crea el proyecto, se muestra de manera predeterminada el archivo de código subyacente Service.cs, que contiene el código para un servicio Web simple (figura 22.11). Si el archivo de código subyacente no está abierto, puede abrirlo haciendo doble clic sobre el archivo en el directorio App_Code que se lista en el Explorador de soluciones. Visual Web Developer incluye cuatro declaraciones using que son útiles para desarrollar servicios

Figura 22.10 | Creación de un Servicio Web ASP.NET en Visual Web Developer.

878

Capítulo 22 Servicios Web

Figura 22.11 | Vista del código de un servicio Web.

Web (líneas 1-4). De manera predeterminada, un nuevo archivo de código subyacente define a una clase llamada Service, la cual está marcada con los atributos WebService y WebServiceBinding (líneas 6-7). La clase contiene un método Web de ejemplo llamado HelloWorld (líneas 14-17). Este método es un receptáculo que usted sustituirá con su(s) propio(s) método(s).

Paso 3: modificar y cambiar el nombre del archivo de código subyacente Para crear el servicio Web EnteroEnorme que desarrollamos en esta sección, modifique el archivo Service.cs, sustituyendo todo el código de ejemplo que proporciona Visual Web Developer con todo el código del archivo de código subyacente EnteroEnorme (figura 22.9). Después, cambie el nombre al archivo por el de EnteroEnorme.cs (haga clic con el botón derecho del ratón en el Explorador de soluciones, y seleccione la opción Cambiar nombre).

Paso 4: examinar el archivo ASMX El Explorador de soluciones lista un archivo (Service.asmx) además del archivo de código subyacente. En la figura 22.2 vimos cuando se accede a la página ASMX de un servicio Web a través de un explorador Web, esta página muestra información acerca de los métodos del servicio Web y proporciona acceso a la información WSDL del servicio Web. No obstante, si abre el archivo ASMX en el disco, verá que en realidad sólo contiene lo siguiente:

para indicar el lenguaje de programación en el que está escrito el archivo de código subyacente del servicio Web, la ubicación del archivo de código subyacente y la clase que define al servicio Web. Cuando se solicita la página ASMX a través de IIS, ASP.NET utiliza esta información para generar el contenido que se muestra en el explorador Web (es decir, la lista de métodos Web y sus descripciones).

Paso 5: modificar el archivo ASMX Cada vez que cambiamos el nombre del archivo de código subyacente o el nombre de la clase que define al servicio Web, debemos modificar el archivo ASMX de manera acorde. Por lo tanto, después de definir la clase EnteroEnorme en el archivo EnteroEnorme.cs de código subyacente, modifique el archivo ASMX para que contenga las siguientes líneas:

22.4 Publicación y consumo de los servicios Web

879

Tip de prevención de errores 22.2 Actualice el archivo ASMX del servicio Web de manera apropiada, cada vez que cambie el nombre del archivo de código subyacente, o el nombre de la clase de un servicio Web. Visual Web Developer crea el archivo ASMX, pero no lo actualiza de manera automática cuando realizamos cambios en otros archivos del proyecto.

Paso 6: cambiar el nombre del archivo ASMX El paso final para crear el servicio Web EnteroEnorme es cambiar el nombre del archivo ASMX a EnteroEnorme. asmx.

22.4.3 Despliegue del servicio Web EnteroEnorme El servicio Web EnteroEnorme ya está desplegado, ya que lo creamos en el servidor IIS local de nuestra computadora. Puede elegir la opción Generar sitio Web del menú Generar para asegurase que el servicio Web se compile sin errores. También puede probar el servicio Web directamente desde Visual Web Developer; seleccione Iniciar sin depuración del menú Depurar. Esta opción abre una ventana del explorador, la cual contiene la página ASMX que se muestra en la figura 22.2. Al hacer clic en el vínculo para un método específico del servicio Web EnteroEnorme, se muestra una página Web como la de la figura 22.4, que le permite probar el método. También puede acceder a la página ASMX del servicio Web desde su computadora, si escribe el siguiente URL en un explorador Web: http://localhost/EnteroEnorme/EnteroEnorme.asmx

Acceso a la página ASMX del servicio Web EnteroEnorme desde otra computadora En algún momento dado, requerirá que otros clientes puedan acceder a su servicio Web para utilizarlo. Si despliega el servicio Web en un servidor Web IIS, un cliente puede conectarse a ese servidor para acceder al servicio Web, con un URL de la forma http://host/EnteroEnorme/EnteroEnorme.asmx

en donde host es el nombre de host, o dirección IP, del servidor Web. Para acceder al servicio Web desde otra computadora en la red de área local de su compañía o escuela, puede sustituir host con el nombre real de la computadora en la que se ejecuta IIS. Si tiene el sistema operativo Windows XP Service Pack 2 en la computadora que ejecuta IIS, tal vez esa computadora no acepte solicitudes de otras computadoras de manera predeterminada. Si desea permitir que otras computadoras se conecten al servidor Web de su computadora, realice los siguientes pasos: 1. Seleccione Inicio > Panel de control para abrir la ventana Panel de control de su sistema; después haga doble clic en Firewall de Windows para que aparezca el cuadro de diálogo de opciones de Firewall de Windows. 2. En ese cuadro de diálogo, haga clic en la ficha Opciones avanzadas, seleccione Conexión de área local (o el nombre de la conexión de su red, si es distinto) en la lista Configuración de conexión de red y haga clic en el botón Configuración para mostrar el cuadro de diálogo Opciones avanzadas. 3. En este cuadro de diálogo, asegúrese que esté seleccionada la casilla de verificación Servidor Web (HTTP), para permitir que los clientes en otras computadoras envíen solicitudes al servidor Web de su computadora. 4. Haga clic en el botón Aceptar del cuadro de diálogo Opciones avanzadas; después en el botón Aceptar del cuadro de diálogo de opciones de Firewall de Windows.

Acceder a la página ASMX del servicio Web EnteroEnorme cuando éste se ejecuta en el servidor Web integrado de Visual Web Developer En el paso 1 de la sección 22.4.2 vimos que, si no tiene acceso a un servidor IIS para desplegar y probar su servicio Web, puede crearlo en el disco duro de su computadora y utilizar el servidor Web integrado de Visual Web Developer para probarlo. En este caso, si selecciona Iniciar sin depuración en el menú Depurar, Visual Web Developer ejecuta su servidor Web integrado y después abre un explorador Web que contiene la página ASMX del servicio Web, para que usted pueda probarlo.

880

Capítulo 22 Servicios Web

Por lo general, los servidores Web reciben las solicitudes en el puerto 80. Para asegurar que el servidor Web integrado de Visual Web Developer no entre en conflicto con otro servidor Web que se ejecute en su equipo local, el servidor Web de Visual Web Developer recibe las solicitudes en un número de puerto elegido al azar. Cuando un servidor Web recibe solicitudes en un número de puerto que no sea el puerto 80, debe especificarse ese número de puerto como parte de la solicitud. En este caso, el URL para acceder a la página ASMX del servicio Web EnteroEnorme sería de siguiente forma http://host:númeroPuerto/EnteroEnorme/EnteroEnorme.asmx

en donde host es el nombre de host, o dirección IP, de la computadora en la que se ejecuta el servidor Web integrado de Visual Web Developer, y númeroPuerto es el puerto específico en el que el servidor Web recibe las solicitudes. Cuando pruebe el servicio Web desde Visual Web Developer podrá ver este número de puerto en el campo Dirección de su explorador Web. Por desgracia, los servicios Web que se ejecutan mediante el uso del servidor Web integrado de Visual Web Developer no pueden utilizarse a través de una red.

22.4.4 Creación de un cliente para consumir el servicio Web EnteroEnorme Ahora que hemos definido y desplegado nuestro servicio Web, demostraremos cómo consumirlo desde una aplicación cliente. En esta sección creará una aplicación Windows que actuará como cliente, mediante el uso de Visual C# 2005. Después de crear la aplicación cliente, agregará una clase proxy al proyecto para permitir que el cliente acceda al servicio Web. Recuerde que la clase proxy (o proxy) se genera a partir del archivo WSDL del servicio Web, y permite al cliente llamar a los métodos Web a través de Internet. La clase proxy se encarga de todos los detalles relacionados con el proceso de comunicarse con el servicio Web. Esta clase proxy está oculta a los programadores de manera predeterminada; puede verla en el Explorador de soluciones, haciendo clic en el botón Mostrar todos los archivos. El propósito de la clase proxy es hacer que los clientes crean que están llamando a los métodos Web directamente. Este ejemplo demuestra cómo crear un cliente de un servicio Web y generar una clase proxy, que permita al cliente acceder al servicio Web EnteroEnorme. Para empezar, creará un proyecto y le agregará una referencia Web. Cuando agregue la referencia Web, Visual C# 2005 generará la clase proxy apropiada. Después creará una instancia de la clase proxy y la utilizará para llamar a los métodos del servicio Web. Primero, cree una aplicación Windows en Visual C# 2005, y después realice los siguientes pasos:

Paso 1: abrir el cuadro de diálogo Agregar referencia Web Haga clic con el botón derecho del ratón en el nombre del proyecto dentro del Explorador de soluciones, y seleccione la opción Agregar referencia Web… (figura 22.12).

Figura 22.12 | Agregar la referencia de un servicio Web a un proyecto.

22.4 Publicación y consumo de los servicios Web

881

Paso 2: localizar los servicios Web en su computadora En el cuadro de diálogo Agregar referencia Web que aparezca (figura 22.13), haga clic en la opción Servicios Web del equipo local para localizar las referencias Web almacenadas en el servidor Web IIS en su computadora local (http://localhost). Los archivos de este servidor se encuentran en C:\Inetpub\wwwroot de manera predeterminada. Observe que el cuadro de diálogo Agregar referencia Web le permite buscar servicios Web en varias ubicaciones distintas. Muchas compañías que proporcionan servicios Web sólo distribuyen los URLs exactos con los que se puede acceder a sus servicios Web. Por esta razón, el cuadro de diálogo Agregar referencia Web también le permite escribir el URL específico de un servicio Web en el campo Dirección URL.

Paso 3: elegir el servicio Web a referenciar Seleccione el servicio Web EnteroEnorme de la lista de servicios Web disponibles (figura 22.14).

Figura 22.13 | El cuadro de diálogo Agregar referencia Web.

Figura 22.14 | Los servicios Web ubicados en localhost.

882

Capítulo 22 Servicios Web

Paso 4: agregar la referencia Web Agregue la referencia Web, haciendo clic en el botón Agregar referencia (figura 22.15).

Paso 5: ver la referencia Web en el Explorador de soluciones Ahora el Explorador de soluciones (figura 22.16) debe contener una carpeta Web References con un nodo cuyo nombre debe ser igual al nombre del dominio en el que se encuentra el servicio Web. En este caso, el nombre es localhost, ya que estamos usando el servidor Web local. Cuando hagamos referencia a la clase EnteroEnorme en la aplicación cliente, lo haremos a través del espacio de nombres localhost.

Observaciones acerca de la creación de un cliente para consumir un servicio Web Los pasos que acabamos de presentar también se aplican al proceso de agregar referencias Web a las aplicaciones Web creadas en Visual Web Developer. En la sección 22.6 presentaremos una aplicación Web que consume un servicio Web. Al crear un cliente para consumir un servicio Web, agregue primero la referencia Web, de manera que Visual C# 2005 (o Visual Web Developer) pueda reconocer a un objeto de la clase proxy del servicio Web. Una vez que agregue la referencia Web al cliente, éste podrá acceder al servicio Web a través de un objeto de la clase proxy. Esta clase (llamada EnteroEnorme) está ubicada en el espacio de nombres localhost, por lo que debe usar localhost.EnteroEnorme para referenciar esta clase. Aunque debe crear un objeto de la clase proxy para acceder al

Figura 22.15 | Selección y descripción de una referencia Web.

Figura 22.16 | Explorador de soluciones, después de agregar una referencia Web a un proyecto.

22.4 Publicación y consumo de los servicios Web

883

servicio Web, no necesita acceder al código de la clase proxy. Como veremos en la sección 22.4.5, puede invocar los métodos del objeto proxy como si fuera un objeto de la clase del servicio Web. Los pasos que describimos en esta sección funcionan bien si conoce la referencia apropiada al servicio Web. No obstante, si está tratando de localizar un nuevo servicio Web, dos tecnologías comunes facilitan este proceso: Descripción universal, descubrimiento e integración (UDDI ) y Descubrimiento de servicios Web (DISCO). En la sección 22.2 hablamos sobre DISCO. UDDI es un proyecto continuo para desarrollar un conjunto de especificaciones que definan la manera en que deben publicarse los servicios Web, para que los programadores en busca de servicios Web puedan encontrarlos. Microsoft y sus socios están trabajando en este proyecto para ayudar a los programadores a localizar servicios Web que se conformen a ciertas especificaciones, con lo cual los desarrolladores podrán buscar servicios Web a través de motores de búsqueda similares a Yahoo!® y Google™. En los sitios www.uddi.org y uddi.microsoft.com podrá aprender más acerca de UDDI y ver una demostración. Estos sitios contienen herramientas de búsqueda que hacen que sea conveniente buscar servicios Web.

22.4.5 Consumo del servicio Web EnteroEnorme El formulario Windows Forms en la figura 22.17 utiliza el servicio Web EnteroEnorme para realizar cálculos con enteros positivos de hasta 100 dígitos de longitud. La línea 22 declara la variable enteroRemoto de tipo localhost.EnteroEnorme. Esta variable se utiliza en cada uno de los manejadores de eventos de la aplicación, para llamar a los métodos del servicio Web EnteroEnorme. El objeto proxy se crea y se asigna a esta variable en la línea 31 del manejador de eventos Load del formulario. Las líneas 52-53, 66-67, 95-96, 116-117 y 135-136 en los diversos manejadores de eventos de los botones invocan a los métodos del servicio Web. Observe que cada llamada se realiza en el objeto proxy local, que a su vez se comunica con el servicio Web a beneficio del cliente. Si descargó el ejemplo de www.deitel.com/books/csharpforprogrammers2, tal vez necesite regenerar el proxy; para ello, elimine la referencia Web y después agréguela de nuevo. Posteriormente haga clic con el botón derecho del ratón en localhost dentro de la carpeta Web References en el Explorador de soluciones, y seleccione la opción Eliminar. Después siga las instrucciones en la sección anterior para agregar la referencia Web al proyecto. El usuario introduce dos enteros, cada uno de hasta 100 dígitos de largo. Al hacer clic en un botón, la aplicación invoca a un método Web para realizar la tarea apropiada y devolver el resultado. Observe que la aplicación cliente UsoServicioEnteroEnorme no puede realizar operaciones usando números de 100 dígitos de manera directa. En vez de eso, la aplicación crea representaciones string de estos números y los pasa como argumentos para los métodos Web que manejan dichas tareas para el cliente. Después utiliza el valor de retorno de cada operación para mostrar un mensaje apropiado. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

// Fig. 22.17: UsoServicioWebEnteroEnorme.cs // Uso del servicio Web EnteroEnorme. using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Web.Services.Protocols; namespace UsoServicioWebEnteroEnorme { public partial class UsoServicioWebEnteroEnormeForm : Form { public UsoServicioWebEnteroEnormeForm() { InitializeComponent(); } // fin del constructor // declara una referencia al servicio Web

Figura 22.17 | Uso del servicio Web EnteroEnorme. (Parte 1 de 5).

884

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

Capítulo 22 Servicios Web

private localhost.EnteroEnorme enteroRemoto; private char[] ceros = { '0' }; // carácter a eliminar de las cadenas // instancia un objeto para interactuar con el servicio Web private void UsoServicioEnteroEnormeForm_Load( object sender, EventArgs e ) { // crea instancia de enteroRemoto enteroRemoto = new localhost.EnteroEnorme(); } // fin del método UsoServicioEnteroEnormeForm_Load // suma dos enteros introducidos por el usuario private void sumarButton_Click( object sender, EventArgs e ) { // se asegura que los números no excedan de 100 dígitos y que ambos // no sean de 100 dígitos de largo, lo cual produciría un desbordamiento if ( primeroTextBox.Text.Length > 100 || segundoTextBox.Text.Length > 100 || ( primeroTextBox.Text.Length == 100 && segundoTextBox.Text.Length == 100 ) ) { MessageBox.Show( "Los Enteros Enormes no deben tener " + "más de 100 dígitos\r\nAmbos enteros no pueden tener " + "longitud de 100: esto produce un desbordamiento", "Error", MessageBoxButtons.OK, MessageBoxIcon.Information ); return; } // fin de if // realiza la suma resultadoLabel.Text = enteroRemoto.Sumar( primeroTextBox.Text, segundoTextBox.Text ).TrimStart( ceros ); } // fin del método sumarButton_Click // resta dos números introducidos por el usuario private void restarButton_Click( object sender, EventArgs e ) { // se asegura que los Enteros Enormes no excedan de 100 dígitos if ( ComprobarTamanio( primeroTextBox, segundoTextBox ) ) return; // realiza la resta try { string resultado = enteroRemoto.Restar( primeroTextBox.Text, segundoTextBox.Text ).TrimStart( ceros ); if ( resultado == "" ) resultadoLabel.Text = "0"; else resultadoLabel.Text = resultado; } // fin de try // si el método Web lanza una excepción, // entonces el primer argumento era menor que el segundo catch ( SoapException exception ) { MessageBox.Show(

Figura 22.17 | Uso del servicio Web EnteroEnorme. (Parte 2 de 5).

22.4 Publicación y consumo de los servicios Web

81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139

885

"El primer argumento era menor que el segundo" ); } // fin de catch } // fin del método restarButton_Click // determina si el primer número introducido // por el usuario es mayor que el segundo private void masGrandeButton_Click( object sender, EventArgs e ) { // se asegura que los Enteros Enormes no excedan de 100 dígitos if ( ComprobarTamanio( primeroTextBox, segundoTextBox ) ) return; // llama al método del servicio Web para determinar si // el primer entero es más grande que el segundo if ( enteroRemoto.MasGrande( primeroTextBox.Text, segundoTextBox.Text ) ) resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + " es mayor que " + segundoTextBox.Text.TrimStart( ceros ); else resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + " no es mayor que " + segundoTextBox.Text.TrimStart( ceros ); } // fin del método masGrandeButton_Click // determina si el primer número introducido // por el usuario es más pequeño que el segundo private void masPequenioButton_Click( object sender, EventArgs e ) { // se asegura que los Enteros Enormes no excedan de 100 dígitos if ( ComprobarTamanio( primeroTextBox, segundoTextBox ) ) return; // llama al método del servicio Web para determinar si // el primer entero es más pequeño que el segundo if ( enteroRemoto.MasPequenio( primeroTextBox.Text, segundoTextBox.Text ) ) resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + " es menor que " + segundoTextBox.Text.TrimStart( ceros ); else resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + " no es menor que " + segundoTextBox.Text.TrimStart( ceros ); } // fin del método masPequenioButton_Click // determina si dos números introducidos por el usuario son iguales private void igualButton_Click( object sender, EventArgs e ) { // se asegura que los Enteros Enormes no excedan de 100 dígitos if ( ComprobarTamanio( primeroTextBox, segundoTextBox ) ) return; // llama al método del servicio Web para determinar si los enteros son iguales if ( enteroRemoto.IgualA( primeroTextBox.Text, segundoTextBox.Text ) ) resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + " es igual a " + segundoTextBox.Text.TrimStart( ceros ); else

Figura 22.17 | Uso del servicio Web EnteroEnorme. (Parte 3 de 5).

886

Capítulo 22 Servicios Web

140 resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + 141 " no es igual a " + 142 segundoTextBox.Text.TrimStart( ceros ); 143 } // fin del método igualButton_Click 144 145 // determina si los números introducidos por el usuario son demasiado grandes 146 private bool ComprobarTamanio( TextBox primero, TextBox segundo ) 147 { 148 // muestra un mensaje de error si cualquier número tiene demasiados dígitos 149 if ( ( primero.Text.Length > 100 ) || 150 ( segundo.Text.Length > 100 ) ) 151 { 152 MessageBox.Show( "Los Enteros Enormes deben ser menores de 100 dígitos" , 153 "Error", MessageBoxButtons.OK, MessageBoxIcon.Information ); 154 return true; 155 } // fin de if 156 157 return false; 158 } // fin del método ComprobarTamanio 159 } // fin de la clase UsingHugeIntegerServiceForm 160 } // fin del espacio de nombres UsoServicioWebEnteroEnorme

Figura 22.17 | Uso del servicio Web EnteroEnorme. (Parte 4 de 5).

22.5 Rastreo de sesiones en los servicios Web

887

Figura 22.17 | Uso del servicio Web EnteroEnorme. (Parte 5 de 5). Observe que la aplicación elimina los ceros a la izquierda en los números antes de mostrarlos, mediante una llamada al método string TrimStart. Al igual que el método string Trim (que vimos en el capítulo 16), TrimStart elimina todas las ocurrencias de los caracteres especificados por un arreglo char (línea 24) desde el principio de un objeto string.

22.5 Rastreo de sesiones en los servicios Web En el capítulo 21, describimos las ventajas de mantener la información sobre los usuarios para personalizar sus experiencias. En especial, hablamos sobre el rastreo de sesiones mediante el uso de cookies y objetos HttpSessionState. Ahora incorporaremos el rastreo de sesiones a un servicio Web. Suponga que una aplicación cliente necesita llamar a varios métodos del mismo servicio Web, posiblemente varias veces a cada uno. En tal caso, sería benéfico para el servicio Web mantener la información de estado para el cliente. El rastreo de sesiones elimina la necesidad de pasar la información del cliente varias veces entre éste y el servicio Web. Por ejemplo, un servicio Web que proporciona acceso a las reseñas de restaurantes locales se beneficiaría de almacenar la dirección del usuario cliente. Una vez que se almacena la dirección del cliente en una variable de sesión, los métodos Web pueden devolver resultados personalizados y localizados, sin requerir que se pase la dirección en cada llamada a los métodos. Esto no sólo aumenta el rendimiento, sino que también requiere un menor esfuerzo por parte del programador; se pasa menos información en cada llamada a los métodos.

22.5.1 Creación de un servicio Web de Blackjack Almacenar la información de las sesiones puede hacer que el servicio Web sea más intuitivo para los programadores de la aplicación cliente. Nuestro siguiente ejemplo es un servicio Web que asiste a los programadores en el desarrollo de un juego de cartas de blackjack (figura 22.18). El servicio Web proporciona métodos Web para repartir una carta y evaluar una mano de cartas. Después de presentar este servicio Web, lo utilizamos para que sirva como el repartidor para un juego de blackjack (figura 22.19). El servicio Web de blackjack utiliza una variable de sesión para mantener un mazo único de cartas para cada aplicación cliente. Varios clientes pueden utilizar el servicio al mismo tiempo, pero las llamadas a los métodos Web que realice un cliente específico sólo utilizarán el mazo almacenado en la sesión de ese cliente. Nuestro ejemplo utiliza un subconjunto simple de las reglas de blackjack de los casinos: Se reparten dos cartas al repartidor y dos cartas al jugador. Las cartas del jugador se reparten con la cara hacia arriba. Sólo la primera de las cartas del repartidor se reparte con la cara hacia arriba. Cada carta tiene un valor. Una carta numerada del 2 al 10 vale lo que indique en su cara. Los jotos, reinas y reyes valen 10 cada uno. Los ases pueden valer 1 u 11, lo que sea más conveniente para el jugador (como pronto veremos). Si la suma de las dos cartas iniciales del jugador es 21 (por ejemplo, que se hayan repartido al jugador una carta con valor de 10 y un as, que cuenta como 11 en esta situación), el jugador tiene “blackjack” y gana el juego de inmediato. En caso contrario, el jugador puede empezar a tomar cartas adicionales, una a la vez. Estas cartas se reparten con la cara hacia arriba, y el jugador decide cuándo dejar de tomar cartas. Si el jugador “se pasa” (es decir, si la suma de las cartas del jugador excede a 21), el juego termina y el jugador pierde. Cuando el jugador está satisfecho con su conjunto actual de cartas, “se planta” (es decir, deja de tomar cartas) y se revela la carta oculta del repartidor. Si el total del repartidor es 16 o menos, debe tomar otra carta; en caso contrario, el repartidor debe plantarse. El repartidor debe seguir tomando cartas hasta que la suma de todas sus cartas sea mayor o igual a 17. Si el repartidor se pasa de 21, el jugador gana. En caso contrario, la mano con el total de puntos que sea mayor gana. Si el repartidor y el jugador tienen el mismo total de puntos, el juego es un “empate” y nadie gana.

888

Capítulo 22 Servicios Web

El servicio Web (figura 22.18) proporciona métodos para repartir una carta y determinar el valor en puntos de una mano. Representamos cada carta como un objeto string que consiste en un dígito (por ejemplo, del 1 al 13), representando la cara de la carta (por ejemplo del as hasta el rey), seguido por un espacio y un dígito (por ejemplo 0-3) el cual representa el palo de la carta (por ejemplo, tréboles, diamantes, corazones o espadas). Por ejemplo, el joto de corazones se representa como "11 2", y el dos de tréboles se representa como “2 0”. Después de desplegar el servicio Web, creamos una aplicación para Windows que utiliza los métodos del servicio Web ServicioBlackjack para implementar un juego de blackjack. Para crear y desplegar este servicio Web, siga los pasos que presentamos en las secciones 22.4.2-22.4.3 para el servicio EnteroEnorme. Las líneas 15-16 definen el método RepartirCarta como un método Web. Al establecer la propiedad EnableSession a True, se indica que debe mantenerse la información de sesión y que debe estar accesible para

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

// Fig. 22.18: ServicioBlackjack.cs // Servicio Web de Blackjack, para repartir y contar cartas. using System; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; using System.Collections; [ WebService( Namespace = "http://www.deitel.com/", Description = "Un servicio Web que reparte y cuenta cartas para el juego de Blackjack" ) ] [ WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 ) ] public class ServicioBlackjack : System.Web.Services.WebService { // reparte una carta que no se ha repartido todavía [ WebMethod( EnableSession = true, Description = "Reparte una nueva carta del mazo." ) ] public string RepartirCarta() { string carta = "2 2"; // obtiene el mazo del cliente ArrayList mazo = ( ArrayList )( Session[ "mazo" ] ); carta = Convert.ToString( mazo[ 0 ] ); mazo.RemoveAt( 0 ); return carta; } // fin del método RepartirCarta // crea y baraja un mazo de cartas [ WebMethod( EnableSession = true, Description = "Crea y baraja un mazo de cartas." ) ] public void Barajar() { object temporal; // almacena una carta temporalmente, durante el intercambio Random objetoAleatorio = new Random(); // genera números aleatorios int nuevoIndice; // índice de la carta seleccionada al azar ArrayList mazo = new ArrayList(); // almacena el mazo de cartas (objetos string) // genera todas las cartas posibles for ( int i = 1; i totalJugador ) JuegoTerminado( EstadoJuego.PIERDE ); else if ( totalJugador > totalRepartidor ) JuegoTerminado( EstadoJuego.GANA ); else JuegoTerminado( EstadoJuego.EMPATE ); } // fin del método RepartidorJuega // muestra la carta representada por valorCarta en el control PictureBox especificado public void MostrarCarta( int carta, string valorCarta )

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 2 de 8).

22.5 Rastreo de sesiones en los servicios Web

116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173

{ // obtiene el control PictureBox apropiado del objeto ArrayList PictureBox mostrarBox = ( PictureBox )( cuadrosCartas[ carta ] ); // si la cadena que representa a carta está vacía, // establece mostrarBox para que muestre la parte posterior de carta if ( valorCarta == "" ) { mostrarBox.Image = Image.FromFile( "imagenes_blackjack/cartaposterior.png" ); return; } // fin de if // obtiene de valorCarta el valor de la cara de la carta string cara = valorCarta.Substring( 0, valorCarta.IndexOf( " " ) ); // obtiene de valorCarta el palo de la carta string palo = valorCarta.Substring( valorCarta.IndexOf( " " ) + 1 ); char letraPalo; // letra del palo que se usa para formar el nombre del archivo de imagen // determina la letra del palo de la carta switch ( Convert.ToInt32( palo ) ) { case 0: // tréboles letraPalo = 't'; break; case 1: // diamantes letraPalo = 'd'; break; case 2: // corazones letraPalo = 'c'; break; default: // espadas letraPalo = 'e'; break; } // fin de switch // establece mostrarBox para mostrar la imagen apropiada mostrarBox.Image = Image.FromFile( "imagenes_blackjack/" + cara + letraPalo + ".png" ); } // fin del método MostrarCarta // muestra todas las cartas del jugador y el // mensaje apropiado del estado del juego public void JuegoTerminado( EstadoJuego ganador ) { char[] tab = { '\t' }; string[] cartas = cartasRepartidor.Split( tab ); // muestra todas las cartas del repartidor for ( int i = 0; i < cartas.Length; i++ ) MostrarCarta( i, cartas[ i ] ); // muestra la imagen apropiada del estado if ( ganador == EstadoJuego.EMPATE ) // empate estadoPictureBox.Image =

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 3 de 8).

893

894

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228

Capítulo 22 Servicios Web

Image.FromFile( "imagenes_blackjack/empate.png" ); else if ( ganador == EstadoJuego.PIERDE ) // el jugador pierde estadoPictureBox.Image = Image.FromFile( "imagenes_blackjack/pierde.png" ); else if ( ganador == EstadoJuego.BLACKJACK ) // el jugador tiene blackjack estadoPictureBox.Image = Image.FromFile( "imagenes_blackjack/blackjack.png" ); else // el jugador gana estadoPictureBox.Image = Image.FromFile( "imagenes_blackjack/gana.png" ); // muestra los totales finales para repartidor y jugador repartidorTotalLabel.Text = "Repartidor: " + repartidor.ObtenerValorMano( cartasRepartidor ); jugadorTotalLabel.Text = "Jugador: " + repartidor.ObtenerValorMano( cartasJugador ); // restablece los controles para un nuevo juego plantarButton.Enabled = false; pedirButton.Enabled = false; repartirButton.Enabled = true; } // fin del método JuegoTerminado // reparte dos cartas al repartidor y dos al jugador private void repartirButton_Click( object sender, EventArgs e ) { string carta; // almacena una carta temporalmente, hasta que se agrega a una mano // borra las imágenes de las cartas foreach ( PictureBox imagenCarta in cuadrosCartas ) imagenCarta.Image = null; estadoPictureBox.Image = null; // borra imagen de estado repartidorTotalLabel.Text = ""; // borra el total final para el repartidor jugadorTotalLabel.Text = ""; // borra el total final para el jugador // crea un nuevo mazo barajado en el equipo remoto repartidor.Barajar(); // reparte dos cartas al jugador cartasJugador = repartidor.RepartirCarta(); // reparte una carta a la mano del jugador // actualiza GUI para mostrar la nueva carta MostrarCarta( 11, cartasJugador ); carta = repartidor.RepartirCarta(); // reparte una segunda carta MostrarCarta( 12, carta ); // actualiza GUI para mostrar la nueva carta cartasJugador += '\t' + carta; // agrega la segunda carta a la mano del jugador // reparte dos cartas al repartidor, sólo muestra la cara de la primera carta cartasRepartidor = repartidor.RepartirCarta(); // reparte una carta a la mano del repartidor MostrarCarta( 0, cartasRepartidor ); // actualiza GUI para mostrar la nueva carta carta = repartidor.RepartirCarta(); // reparte una segunda carta MostrarCarta( 1, "" ); // actualiza GUI para mostrar la carta cara abajo cartasRepartidor += '\t' + carta; // agrega la segunda carta a la mano del repartidor

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 4 de 8).

22.5 Rastreo de sesiones en los servicios Web

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286

895

plantarButton.Enabled = true; // permite al jugador plantarse pedirButton.Enabled = true; // permite al jugador pedir otra carta repartirButton.Enabled = false; // deshabilita el botón Repartir // determina el valor de las dos manos int totalRepartidor = repartidor.ObtenerValorMano( cartasRepartidor ); int totalJugador = repartidor.ObtenerValorMano( cartasJugador ); // si las manos son iguales a 21, es un empate if ( totalRepartidor == totalJugador && totalRepartidor == 21 ) JuegoTerminado( EstadoJuego.EMPATE ); else if ( totalRepartidor == 21 ) // si el repartidor tiene 21, gana JuegoTerminado( EstadoJuego.PIERDE ); else if ( totalJugador == 21 ) // el jugador tiene blackjack JuegoTerminado( EstadoJuego.BLACKJACK ); // la siguiente carta del repartidor tiene el índice 2 en cuadrosCartas cartaActualRepartidor = 2; // la siguiente carta del jugador tiene el índice 13 en cuadrosCartas cartaActualJugador = 13; } // fin del método repartirButton // reparte otra carta al jugador private void pedirButton_Click( object sender, EventArgs e ) { // da otra carta al jugador string carta = repartidor.RepartirCarta(); // reparte una nueva carta cartasJugador += '\t' + carta; // agrega la nueva carta a la mano del jugador // actualiza GUI para mostrar la nueva carta MostrarCarta( cartaActualJugador, carta ); cartaActualJugador++; // determina el valor de la mano del jugador int total = repartidor.ObtenerValorMano( cartasJugador ); // si el jugador excede a 21, la casa gana if ( total > 21 ) JuegoTerminado( EstadoJuego.PIERDE ); // si el jugador tiene 21, // no pueden tomar más cartas, y el repartidor juega if ( total == 21 ) { pedirButton.Enabled = false; RepartidorJuega(); } // fin de if } // fin del método pedirButton_Click // juega la mano del repartidor después de que el jugador elije plantarse private void plantarButton_Click( object sender, EventArgs e ) { plantarButton.Enabled = false; // deshabilita el botón Plantar pedirButton.Enabled = false; // deshabilita el botón Pedir repartirButton.Enabled = true; // rehabilita el botón Repartir RepartidorJuega(); // el jugador elije plantarse, por lo que se juega la mano del repartidor

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 5 de 8).

896

Capítulo 22 Servicios Web

287 } // fin del método plantarButton_Click 288 } // fin de la clase BlackjackForm 289 } // fin del espacio de nombres Blackjack a) Se reparten las cartas iniciales al jugador y al repartidor cuando el usuario oprime el botón Repartir.

b) Las cartas, después de que el jugador oprime el botón Pedir dos veces, y después el botón Plantar. En este caso, el jugador ganó el juego debido a que el repartidor se pasó (se excedió de 21).

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 6 de 8).

22.5 Rastreo de sesiones en los servicios Web

c) Las cartas, después de que el jugador oprime el botón Pedir una vez, y luego el botón Plantar. En este caso, el jugador se pasó (se excedió de 21) y el repartidor ganó el juego.

d) Las cartas, después de que el jugador oprime el botón Repartir. En este caso, el jugador ganó con Blackjack, ya que las primeras dos cartas fueron un as y una carta con un valor de 10 (una qüina en este caso).

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 7 de 8).

897

898

Capítulo 22 Servicios Web

e) Las cartas, después de que el jugador oprime el botón Plantar. En este caso, el jugador y el repartidor empatan; tienen el mismo total de puntos.

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 8 de 8). Cada jugador tiene 11 controles PictureBox; el número máximo de cartas que pueden repartirse sin excederse automáticamente de 21 (es decir, cuatro ases, cuatro números dos y tres números tres). Estos controles PictureBox se colocan en un objeto ArrayList (líneas 54-75), por lo que podemos indexar el objeto ArrayList durante el juego, para determinar el control PictureBox que mostrará la imagen de una carta específica. En la sección 22.5.1, mencionamos que el cliente debe proporcionar una forma de aceptar las cookies creadas por el servicio Web, para identificar a los usuarios en forma única. La línea 50 en el manejador de eventos Load de BlackjackForm crea un nuevo objeto CookieContainer para la propiedad CookieContainer de repartidor. Un objeto CookieContainer (espacio de nombres System.Net) almacena la información de una cookie (creada por el servicio Web) en un objeto Cookie, en el objeto CookieContainer. El objeto Cookie contiene un identificador único, que el servicio Web puede usar para reconocer al cliente cuando éste realice solicitudes a futuro. Como parte de cada solicitud, la cookie se envía de vuelta al servidor en forma automática. Si el cliente no creara un objeto CookieContainer, el servicio Web crearía un nuevo objeto Session para cada solicitud, y la información de estado del usuario no persistiría de solicitud en solicitud. El método JuegoTerminado (líneas 162-196) muestra todas las cartas del repartidor, muestra el mensaje apropiado en el control PictureBox de estado y muestra el total final de puntos del repartidor y del jugador. El método JuegoTerminado recibe como argumento a un miembro de la enumeración EstadoJuego (que se define en las líneas 30-36). La enumeración representa si el jugador empató, perdió o ganó el juego; sus cuatro miembros son EMPATE, PIERDE, GANA y BLACKJACK. Cuando el jugador hace clic en el botón Repartir (cuyo manejador de eventos aparece en las líneas 199-251), se borran todos los controles PictureBox y Label que muestran el total final de puntos. A continuación se baraja el mazo y tanto el jugador como el repartidor reciben dos cartas cada uno. Si ambos obtienen puntuaciones de 21, el programa llama al método JuegoTerminado y le pasa EstadoJuego.PUSH. Si sólo el jugador tiene 21 después de repartir las dos cartas, el programa pasa EstadoJuego.BLACKJACK al método JuegoTerminado. Si sólo el repartidor tiene 21, el programa pasa EstadoJuego.PIERDE al método JuegoTerminado. Si repartirButton_Click no llama a JuegoTerminado, el jugador puede tomar más cartas, haciendo clic en el botón Pedir. El manejador de eventos para este botón se encuentra en las líneas 254-278. Cada vez que un jugador hace clic en Pedir, el programa reparte una carta más al jugador y la muestra en la GUI. Si el jugador se excede de 21, el juego se acaba y el jugador pierde. Si el jugador tiene exactamente 21, no puede tomar más cartas y

22.4 Publicación y consumo de los servicios Web

883

servicio Web, no necesita acceder al código de la clase proxy. Como veremos en la sección 22.4.5, puede invocar los métodos del objeto proxy como si fuera un objeto de la clase del servicio Web. Los pasos que describimos en esta sección funcionan bien si conoce la referencia apropiada al servicio Web. No obstante, si está tratando de localizar un nuevo servicio Web, dos tecnologías comunes facilitan este proceso: Descripción universal, descubrimiento e integración (UDDI ) y Descubrimiento de servicios Web (DISCO). En la sección 22.2 hablamos sobre DISCO. UDDI es un proyecto continuo para desarrollar un conjunto de especificaciones que definan la manera en que deben publicarse los servicios Web, para que los programadores en busca de servicios Web puedan encontrarlos. Microsoft y sus socios están trabajando en este proyecto para ayudar a los programadores a localizar servicios Web que se conformen a ciertas especificaciones, con lo cual los desarrolladores podrán buscar servicios Web a través de motores de búsqueda similares a Yahoo!® y Google™. En los sitios www.uddi.org y uddi.microsoft.com podrá aprender más acerca de UDDI y ver una demostración. Estos sitios contienen herramientas de búsqueda que hacen que sea conveniente buscar servicios Web.

22.4.5 Consumo del servicio Web EnteroEnorme El formulario Windows Forms en la figura 22.17 utiliza el servicio Web EnteroEnorme para realizar cálculos con enteros positivos de hasta 100 dígitos de longitud. La línea 22 declara la variable enteroRemoto de tipo localhost.EnteroEnorme. Esta variable se utiliza en cada uno de los manejadores de eventos de la aplicación, para llamar a los métodos del servicio Web EnteroEnorme. El objeto proxy se crea y se asigna a esta variable en la línea 31 del manejador de eventos Load del formulario. Las líneas 52-53, 66-67, 95-96, 116-117 y 135-136 en los diversos manejadores de eventos de los botones invocan a los métodos del servicio Web. Observe que cada llamada se realiza en el objeto proxy local, que a su vez se comunica con el servicio Web a beneficio del cliente. Si descargó el ejemplo de www.deitel.com/books/csharpforprogrammers2, tal vez necesite regenerar el proxy; para ello, elimine la referencia Web y después agréguela de nuevo. Posteriormente haga clic con el botón derecho del ratón en localhost dentro de la carpeta Web References en el Explorador de soluciones, y seleccione la opción Eliminar. Después siga las instrucciones en la sección anterior para agregar la referencia Web al proyecto. El usuario introduce dos enteros, cada uno de hasta 100 dígitos de largo. Al hacer clic en un botón, la aplicación invoca a un método Web para realizar la tarea apropiada y devolver el resultado. Observe que la aplicación cliente UsoServicioEnteroEnorme no puede realizar operaciones usando números de 100 dígitos de manera directa. En vez de eso, la aplicación crea representaciones string de estos números y los pasa como argumentos para los métodos Web que manejan dichas tareas para el cliente. Después utiliza el valor de retorno de cada operación para mostrar un mensaje apropiado. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

// Fig. 22.17: UsoServicioWebEnteroEnorme.cs // Uso del servicio Web EnteroEnorme. using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Web.Services.Protocols; namespace UsoServicioWebEnteroEnorme { public partial class UsoServicioWebEnteroEnormeForm : Form { public UsoServicioWebEnteroEnormeForm() { InitializeComponent(); } // fin del constructor // declara una referencia al servicio Web

Figura 22.17 | Uso del servicio Web EnteroEnorme. (Parte 1 de 5).

884

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

Capítulo 22 Servicios Web

private localhost.EnteroEnorme enteroRemoto; private char[] ceros = { '0' }; // carácter a eliminar de las cadenas // instancia un objeto para interactuar con el servicio Web private void UsoServicioEnteroEnormeForm_Load( object sender, EventArgs e ) { // crea instancia de enteroRemoto enteroRemoto = new localhost.EnteroEnorme(); } // fin del método UsoServicioEnteroEnormeForm_Load // suma dos enteros introducidos por el usuario private void sumarButton_Click( object sender, EventArgs e ) { // se asegura que los números no excedan de 100 dígitos y que ambos // no sean de 100 dígitos de largo, lo cual produciría un desbordamiento if ( primeroTextBox.Text.Length > 100 || segundoTextBox.Text.Length > 100 || ( primeroTextBox.Text.Length == 100 && segundoTextBox.Text.Length == 100 ) ) { MessageBox.Show( "Los Enteros Enormes no deben tener " + "más de 100 dígitos\r\nAmbos enteros no pueden tener " + "longitud de 100: esto produce un desbordamiento", "Error", MessageBoxButtons.OK, MessageBoxIcon.Information ); return; } // fin de if // realiza la suma resultadoLabel.Text = enteroRemoto.Sumar( primeroTextBox.Text, segundoTextBox.Text ).TrimStart( ceros ); } // fin del método sumarButton_Click // resta dos números introducidos por el usuario private void restarButton_Click( object sender, EventArgs e ) { // se asegura que los Enteros Enormes no excedan de 100 dígitos if ( ComprobarTamanio( primeroTextBox, segundoTextBox ) ) return; // realiza la resta try { string resultado = enteroRemoto.Restar( primeroTextBox.Text, segundoTextBox.Text ).TrimStart( ceros ); if ( resultado == "" ) resultadoLabel.Text = "0"; else resultadoLabel.Text = resultado; } // fin de try // si el método Web lanza una excepción, // entonces el primer argumento era menor que el segundo catch ( SoapException exception ) { MessageBox.Show(

Figura 22.17 | Uso del servicio Web EnteroEnorme. (Parte 2 de 5).

22.4 Publicación y consumo de los servicios Web

81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139

885

"El primer argumento era menor que el segundo" ); } // fin de catch } // fin del método restarButton_Click // determina si el primer número introducido // por el usuario es mayor que el segundo private void masGrandeButton_Click( object sender, EventArgs e ) { // se asegura que los Enteros Enormes no excedan de 100 dígitos if ( ComprobarTamanio( primeroTextBox, segundoTextBox ) ) return; // llama al método del servicio Web para determinar si // el primer entero es más grande que el segundo if ( enteroRemoto.MasGrande( primeroTextBox.Text, segundoTextBox.Text ) ) resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + " es mayor que " + segundoTextBox.Text.TrimStart( ceros ); else resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + " no es mayor que " + segundoTextBox.Text.TrimStart( ceros ); } // fin del método masGrandeButton_Click // determina si el primer número introducido // por el usuario es más pequeño que el segundo private void masPequenioButton_Click( object sender, EventArgs e ) { // se asegura que los Enteros Enormes no excedan de 100 dígitos if ( ComprobarTamanio( primeroTextBox, segundoTextBox ) ) return; // llama al método del servicio Web para determinar si // el primer entero es más pequeño que el segundo if ( enteroRemoto.MasPequenio( primeroTextBox.Text, segundoTextBox.Text ) ) resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + " es menor que " + segundoTextBox.Text.TrimStart( ceros ); else resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + " no es menor que " + segundoTextBox.Text.TrimStart( ceros ); } // fin del método masPequenioButton_Click // determina si dos números introducidos por el usuario son iguales private void igualButton_Click( object sender, EventArgs e ) { // se asegura que los Enteros Enormes no excedan de 100 dígitos if ( ComprobarTamanio( primeroTextBox, segundoTextBox ) ) return; // llama al método del servicio Web para determinar si los enteros son iguales if ( enteroRemoto.IgualA( primeroTextBox.Text, segundoTextBox.Text ) ) resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + " es igual a " + segundoTextBox.Text.TrimStart( ceros ); else

Figura 22.17 | Uso del servicio Web EnteroEnorme. (Parte 3 de 5).

886

Capítulo 22 Servicios Web

140 resultadoLabel.Text = primeroTextBox.Text.TrimStart( ceros ) + 141 " no es igual a " + 142 segundoTextBox.Text.TrimStart( ceros ); 143 } // fin del método igualButton_Click 144 145 // determina si los números introducidos por el usuario son demasiado grandes 146 private bool ComprobarTamanio( TextBox primero, TextBox segundo ) 147 { 148 // muestra un mensaje de error si cualquier número tiene demasiados dígitos 149 if ( ( primero.Text.Length > 100 ) || 150 ( segundo.Text.Length > 100 ) ) 151 { 152 MessageBox.Show( "Los Enteros Enormes deben ser menores de 100 dígitos" , 153 "Error", MessageBoxButtons.OK, MessageBoxIcon.Information ); 154 return true; 155 } // fin de if 156 157 return false; 158 } // fin del método ComprobarTamanio 159 } // fin de la clase UsingHugeIntegerServiceForm 160 } // fin del espacio de nombres UsoServicioWebEnteroEnorme

Figura 22.17 | Uso del servicio Web EnteroEnorme. (Parte 4 de 5).

22.5 Rastreo de sesiones en los servicios Web

887

Figura 22.17 | Uso del servicio Web EnteroEnorme. (Parte 5 de 5). Observe que la aplicación elimina los ceros a la izquierda en los números antes de mostrarlos, mediante una llamada al método string TrimStart. Al igual que el método string Trim (que vimos en el capítulo 16), TrimStart elimina todas las ocurrencias de los caracteres especificados por un arreglo char (línea 24) desde el principio de un objeto string.

22.5 Rastreo de sesiones en los servicios Web En el capítulo 21, describimos las ventajas de mantener la información sobre los usuarios para personalizar sus experiencias. En especial, hablamos sobre el rastreo de sesiones mediante el uso de cookies y objetos HttpSessionState. Ahora incorporaremos el rastreo de sesiones a un servicio Web. Suponga que una aplicación cliente necesita llamar a varios métodos del mismo servicio Web, posiblemente varias veces a cada uno. En tal caso, sería benéfico para el servicio Web mantener la información de estado para el cliente. El rastreo de sesiones elimina la necesidad de pasar la información del cliente varias veces entre éste y el servicio Web. Por ejemplo, un servicio Web que proporciona acceso a las reseñas de restaurantes locales se beneficiaría de almacenar la dirección del usuario cliente. Una vez que se almacena la dirección del cliente en una variable de sesión, los métodos Web pueden devolver resultados personalizados y localizados, sin requerir que se pase la dirección en cada llamada a los métodos. Esto no sólo aumenta el rendimiento, sino que también requiere un menor esfuerzo por parte del programador; se pasa menos información en cada llamada a los métodos.

22.5.1 Creación de un servicio Web de Blackjack Almacenar la información de las sesiones puede hacer que el servicio Web sea más intuitivo para los programadores de la aplicación cliente. Nuestro siguiente ejemplo es un servicio Web que asiste a los programadores en el desarrollo de un juego de cartas de blackjack (figura 22.18). El servicio Web proporciona métodos Web para repartir una carta y evaluar una mano de cartas. Después de presentar este servicio Web, lo utilizamos para que sirva como el repartidor para un juego de blackjack (figura 22.19). El servicio Web de blackjack utiliza una variable de sesión para mantener un mazo único de cartas para cada aplicación cliente. Varios clientes pueden utilizar el servicio al mismo tiempo, pero las llamadas a los métodos Web que realice un cliente específico sólo utilizarán el mazo almacenado en la sesión de ese cliente. Nuestro ejemplo utiliza un subconjunto simple de las reglas de blackjack de los casinos: Se reparten dos cartas al repartidor y dos cartas al jugador. Las cartas del jugador se reparten con la cara hacia arriba. Sólo la primera de las cartas del repartidor se reparte con la cara hacia arriba. Cada carta tiene un valor. Una carta numerada del 2 al 10 vale lo que indique en su cara. Los jotos, reinas y reyes valen 10 cada uno. Los ases pueden valer 1 u 11, lo que sea más conveniente para el jugador (como pronto veremos). Si la suma de las dos cartas iniciales del jugador es 21 (por ejemplo, que se hayan repartido al jugador una carta con valor de 10 y un as, que cuenta como 11 en esta situación), el jugador tiene “blackjack” y gana el juego de inmediato. En caso contrario, el jugador puede empezar a tomar cartas adicionales, una a la vez. Estas cartas se reparten con la cara hacia arriba, y el jugador decide cuándo dejar de tomar cartas. Si el jugador “se pasa” (es decir, si la suma de las cartas del jugador excede a 21), el juego termina y el jugador pierde. Cuando el jugador está satisfecho con su conjunto actual de cartas, “se planta” (es decir, deja de tomar cartas) y se revela la carta oculta del repartidor. Si el total del repartidor es 16 o menos, debe tomar otra carta; en caso contrario, el repartidor debe plantarse. El repartidor debe seguir tomando cartas hasta que la suma de todas sus cartas sea mayor o igual a 17. Si el repartidor se pasa de 21, el jugador gana. En caso contrario, la mano con el total de puntos que sea mayor gana. Si el repartidor y el jugador tienen el mismo total de puntos, el juego es un “empate” y nadie gana.

888

Capítulo 22 Servicios Web

El servicio Web (figura 22.18) proporciona métodos para repartir una carta y determinar el valor en puntos de una mano. Representamos cada carta como un objeto string que consiste en un dígito (por ejemplo, del 1 al 13), representando la cara de la carta (por ejemplo del as hasta el rey), seguido por un espacio y un dígito (por ejemplo 0-3) el cual representa el palo de la carta (por ejemplo, tréboles, diamantes, corazones o espadas). Por ejemplo, el joto de corazones se representa como "11 2", y el dos de tréboles se representa como “2 0”. Después de desplegar el servicio Web, creamos una aplicación para Windows que utiliza los métodos del servicio Web ServicioBlackjack para implementar un juego de blackjack. Para crear y desplegar este servicio Web, siga los pasos que presentamos en las secciones 22.4.2-22.4.3 para el servicio EnteroEnorme. Las líneas 15-16 definen el método RepartirCarta como un método Web. Al establecer la propiedad EnableSession a True, se indica que debe mantenerse la información de sesión y que debe estar accesible para

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

// Fig. 22.18: ServicioBlackjack.cs // Servicio Web de Blackjack, para repartir y contar cartas. using System; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; using System.Collections; [ WebService( Namespace = "http://www.deitel.com/", Description = "Un servicio Web que reparte y cuenta cartas para el juego de Blackjack" ) ] [ WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 ) ] public class ServicioBlackjack : System.Web.Services.WebService { // reparte una carta que no se ha repartido todavía [ WebMethod( EnableSession = true, Description = "Reparte una nueva carta del mazo." ) ] public string RepartirCarta() { string carta = "2 2"; // obtiene el mazo del cliente ArrayList mazo = ( ArrayList )( Session[ "mazo" ] ); carta = Convert.ToString( mazo[ 0 ] ); mazo.RemoveAt( 0 ); return carta; } // fin del método RepartirCarta // crea y baraja un mazo de cartas [ WebMethod( EnableSession = true, Description = "Crea y baraja un mazo de cartas." ) ] public void Barajar() { object temporal; // almacena una carta temporalmente, durante el intercambio Random objetoAleatorio = new Random(); // genera números aleatorios int nuevoIndice; // índice de la carta seleccionada al azar ArrayList mazo = new ArrayList(); // almacena el mazo de cartas (objetos string) // genera todas las cartas posibles for ( int i = 1; i totalJugador ) JuegoTerminado( EstadoJuego.PIERDE ); else if ( totalJugador > totalRepartidor ) JuegoTerminado( EstadoJuego.GANA ); else JuegoTerminado( EstadoJuego.EMPATE ); } // fin del método RepartidorJuega // muestra la carta representada por valorCarta en el control PictureBox especificado public void MostrarCarta( int carta, string valorCarta )

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 2 de 8).

22.5 Rastreo de sesiones en los servicios Web

116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173

{ // obtiene el control PictureBox apropiado del objeto ArrayList PictureBox mostrarBox = ( PictureBox )( cuadrosCartas[ carta ] ); // si la cadena que representa a carta está vacía, // establece mostrarBox para que muestre la parte posterior de carta if ( valorCarta == "" ) { mostrarBox.Image = Image.FromFile( "imagenes_blackjack/cartaposterior.png" ); return; } // fin de if // obtiene de valorCarta el valor de la cara de la carta string cara = valorCarta.Substring( 0, valorCarta.IndexOf( " " ) ); // obtiene de valorCarta el palo de la carta string palo = valorCarta.Substring( valorCarta.IndexOf( " " ) + 1 ); char letraPalo; // letra del palo que se usa para formar el nombre del archivo de imagen // determina la letra del palo de la carta switch ( Convert.ToInt32( palo ) ) { case 0: // tréboles letraPalo = 't'; break; case 1: // diamantes letraPalo = 'd'; break; case 2: // corazones letraPalo = 'c'; break; default: // espadas letraPalo = 'e'; break; } // fin de switch // establece mostrarBox para mostrar la imagen apropiada mostrarBox.Image = Image.FromFile( "imagenes_blackjack/" + cara + letraPalo + ".png" ); } // fin del método MostrarCarta // muestra todas las cartas del jugador y el // mensaje apropiado del estado del juego public void JuegoTerminado( EstadoJuego ganador ) { char[] tab = { '\t' }; string[] cartas = cartasRepartidor.Split( tab ); // muestra todas las cartas del repartidor for ( int i = 0; i < cartas.Length; i++ ) MostrarCarta( i, cartas[ i ] ); // muestra la imagen apropiada del estado if ( ganador == EstadoJuego.EMPATE ) // empate estadoPictureBox.Image =

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 3 de 8).

893

894

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228

Capítulo 22 Servicios Web

Image.FromFile( "imagenes_blackjack/empate.png" ); else if ( ganador == EstadoJuego.PIERDE ) // el jugador pierde estadoPictureBox.Image = Image.FromFile( "imagenes_blackjack/pierde.png" ); else if ( ganador == EstadoJuego.BLACKJACK ) // el jugador tiene blackjack estadoPictureBox.Image = Image.FromFile( "imagenes_blackjack/blackjack.png" ); else // el jugador gana estadoPictureBox.Image = Image.FromFile( "imagenes_blackjack/gana.png" ); // muestra los totales finales para repartidor y jugador repartidorTotalLabel.Text = "Repartidor: " + repartidor.ObtenerValorMano( cartasRepartidor ); jugadorTotalLabel.Text = "Jugador: " + repartidor.ObtenerValorMano( cartasJugador ); // restablece los controles para un nuevo juego plantarButton.Enabled = false; pedirButton.Enabled = false; repartirButton.Enabled = true; } // fin del método JuegoTerminado // reparte dos cartas al repartidor y dos al jugador private void repartirButton_Click( object sender, EventArgs e ) { string carta; // almacena una carta temporalmente, hasta que se agrega a una mano // borra las imágenes de las cartas foreach ( PictureBox imagenCarta in cuadrosCartas ) imagenCarta.Image = null; estadoPictureBox.Image = null; // borra imagen de estado repartidorTotalLabel.Text = ""; // borra el total final para el repartidor jugadorTotalLabel.Text = ""; // borra el total final para el jugador // crea un nuevo mazo barajado en el equipo remoto repartidor.Barajar(); // reparte dos cartas al jugador cartasJugador = repartidor.RepartirCarta(); // reparte una carta a la mano del jugador // actualiza GUI para mostrar la nueva carta MostrarCarta( 11, cartasJugador ); carta = repartidor.RepartirCarta(); // reparte una segunda carta MostrarCarta( 12, carta ); // actualiza GUI para mostrar la nueva carta cartasJugador += '\t' + carta; // agrega la segunda carta a la mano del jugador // reparte dos cartas al repartidor, sólo muestra la cara de la primera carta cartasRepartidor = repartidor.RepartirCarta(); // reparte una carta a la mano del repartidor MostrarCarta( 0, cartasRepartidor ); // actualiza GUI para mostrar la nueva carta carta = repartidor.RepartirCarta(); // reparte una segunda carta MostrarCarta( 1, "" ); // actualiza GUI para mostrar la carta cara abajo cartasRepartidor += '\t' + carta; // agrega la segunda carta a la mano del repartidor

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 4 de 8).

22.5 Rastreo de sesiones en los servicios Web

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286

895

plantarButton.Enabled = true; // permite al jugador plantarse pedirButton.Enabled = true; // permite al jugador pedir otra carta repartirButton.Enabled = false; // deshabilita el botón Repartir // determina el valor de las dos manos int totalRepartidor = repartidor.ObtenerValorMano( cartasRepartidor ); int totalJugador = repartidor.ObtenerValorMano( cartasJugador ); // si las manos son iguales a 21, es un empate if ( totalRepartidor == totalJugador && totalRepartidor == 21 ) JuegoTerminado( EstadoJuego.EMPATE ); else if ( totalRepartidor == 21 ) // si el repartidor tiene 21, gana JuegoTerminado( EstadoJuego.PIERDE ); else if ( totalJugador == 21 ) // el jugador tiene blackjack JuegoTerminado( EstadoJuego.BLACKJACK ); // la siguiente carta del repartidor tiene el índice 2 en cuadrosCartas cartaActualRepartidor = 2; // la siguiente carta del jugador tiene el índice 13 en cuadrosCartas cartaActualJugador = 13; } // fin del método repartirButton // reparte otra carta al jugador private void pedirButton_Click( object sender, EventArgs e ) { // da otra carta al jugador string carta = repartidor.RepartirCarta(); // reparte una nueva carta cartasJugador += '\t' + carta; // agrega la nueva carta a la mano del jugador // actualiza GUI para mostrar la nueva carta MostrarCarta( cartaActualJugador, carta ); cartaActualJugador++; // determina el valor de la mano del jugador int total = repartidor.ObtenerValorMano( cartasJugador ); // si el jugador excede a 21, la casa gana if ( total > 21 ) JuegoTerminado( EstadoJuego.PIERDE ); // si el jugador tiene 21, // no pueden tomar más cartas, y el repartidor juega if ( total == 21 ) { pedirButton.Enabled = false; RepartidorJuega(); } // fin de if } // fin del método pedirButton_Click // juega la mano del repartidor después de que el jugador elije plantarse private void plantarButton_Click( object sender, EventArgs e ) { plantarButton.Enabled = false; // deshabilita el botón Plantar pedirButton.Enabled = false; // deshabilita el botón Pedir repartirButton.Enabled = true; // rehabilita el botón Repartir RepartidorJuega(); // el jugador elije plantarse, por lo que se juega la mano del repartidor

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 5 de 8).

896

Capítulo 22 Servicios Web

287 } // fin del método plantarButton_Click 288 } // fin de la clase BlackjackForm 289 } // fin del espacio de nombres Blackjack a) Se reparten las cartas iniciales al jugador y al repartidor cuando el usuario oprime el botón Repartir.

b) Las cartas, después de que el jugador oprime el botón Pedir dos veces, y después el botón Plantar. En este caso, el jugador ganó el juego debido a que el repartidor se pasó (se excedió de 21).

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 6 de 8).

22.5 Rastreo de sesiones en los servicios Web

c) Las cartas, después de que el jugador oprime el botón Pedir una vez, y luego el botón Plantar. En este caso, el jugador se pasó (se excedió de 21) y el repartidor ganó el juego.

d) Las cartas, después de que el jugador oprime el botón Repartir. En este caso, el jugador ganó con Blackjack, ya que las primeras dos cartas fueron un as y una carta con un valor de 10 (una qüina en este caso).

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 7 de 8).

897

898

Capítulo 22 Servicios Web

e) Las cartas, después de que el jugador oprime el botón Plantar. En este caso, el jugador y el repartidor empatan; tienen el mismo total de puntos.

Figura 22.19 | Juego de blackjack que utiliza el servicio Web BlackJack. (Parte 8 de 8). Cada jugador tiene 11 controles PictureBox; el número máximo de cartas que pueden repartirse sin excederse automáticamente de 21 (es decir, cuatro ases, cuatro números dos y tres números tres). Estos controles PictureBox se colocan en un objeto ArrayList (líneas 54-75), por lo que podemos indexar el objeto ArrayList durante el juego, para determinar el control PictureBox que mostrará la imagen de una carta específica. En la sección 22.5.1, mencionamos que el cliente debe proporcionar una forma de aceptar las cookies creadas por el servicio Web, para identificar a los usuarios en forma única. La línea 50 en el manejador de eventos Load de BlackjackForm crea un nuevo objeto CookieContainer para la propiedad CookieContainer de repartidor. Un objeto CookieContainer (espacio de nombres System.Net) almacena la información de una cookie (creada por el servicio Web) en un objeto Cookie, en el objeto CookieContainer. El objeto Cookie contiene un identificador único, que el servicio Web puede usar para reconocer al cliente cuando éste realice solicitudes a futuro. Como parte de cada solicitud, la cookie se envía de vuelta al servidor en forma automática. Si el cliente no creara un objeto CookieContainer, el servicio Web crearía un nuevo objeto Session para cada solicitud, y la información de estado del usuario no persistiría de solicitud en solicitud. El método JuegoTerminado (líneas 162-196) muestra todas las cartas del repartidor, muestra el mensaje apropiado en el control PictureBox de estado y muestra el total final de puntos del repartidor y del jugador. El método JuegoTerminado recibe como argumento a un miembro de la enumeración EstadoJuego (que se define en las líneas 30-36). La enumeración representa si el jugador empató, perdió o ganó el juego; sus cuatro miembros son EMPATE, PIERDE, GANA y BLACKJACK. Cuando el jugador hace clic en el botón Repartir (cuyo manejador de eventos aparece en las líneas 199-251), se borran todos los controles PictureBox y Label que muestran el total final de puntos. A continuación se baraja el mazo y tanto el jugador como el repartidor reciben dos cartas cada uno. Si ambos obtienen puntuaciones de 21, el programa llama al método JuegoTerminado y le pasa EstadoJuego.PUSH. Si sólo el jugador tiene 21 después de repartir las dos cartas, el programa pasa EstadoJuego.BLACKJACK al método JuegoTerminado. Si sólo el repartidor tiene 21, el programa pasa EstadoJuego.PIERDE al método JuegoTerminado. Si repartirButton_Click no llama a JuegoTerminado, el jugador puede tomar más cartas, haciendo clic en el botón Pedir. El manejador de eventos para este botón se encuentra en las líneas 254-278. Cada vez que un jugador hace clic en Pedir, el programa reparte una carta más al jugador y la muestra en la GUI. Si el jugador se excede de 21, el juego se acaba y el jugador pierde. Si el jugador tiene exactamente 21, no puede tomar más cartas y

22.6 Uso de formularios Web Forms y servicios Web

899

se hace una llamada al método RepartidorJuega (líneas 80-112) para que el repartidor continúe tomando cartas hasta que su mano tenga un valor de 17 o más (líneas 84-92). Si el repartidor se excede de 21, el jugador gana (línea 100); en cualquier otro caso se comparan los valores de las manos, y se hace una llamada a JuegoTerminado con el argumento apropiado (líneas 106-111). Al hacer clic en el botón Plantar se indica que un jugador no quiere que se le reparta otra carta. El manejador de eventos para este botón (líneas 281-287) deshabilita los botones Pedir y Plantar, y después llama al método RepartidorJuega. El método MostrarCarta (líneas 115-158) actualiza la GUI para mostrar una carta recién repartida. El método recibe como argumentos un entero que representa el índice del control PictureBox en el objeto ArrayList que debe establecer su imagen, y un objeto string que representa a la carta. Un objeto string vacío indica que deseamos mostrar la carta hacia abajo. Si el método MostrarCarta recibe un objeto string que no está vacío, el programa extrae la cara y el palo del objeto string, y utiliza esta información para buscar la imagen correcta. La instrucción switch (líneas 139-153) convierte el número que representa al palo en un entero, y asigna el carácter apropiado a letraPalo (t para tréboles, d para diamantes, c para corazones y e para espadas). El carácter en letraPalo se utiliza para completar el nombre del archivo de la imagen (líneas 156-157).

22.6 Uso de formularios Web Forms y servicios Web Nuestros ejemplos anteriores acceden a los servicios Web desde aplicaciones Windows en Visual C# 2005. No obstante, podemos utilizarlos con la misma facilidad en aplicaciones Web creadas con Visual Web Developer. De hecho, como los comercios basados en Web están prevaleciendo cada vez más, es común que las aplicaciones Web consuman servicios Web. La figura 22.20 presenta un servicio Web de reservación de un aerolínea, el cual recibe información relacionada con el tipo de asiento que desea reservar un cliente, y hace una reservación si está disponible dicho asiento. Más adelante en esta sección, presentaremos una aplicación Web que permite a un cliente especificar una solicitud de reservación, y después utiliza el servicio Web de reservación de la aerolínea para tratar de ejecutar la solicitud. El servicio Web de reservación de la aerolínea tiene un solo método Web: Reservar (líneas 24-42), el cual busca en su base de datos de asientos (Boletos.mdf) para localizar un asiento que coincida con la solicitud del usuario. Si encuentra un asiento apropiado, Reservar actualiza la base de datos, hace la reservación y devuelve true; en caso contrario, no se hace la reservación y el método devuelve false. Observe que las instrucciones en las líneas 28-29 y en la línea 37, que consultan y actualizan la base de datos, usan objetos de las clases BoletosDataSet y BoletosDataSetTableAdapters.AsientosTableAdapter. En el capítulo 20 vimos que las clases DataSet y TableAdapter se crean por usted cuando utiliza el Diseñador de DataSet para agregar un objeto DataSet a un proyecto. En la sección 22.6.1 hablaremos sobre los pasos para agregar el conjunto BoletosDataSet. Reservar recibe dos argumentos: un objeto string que representa el tipo de asiento deseado (es decir, Ventana, Intermedio o Pasillo) y un objeto string que representa el tipo de clase deseada (es decir, Económica

1 2 3 4 5 6 7

// Fig. 22.20: ServicioReservacion.cs // Servicio Web de reservación de una aerolínea. using System; using System.Web; using System.Web.Services; using System.Web.Services.Protocols;

8

[ WebService( Namespace = "http://www.deitel.com/", Description = "Servicio que permite a un usuario reservar un asiento en un avión." ) ] [ WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 ) ] public class ServicioReservacion : System.Web.Services.WebService { // crea objeto BoletosDataSet para colocar en caché los datos // de la base de datos Boletos private BoletosDataSet boletosDataSet = new BoletosDataSet();

9 10 11 12 13 14 15 16

Figura 22.20 | Servicio Web de reservación de una aerolínea. (Parte 1 de 2).

900

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

Capítulo 22 Servicios Web

// crea AsientosTableAdapter para interactuar con la base de datos private BoletosDataSetTableAdapters.AsientosTableAdapter AsientosTableAdapter = new BoletosDataSetTableAdapters.AsientosTableAdapter(); // comprueba la base de datos par determinar si hay un asiento disponible que coincida [ WebMethod( Description = "Método para reservar un asiento." ) ] public bool Reservar( string tipoAsiento, string tipoClase ) { // llena BoletosDataSet.Asientos con filas que representan los asientos // desocupados que coinciden con el tipoAsiento y tipoClase especificados AsientosTableAdapter.LlenarPorTipoYClase( boletosDataSet.Asientos, tipoAsiento, tipoClase ); // si el número de asientos devuelto es distinto de cero, // obtiene el primer número de asiento que coincida y lo marca como ocupado if ( boletosDataSet.Asientos.Count != 0 ) { string numeroAsiento = boletosDataSet.Asientos[ 0 ].Numero; AsientosTableAdapter.ActualizarAsientoComoOcupado( numeroAsiento ); return true; // el asiento se reservó } // fin de if return false; // el asiento no se reservó } // fin del método Reservar } // fin de la clase ServicioReservación

Figura 22.20 | Servicio Web de reservación de una aerolínea. (Parte 2 de 2). o Primera). Nuestra base de datos contiene cuatro columnas: el número de asiento (es decir, 1-10), el tipo de asiento (Ventana, Intermedio o Pasillo), el tipo de clase (Económica o Primera) y una columna que contiene 1 (verdadero) o 0 (falso) para indicar si el asiento está ocupado. Las líneas 28-29 obtienen los números de asiento de cualquier asiento disponible que coincida con el asiento y tipo de clase solicitados. Esta instrucción llena la tabla Asientos en boletosDataSet con los resultados de la consulta SELECT Numero FROM Asientos WHERE (Ocupado = 0) AND (Tipo = @tipo) AND (Clase = @clase)

Los parámetros @tipo y @clase en la consulta se sustituyen con los valores de los argumentos tipoAsiento y tipoClase del método LlenarPorTipoYClase de AsientosTableAdapter. En la línea 33, si el número de filas en la tabla Asientos (boletosDataSet.Asientos.Count) no es cero, hubo cuando menos un asiento que coincidió con la solicitud del usuario. En este caso, el servicio Web reserva el primer número de asiento que coincida. En la línea 35 obtenemos el número de asiento al acceder al primer elemento de la tabla Asientos (es decir, Asientos[ 0 ]: la primera fila en la tabla), y después obtener el valor de la columna Numero de esa fila. La línea 37 invoca al método ActualizarAsientoComoOcupado de AsientosTableAdapter y lo pasa a numeroAsiento, el asiento a reservar. El método ActualizarAsientoComoOcupado utiliza la instrucción UPDATE UPDATE Asientos SET Ocupado = 1 WHERE (Numero = @numero)

para marcar el asiento como ocupado en la base de datos, sustituyendo el parámetro @numero con el valor de numeroAsiento. El método Reservar devuelve true (línea 38) para indicar que la reservación fue exitosa. Si no hay asientos que coincidan (línea 33), Reservar devuelve false (línea 41) para indicar que ningún asiento coincidió con la solicitud del usuario.

22.6 Uso de formularios Web Forms y servicios Web

901

22.6.1 Agregar componentes de datos a un servicio Web Ahora usaremos las herramientas de Visual Web Developer para configurar un objeto DataSet que permita a nuestro servicio Web interactuar con el archivo de base de datos SQL Server Boletos.mdf que se proporciona en la carpeta de ejemplo para la figura 22.20. Agregaremos un nuevo objeto DataSet al proyecto, y después configuraremos el objeto TableAdapter de DataSet mediante el Asistente para la configuración de TableAdapter. Utilizaremos el asistente para seleccionar el origen de datos (Boletos.mdf) y para crear las instrucciones SQL necesarias para soportar las operaciones en la base de datos que vimos en la descripción de la figura 22.20. Los siguientes pasos para configurar el DataSet y su correspondiente TableAdapter son similares a los que vimos en los capítulos 20-21.

Paso 1: crear ServicioReservacion y agregar un DataSet al proyecto Empiece por crear un proyecto tipo Servicio Web ASP.NET llamado ServicioReservacion. Cambie el nombre del archivo Service.cs por el de ServicioReservacion.cs y sustituya su código con el código de la figura 22.20. A continuación, agregue un DataSet llamado BoletosDataSet al proyecto. Haga clic con el botón derecho del ratón en la carpeta App_Code dentro del Explorador de soluciones y seleccione la opción Agregar nuevo elemento… del menú contextual. En el cuadro de diálogo Agregar nuevo elemento, seleccione DataSet, especifique BoletosDataSet.xsd en el campo Nombre y haga clic en Agregar. A continuación se mostrará el conjunto BoletosDataSet en modo de diseño y se abrirá el Asistente para la configuración de TableAdapter. Cuando agregamos un DataSet a un proyecto, el IDE crea las clases TableAdapter apropiadas para interactuar con las tablas de la base de datos.

Paso 2: seleccionar el origen de datos y crear una conexión En los siguientes pasos usaremos el Asistente para la configuración de Table Adapter, para configurar un TableAdapter de manera que manipule la tabla Asientos en la base de datos Boletos.mdf. Ahora, debe seleccionar la base de datos. En el Asistente para la configuración de TableAdapter, haga clic en el botón Nueva conexión… para mostrar el cuadro de diálogo Agregar conexión. En este cuadro de diálogo, especifique Archivo de base de datos de Microsoft SQL Server como el Origen de datos y después haga clic en el botón Examinar… para mostrar el cuadro de diálogo Seleccione el archivo de base de datos de SQL Server. Localice el archivo Boletos.mdf en su computadora, selecciónelo y haga clic en el botón Abrir para regresar al cuadro de diálogo Agregar conexión. Haga clic en el botón Probar conexión para probar la conexión con la base de datos, y después haga clic en Aceptar para regresar al Asistente para la configuración de TableAdapter. Haga clic en el botón Siguiente > y después en Sí cuando el asistente le pregunte si desea agregar el archivo a su proyecto y modificar la conexión. Haga clic en Siguiente > para guardar la cadena de conexión en el archivo de configuración de la aplicación.

Paso 3: abrir el Generador de consultas y agregar la tabla Asientos de Boletos.mdf Ahora debemos especificar cómo accederá el TableAdapter a la base de datos. Como en este ejemplo utilizaremos instrucciones SQL, elija Usar instrucciones SQL y después haga clic en Siguiente >. Ahora haga clic en Generador de consultas… para mostrar los cuadros de diálogo Generador de consultas y Agregar tabla. Antes de generar una consulta SQL, debemos especificar la(s) tabla(s) que se va(n) a utilizar en la consulta. La base de datos Boletos.mdf sólo contiene una tabla, llamada Asientos. Seleccione esta tabla de la ficha Tablas y haga clic en Agregar. Haga clic en Cerrar para cerrar el cuadro de diálogo Agregar tabla.

Paso 4: configurar una consulta SELECT para obtener los asientos disponibles Ahora crearemos una consulta que seleccione los asientos que no estén ya reservados y que coincidan con un tipo y una clase específicos. Empiece por seleccionar Numero de la tabla Asientos en la parte superior del cuadro de diálogo Generador de consultas. A continuación, debemos especificar los criterios para seleccionar asientos. En la parte media del cuadro de diálogo Generador de consultas, haga clic en la celda debajo de Numero en la columna Columna y seleccione Ocupado. En la columna Filtro de esta fila, escriba 0 (es decir, falso) para indicar que debemos seleccionar sólo los números de los asientos que no estén ocupados. En la siguiente fila, seleccione Tipo en la columna Columna y especifique @tipo como el Filtro, para indicar que el valor del filtro se especificará en un argumento para el método que implementa esta consulta. En la siguiente fila, seleccione Clase en la columna Columna y especifique @clase como el Filtro, para indicar que el valor de este filtro también se especificará como un argumento para el método. Deseleccione las casillas de verificación en la columna Resultados para las

902

Capítulo 22 Servicios Web

filas Ocupado, Tipo y Clase. Ahora el cuadro de diálogo Generador de consultas debe aparecer como se muestra en la figura 22.21. Haga clic en Aceptar para cerrar el cuadro de diálogo Generador de consultas. Haga clic en el botón Siguiente > para elegir los métodos que se van a generar. Para el nombre del método debajo de Rellenar un DataTable, escriba LlenarPorTipoYClase. Para el nombre del método debajo de Devolver un DataTable, escriba ObtenerDatosPorTipoYClase. Haga clic en el botón Finalizar para generar estos métodos.

Paso 5: agregar otra consulta a AsientosTableAdapter para el BoletosDataSet Los últimos dos pasos que necesitamos realizar crean una consulta UPDATE que reserva un asiento. En el área de diseño para el BoletosDataSet, haga clic en AsientosTableAdapter para seleccionarlo, después haga clic con el botón derecho del ratón en este objeto y seleccione Agregar consulta… para mostrar el Asistente para la configuración de consultas de TableAdapter. Seleccione Usar instrucciones SQL y haga clic en el botón Siguiente >. Seleccione UPDATE como el tipo de la consulta y haga clic en el botón Siguiente >. Elimine la consulta UPDATE existente. Haga clic en Generador de consultas… para mostrar los cuadros de diálogo Generador de consultas y Agregar tabla. Después agregue la tabla Asientos, como hicimos en el paso 3, y haga clic en Cerrar para regresar al cuadro de diálogo Generador de consultas.

Paso 6: configurar una instrucción UPDATE para reservar un asiento En el cuadro de diálogo Generador de consultas, seleccione la columna Ocupada de la tabla Asientos en la parte superior del cuadro de diálogo. En la parte media del cuadro de diálogo, coloque el valor 1 (es decir, verdadero) en la columna Valor nuevo para la fila Ocupada. En la fila debajo de Ocupada, seleccione Numero, desactive la casilla de verificación en la columna Set y especifique @numero como el valor de Filtro, para indicar que el número de asiento se especificará como argumento para el método que implementa esta consulta. Ahora el cuadro de diálogo Generador de consultas debe aparecer como se muestra en la figura 22.22. Haga clic en el botón Aceptar del cuadro de diálogo Generador de consultas para regresar al Asistente para la configuración de consultas de TableAdapter. Después haga clic en el botón Siguiente > para elegir el nombre del método que ejecutará la consulta

Figura 22.21 | El cuadro de diálogo Generador de consultas especifica una consulta SELECT, que selecciona los asientos que no están ya reservados y que coinciden con un tipo y una clase específicos.

22.6 Uso de formularios Web Forms y servicios Web

903

Figura 22.22 | El cuadro de diálogo Generador de consultas especifica una instrucción UPDATE que reserva un asiento.

UPDATE.

Cambie el nombre del método por ActualizarAsientoComoOcupado, y después haga clic en Finalizar para cerrar el Asistente para la configuración de consultas de TableAdapter. En este punto, puede usar la página ServicioReservacion.asmx para probar el método Reservar del servicio Web. Para ello, seleccione Iniciar sin depuración del menú Depurar. En la sección 22.6.2 generaremos un formulario Web Forms para que consuma este servicio Web.

22.6.2 Creación de un formulario Web Forms para interactuar con el servicio Web de reservaciones de una aerolínea La figura 22.23 presenta el listado ASPX para un formulario Web Forms, a través del cual los usuarios pueden seleccionar los tipos de asientos. Esta página permite a los usuarios reservar un asiento en base a su clase (Económica o Primera) y ubicación (Pasillo, Intermedio o Ventana) en una fila de asientos. Después, la página utiliza el servicio Web de reservación de la aerolínea para llevar a cabo las solicitudes del usuario. Si la solicitud de la base de datos no tiene éxito, se instruye al usuario para que modifique la solicitud e intente de nuevo. Esta página define dos objetos DropDownList y un objeto Button. Un DropDownList (líneas 22-27) muestra todos los tipos de asientos que pueden seleccionar los usuarios. El segundo (líneas 29-32) proporciona opciones

1 2 3 4 5 6 7 8 9 10





Figura 22.23 | Archivo ASPX que recibe la información de la reservación. (Parte 1 de 2).

904

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

Capítulo 22 Servicios Web

Reservación de boletos







Pasillo Intermedio Ventana      

Económica Primera      





Figura 22.23 | Archivo ASPX que recibe la información de la reservación. (Parte 2 de 2). para el tipo de clase. Los usuarios hacen clic en el control Button llamado reservarButton (líneas 34-36) para enviar solicitudes después de realizar selecciones de los objetos DropDownList. La página también define un objeto Label llamado errorLabel, que en un principio está en blanco (líneas 38-39) y que muestra un mensaje apropiado si no hay un asiento que coincida con la selección del usuario. El archivo de código subyacente (figura 22.24) adjunta un manejador de eventos a reservarButton. 1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 22.24: ClienteReservacion.aspx.cs // Archivo de código subyacente de ClienteReservacion. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class ClienteReservacion : System.Web.UI.Page

Figura 22.24 | Archivo de código subyacente para la página de reservación. (Parte 1 de 2).

22.6 Uso de formularios Web Forms y servicios Web

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

905

{ // objeto de tipo proxy, que se utiliza para conectarse al servicio Web de reservación private localhost.ServicioReservacion agenteBoletos = new localhost.ServicioReservacion(); // trata de reservar el tipo de asiento seleccionado protected void reservarButton_Click( object sender, EventArgs e ) { // si el método Web devolvió verdadero, indica éxito if ( agenteBoletos.Reservar( asientoList.SelectedItem.Text, claseList.SelectedItem.Text.ToString() ) ) { // oculta otros controles instruccionesLabel.Visible = false; asientoList.Visible = false; claseList.Visible = false; reservarButton.Visible = false; errorLabel.Visible = false; // muestra mensaje indicando éxito Response.Write( "Su reservación está hecha. Gracias." ); } // fin de if else // el método Web devolvió falso, por lo que indica falla { // muestra mensaje en el control errorLabel que al principio estaba en blanco errorLabel.Text = "Este tipo de asiento no está disponible. " + "Modifique su solicitud e intente otra vez."; } // fin de else } // fin del método reservarButton_Click } // fin de la clase ClienteReservacion

Figura 22.24 | Archivo de código subyacente para la página de reservación. (Parte 2 de 2). Las líneas 16-17 de la figura 22.24 crean un objeto ServicioReservacion. (Recuerde que debe agregar una referencia Web a este servicio Web). Cuando el usuario hace clic en Reservar (figura 22.25), se ejecuta el manejador de eventos reservarButton_Click (líneas 20-42 de la figura 22.24) y se vuelve a cargar la página. El manejador de eventos llama al método Reservar del servicio Web y le pasa como argumentos el asiento seleccionado y el tipo de clase (líneas 23-24). Si Reservar devuelve true, la aplicación muestra un mensaje agradeciendo al usuario por hacer la reservación (línea 34); en caso contrario, errorLabel notifica al usuario que el tipo de asiento solicitado no está disponible, y le pide que intente de nuevo (líneas 39-40). Use las técnicas que presentamos en el capítulo 21 para generar el formulario Web Forms ASP.NET.

a) Seleccionar un asiento.

Figura 22.25 | Ejemplo de ejecución del formulario Web Forms para reservación de boletos. (Parte 1 de 2).

906

Capítulo 22 Servicios Web

b) El asiento se reservó con éxito.

c) Tratar de reservar otro asiento.

d) No coincidió ningún asiento con el tipo y la clase solicitados.

Figura 22.25 | Ejemplo de ejecución del formulario Web Forms para reservación de boletos. (Parte 2 de 2).

22.7 Tipos definidos por el usuario en los servicios Web Los métodos Web que hemos demostrado hasta ahora reciben y devuelven valores de tipos simples. También es posible procesar tipos definidos por el usuario (conocidos como tipos personalizados) en un servicio Web. Estos tipos pueden pasarse a los métodos Web, o éstos pueden devolverlos. Los clientes de un servicio Web también pueden usar estos tipos definidos por el usuario, ya que la clase proxy creada para el cliente contiene las definiciones de los tipos. Esta sección presenta un servicio Web llamado GeneradorEcuaciones, que genera al azar ecuaciones aritméticas de tipo Ecuacion. El cliente es una aplicación para enseñar matemáticas, que recibe como entrada la información acerca de la pregunta matemática que desea hacer el usuario (suma, resta o multiplicación) y el nivel de habilidad del mismo (1 especifica ecuaciones que utilizan números de un dígito, 2 especifica ecuaciones con números de dos dígitos y 3 especifica ecuaciones que contienen números de tres dígitos). Después, el servicio Web genera una ecuación que consiste en números aleatorios con el número apropiado de dígitos. La aplicación cliente recibe la Ecuacion y muestra la pregunta de ejemplo al usuario en un formulario Windows Forms.

Serialización de los tipos definidos por el usuario Anteriormente mencionamos que SOAP debe soportar todos los tipos que se pasan desde/hacia los servicios Web. Entonces, ¿cómo es que SOAP puede soportar un tipo que ni siquiera se ha creado aún? Los tipos personalizados que se envían hacia o desde un servicio Web se serializan, con lo cual se pueden pasar en formato XML. A este proceso se le conoce como serialización XML.

22.7 Tipos definidos por el usuario en los servicios Web

907

Requerimientos para los tipos definidos por el usuario que se utilizan con métodos Web Las clases que se utilizan para especificar tipos de retorno y tipos de parámetros para los métodos Web deben cumplir ciertos requerimientos: 1. Deben proporcionar un constructor public predeterminado, o uno sin parámetros. Cuando un servicio Web o el consumidor de un servicio Web reciben un objeto serializado en XML, el .NET Framework debe ser capaz de llamar a este constructor como parte del proceso de deserialización del objeto (es decir, convertirlo de vuelta en un objeto en C#). 2. Las propiedades y las variables de instancia que deben serializarse en formato XML deben declararse public. (Tenga en cuenta que las propiedades public pueden usarse para proporcionar acceso a las variables de instancia private.) 3. Las propiedades que se van a serializar deben proporcionar descriptores de acceso aunque tengan cuerpos vacíos). Las propiedades de sólo lectura no se serializan.

get

y

set

(incluso

Cualquier información que no esté serializada simplemente recibe su valor predeterminado (o el valor proporcionado por el constructor predeterminado o sin parámetros) cuando se deserializa un objeto de la clase.

Error común de programación 22.3 Si no se define un constructor public predeterminado o sin parámetros para un tipo que se va a pasar a un método Web, o un tipo que este método va a devolver, se produce un error en tiempo de ejecución.

Error común de programación 22.4 Si se define sólo el descriptor de acceso get o el descriptor de acceso set de una propiedad, para un tipo definido por el usuario que se va a pasar a un método Web, o un tipo que este método va a devolver, la propiedad será inaccesible para el cliente.

Observación de ingeniería de software 22.1 Los clientes de un servicio Web sólo pueden acceder a los miembros public de ese servicio. El programador puede proporcionar propiedades public para permitir el acceso a los datos private.

Definición de la clase Ecuacion En la figura 22.26 definimos la clase Ecuacion. Las líneas 28-46 definen un constructor que recibe tres argumentos: dos valores int que representan a los operandos izquierdo y derecho, y un objeto string que representa la operación aritmética a realizar. El constructor establece las variables de instancia operandoIzquierdo, operandoDerecho y tipoOperacion, y después calcula el resultado apropiado. El constructor sin parámetros (líneas 21-25) llama al constructor de tres argumentos (líneas 28-46) y le pasa algunos valores predeterminados. No utilizamos el constructor sin parámetros en forma explícita, pero el mecanismo de serialización XML lo utiliza cuando se deserializan objetos de esta clase. Debido a que proporcionamos un constructor con parámetros, debemos definir explícitamente el constructor sin parámetros en esta clase, para que los objetos de la misma puedan pasarse a los métodos Web, o que estos métodos puedan devolverlos.

1 2 3 4 5 6 7 8 9 10

// Fig. 22.26: Ecuacion.cs // Clase Ecuacion que contiene información acerca de una ecuación. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts;

Figura 22.26 | Clase que almacena la información de la ecuación. (Parte 1 de 4).

908

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69

Capítulo 22 Servicios Web

using System.Web.UI.HtmlControls; public class Ecuacion { private int operandoIzquierdo; // número private int operandoDerecho; // número a private int valorResultado; // resultado private string tipoOperacion; // tipo de

a la izquierda del operador la derecha del operador de la operación la operación

// constructor predeterminado requerido public Ecuacion() : this( 0, 0, "+" ) { // cuerpo vacío } // fin del constructor predeterminado // constructor con tres argumentos para la clase Ecuacion public Ecuacion( int valorIzq, int valorDer, string tipo ) { operandoIzquierdo = valorIzq; operandoDerecho = valorDer; tipoOperacion = tipo; switch ( tipoOperacion ) // realiza la operación apropiada { case "+": // suma valorResultado = operandoIzquierdo + operandoDerecho; break; case "-": // resta valorResultado = operandoIzquierdo - operandoDerecho; break; case "*": // multiplicación valorResultado = operandoIzquierdo * operandoDerecho; break; } // fin de switch } // fin del constructor con tres argumentos // devuelve representación string del objeto Ecuacion public override string ToString() { return operandoIzquierdo.ToString() + " " + tipoOperacion + " " + operandoDerecho.ToString() + " = " + valorResultado.ToString(); } // fin del método ToString // propiedad que devuelve un objeto string que representa el lado izquierdo public string LadoIzquierdo { get { return operandoIzquierdo.ToString() + " " + tipoOperacion + " " + operandoDerecho.ToString(); } // fin de get set // descriptor de acceso set requerido { // cuerpo vacío } // fin de set } // fin de la propiedad LadoIzquierdo

Figura 22.26 | Clase que almacena la información de la ecuación. (Parte 2 de 4).

22.7 Tipos definidos por el usuario en los servicios Web

70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128

// propiedad que devuelve un objeto string que representa el lado derecho public string LadoDerecho { get { return valorResultado.ToString(); } // fin de get set // descriptor de acceso set requerido { // cuerpo vacío } // fin de set } // fin de la propiedad LadoDerecho // propiedad para acceder al operando izquierdo public int Izquierdo { get { return operandoIzquierdo; } // fin de get set { operandoIzquierdo = value; } // fin de set } // fin de la propiedad Izquierdo // propiedad para acceder al operando derecho public int Derecho { get { return operandoDerecho; } // fin de get set { operandoDerecho = value; } // fin de set } // fin de la propiedad Derecho // propiedad para acceder al resultado de aplicar // una operación a los operandos izquierdo y derecho public int Resultado { get { return valorResultado; } // fin de get set { valorResultado = value; } // fin de set } // fin de la propiedad Resultado // propiedad para acceder a la operación public string Operacion

Figura 22.26 | Clase que almacena la información de la ecuación. (Parte 3 de 4).

909

910

Capítulo 22 Servicios Web

129 { 130 get 131 { 132 return tipoOperacion; 133 } // fin de get 134 135 set 136 { 137 tipoOperacion = value; 138 } // fin de set 139 } // fin de la propiedad Operacion 140 } // fin de la clase Ecuacion

Figura 22.26 | Clase que almacena la información de la ecuación. (Parte 4 de 4). La clase Ecuacion define las propiedades LadoIzquierdo (líneas 56-68), LadoDerecho (líneas 71-82), Izquierdo (líneas 85-96), Derecho (líneas 99-110), Resultado (líneas 114-125) y Operacion (líneas 128-139). El cliente del servicio Web no necesita modificar los valores de las propiedades LadoIzquierdo y LadoDerecho. No obstante, recuerde que una propiedad puede serializarse sólo si tiene los descriptores de acceso get y set; esto es cierto aun cuando el descriptor de acceso set tiene un cuerpo vacío. LadoIzquierdo (líneas 56-68) devuelve un objeto string que representa todo lo que hay a la izquierda del signo igual (=) en la ecuación, y LadoDerecho (líneas 71-82) devuelve un objeto string que representa todo lo que hay a la derecha del signo igual (=). Izquierdo (líneas 85-96) devuelve el objeto int a la izquierda del operador (que se denomina operador izquierdo), y Derecho (líneas 99-110) devuelve el objeto int a la derecha del operador (que se denomina operando derecho). Resultado (líneas 114125) devuelve la solución a la ecuación, y Operacion (líneas 128-139) devuelve el operador en la ecuación. El cliente en este caso de estudio no utiliza la propiedad LadoDerecho, pero la incluimos en caso de que otros clientes elijan usarla a futuro.

Creación del servicio Web GeneradorEcuaciones La figura 22.27 presenta el servicio Web GeneradorEcuaciones, el cual crea al azar objetos Ecuacion personalizados. Este servicio Web sólo contiene el método GenerarEcuacion (líneas 16-32), el cual recibe dos parámetros: un objeto string que representa a la operación matemática (suma, resta o multiplicación) y un objeto int que representa el nivel de dificultad. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

// Fig. 22.27: Generador.cs // Servicio Web para generar ecuaciones al azar, con base en una // operación y un nivel de dificultad especificados. using System; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; [ WebService( Namespace = "http://www.deitel.com/", Description "Servicio Web que genera una ecuación matemática." ) ] [ WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 ) public class Generador : System.Web.Services.WebService { // Método para generar una ecuación matemática [ WebMethod( Description = "Método para generar una ecuación public Ecuacion GenerarEcuacion( string operacion, int nivel { // encuentra los números máximo y mínimo a utilizar int maximo = Convert.ToInt32(Math.Pow( 10, nivel)); int minimo = Convert.ToInt32(Math.Pow( 10, nivel - 1 ) );

Figura 22.27 | Servicio Web que genera ecuaciones al azar. (Parte 1 de 2).

= ]

matemática." ) ] )

22.7 Tipos definidos por el usuario en los servicios Web

21 22 23 24 25 26 27 28 29 30 31 32 33

911

// objeto para generar números aleatorios Random objetoAleatorio = new Random(); // crea una ecuación que consiste en dos números // aleatorios, entre los parámetros mínimo y máximo Ecuacion ecuacion = new Ecuacion( objetoAleatorio.Next( minimo, maximo ), objetoAleatorio.Next( minimo, maximo ), operacion ); return ecuacion; } // fin del método GenerarEcuacion } // fin de la clase Generador

Figura 22.27 | Servicio Web que genera ecuaciones al azar. (Parte 2 de 2).

Prueba del servicio Web GeneradorEcuaciones La figura 22.28 muestra el resultado de evaluar el servicio Web GeneradorEcuaciones. Observe que el valor de retorno de nuestro método Web está codificado en XML. No obstante, este ejemplo difiere de los anteriores, en cuanto a que el XML especifica los valores para todas las propiedades y datos public del objeto que se va a devolver. El objeto de retorno está serializado en XML. Nuestra clase proxy toma este valor de retorno y lo deserializa en un objeto de la clase Ecuacion, para después pasarlo al cliente. a) Invocación del método GenerarEcuacion para

crear una ecuación de resta con números de dos dígitos.

b) Resultados codificados en XML de la invocación del método GenerarEcuacion, para crear una ecuación de resta con números de dos dígitos.

Figura 22.28 | Devolución de un objeto serializado XML de un método Web.

912

Capítulo 22 Servicios Web

Observe que el objeto Ecuacion no se pasa entre el servicio Web y el cliente, sino que la información en el objeto se envía como datos codificados en XML. Los clientes creados en .NET recibirán la información y crearán un nuevo objeto Ecuacion. Sin embargo, los clientes creados en otras plataformas pueden utilizar la información de manera distinta. Los lectores que creen clientes en otras plataformas deben revisar la documentación de los servicios Web para la plataforma específica que estén usando, para ver cómo sus clientes pueden procesar los tipos personalizados. Examinaremos el método Web GenerarEcuacion más de cerca. Las líneas 19-20 de la figura 22.27 definen los límites superior e inferior para los números aleatorios que utiliza el método para generar una Ecuacion. Para establecer estos límites, el programa llama primero al método static Pow de la clase Math; este método eleva su primer argumento a la potencia de su segundo argumento. Para calcular el valor de maximo (el límite superior para cualquier número generado al azar, que se utilice para formar una Ecuacion), el programa eleva el número 10 a la potencia del argumento nivel especificado (línea 19). Si nivel es 1, maximo es 10; si nivel es 2, maximo es 100; y si nivel es 3, maximo es 1000. El valor de la variable minimo se determina elevando 10 a una potencia menor que nivel (línea 20). Esto calcula el número más pequeño con un número de dígitos determinado por nivel. Si nivel es 1, minimo es 1; si nivel es 2, minimo es 10; y si nivel es 3, minimo es 100. Las líneas 27-29 crean un nuevo objeto Ecuacion. El programa llama al método Next de Random, el cual devuelve un int que es mayor o igual que el límite inferior especificado, pero menor que el límite superior especificado. Este método genera un valor del operando izquierdo que es mayor o igual a minimo, pero menor que maximo (es decir, un número con nivel dígitos). El operando derecho es otro número aleatorio con las mismas características. La línea 29 pasa el objeto string operacion, que recibe GenerarEcuacion, al constructor de Ecuacion. La línea 31 devuelve el nuevo objeto Ecuacion al cliente.

Consumo del servicio Web GeneradorEcuaciones La aplicación TutorMatematicas (figura 22.29) utiliza el servicio Web GeneradorEcuaciones. La aplicación llama al método GenerarEcuacion del servicio Web para crear un objeto Ecuacion. Después, el tutor muestra el lado izquierdo de la Ecuacion y espera la entrada del usuario. Este ejemplo accede a las clases Generador y Ecuacion del espacio de nombres localhost; ambas se colocan en este espacio de nombres de manera predeterminada, cuando se genera el proxy. En las líneas 22-23 declaramos variables de estos tipos. La línea 23 también crea el proxy de Generador. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// Fig. 22.29: TutorMatematicas.cs // Programa para enseñar matemáticas, que utiliza un servicio Web para generar ecuaciones al azar. using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace TutorMatematicas { public partial class TutorMatematicasForm : Form { public TutorMatematicasForm() { InitializeComponent(); } // fin del constructor private private private private

string operacion = "+"; int nivel = 1; localhost.Ecuacion ecuacion; localhost.Generador generador = new localhost.Generador();

Figura 22.29 | Aplicación para enseñar matemáticas. (Parte 1 de 4).

22.7 Tipos definidos por el usuario en los servicios Web

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82

// genera una nueva ecuación cuando el usuario hace clic en el botón private void generarButton_Click( object sender, EventArgs e ) { // genera ecuación usando la operación y el nivel actuales ecuacion = generador.GenerarEcuacion( operacion, nivel ); // muestra el lado izquierdo de la ecuación preguntaLabel.Text = ecuacion.LadoIzquierdo; aceptarButton.Enabled = true; respuestaTextBox.Enabled = true; } // fin del método generarButton_Click // verifica la respuesta del usuario private void aceptarButton_Click( object sender, EventArgs e ) { // determina el resultado correcto del objeto Ecuacion int respuesta = ecuacion.Resultado; if ( respuestaTextBox.Text == "" ) return; // obtiene la respuesta del usuario int respuestaUsuario = Int32.Parse( respuestaTextBox.Text ); // determina si la respuesta del usuario es correcta if ( respuesta == respuestaUsuario ) { preguntaLabel.Text = ""; // borra la pregunta respuestaTextBox.Text = ""; // borra la respuesta aceptarButton.Enabled = false; // deshabilita botón Aceptar MessageBox.Show( "¡Correcto! ¡Buen trabajo!" ); } // fin de if else MessageBox.Show( "Incorrecto. Intente de nuevo." ); } // fin del método aceptarButton_Click // establece el nivel de dificultad a 1 private void nivelUnoRadioButton_CheckedChanged( object sender, EventArgs e ) { nivel = 1; } // fin del método nivelUnoRadioButton_CheckedChanged // establece el nivel de dificultad a 2 private void nivelDosRadioButton_CheckedChanged( object sender, EventArgs e ) { nivel = 2; } // fin del método nivelDosRadioButton_CheckedChanged // establece el nivel de dificultad a 3 private void nivelTresRadioButton_CheckedChanged( object sender, EventArgs e ) { nivel = 3; } // fin del método nivelTresRadioButton_CheckedChanged

Figura 22.29 | Aplicación para enseñar matemáticas. (Parte 2 de 4).

913

914

Capítulo 22 Servicios Web

83 // establece la operación a suma 84 private void sumaRadioButton_CheckedChanged( object sender, 85 EventArgs e ) 86 { 87 operacion = "+"; 88 generarButton.Text = 89 "Generar ejemplo de " + sumaRadioButton.Text; 90 } // fin del método sumaRadioButton_CheckedChanged 91 92 // establece la operación a resta 93 private void restaRadioButton_CheckedChanged( object sender, 94 EventArgs e ) 95 { 96 operacion = "-"; 97 generarButton.Text = "Generar ejemplo de " + 98 restaRadioButton.Text; 99 } // fin del método restaRadioButton_CheckedChanged 100 101 // establece la operación a multiplicación 102 private void multiplicacionRadioButton_CheckedChanged( 103 object sender, EventArgs e ) 104 { 105 operacion = "*"; 106 generarButton.Text = "Generar ejemplo de " + 107 multiplicacionRadioButton.Text; 108 } // fin del método multiplicacionRadioButton_CheckedChanged 109 } // fin de la clase TutorMatematicasForm 110 } // fin del espacio de nombres TutorMatematicas a) Generación de una ecuación de suma, nivel 1

b) Respuesta incorrecta a la ecuación.

c) Respuesta correcta a la ecuación.

Figura 22.29 | Aplicación para enseñar matemáticas. (Parte 3 de 4).

22.9 Recursos Web

915

d) Generación de una ecuación de multiplicación, nivel 2.

Figura 22.29 | Aplicación para enseñar matemáticas. (Parte 4 de 4). La aplicación tutor de matemáticas muestra una ecuación y espera a que el usuario escriba una respuesta. La opción predeterminada para el nivel de dificultad es 1, pero el usuario puede modificarla eligiendo un nivel de los controles RadioButton que están dentro del control GroupBox con la leyenda Dificultad. Al hacer clic en cualquiera de los niveles se invoca el manejador de eventos CheckedChanged del correspondiente control RadioButton (líneas 63-81), el cual establece el entero nivel al nivel seleccionado por el usuario. Aunque la opción predeterminada para el tipo de pregunta es Suma, el usuario también puede modificar esto si selecciona uno de los controles RadioButton dentro del control GroupBox con la leyenda Operación. Al hacer esto, invoca a los correspondientes manejadores de eventos en las líneas 84-108. que asignan al objeto string llamado operacion el símbolo correspondiente a la selección del usuario. Cada manejador de eventos también actualiza la propiedad Texto del botón Generar para que coincida con la operación recién seleccionada. El manejador de eventos generarButton_Click (líneas 26-36) invoca al método GenerarEcuacion de GeneradorEcuaciones (línea 29). Después de recibir un objeto Ecuación del servicio Web, el manejador muestra el lado izquierdo de la ecuación en preguntaLabel (línea 32) y habilita aceptarButton, de manera que el usuario pueda escribir una respuesta. Cuando el usuario hace clic en Aceptar, aceptarButton_Click (líneas 39-60) verifica que el usuario haya proporcionado la respuesta correcta.

22.8 Conclusión En este capítulo presentamos los servicios Web ASP.NET: una tecnología que permite a los usuarios solicitar y recibir datos a través de Internet, y que promueve la reutilización de software en sistemas distribuidos. En este capítulo aprendió que un servicio Web es una clase que permite a los equipos cliente llamar a los métodos del servicio Web en forma remota, a través de formatos de datos y protocolos comunes, como XML, HTTP y SOAP. Hablamos sobre varios beneficios de este tipo de computación distribuida; por ejemplo, los clientes pueden acceder a ciertos datos en equipos remotos, y los clientes que carecen del poder de procesamiento necesario para realizar cálculos específicos pueden aprovechar los recursos de los equipos remotos. Explicamos cómo Visual C# 2005, Visual Web Developer 2005 y el .NET Framework facilitan la creación y el consumo de los servicios Web. Aprendió a definir los servicios Web y los métodos Web, así como a consumirlos desde aplicaciones Windows y aplicaciones Web ASP.NET. Después de explicar la mecánica de los servicios Web a través de nuestro ejemplo EnteroEnorme, demostramos servicios Web más sofisticados, que utilizan rastreo de sesiones y tipos definidos por el usuario. En el siguiente capítulo, hablaremos sobre los detalles de bajo nivel relacionados con las redes computacionales. Le mostraremos cómo implementar servidores y clientes que se comunican entre sí, cómo enviar y recibir datos mediante sockets (que simplifican dichas transmisiones de tal forma que es como si se realizaran operaciones de lectura/escritura con archivos, respectivamente) y cómo crear un servidor con subprocesamiento múltiple para jugar una versión en red del popular juego Tres en raya.

22.9 Recursos Web Además de los recursos Web que se muestran aquí, es conveniente que consulte los recursos Web relacionados con ASP.NET, que se proporcionan al final del capítulo 21. msdn.microsoft.com/webservices

El Centro para desarrolladores de servicios Web de Microsoft incluye las especificaciones y hojas técnicas sobre la tecnología de los servicios Web .NET, así como artículos, columnas y vínculos sobre XML/SOAP.

916

Capítulo 22 Servicios Web

www.webservices.org

Este sitio proporciona noticias, artículos, recursos y vínculos relacionados con la industria, acerca de los servicios Web. www-130.ibm.com/developerworks/webservices

El sitio de IBM para la arquitectura orientada a servicios (SOA) y los servicios Web incluye artículos, descargas, demos y grupos de discusión, en relación con la tecnología de los servicios Web. www.w3.org/TR/wsdl

Este sitio proporciona una gran cantidad de información acerca de WSDL, incluyendo una discusión detallada sobre las tecnologías relacionadas con los servicios Web, como XML, SOAP, HTTP y los tipos MIME, dentro del contexto de WSDL. www.w3.org/TR/soap

Este sitio proporciona una gran cantidad de información sobre los mensajes SOAP, el uso de SOAP con HTTP y cuestiones de seguridad relacionadas con SOAP. www.uddi.com

El sitio de Integración, descubrimiento y descripción universal proporciona discusiones, especificaciones, hojas técnicas e información general acerca de UDDL. www.ws-i.org

El sitio Web de la Organización de interoperabilidad de servicios Web proporciona integración detallada en relación con los esfuerzos para generar servicios Web basados en estándares que promuevan la interoperabilidad y una verdadera independencia de plataformas. webservices.xml.com/security

Este sitio contiene artículos acerca de las cuestiones de seguridad relacionadas con los servicios Web, y los protocolos estándar de seguridad.

23

Redes: sockets basados en flujos y datagramas

OBJETIVOS En este capítulo aprenderá lo siguiente: Q

Implementar aplicaciones de red que utilicen sockets y datagramas.

Q

Implementar clientes y servidores que se comuniquen entre sí.

Q

Implementar aplicaciones de colaboración basada en red.

Q

Construir un servidor con subprocesamiento múltiple.

Q

Utilizar el control WebBrowser para agregar herramientas de exploración Web a cualquier aplicación.

Q

Utilizar la tecnología .NET Remoting para permitir que una aplicación que se ejecuta en una computadora invoque a los métodos de una aplicación que se ejecuta en otra computadora.

Si la presencia de electricidad puede hacerse visible en cualquier parte de un circuito, no veo por qué razón la inteligencia no pueda transmitirse en forma instantánea mediante la electricidad. —Samuel F. B. Moore

El protocolo es todo. —Francois Giuliani

Las redes de telecomunicaciones, información y computación son hoy lo que las redes de ferrocarriles, autopistas y canales fueron en otra época. —Bruno Kreisky

El puerto está cerca, escucho las campanas, toda la gente se regocija. —Walt Whitman

Pla n g e ne r a l

918

23.1 23.2 23.3 23.4 23.5 23.6 23.7 23.8 23.9 23.10 23.11

Capítulo 23 Redes: sockets basados en flujos y datagramas

Introducción Comparación entre la comunicación orientada a la conexión y la comunicación sin conexión Protocolos para transportar datos Establecimiento de un servidor TCP simple (mediante el uso de sockets de flujo) Establecimiento de un cliente TCP simple (mediante el uso de sockets de flujo) Interacción entre cliente/servidor mediante conexiones de sockets de flujo Interacción entre cliente/servidor sin conexión mediante datagramas Juego de Tres en raya cliente/servidor mediante el uso de un servidor con subprocesamiento múltiple Control WebBrowser .NET Remoting Conclusión

23.1 Introducción Hay mucho entusiasmo acerca de Internet y Web, ya que Internet enlaza toda la información a nivel mundial y la Web facilita el uso de Internet y le proporciona el toque atractivo del contenido multimedia. Las organizaciones ven a Internet y a la Web como algo crucial para las estrategias de sus sistemas de información. La FCL de .NET cuenta con una variedad de herramientas de red integradas, que facilitan el desarrollo de aplicaciones basadas en Internet y Web. Los programas pueden buscar en todo el mundo información y colaborar con programas que se ejecutan en otras computadoras a nivel internacional, nacional o sólo dentro de una organización. En el capítulo 21, ASP.NET 2.0, formularios Web Forms y controles Web, y en el capítulo 22, Servicios Web, empezamos nuestra presentación de las herramientas de redes y computación distribuida en C#. Hablamos sobre ASP.NET, los formularios Web Forms y los servicios Web: tecnologías de red de alto nivel que permiten a los programadores desarrollar aplicaciones distribuidas. En este capítulo nos enfocamos en las tecnologías de red subyacentes que dan soporte a las herramientas de C#, ASP.NET y los servicios Web. Este capítulo comienza con las generalidades de las técnicas y tecnologías de computación que se utilizan para transmitir datos a través de Internet. A continuación presentaremos los conceptos básicos acerca de cómo establecer una conexión entre dos aplicaciones que utilizan flujos de datos, similares a la E/S de archivos. Este enfoque orientado a las conexiones permite que los programas se comuniquen entre sí con la misma facilidad que se realizan las operaciones de escritura y lectura en los archivos en disco. Después presentaremos una aplicación simple de chat, en la que se utilizan estas técnicas para enviar mensajes entre un cliente y un servidor. El capítulo continuará con una presentación y un ejemplo de las técnicas sin conexión para transmitir datos entre aplicaciones, lo cual es menos confiable que establecer una conexión entre aplicaciones, pero mucho más eficiente. Dichas técnicas se utilizan por lo general en aplicaciones como audio y video en tiempo real a través de Internet. A continuación presentaremos un ejemplo de un juego de Tres en raya tipo cliente/servidor, el cual demuestra cómo crear un servidor simple con subprocesamiento múltiple. Después mostraremos el nuevo control WebBrowser para agregar herramientas de exploración Web a cualquier aplicación. El capítulo termina con una breve introducción a la tecnología .NET Remoting que, al igual que los servicios Web (capítulo 22), permite la computación distribuida a través de redes.

23.2 Comparación entre la comunicación orientada a la conexión y la comunicación sin conexión

Hay dos enfoques principales para las comunicaciones entre aplicaciones: orientadas a la conexión y sin conexión. Las comunicaciones orientadas a la conexión son similares al sistema telefónico, en el cual se establece una conexión y se mantiene el tiempo que dure la sesión. Los servicios sin conexión son similares al servicio postal, en el cual dos cartas enviadas desde el mismo lugar y hacia el mismo destino pueden tomar dos rutas drásticamente distintas a través del sistema, e incluso llegar a horas distintas, o no llegar. En un enfoque orientado a la conexión, las computadoras se envían una a otra información de control (a través de una técnica conocida como intercambio, o handshaking) para iniciar una conexión de un extremo al otro.

23.4 Establecimiento de un servidor TCP simple (mediante el uso de sockets de flujo)

919

Internet es una red no confiable, lo cual significa que los datos que se envían a través de esta red pueden dañarse o perderse. Los datos se envían en paquetes, que contienen piezas de los datos, junto con información que ayuda a Internet a enrutar los paquetes hacia el destino apropiado. Internet no garantiza nada acerca de los paquetes enviados; podrían llegar corruptos o desordenados, duplicados o incluso podrían no llegar. Internet sólo hace su “mejor esfuerzo” por entregar los paquetes. Un enfoque orientado a la conexión asegura que las comunicaciones sean confiables en redes no confiables, garantizando de esta forma que los paquetes enviados llegarán al receptor destinado sin daños, y se ensamblarán en la secuencia correcta. En un enfoque sin conexión, las dos computadoras no realizan el intercambio antes de la transmisión, por lo que no se garantiza la confiabilidad; los datos enviados tal vez nunca lleguen al receptor destinado. Sin embargo, un enfoque sin conexión evita la sobrecarga asociada con el intercambio y con el aseguramiento de la confiabilidad; a menudo se requiere pasar menos información entre un host y otro.

23.3 Protocolos para transportar datos

Existen muchos protocolos para la comunicación entre aplicaciones. Los protocolos son conjuntos de reglas que gobiernan la forma en que deben interactuar dos entidades. En este capítulo nos enfocaremos en el Protocolo de control de transmisión (TCP) y en el Protocolo de datagramas de usuario (UDP). Las herramientas de red TCP y UDP de .NET se definen en el espacio de nombres System.Net.Sockets. El Protocolo de control de transmisión (TCP ) es un protocolo de comunicación orientado a la conexión, el cual garantiza que los paquetes enviados llegarán al receptor destinado sin daños y en la secuencia correcta. TCP permite que protocolos como HTTP (capítulo 21) envíen información a través de una red, de una manera tan simple y confiable como escribir a un archivo en una computadora local. Si los paquetes de información no llegan al recipiente, TCP se asegura que los paquetes se envíen de nuevo. Si los paquetes llegan fuera de orden, TCP los vuelve a ensamblar en el orden correcto, en forma transparente para la aplicación receptora. Si llegan paquetes duplicados, TCP los descarta. Las aplicaciones que no requieren de la garantía de transmisión confiable de un extremo a otro que ofrece TCP, por lo general utilizan el Protocolo de datagramas de usuario (UDP ) sin conexión. UDP incurre en la mínima sobrecarga necesaria para la comunicación entre aplicaciones; no garantiza que los paquetes, llamados datagramas, lleguen a su destino o que lleguen en el orden original. Existen ciertas ventajas en cuanto al uso de UDP en comparación con TCP. UDP tiene poca sobrecarga, ya que los datagramas no necesitan llevar la información que llevan los paquetes TCP para asegurar la confiabilidad. Además, reduce el tráfico de red relativo a TCP, debido a la ausencia de intercambio, retransmisiones, etcétera. La comunicación no confiable es aceptable en muchas situaciones. En primer lugar, la confiabilidad no es necesaria para algunas aplicaciones, por lo que puede evitarse la sobrecarga impuesta por un protocolo que garantice la confiabilidad. En segundo lugar, algunas aplicaciones como el audio y video en tiempo real pueden tolerar la pérdida ocasional de datagramas. Esto por lo general produce una pequeña pausa (o “hipo”) en el audio o video que se está reproduciendo. Si la misma aplicación se ejecutara sobre TCP, un segmento perdido podría producir una pausa considerable, ya que el protocolo esperaría a que se retransmitiera el segmento perdido y se entregara en forma correcta antes de continuar. Por último, las aplicaciones que necesitan implementar sus propios mecanismos de confiabilidad distintos a los de TCP pueden crear tales mecanismos sobre UDP.

23.4 Establecimiento de un servidor TCP simple (mediante el uso de sockets de flujo) Por lo general, con TCP un servidor “espera” una solicitud de conexión de un cliente. A menudo, el programa servidor contiene una instrucción de control o un bloque de código que se ejecuta en forma continua, hasta que el servidor recibe una solicitud. Al recibir esta solicitud, el servidor establece una conexión con el cliente. Después, utiliza esta conexión (administrada por un objeto de la clase Socket) para manejar las próximas solicitudes de ese cliente y enviarle datos. Como los programas que se comunican a través de TCP procesan los datos que envían y reciben como flujos de bytes, en ocasiones los programadores se refieren a los objetos Socket como “Sockets de flujo”. Para establecer un servidor simple con TCP y sockets de flujo, se requieren cinco pasos. Primero, es necesario crear un objeto de la clase TcpListener del espacio de nombres System.Net.Sockets. Esta clase representa a

920

Capítulo 23 Redes: sockets basados en flujos y datagramas

un socket de flujo TCP, a través del cual un servidor puede escuchar en espera de solicitudes. Al crear un nuevo objeto TcpListener, como en TcpListener servidor = new TcpListener( direcciónIP, puerto );

se enlaza (asigna) la aplicación servidor al número de puerto especificado. Un número de puerto es un identificador numérico que utiliza una aplicación para identificarse a sí misma, en una dirección de red dada, también conocida como Dirección de protocolo Internet (Dirección IP ). Las direcciones IP identifican a las computadoras en Internet. De hecho, los nombres de los sitios Web como www.deitel.com son alias para las direcciones IP. Una dirección IP se representa mediante un objeto de la clase IPAddress, del espacio de nombres System.Net. Cualquier aplicación que utiliza redes se identifica a sí misma a través de un par dirección IP/número de puerto; dos aplicaciones no pueden tener el mismo número de puerto, en una dirección IP dada. Por lo general, no es necesario enlazar en forma explícita un socket a un puerto de conexión (usando el método Bind de la clase Socket), ya que la clase TcpListener y otras clases que veremos en este capítulo lo hacen en forma automática, junto con otras operaciones de inicialización de sockets.

Observación de ingeniería de software 23.1 Los números de puerto pueden tener valores entre 0 y 65535. Muchos sistemas operativos reservan los números de puerto menores a 1024 para los servicios del sistema (como los servidores Web y de correo electrónico). Las aplicaciones deben recibir privilegios especiales para poder usar estos números de puerto reservados.

Para recibir solicitudes, TcpListener primero debe escuchar en espera de ellas. El segundo paso en el proceso de conexión es llamar al método Start de TcpListener, el cual hace que el objeto TcpListener empiece a escuchar en espera de solicitudes de conexión. El servidor escucha en forma indefinida en espera de una solicitud; es decir, la ejecución de la aplicación del lado servidor espera hasta que algún cliente trata de conectarse con ella. El servidor crea una conexión con el cliente cuando recibe una solicitud de conexión. Un objeto de la clase Socket (espacio de nombres System.Net.Sockets) administra una conexión con un cliente. El método AcceptSocket de la clase TcpListener acepta una solicitud de conexión. Este método devuelve un objeto Socket al momento de la conexión, como en la siguiente instrucción: Socket conexion = servidor.AcceptSocket();

Cuando el servidor recibe una solicitud, AcceptSocket llama al método Accept del Socket subyacente de TcpListener para realizar la conexión. Éste es un ejemplo de cómo se oculta al programador la complejidad del trabajo en red. Usted simplemente coloca la instrucción anterior en un programa del lado servidor; las clases del espacio de nombres System.Net.Sockets se encargan de los detalles relacionados con la aceptación de solicitudes y el establecimiento de conexiones. El tercer paso establece los flujos que se utilizan para la comunicación con el cliente. En este paso, creamos un objeto NetworkStream que utiliza el objeto Socket (que representa la conexión) para realizar el envío y la recepción de los datos. En nuestro siguiente ejemplo, utilizamos este objeto NetworkStream para crear un BinaryWriter y un BinaryReader, que se utilizarán para enviar información hacia, y recibir información desde el cliente, respectivamente. El paso cuatro es la fase de procesamiento, en la cual el servidor y el cliente se comunican mediante la conexión establecida en el tercer paso. En esta fase, el cliente utiliza el método Write de BinaryWriter y el método ReadString de BinaryReader para realizar las comunicaciones apropiadas. El quinto paso es la fase de terminación de la conexión. Cuando el cliente y el servidor terminan de comunicarse, el servidor llama al método Close de BinaryReader, BinaryWriter, NetworkStream y Socket para terminar la conexión. Entonces, el servidor puede regresar al paso dos para esperar la siguiente solicitud de conexión. La documentación para la clase Socket recomienda que se llame al método Shutdown antes del método Close, para asegurar que todos los datos se envíen y se reciban antes de que se cierre el Socket. Un problema asociado con el esquema de servidor descrito en esta sección es que el paso cuatro bloquea las demás solicitudes mientras procesa la solicitud del cliente conectado, por lo que ningún otro cliente puede conectarse con el servidor mientras se esté ejecutando el código que define a la fase de procesamiento. La técnica más común para tratar este problema es utilizar servidores con subprocesamiento múltiple, los cuales colocan el

23.5 Establecimiento de un cliente TCP simple (mediante el uso de sockets de flujo)

921

código de la fase de procesamiento en un subproceso separado. Para cada solicitud de conexión que recibe el servidor, crea un objeto Thread para procesar la conexión, dejando a su objeto TcpListener (o Socket) libre para recibir otras conexiones. En la sección 23.8 demostramos un servidor con subprocesamiento múltiple.

Observación de ingeniería de software 23.2 Los servidores con subprocesamiento múltiple pueden administrar con eficiencia conexiones simultáneas con varios clientes. Esta arquitectura es precisamente la que utilizan los populares servidores UNIX y Windows para trabajo en red.

Observación de ingeniería de software 23.3 Puede implementarse un servidor con subprocesamiento múltiple para crear un subproceso que administre la E/S de red a través de un objeto Socket devuelto por el método AcceptSocket. Un servidor con subprocesamiento múltiple también puede implementarse para mantener una reserva de subprocesos que administren la E/S de red a través de objetos Socket recién creados.

Tip de rendimiento 23.1 En los sistemas de alto rendimiento con mucha memoria, puede implementarse un servidor con subprocesamiento múltiple para crear una reserva de subprocesos. Estos subprocesos pueden asignarse rápidamente para manejar la E/S de red a través de varios objetos Socket. Por ende, cuando se recibe una conexión, el servidor no incurre en la sobrecarga de la creación de subprocesos.

23.5 Establecimiento de un cliente TCP simple (mediante el uso de sockets de flujo) Hay cuatro pasos para crear un cliente TCP simple. Primero es necesario crear un objeto de la clase TcpClient (espacio de nombres System.Net.Sockets) para conectarse al servidor. La conexión se establece llamando al método Connect de TcpClient. Una versión sobrecargada de este método recibe dos argumentos: la dirección IP del servidor y su número de puerto, como en: TcpClient cliente = new TcpClient(); cliente.Connect( direcciónServidor, puertoServidor );

El puertoServidor es un objeto int que representa el número de puerto al que está enlazada la aplicación servidor para escuchar en espera de las solicitudes de conexión. La direcciónServidor puede ser una instancia de IPAddress (que encapsula la dirección IP del servidor) o un objeto string que especifique el nombre de host del servidor o su dirección IP. El método Connect también tiene una versión sobrecargada, a la cual se le puede pasar un objeto IPEndPoint que representa un par direcciónIP/número de puerto. El método Connect de TcpClient llama al método Connect de Socket para establecer la conexión. Si hubo éxito, el método Connect de TcpClient devuelve un entero positivo; en caso contrario, devuelve 0. En el paso dos, el objeto TcpClient usa su método GetStream para obtener un objeto NetworkStream, de manera que pueda realizar operaciones de lectura y escritura con el servidor. Después usamos el objeto NetworkStream para crear un objeto BinaryWriter y un objeto BinaryReader, los cuales se utilizarán para enviar información hacia, y recibir información desde el servidor, respectivamente. El tercer paso es la fase de procesamiento, en la que se comunican el cliente y el servidor. En esta fase de nuestro ejemplo, el cliente utiliza el método Write de BinaryWriter y el método ReadString de BinaryReader para realizar las comunicaciones apropiadas. Mediante el uso de un proceso similar al que utilizan los servidores, un cliente puede emplear subprocesos para evitar el bloqueo de la comunicación con otros servidores, mientras se procesan los datos de una conexión. Una vez que se completa la transmisión, el paso cuatro requiere que el cliente cierre la conexión, llamando al método Close de BinaryReader, BinaryWriter, NetworkStream y TcpClient. Esto cierra cada uno de los flujos y el objeto Socket de TcpClient para terminar la conexión con el servidor. En este punto, puede establecerse una nueva conexión a través del método Connect, como describimos antes.

922

Capítulo 23 Redes: sockets basados en flujos y datagramas

23.6 Interacción entre cliente/servidor mediante conexiones de sockets de flujo Las aplicaciones de las figuras 23.1 y 23.2 utilizan las clases y las técnicas descritas en las dos secciones anteriores para crear una aplicación de chat cliente/servidor simple. El servidor espera una solicitud del cliente para hacer una conexión. Cuando una aplicación cliente se conecta al servidor, la aplicación servidor envía, al cliente, un arreglo de bytes, indicando que la conexión tuvo éxito. Después, el cliente muestra un mensaje, notificando al usuario que se estableció una conexión. Tanto la aplicación cliente como la aplicación servidor contienen controles TextBox que permiten a los usuarios escribir mensajes y enviarlos a la otra aplicación. Cuando el cliente o el servidor envían el mensaje “TERMINAR”, termina la conexión entre el cliente y el servidor. Después, el servidor espera a que otro cliente solicite una conexión. Las figuras 23.1 y 23.2 proporcionan el código para las clases Servidor y Cliente, respectivamente. La figura 23.2 también contiene capturas de pantalla que muestran la ejecución entre el cliente y el servidor.

Clase ServidorChatForm Empezaremos por ver la clase ServidorChatForm (figura 23.1). En el constructor, la línea 27 crea un objeto Thread que acepta conexiones de los clientes. El objeto delegado ThreadStart que se pasa como argumento para el constructor especifica el método que el objeto Thread debe ejecutar. La línea 28 empieza el objeto Thread, el cual utiliza al delegado ThreadStart para invocar al método EjecutarServidor (líneas 104-179). Este método inicializa el servidor para que reciba solicitudes de conexión y las procese. La línea 115 crea una instancia de un objeto TcpListener, para que escuche en espera de una solicitud de conexión de un cliente en el puerto 50000 (paso 1). La línea 118 llama después al método Start de TcpListener, lo cual hace que el objeto TcpListener empiece a esperar solicitudes ( paso 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

// Fig. 23.1: ServidorChat.cs // Establece un servidor que recibe una conexión de un cliente, le envía // una cadena, chatea con él y cierra la conexión. using System; using System.Windows.Forms; using System.Threading; using System.Net; using System.Net.Sockets; using System.IO; public partial class ServidorChatForm : Form { public ServidorChatForm() { InitializeComponent(); } // fin del constructor private private private private private

Socket conexion; // Socket para aceptar una conexión Thread lecturaThread; // Thread para procesar los mensajes entrantes NetworkStream socketStream; // flujo de datos de red BinaryWriter escritor; // facilita la escritura en el flujo BinaryReader lector; // facilita la lectura del flujo

// inicializa el subproceso para la lectura private void ServidorChatForm_Load( object sender, EventArgs e ) { lecturaThread = new Thread( new ThreadStart( EjecutarServidor ) ); lecturaThread.Start(); } // fin del método ServidorChatForm_Load

Figura 23.1 | Parte correspondiente al servidor de una conexión cliente/servidor con sockets de flujo. (Parte 1 de 4).

23.6 Interacción entre cliente/servidor mediante conexiones de sockets de flujo

30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88

923

// cierra todos los subprocesos asociados con esta aplicación private void ServidorChatForm_FormClosing( object sender, FormClosingEventArgs e ) { System.Environment.Exit( System.Environment.ExitCode ); } // fin del método CharServerForm_FormClosing // delegado que permite que se haga una llamada al método MostrarMensaje // en el subproceso que crea y mantiene la GUI private delegate void DisplayDelegate( string mensaje ); // el método DisplayDelegate establece la propiedad Text de mostrarTextBox // en forma segura para los subprocesos private void MostrarMensaje( string mensaje ) { // si la modificación de mostrarTextBox no es segura para el subproceso if ( mostrarTextBox.InvokeRequired ) { // usa el método Invoke heredado para ejecutar MostrarMensaje // a través de un delegado Invoke( new DisplayDelegate( MostrarMensaje ), new object[] { mensaje } ); } // fin de if else // Se puede modificar mostrarTextBox en el subproceso actual mostrarTextBox.Text += mensaje; } // fin del método MostrarMensaje // delegado que permite llamar al método DeshabilitarSalida // en el subprofeso que crea y mantiene la GUI private delegate void DisableInputDelegate( bool value ); // el método DeshabilitarSalida establece la propiedad ReadOnly de entradaTextBox // de una manera segura para los subprocesos private void DeshabilitarEntrada( bool valor ) { // si la modificación de entrarTextBox no es segura para el subproceso if ( entradaTextBox.InvokeRequired ) { // usa el método heredado Invoke para ejecutar DeshabilitarEntrada // a través de un delegado Invoke( new DisableInputDelegate( DeshabilitarEntrada ), new object[] { valor } ); } // fin de if else // Se puede modificar entradaTextBox en el subproceso actual entradaTextBox.ReadOnly = valor; } // fin del método DeshabilitarEntrada // envía al cliente el texto escrito en el servidor private void entradaTextBox_KeyDown( object sender, KeyEventArgs e ) { // envía el texto al cliente try { if ( e.KeyCode == Keys.Enter && entradaTextBox.ReadOnly == false ) { escritor.Write( "SERVIDOR>>> " + entradaTextBox.Text ); mostrarTextBox.Text += "\r\nSERVIDOR>>> " + entradaTextBox.Text;

Figura 23.1 | Parte correspondiente al servidor de una conexión cliente/servidor con sockets de flujo. (Parte 2 de 4).

924

89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147

Capítulo 23 Redes: sockets basados en flujos y datagramas

// si el usuario en el servidor indicó la terminación // de la conexión con el cliente if ( entradaTextBox.Text == "TERMINAR" ) conexion.Close(); entradaTextBox.Clear(); // borra la entrada del usuario } // fin de if } // fin de try catch ( SocketException ) { mostrarTextBox.Text += "\nError al escribir objeto"; } // fin de catch } // fin del método entradaTextBox_KeyDown // permite que un cliente se conecte; muestra el texto que envía el cliente public void EjecutarServidor() { TcpListener oyente; int contador = 1; // espera la conexión de un cliente y muestra el texto // que envía el cliente try { // Paso 1: crea TcpListener IPAddress local = IPAddress.Parse( "127.0.0.1" ); oyente = new TcpListener( local, 50000 ); // Paso 2: TcpListener espera la solicitud de conexión oyente.Start(); // Paso 3: establece la conexión con base en la solicitud del cliente while ( true ) { MostrarMensaje( "Esperando una conexión\r\n" ); // acepta una conexión entrante conexion = oyente.AcceptSocket(); // crea objeto NetworkStream asociado con el socket socketStream = new NetworkStream( conexion ); // crea objetos para transferir datos a través de un flujo escritor = new BinaryWriter( socketStream ); lector = new BinaryReader( socketStream ); MostrarMensaje( "Conexión " + contador + " recibida.\r\n" ); // informa al cliente que la conexión fue exitosa escritor.Write( "SERVIDOR>>> Conexión exitosa" ); DeshabilitarEntrada( false ); // habilita entradaTextBox string laRespuesta = ""; // Paso 4: lee los datos de cadena que envía el cliente do { try

Figura 23.1 | Parte correspondiente al servidor de una conexión cliente/servidor con sockets de flujo. (Parte 3 de 4).

23.6 Interacción entre cliente/servidor mediante conexiones de sockets de flujo

925

148 { 149 // lee la cadena que se envía al cliente 150 laRespuesta = lector.ReadString(); 151 152 // muestra el mensaje 153 MostrarMensaje( "\r\n" + laRespuesta ); 154 } // fin de try 155 catch ( Exception ) 156 { 157 // maneja la excepción si hay error al leer los datos 158 break; 159 } // fin de catch 160 } while ( laRespuesta != "CLIENTE>>> TERMINAR" && 161 conexion.Connected ); 162 163 MostrarMensaje( "\r\nEl usuario terminó la conexión\r\n" ); 164 165 // Paso 5: cierra la conexión 166 escritor.Close(); 167 lector.Close(); 168 socketStream.Close(); 169 conexion.Close(); 170 171 DeshabilitarEntrada( true ); // deshabilita entradaTextBox 172 contador++; 173 } // fin de while 174 } // fin de try 175 catch ( Exception error ) 176 { 177 MessageBox.Show( error.ToString() ); 178 } // fin de catch 179 } // fin del método EjecutarServidor 180 } // fin de la clase ServidorChatForm

Figura 23.1 | Parte correspondiente al servidor de una conexión cliente/servidor con sockets de flujo. (Parte 4 de 4).

Aceptar la conexión y establecer los flujos Las líneas 121-173 declaran un ciclo infinito que empieza estableciendo la conexión solicitada por el cliente (paso 3). La línea 126 llama al método AcceptSocket del objeto TcpListener, el cual devuelve un objeto Socket al momento en que se realiza la conexión. El subproceso en el que se hace la llamada al método Accept se bloquea (es decir, se deja de ejecutar) hasta que se establece una conexión. El objeto Socket devuelto administra la conexión. La línea 129 pasa este objeto Socket como un argumento para el constructor de un objeto NetworkStream, el cual proporciona acceso a los flujos a través de una red. En este ejemplo, el objeto NetworkStream utiliza los flujos del Socket especificado. Las líneas 132-133 crean instancias de las clases BinaryWriter y BinaryReader para escribir y leer datos. Pasamos el objeto NetworkStream como un argumento para cada constructor; BinaryWriter puede escribir bytes en el objeto NetworkStream, y BinaryReader puede leer bytes del objeto NetworkStream. La línea 135 llama a MostrarMensaje, indicando que se recibió una conexión. A continuación, enviamos un mensaje al cliente indicando que se recibió la conexión. El método Write de BinaryWriter tiene muchas versiones sobrecargadas que escriben datos de varios tipos a un flujo. La línea 138 utiliza el método Write para enviar un objeto string al cliente, notificándole acerca de la conexión exitosa. Esto completa el paso 3.

Recibir mensajes del cliente A continuación empezaremos la fase de procesamiento (paso 4). Las líneas 145-161 declaran una instrucción do…while que se ejecuta hasta que el servidor recibe un mensaje indicando la terminación de la conexión (es decir, CLIENTE>>> TERMINAR). La línea 150 utiliza el método ReadString de BinaryReader para leer un objeto

926

Capítulo 23 Redes: sockets basados en flujos y datagramas

del flujo. El método ReadString se bloquea hasta que se lea un objeto string. Ésta es la razón por la que ejecutamos el método EjecutarServidor en un subproceso separado (el cual se crea en las líneas 27-28, cuando se carga el formulario). Este subproceso asegura que el usuario de nuestra aplicación Servidor pueda seguir interactuando con la GUI para enviar mensajes al cliente, aun cuando este subproceso esté bloqueado, esperando un mensaje del cliente. string

Modificar los controles de la GUI desde subprocesos separados Los controles Windows Forms no son seguros para los subprocesos; no se garantiza que un control que se modifica desde varios subprocesos se modifique en forma correcta. La documentación de Visual Studio 20051 recomienda que sólo el subproceso que creó la GUI es el que debe modificar los controles. La clase Control proporciona el método Invoke para ayudar a asegurar esto. Invoke recibe dos argumentos: un delegate que representa a un método que modificará la GUI, y un arreglo de objetos object que representan los parámetros del método. En algún punto después de llamar a Invoke, el subproceso que creó originalmente la GUI ejecutará (cuando no esté ejecutando algún otro código) el método representado por el objeto delegate, y le pasará el contenido del arreglo object como argumentos para el método. La línea 40 declara un tipo delegate llamado DisplayDelegate, el cual representa a los métodos que toman un argumento string y que no devuelven un valor. El método MostrarMensaje (líneas 44-56) cumple con esos requerimientos: recibe un parámetro string llamado mensaje y no devuelve un valor. La instrucción if en la línea 47 evalúa la propiedad InvokeRequired (heredada de la clase Control) de mostrarTextBox, la cual devuelve true si el subproceso actual no puede modificar este control en forma directa, y devuelve false en caso contrario. Si el subproceso actual que ejecuta al método MostrarMensaje no es el subproceso que creó la GUI, entonces la condición if se evalúa a true y las líneas 51-52 llaman al método Invoke, al cual le pasan un nuevo DisplayDelegate, que representa al método MostrarMensaje en sí y a un nuevo arreglo object, que consiste en el argumento string mensaje. Esto hace que el subproceso que creó la GUI llame nuevamente al método MostrarMensaje, con el mismo argumento string que el de la llamada original. Cuando esa llamada ocurre desde el subproceso que creó la GUI, el método puede modificar mostrarTextBox en forma directa, por lo que se ejecuta el cuerpo else (línea 55) y se adjunta mensaje a la propiedad Text de mostrarTextBox. Las líneas 60-76 proporcionan una definición de delegate, DisableInputDelegate y un método llamado DeshabilitarEntrada, para permitir que cualquier subproceso modifique la propiedad ReadOnly de entradaTextBox, utilizando las mismas técnicas. Un subproceso llama a DeshabilitarEntrada con un argumento bool (true para deshabilitar; false para habilitar). Si DeshabilitarEntrada no puede modificar el control desde el subproceso actual, DeshabilitarEntrada llama al método Invoke. Esto hace que el subproceso que creó la GUI llame posteriormente a DeshabilitarEntrada y establezca entradaTextBox.ReadOnly al valor del argumento bool.

Terminar la conexión con el cliente Cuando se completa el chat, las líneas 166-169 cierran los objetos BinaryWriter, BinaryReader, NetworkStream y Socket ( paso 5), invocando a sus respectivos métodos Close. Después, el servidor espera otra solicitud de conexión de un cliente; para ello, regresa al principio del ciclo while (línea 121).

Enviar mensajes al cliente Cuando el usuario de la aplicación servidor introduce un objeto string en el control TextBox y oprime la tecla Intro, el manejador de eventos entradaTextBox_KeyDown (líneas 79-101) lee el objeto string y lo envía a través del método Write de la clase BinaryWriter. Si un usuario termina la aplicación servidor, la línea 92 llama al método Close del objeto Socket para cerrar la conexión.

Terminar la aplicación servidor Las líneas 32-36 definen el manejador de eventos ServidorChatForm_FormClosing para el evento FormClosing. El evento cierra la aplicación y llama al método Exit de la clase Environment con el parámetro ExitCode, para terminar todos los subprocesos. El método Exit de la clase Environment cierra todos los subprocesos asociados con la aplicación. 1.

El artículo de MSDN “Cómo: Realizar llamadas seguras para subprocesos en controles de formularios Windows Forms” se encuentra en msdn2.microsoft.com/es-es/library/ms171728(VS.80).aspx.

23.6 Interacción entre cliente/servidor mediante conexiones de sockets de flujo

927

Clase ClienteChatForm La figura 23.2 lista el código para la clase ClienteChatForm. Al igual que el objeto ServidorChatForm, el objeto ClienteChatForm crea un objeto Thread (líneas 26-27) en su constructor para manejar todos los mensajes entrantes. El método EjecutarCliente de ClienteChatForm (líneas 96-151) se conecta al objeto ServidorChatForm, recibe datos de ServidorChatForm y envía los datos a ServerChatForm. Las líneas 106-107 crean una instancia de un objeto TcpClient y después llaman al método Connect para establecer una conexión (paso 1). El primer argumento para el método Connect es el nombre del servidor; en nuestro caso, el nombre del servidor es "localhost", lo que significa que el servidor está ubicado en la misma máquina que el cliente. El nombre localhost también se conoce como dirección de retorno de bucle (loopback) IP, y es equivalente a la dirección IP 127.0.0.1. Este valor envía la transmisión de datos de vuelta a la dirección IP del emisor. [Nota: Elegimos demostrar la relación cliente/servidor mediante la conexión entre programas que se ejecutan en la misma computadora (localhost). Por lo general, este argumento contiene la dirección de Internet de otra computadora.] El segundo argumento para el método Connect es el número de puerto del servidor. Este número debe coincidir con el número de puerto en el que el servidor espera las conexiones. El formulario ClienteChatForm usa un NetworkStream para enviar datos hacia, y recibir datos del servidor. El cliente obtiene el objeto NetworkStream en la línea 110, a través de una llamada al método GetStream de TcpClient (paso 2). La instrucción do…while en las líneas 120-134 itera hasta que el cliente reciba el mensaje de terminación de la conexión (SERVIDOR>>> TERMINAR). La línea 126 utiliza el método ReadString de BinaryReader para obtener el siguiente mensaje del servidor (paso 3). La línea 127 muestra el mensaje, y las líneas 137-140 cierran los objetos BinaryWriter, BinaryReader, NetworkStream y TcpClient (paso 4). Las líneas 39-75 declaran los métodos DisplayDelegate, MostrarMensaje, DisableInputDelegate y DeshabilitarEntrada, exactamente igual que en las líneas 40-76 de la figura 23.1. Estos métodos se utilizan una vez más para asegurar que la GUI se modifique sólo por el subproceso que creo los controles de la GUI. Cuando el usuario de la aplicación cliente escribe una cadena en el control TextBox y oprime la tecla Intro, el manejador de eventos entrada_TextBox_KeyDown (líneas 78-93) lee el objeto string del control TextBox y lo envía a través del método Write de BinaryWriter. Observe aquí que ServidorChatForm recibe una conexión, la procesa, la cierra y espera a la siguiente. En una aplicación real, lo más común es que un servidor reciba una conexión, la configure para que se procese como un subproceso separado de ejecución, y espere nuevas conexiones. Así, los subprocesos separados que procesan las conexiones existentes podrían seguir ejecutándose mientras el servidor se concentra en las nuevas solicitudes de conexión.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

// Fig. 23.2: ClienteChat.cs // Establece un cliente que enviará información hacia, // y leerá información de, un servidor. using System; using System.Windows.Forms; using System.Threading; using System.Net.Sockets; using System.IO; public partial class ClienteChatForm : Form { public ClienteChatForm() { InitializeComponent(); } // fin del constructor private private private private private

NetworkStream salida; // flujo para recibir datos BinaryWriter escritor; // facilita la escritura en el flujo BinaryReader lector; // facilita la lectura del flujo Thread lecturaThread; // Thread para procesar mensajes entrantes string mensaje = "";

Figura 23.2 | Parte relacionada al cliente de una conexión cliente/servidor con sockets de flujo. (Parte 1 de 5).

928

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81

Capítulo 23 Redes: sockets basados en flujos y datagramas

// inicializa el subproceso para lectura private void ClienteChatForm_Load( object sender, EventArgs e ) { lecturaThread = new Thread( new ThreadStart( EjecutarCliente ) ); lecturaThread.Start(); } // fin del método ClienteChatForm_Load // cierra todos los subprocesos asociados con esta aplicación private void ClienteChatForm_FormClosing( object sender, FormClosingEventArgs e ) { System.Environment.Exit( System.Environment.ExitCode ); } // fin del método ClienteChatForm_FormClosing // delegado que permite llamar al método MostrarMensaje // en el subproceso que crea y mantiene la GUI private delegate void DisplayDelegate( string message ); // el método MostrarMensaje establece la propiedad Text de mostrarTextBox // en una manera segura para el subproceso private void MostrarMensaje( string mensaje ) { // si la modificación de mostrarTextBox no es segura para el subproceso if ( mostrarTextBox.InvokeRequired ) { // usa el método heredado Invoke para ejecutar MostrarMensaje // a través de un delegado Invoke( new DisplayDelegate( MostrarMensaje ), new object[] { mensaje } ); } // fin de if else // Se puede modificar mostrarTextBox en el subproceso actual mostrarTextBox.Text += mensaje; } // fin del método MostrarMensaje // delegado que permite llamar al método DeshabilitarSalida // en el subproceso que crea y mantiene la GUI private delegate void DisableInputDelegate( bool value ); // el método DeshabilitarSalida establece la propiedad ReadOnly de entradaTextBox // de una manera segura para el subproceso private void DeshabilitarSalida( bool valor ) { // si la modificación de entradaTextBox no es segura para el subproceso if ( entradaTextBox.InvokeRequired ) { // usa el método heredado Invoke para ejecutar a DeshabilitarSalida // a través de un delegado Invoke( new DisableInputDelegate( DeshabilitarSalida ), new object[] { valor } ); } // fin de if else // Se puede modificar entradaTextBox en el subproceso actual entradaTextBox.ReadOnly = valor; } // fin del método DeshabilitarSalida // envía al servidor el texto que escribe el usuario private void entradaTextBox_KeyDown( object sender, KeyEventArgs e ) { try {

Figura 23.2 | Parte relacionada al cliente de una conexión cliente/servidor con sockets de flujo. (Parte 2 de 5).

23.6 Interacción entre cliente/servidor mediante conexiones de sockets de flujo

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140

if ( e.KeyCode == Keys.Enter && entradaTextBox.ReadOnly == false ) { escritor.Write( "CLIENTE>>> " + entradaTextBox.Text ); mostrarTextBox.Text += "\r\nCLIENTE>>> " + entradaTextBox.Text; entradaTextBox.Clear(); } // fin de if } // fin de try catch ( SocketException ) { mostrarTextBox.Text += "\nError al escribir el objeto"; } // fin de catch } // fin del método entradaTextBox_KeyDown // se conecta al servidor y muestra el texto generado por el servidor public void EjecutarCliente() { TcpClient cliente; // crea instancia de TcpClient para enviar datos al servidor try { MostrarMensaje( "Tratando de conectar\r\n" ); // Paso 1: crear TcpClient y conectar al servidor cliente = new TcpClient(); cliente.Connect( "127.0.0.1", 50000 ); // Paso 2: obtener NetworkStream asociado con TcpClient salida = cliente.GetStream(); // crea objetos para escribir y leer a través del flujo escritor = new BinaryWriter( salida ); lector = new BinaryReader( salida ); MostrarMensaje( "\r\nSe recibieron flujos de E/S\r\n" ); DeshabilitarSalida( false ); // habilita entradaTextBox // itera hasta que el servidor indica la terminación do { // Paso 3: fase de procesamiento try { // lee mensaje del servidor mensaje = lector.ReadString(); MostrarMensaje( "\r\n" + mensaje ); } // fin de try catch ( Exception ) { // maneja excepcion si hay error al leer datos del servidor System.Environment.Exit( System.Environment.ExitCode ); } // fin de catch } while ( mensaje != "SERVIDOR>>> TERMINAR" ); // Paso 4: cierra la conexión escritor.Close(); lector.Close(); salida.Close(); cliente.Close();

Figura 23.2 | Parte relacionada al cliente de una conexión cliente/servidor con sockets de flujo. (Parte 3 de 5).

929

930

Capítulo 23 Redes: sockets basados en flujos y datagramas

141 142 Application.Exit(); 143 } // fin de try 144 catch ( Exception error ) 145 { 146 // maneja excepción si hay error al establecer la conexión 147 MessageBox.Show( error.ToString(), "Error en la conexión", 148 MessageBoxButtons.OK, MessageBoxIcon.Error ); 149 System.Environment.Exit( System.Environment.ExitCode ); 150 } // fin de catch 151 } // fin del método EjecutarCliente 152 } // fin de la clase ClienteChatForm a)

b)

c)

d)

e)

f)

Figura 23.2 | Parte relacionada al cliente de una conexión cliente/servidor con sockets de flujo. (Parte 4 de 5).

23.7 Interacción entre cliente/servidor sin conexión mediante datagramas

931

g)

Figura 23.2 | Parte relacionada al cliente de una conexión cliente/servidor con sockets de flujo. (Parte 5 de 5).

23.7 Interacción entre cliente/servidor sin conexión mediante datagramas Hasta este punto, hemos hablado sobre las transmisiones orientadas a la conexión y basadas en flujos que utilizan el protocolo TCP para asegurar que los paquetes de datos se transmitan en forma confiable. Ahora veremos las transmisiones sin conexión que utilizan datagramas y UDP. La transmisión sin conexión a través de datagramas se asemeja al método mediante el cual el servicio postal transporta y entrega el correo. La transmisión sin conexión empaqueta y envía la información en paquetes conocidos como datagramas, que se pueden considerar como algo similar a las cartas que se envían por el correo convencional. Si un mensaje grande no cabe en un solo sobre, ese mensaje se descompone en piezas separadas y se coloca en sobres separados, numerados en secuencia. Todas las cartas se envían por correo al mismo tiempo, y podrían llegar en orden, desordenadas o tal vez no lleguen. La persona en el extremo receptor vuelve a ensamblar las piezas del mensaje en orden secuencial, antes de tratar de interpretar el mensaje. Si éste es lo bastante pequeño como para caber en un sobre, se elimina el problema de secuencia, pero aún es posible que el mensaje nunca llegue. (A diferencia del correo postal, los duplicados de los datagramas podrían llegar a las computadoras receptoras.) C# cuenta con la clase UdpClient para la transmisión sin conexión. Al igual que TcpListener y TcpClient, UdpClient usa métodos de la clase Socket. Los métodos Send y Receive de UdpClient transmiten datos con el método SendTo de Socket y leen datos con el método ReceiveFrom de Socket, respectivamente. Los programas en las figuras 23.3 y 23.4 utilizan datagramas para enviar paquetes de información entre las aplicaciones cliente y servidor. En la aplicación ClientePaquetes, el usuario escribe un mensaje en un control TextBox y oprime Intro. El cliente convierte el mensaje en un arreglo byte y lo envía al servidor. El servidor recibe el paquete y muestra la información que contiene; después hace eco y devuelve el paquete al cliente. Cuando el cliente recibe el paquete, muestra la información que contiene. En este ejemplo, las implementaciones de las clases ClientePaquetesForm y ServidorPaquetesForm son similares.

Clase ServidorPaquetesForm El código de la figura 23.3 define el formulario ServidorPaquetesForm para esta aplicación. La línea 23 en el manejador de eventos para la clase ServidorPaquetesForm crea una instancia de la clase UdpClient, la cual recibe datos en el puerto 50000. Esto inicializa el Socket subyacente para las conexiones. La línea 24 crea una instancia de la clase IpEndPoint para almacenar la dirección IP y número de(l) cliente(s) que transmiten a ServidorPaquetesForm. El primer argumento para el constructor IPEndPoint es un objeto IPAddress; el segundo argumento es el número de puerto del punto final. Estos dos valores son 0, ya que sólo necesitamos instanciar un objeto IPEndPoint vacío. Las direcciones IP y los números de puerto de los clientes se copian en el objeto IPEndPoint cuando se reciben los datagramas de los clientes. Las líneas 39-55 definen a DisplayDelegate y MostrarMensaje, lo cual permite que cualquier subproceso pueda modificar la propiedad Text de mostrarTextBox.

932

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Capítulo 23 Redes: sockets basados en flujos y datagramas

// Fig. 23.3: ServidorPaquetes.cs // Establece un servidor que recibe paquetes de un // cliente y los envía de vuelta al cliente. using System; using System.Windows.Forms; using System.Net; using System.Net.Sockets; using System.Threading; public partial class ServidorPaquetesForm : Form { public ServidorPaquetesForm() { InitializeComponent(); } // fin de constructor private UdpClient cliente; private IPEndPoint puntoRecepcion; // inicializa las variables y el subproceso para recibir paquetes private void ServidorPaquetesForm_Load( object sender, EventArgs e ) { cliente = new UdpClient( 50000 ); puntoRecepcion = new IPEndPoint( new IPAddress( 0 ), 0 ); Thread lecturaThread = new Thread( new ThreadStart( EsperarPaquetes ) ); lecturaThread.Start(); } // fin del método ServidorPaquetesForm_Load // cierra el servidor private void ServidorPaquetesForm_FormClosing( object sender, FormClosingEventArgs e ) { System.Environment.Exit( System.Environment.ExitCode ); } // fin del método ServidorPaquetesForm_FormClosing // delegado que permite llamar al método MostrarMensaje // en el subproceso que crea y mantiene la GUI private delegate void DisplayDelegate( string message ); // el método MostrarMensaje establece la propiedad Text de mostrarTextBox // de una manera segura para el proceso private void MostrarMensaje( string mensaje ) { // si la modificación de mostrarTextBox no es segura para el subproceso if ( mostrarTextBox.InvokeRequired ) { // usa el método heredado Invoke para ejecutar MostrarMensaje // a través de un delegado Invoke( new DisplayDelegate( MostrarMensaje ), new object[] { mensaje } ); } // fin de if else // sí se puede modificar mostrarTextBox en el subproceso actual mostrarTextBox.Text += mensaje; } // fin del método MostrarMensaje // espera a que llegue un paquete public void EsperarPaquetes() {

Figura 23.3 | Parte correspondiente al lado servidor de una aplicación cliente/servidor sin conexión. (Parte 1 de 2).

23.7 Interacción entre cliente/servidor sin conexión mediante datagramas

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75

933

while ( true ) { // prepara el paquete byte[] datos = cliente.Receive( ref puntoRecepcion ); MostrarMensaje( "\r\nSe recibió paquete:" + "\r\nLongitud: " + datos.Length + "\r\nContenido: " + System.Text.Encoding.ASCII.GetString( datos ) ); // devuelve (eco) la información del paquete de vuelta al cliente MostrarMensaje( "\r\n\r\nEnviando de vuelta datos al cliente..." ); cliente.Send( datos, datos.Length, puntoRecepcion ); MostrarMensaje( "\r\nPaquete enviado\r\n" ); } // fin de while } // fin del método EsperarPaquetes } // fin de la clase ServidorPaquetesForm

Figura 23.3 | Parte correspondiente al lado servidor de una aplicación cliente/servidor sin conexión. (Parte 2 de 2). El método EsperarPaquetes de ServidorPaquetesForm (líneas 58-74) ejecuta un ciclo infinito mientras espera a que lleguen los datos al formulario. Cuando llega la información, el método Receive de UdpClient (línea 63) recibe un arreglo byte del cliente. A Receive le pasamos el objeto IPEndPoint creado en el constructor; esto proporciona al método un valor de IPEndPoint al que el programa copia la dirección IP y el número de puerto del cliente. Este programa se compila y ejecuta sin una excepción, aun si la referencia al objeto IPEndPoint es null, ya que el método Receive inicializa el objeto IPEndPoint si es null. Las líneas 64-67 actualizan la pantalla de ServidorPaquetesForm para incluir la información del paquete y su contenido. La línea 71 envía los datos de vuelta al cliente, usando el método Send de UdpClient. Esta versión de Send recibe tres argumentos: el arreglo byte a enviar, un int que representa la longitud del arreglo y el IPEndPoint al que se van a enviar los datos. Utilizamos el arreglo datos devuelto por el método Receive como los datos, la longitud del arreglo datos como la longitud y el IPEndPoint que se pasa al método Receive como el destino de los datos. La dirección IP y el número de puerto del cliente que envió los datos se almacenan en puntoRecepcion, por lo que el hecho de sólo pasar puntoRecepcion a Send permite que ServidorPaquetesForm responda al cliente.

Clase ClientePaquetesForm La clase ClientePaquetesForm (figura 23.4) funciona de manera similar a ServidorPaquetesForm, excepto que el objeto Cliente envía paquetes sólo cuando el usuario escribe un mensaje en un control TextBox y oprime Intro. Cuando esto ocurre, el programa llama al manejador de eventos entradaTextBox_KeyDown (líneas 58-75). La línea 68 convierte el objeto string, que el usuario introdujo en el control TextBox, en un arreglo byte. La línea 71 llama al método Send de UdpClient para enviar el arreglo byte al formulario ServidorPaquetesForm que se encuentra en localhost (es decir, en el mismo equipo). Especificamos el puerto como 50000, para lo cual sabemos que es el puerto de ServidorPaquetesForm.

934

Capítulo 23 Redes: sockets basados en flujos y datagramas

Las líneas 39-55 definen a DisplayDelegate y MostrarMensaje, con lo que se permite a cualquier subproceso que modifique la propiedad Text del control mostrarTextBox. La línea 24 crea una instancia de un objeto UdpClient para recibir paquetes en el puerto 50001; elegimos este puerto ya que el objeto ServidorPaquetesForm está ocupando el puerto 50000. El método EsperarPaquetes de la clase ClientePaquetesForm (líneas 78-90) utiliza un ciclo infinito para esperar estos paquetes. El método Receive de UdpClient se bloquea hasta que se reciba un paquete de datos (línea 83). El bloqueo realizado por 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50

// Fig. 23.4: ClientePaquetes.cs // Establece un cliente que envía paquetes a un servidor y recibe // paquetes de ese servidor. using System; using System.Windows.Forms; using System.Net; using System.Net.Sockets; using System.Threading; public partial class ClientePaquetesForm : Form { public ClientePaquetesForm() { InitializeComponent(); } // fin del constructor private UdpClient cliente; private IPEndPoint puntoRecepcion; // inicializa las variables y el subproceso para recibir paquetes private void ClientePaquetesForm_Load( object sender, EventArgs e ) { puntoRecepcion = new IPEndPoint( new IPAddress( 0 ), 0 ); cliente = new UdpClient( 50001 ); Thread subproceso = new Thread( new ThreadStart( EsperarPaquetes ) ); subproceso.Start(); } // fin del método ClientePaquetesForm_Load // cierra el cliente private void ClientePaquetesForm_FormClosing( object sender, FormClosingEventArgs e ) { System.Environment.Exit( System.Environment.ExitCode ); } // fin del método ClientePaquetesForm_FormClosing // delegado que permite llamar al método MostrarMensaje // en el subproceso que crea y mantiene la GUI private delegate void DisplayDelegate( string message ); // el método MostrarMensaje establece la propiedad Text de mostrarTextBox // de una manera segura para el subproceso private void MostrarMensaje( string mensaje ) { // si la modificación de mostrarTextBox no es segura para el subproceso if ( mostrarTextBox.InvokeRequired ) { // usa el método heredado Invoke para ejecutar MostrarMensaje // a través de un delegado Invoke( new DisplayDelegate( MostrarMensaje ),

Figura 23.4 | Parte correspondiente al cliente de una aplicación cliente/servidor sin conexión. (Parte 1 de 2).

23.7 Interacción entre cliente/servidor sin conexión mediante datagramas

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91

new object[] { mensaje } ); } // fin de if else // sí se puede modificar mostrarTextBox en el subproceso actual mostrarTextBox.Text += mensaje; } // fin del método MostrarMensaje // envía un paquete private void entradaTextBox_KeyDown( object sender, KeyEventArgs e ) { if ( e.KeyCode == Keys.Enter ) { // crea un paquete (datagrama) como objeto string string paquete = entradaTextBox.Text; mostrarTextBox.Text += "\r\nEnviando paquete que contiene: " + paquete; // convierte el paquete en arreglo de bytes byte[] datos = System.Text.Encoding.ASCII.GetBytes( paquete ); // envía el paquete al servidor en el puerto 50000 cliente.Send( datos, datos.Length, "127.0.0.1", 50000 ); mostrarTextBox.Text += "\r\nPaquete enviado\r\n"; entradaTextBox.Clear(); } // fin de if } // fin del método entradaTextBox_KeyDown // espera a que lleguen los paquetes public void EsperarPaquetes() { while ( true ) { // recibe arreglo de bytes del servidor byte[] datos = cliente.Receive( ref puntoRecepcion ); // envía el paquete de datos al control TextBox MostrarMensaje( "\r\nPaquete recibido:" + "\r\nLongitud: " + datos.Length + "\r\nContenido: " + System.Text.Encoding.ASCII.GetString( datos ) + "\r\n" ); } // fin de while } // fin del método EsperarPaquetes } // fin de la clase ClientePaquetesForm a) La ventana Cliente de paquetes, antes de enviar un paquete al servidor

b) La ventana Cliente de paquetes, despuése de enviar un paquete al servidor y recibirlo de vuelta

Figura 23.4 | Parte correspondiente al cliente de una aplicación cliente/servidor sin conexión. (Parte 2 de 2).

935

936

Capítulo 23 Redes: sockets basados en flujos y datagramas

el método Receive no evita que la clase ClientePaquetesForm realice otros servicios (por ejemplo, manejar la entrada del usuario), ya que un subproceso separado ejecuta el método EsperarPaquetes. Cuando llega un paquete, las líneas 86-88 muestran su contenido en el control TextBox. El usuario puede escribir información en el control TextBox de la ventana ClientePaquetesForm y oprimir Intro en cualquier momento, incluso mientras se recibe un paquete. El manejador de eventos para el control TextBox procesa el evento y envía los datos al servidor.

23.8 Juego de Tres en raya cliente/servidor mediante el uso de un servidor con subprocesamiento múltiple En esta sección presentamos una versión en red del popular juego Tres en raya, implementado con sockets de flujo y técnicas de cliente/servidor. El programa consiste en una aplicación ServidorTresEnRaya (figura 23.5) y una aplicación ClienteTresEnRaya (figura 23.6). ServidorTresEnRaya permite que dos instancias de ClienteTresEnRaya se conecten al servidor y jueguen Tres en raya, una contra la otra. En la figura 23.6 se muestran los resultados. Cuando el servidor recibe una conexión de un cliente, las líneas 78-87 de la figura 23.5 crean instancias de la clase Jugador, para procesar a cada uno de los clientes en un subproceso de ejecución separado. Esto permite al servidor manejar las solicitudes de ambos clientes. El servidor asigna el valor "X" al primer cliente que se conecta (el jugador X hace el primer movimiento), y después asigna el valor "0" al segundo cliente. Durante el juego, el servidor mantiene información acerca del estado del tablero, de manera que pueda validar los movimientos solicitados por los jugadores. Sin embargo, ni el servidor ni el cliente pueden establecer si uno de los jugadores ganó el juego; en esta aplicación, el método JuegoTerminado (líneas 139-143) siempre devuelve false. Cada cliente mantiene su propia versión de la GUI del tablero Tres en raya para visualizar el juego. Los clientes sólo pueden colocar marcas en los lugares vacíos del tablero. La clase Cuadro (figura 23.7) se utiliza para definir los cuadros en el tablero de Tres en raya.

Clase ServidorTresEnRaya ServidorTresEnRaya (figura 23.5) utiliza su manejador de eventos Load (líneas 27-37) para crear un arreglo byte que almacene los movimientos realizados por los jugadores (línea 29). El programa crea un arreglo de dos referencias a objetos Jugador (línea 30) y un arreglo de dos referencias a objetos Thread (línea 31). Cada elemento en ambos arreglos corresponde a un jugador de Tres en raya. La variable jugadorActual se establece a 0 (línea 32), que corresponde al jugador "X". En nuestro programa, el jugador "X" hace el primer movimiento. Las líneas 35-36 crean e inician el objeto Thread obtenerJugadores, utilizado por ServidorTresEnRayaForm para aceptar conexiones, de manera que el objeto Thread actual no se bloquee mientras espera a los jugadores.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

// Fig. 23.5: ServidorTresEnRaya.cs // Esta clase mantiene un juego de Tres en raya para // dos aplicaciones cliente. using System; using System.Windows.Forms; using System.Net; using System.Net.Sockets; using System.Threading; using System.IO; public partial class ServidorTresEnRayaForm : Form { public ServidorTresEnRayaForm() { InitializeComponent(); } // fin del constructor private byte[] tablero; // la representación local del tablero del juego private Jugador[] jugadores; // dos objetos Jugador

Figura 23.5 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 1 de 5).

23.8 Juego de Tres en raya cliente/servidor mediante el uso de un servidor con...

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77

937

private Thread[] subprocesosJugadores; // subprocesos para la interacción con los clientes private TcpListener oyente; // escucha en espera de la conexión del cliente private int jugadorActual; // lleva la cuenta de quién sigue private Thread obtenerJugadores; // subproceso para adquirir las conexiones de los clientes internal bool desconectado = false; // verdadero si el servidor se cierra // inicializa las variables y el subproceso para recibir los clientes private void ServidorTresEnRayaForm_Load( object sender, EventArgs e ) { tablero = new byte[ 9 ]; jugadores = new Jugador[ 2 ]; subprocesosJugadores = new Thread[ 2 ]; jugadorActual = 0; // acepta conexiones en un subproceso distinto obtenerJugadores = new Thread( new ThreadStart( Establecer ) ); obtenerJugadores.Start(); } // fin del método ServidorTresEnRayaForm_Load // notifica a los Jugadores para que dejen de ejecutarse private void ServidorTresEnRayaForm_FormClosing( object sender, FormClosingEventArgs e ) { desconectado = true; System.Environment.Exit( System.Environment.ExitCode ); } // fin del método ServidorTresEnRayaForm_FormClosing // delegado que permite llamar al método MostrarMensaje // en el subproceso que crea y mantiene la GUI private delegate void DisplayDelegate( string message ); // el método MostrarMensaje establece la propiedad Text de mostrarTextBox // de una manera segura para el subproceso internal void MostrarMensaje( string mensaje ) { // si la modificación de mostrarTextBox no es segura para el subproceso if ( mostrarTextBox.InvokeRequired ) { // usa el método heredado Invoke para ejecutar MostrarMensaje // a través de un delegado Invoke( new DisplayDelegate( MostrarMensaje ), new object[] { mensaje } ); } // fin de if else // sí se puede modificar mostrarTextBox en el subproceso actual mostrarTextBox.Text += mensaje; } // fin del método MostrarMensaje // acepta conexiones de 2 jugadores public void Establecer() { MostrarMensaje( "Esperando a los jugadores...\r\n" ); // establece Socket oyente = new TcpListener( IPAddress.Parse( "127.0.0.1" ), 50000 ); oyente.Start(); // acepta el primer jugador e inicia un subproceso jugador

Figura 23.5 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 2 de 5).

938

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136

Capítulo 23 Redes: sockets basados en flujos y datagramas

jugadores[ 0 ] = new Jugador( oyente.AcceptSocket(), this, 0 ); subprocesosJugadores[ 0 ] = new Thread( new ThreadStart( jugadores[ 0 ].Ejecutar ) ); subprocesosJugadores[ 0 ].Start(); // acepta el segundo jugador e inicia otro subproceso jugador jugadores[ 1 ] = new Jugador( oyente.AcceptSocket(), this, 1 ); subprocesosJugadores[ 1 ] = new Thread( new ThreadStart( jugadores[ 1 ].Ejecutar ) ); subprocesosJugadores[ 1 ].Start(); // hace saber al primer jugador que el otro jugador se conectó lock ( jugadores[ 0 ] ) { jugadores[ 0 ].subprocesoSuspendido = false; Monitor.Pulse( jugadores[ 0 ] ); } // fin de lock } // fin del método Establecer // determina si un movimiento es válido public bool MovimientoValido( int ubicacion, int jugador ) { // evita que otro subproceso haga un movimiento lock ( this ) { // mientras no sea el turno del jugador actual, espera while ( jugador != jugadorActual ) Monitor.Wait( this ); // si el cuadro deseado no está ocupado if ( !EstaOcupado( ubicacion ) ) { // establece el tablero para que contenga la marca del jugador actual tablero[ ubicacion ] = ( byte ) ( jugadorActual == 0 ? 'X' : 'O' ); // establece jugadorActual para que sea el otro jugador jugadorActual = ( jugadorActual + 1 ) % 2; // notifica el movimiento al otro jugador jugadores[ jugadorActual ].OtroJugadorMovio( ubicacion ); // alerta al otro jugador que es tiempo de moverse Monitor.Pulse( this ); return true; } // fin de if else return false; } // fin de lock } // fin del método MovimientoValido // determina si el cuadro especificado está ocupado public bool EstaOcupado( int ubicacion ) { if ( tablero[ ubicacion ] == 'X' || tablero[ ubicacion ] == 'O' ) return true; else return false; } // fin del método EstaOcupado

Figura 23.5 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 3 de 5).

23.8 Juego de Tres en raya cliente/servidor mediante el uso de un servidor con...

137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195

// determina si el juego terminó public bool JuegoTerminado() { // colocar aquí el código para evaluar si alguien ganó el juego return false; } // fin del método JuegoTerminado } // fin de la clase ServidorTresEnRayaForm // la clase Jugador representa a un jugador de tres en raya public class Jugador { internal Socket conexion; // Socket para aceptar una conexión private NetworkStream socketStream; // flujo de datos de red private ServidorTresEnRayaForm servidor; // referencia al servidor private BinaryWriter escritor; // facilita la escritura en el flujo private BinaryReader lector; // facilita la lectura del flujo private int numero; // número de jugador private char marca; // marca del jugador en el tablero internal bool subprocesoSuspendido = true; // si está esperando al otro jugador // constructor que requiere objetos Socket, ServidorTresEnRayaForm // e int como argumentos public Jugador( Socket socket, ServidorTresEnRayaForm valorServidor, int nuevoNumero ) { marca = (nuevoNumero == 0 ? 'X' : 'O'); conexion = socket; servidor = valorServidor; numero = nuevoNumero; // crea objeto NetworkStream para Socket socketStream = new NetworkStream( conexion ); // crea flujos para escribir/leer bytes escritor = new BinaryWriter( socketStream ); lector = new BinaryReader( socketStream ); } // fin del constructor // indica la jugada al otro jugador public void OtroJugadorMovio( int ubicacion ) { // signal that opponent moved escritor.Write( "El oponente movió." ); escritor.Write( ubicacion ); // envía la ubicación del movimiento } // fin del método OtroJugadorMovio // permite a los jugadores hacer movimientos y recibir los movimientos // del otro jugador public void Ejecutar() { bool listo = false; // muestra en el servidor que se hizo una conexión servidor.MostrarMensaje( "Jugador " + ( numero == 0 ? 'X' : 'O' ) + " conectado\r\n" ); // envía la marca del jugador actual al cliente escritor.Write( marca );

Figura 23.5 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 4 de 5).

939

940

Capítulo 23 Redes: sockets basados en flujos y datagramas

196 197 // si el número es igual a 0, entonces este jugador es X, 198 // en caso contrario, 0 debe esperar el primer movimiento de X 199 escritor.Write( "Jugador " + ( numero == 0 ? 200 "X conectado.\r\n" : "O conectado, por favor espere.\r\n" ) ); 201 202 // X debe esperar a que llegue otro jugador 203 if ( marca == 'X' ) 204 { 205 escritor.Write( "Esperando al otro jugador." ); 206 207 // espera la notificación del servidor, de que 208 // se conectó el otro jugador 209 lock ( this ) 210 { 211 while ( subprocesoSuspendido ) 212 Monitor.Wait( this ); 213 } // fin de lock 214 215 escritor.Write( "Se conectó el otro jugador. Es su turno." ); 216 } // fin de if 217 218 // iniciar el juego 219 while ( !listo ) 220 { 221 // espera a que haya datos disponibles 222 while ( conexion.Available == 0 ) 223 { 224 Thread.Sleep( 1000 ); 225 226 if ( servidor.desconectado ) 227 return; 228 } // fin de while 229 230 // recibe los datos 231 int ubicacion = lector.ReadInt32(); 232 233 // si el movimiento es válido, lo muestra en el 234 // servidor e indica que el movimiento es válido 235 if ( servidor.MovimientoValido( ubicacion, numero ) ) 236 { 237 servidor.MostrarMensaje( "ubic: " + ubicacion + "\r\n" ); 238 escritor.Write( "Movimiento válido." ); 239 } // fin de if 240 else // indica que el movimiento es inválido 241 escritor.Write( "Movimiento inválido, intente de nuevo." ); 242 243 // si el juego terminó, establece listo a true para salir del ciclo while 244 if ( servidor.JuegoTerminado() ) 245 listo = true; 246 } // fin de ciclo while 247 248 // cierra la conexión de los sockets 249 escritor.Close(); 250 lector.Close(); 251 socketStream.Close(); 252 conexion.Close(); 253 } // fin del método Ejecutar 254 } // fin de la clase Jugador

Figura 23.5 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 5 de 5).

23.8 Juego de Tres en raya cliente/servidor mediante el uso de un servidor con...

941

Las líneas 49-65 definen a DisplayDelegate y MostrarMensaje, lo cual permite que cualquier subproceso pueda modificar la propiedad Text de mostrarTextBox. Esta vez, el método MostrarMensaje se declara como internal, por lo que puede llamarse dentro de un método de la clase Jugador, a través de una referencia a ServidorTresEnRayaForm. El subproceso obtenerJugadores ejecuta el método Establecer (líneas 68-95), el cual crea un objeto TcpListener para escuchar solicitudes en el puerto 50000 (líneas 73-75). Después, este objeto escucha en espera de solicitudes de conexión de los jugadores primero y segundo. Las líneas 78 y 84 crean instancias de objetos Jugador, los cuales representan a los jugadores, y las líneas 79-81 y 85-87 crean dos objetos Thread que ejecutan los métodos Ejecutar de cada objeto Jugador. El constructor de Jugador (figura 23.5, líneas 160-174) recibe como argumentos una referencia al objeto Socket (es decir, la conexión con el cliente), una referencia al objeto ServidorTresEnRayaForm y un objeto int que indica el número de jugador (de donde el constructor infiere la marca "X" o "O" utilizada por ese jugador). En este caso de estudio, ServidorTresEnRayaForm llama al método Ejecutar (líneas 186-253) después de instanciar un objeto Jugador. Las líneas 191-200 notifican al servidor de una conexión exitosa y envían al cliente el char que éste colocará en el tablero, cuando haga un movimiento. Si Ejecutar se está ejecutando para el Jugador "X", se ejecutan las líneas 205-215 y el Jugador "X" tiene que esperar a que se conecte un segundo jugador. Las líneas 211-212 definen una instrucción while que suspende el subproceso del Jugador "X" hasta que el servidor indica que el Jugador "O" se ha conectado. El servidor notifica la conexión al Jugador estableciendo la variable subprocesoSuspendido de Jugador a false (línea 92). Cuando subprocesoSuspendido se vuelve false, Jugador sale de la instrucción while en las líneas 211-212. El método Ejecutar ejecuta la instrucción while en las líneas 219-246, para permitir al usuario que juegue. Cada iteración de esta instrucción espera a que el cliente envíe un valor int que especifica en dónde se colocará la "X" o la "O" en el tablero; después, el Jugador coloca la marca en el tablero, si la ubicación es válida (es decir, que la ubicación no contenga ya una marca). Observe que la instrucción while continúa su ejecución sólo si la variable bool listo es false. Esta variable se establece a true mediante el manejador de eventos ServidorTresEnRayaForm_FormClosing de la clase ServidorTresEnRayaForm, el cual se invoca cuando el servidor cierra la conexión. La línea 222 de la figura 23.5 empieza una instrucción while que itera hasta que la propiedad Available de Socket indica que hay información proveniente del Socket (o hasta que el servidor se desconecte del cliente). Si no hay información, el objeto Thread pasa al estado inactivo durante un segundo. Al despertar, el objeto Thread utiliza la propiedad Disconnected para verificar si la variable desconectado del servidor es true (línea 226). Si el valor es true, el objeto Thread sale del método (con lo cual termina la ejecución del subproceso); en caso contrario, el objeto Thread entra de nuevo al ciclo. No obstante, si la propiedad Available indica que hay datos para recibir, termina la instrucción while de las líneas 222-228, permitiendo que se procese la información. Esta información contiene un valor int, que representa la ubicación en la que el cliente desea colocar una marca. La línea 231 llama al método ReadInt32 del objeto BinaryReader (que lee del objeto NetworkStream creado con el Socket) para leer este int. Después, la línea 235 pasa el int al método MovimientoValido de ServidorTresEnRayaForm. Si este método valida el movimiento, el Jugador coloca la marca en la ubicación deseada. El método MovimientoValido (líneas 98-127) envía al cliente un mensaje que indica si el movimiento fue válido. Las ubicaciones en el tablero corresponden a los números del 0 al 8 (0-2 para la fila superior, 3-5 para la fila intermedia y 6-8 para la fila superior). Todas las instrucciones en el método MovimientoValido se encierran en una instrucción lock, que permite realizar sólo un movimiento a la vez. Esto evita que dos jugadores modifiquen la información de estado del juego al mismo tiempo. Si el Jugador que intenta validar un movimiento no es el jugador actual (es decir, el que tiene permitido hacer un movimiento), ese Jugador se coloca en un estado de Espera hasta que sea su turno para hacer el movimiento. Si el usuario trata de colocar una marca en una ubicación que ya contenga una marca, el método MovimientoValido devuelve false. No obstante, si el usuario seleccionó una ubicación desocupada (línea 108), las líneas 111-112 colocan la marca en la representación local del tablero. La línea 118 notifica al otro Jugador que se realizó un movimiento, y la línea 121 invoca al método Pulse, para que el Jugador en espera pueda validar un movimiento. Después, el método devuelve true para indicar que el movimiento es válido. Cuando se ejecuta una aplicación ClienteTresEnRayaForm (figura 23.6), crea un control TextBox para mostrar los mensajes del servidor y la representación del tablero de Tres en raya. El tablero se crea a partir de nueve objetos Cuadro (figura 23.7), el cual contiene controles Panel en los que el usuario puede hacer clic, indicando

942

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Capítulo 23 Redes: sockets basados en flujos y datagramas

// Fig. 23.6: ClienteTresEnRaya.cs // Cliente para el programa de Tres en raya. using System; using System.Drawing; using System.Windows.Forms; using System.Net.Sockets; using System.Threading; using System.IO; public partial class ClienteTresEnRayaForm : Form { public ClienteTresEnRayaForm() { InitializeComponent(); } // fin del constructor private private private private private private private private private private private

Cuadro[ , ] tablero; // representación local del tablero del juego Cuadro cuadroActual; // el Cuadro que eligió este jugador Thread subprocesoSalida; // subproceso para recibir datos del servidor TcpClient conexion; // cliente para establecer la conexión NetworkStream flujo; // flujo de datos de red BinaryWriter escritor; // facilita la escritura en el flujo BinaryReader lector; // facilita la lectura del flujo char miMarca; // la marca del jugador en el tablero bool miTurno; // ¿es el turno de este jugador? SolidBrush brocha; // brocha para dibujar Xs y Os bool listo = false; // verdadero cuando se termina el juego

// inicializa variables y subproceso para conectarse al servidor private void ClienteTresEnRayaForm_Load( object sender, EventArgs e ) { tablero = new Cuadro[ 3, 3 ]; // crea 9 objetos tablero[ 0, 0 ] = tablero[ 0, 1 ] = tablero[ 0, 2 ] = tablero[ 1, 0 ] = tablero[ 1, 1 ] = tablero[ 1, 2 ] = tablero[ 2, 0 ] = tablero[ 2, 1 ] = tablero[ 2, 2 ] =

Cuadro y los coloca en el tablero new Cuadro( tablero0Panel, ' ', 0 new Cuadro( tablero1Panel, ' ', 1 new Cuadro( tablero2Panel, ' ', 2 new Cuadro( tablero3Panel, ' ', 3 new Cuadro( tablero4Panel, ' ', 4 new Cuadro( tablero5Panel, ' ', 5 new Cuadro( tablero6Panel, ' ', 6 new Cuadro( tablero7Panel, ' ', 7 new Cuadro( tablero8Panel, ' ', 8

); ); ); ); ); ); ); ); );

// crea un objeto SolidBrush para escribir en los cuadros brocha = new SolidBrush( Color.Black ); // hace conexión con el servidor y obtiene el flujo // de red asociado conexion = new TcpClient( "127.0.0.1", 50000 ); flujo = conexion.GetStream(); escritor = new BinaryWriter( flujo ); lector = new BinaryReader( flujo ); // inicia un nuevo subproceso para enviar y recibir mensajes subprocesoSalida = new Thread( new ThreadStart( Ejecutar ) ); subprocesoSalida.Start(); } // fin del método ClienteTresEnRayaForm_Load

Figura 23.6 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 1 de 6).

23.8 Juego de Tres en raya cliente/servidor mediante el uso de un servidor con...

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118

// vuelve a pintar los cuadros private void ClienteTresEnRayaForm_Paint( object sender, PaintEventArgs e ) { PintarCuadros(); } // fin del método ClienteTresEnRayaForm_Load // el juego terminó private void ClienteTresEnRayaForm_FormClosing( object sender, FormClosingEventArgs e ) { listo = true; System.Environment.Exit( System.Environment.ExitCode ); } // fin de ClienteTresEnRayaForm_FormClosing // delegado que permite llamar al método MostrarMensaje // en el subproceso que crea y mantiene la GUI private delegate void DisplayDelegate( string message ); // el método MostrarMensaje establece la propiedad Text de mostrarTextBox // de una manera segura para el subproceso private void MostrarMensaje( string mensaje ) { // si la modificación de mostrarTextBox no es segura para el subproceso if ( mostrarTextBox.InvokeRequired ) { // usa el método heredado Invoke para ejecutar MostrarMensaje // a través de un delegado Invoke( new DisplayDelegate( MostrarMensaje ), new object[] { mensaje } ); } // fin de if else // sí se pude modificar mostrarTextBox en el subproceso actual mostrarTextBox.Text += mensaje; } // fin del método MostrarMensaje // delegado que permite llamar al método CambiarIdLabel // en el subproceso que crea y mantiene la GUI private delegate void ChangeIdLabelDelegate( string message ); // el método CambiarIdLabel establece la propiedad Text de mostrarTextBox // de una manera segura para el subproceso private void CambiarIdLabel( string etiqueta ) { // si la modificación de idLabel no es segura para el subproceso if ( idLabel.InvokeRequired ) { // usa el método heredado Invoke para ejecutar CambiarIdLabel // a través de un delegado Invoke( new ChangeIdLabelDelegate( CambiarIdLabel ), new object[] { etiqueta } ); } // fin de if else // sí se puede modificar idLabel en el subproceso actual idLabel.Text = etiqueta; } // fin del método CambiarIdLabel // dibuja la marca de cada cuadrado public void PintarCuadros() { Graphics g;

Figura 23.6 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 2 de 6).

943

944

119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177

Capítulo 23 Redes: sockets basados en flujos y datagramas

// dibuja la marca apropiada en cada panel for ( int fila = 0; fila < 3; fila++ ) { for ( int columna = 0; columna < 3; columna++ ) { // obtiene el objeto Graphics para cada Panel g = tablero[ fila, columna ].PanelCuadros.CreateGraphics(); // dibuja la letra apropiada en el panel g.DrawString( tablero[ fila, columna ].Marca.ToString(), tablero0Panel.Font, brocha, 10, 8 ); } // fin de for } // fin de for } // fin del método PintarCuadros // envía al servidor la ubicación del cuadro en el que se hizo clic private void cuadro_MouseUp( object sender, System.Windows.Forms.MouseEventArgs e ) { // para cada cuadro, verifica si se hizo clic en él for ( int fila = 0; fila < 3; fila++ ) { for ( int columna = 0; columna < 3; columna++ ) { if ( tablero[ fila, columna ].PanelCuadros == sender ) { CuadroActual = tablero[ fila, columna ]; // envía el movimiento al servidor EnviarCuadradoClic( tablero[ fila, columna ].Ubicacion ); } // fin de if } // fin de for } // fin de for } // fin del método cuadro_MouseUp // subproceso de control que permite la actualización continua // del control TextBox en la pantalla public void Ejecutar() { // primero obtiene la marca del jugador (X o O) miMarca = lector.ReadChar(); CambiarIdLabel( "Usted es el jugador \"" + miMarca + "\"" ); miTurno = ( miMarca == 'X' ? true : false ); // procesa los mensajes entrantes try { // recibe los mensajes que se envían al cliente while ( !listo ) ProcesarMensaje( lector.ReadString() ); } // fin de try catch ( IOException ) { MessageBox.Show( "Servidor desconectado, el juego terminó", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // fin de catch } // fin del método Ejecutar

Figura 23.6 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 3 de 6).

23.8 Juego de Tres en raya cliente/servidor mediante el uso de un servidor con...

178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236

// procesa los mensajes enviados al cliente public void ProcesarMensaje( string mensaje ) { // si el movimiento que envió el jugador al servidor es válido // actualiza la pantalla, establece la marca de ese cuadro para que sea // la marca del jugador actual y vuelve a pintar el tablero if ( mensaje == "Movimiento válido." ) { MostrarMensaje( "Movimiento válido, por favor espere.\r\n" ); cuadroActual.Marca = miMarca; PintarCuadros(); } // fin de if else if ( mensaje == "Movimiento inválido, intente de nuevo." ) { // si el movimiento es inválido, muestra ese mensaje y ahora // es turno de este jugador otra vez MostrarMensaje( mensaje + "\r\n" ); miTurno = true; } // fin de else if else if ( mensaje == "El oponente movió." ) { // si el oponente movió, busca la ubicación de su movimiento int ubicacion = lector.ReadInt32(); // establece ese cuadro para que tenga la marca del oponente y // vuelve a pintar el tablero tablero[ ubicacion / 3, ubicacion % 3 ].Marca = ( miMarca == 'X' ? 'O' : 'X' ); PintarCuadros(); MostrarMensaje( "El oponente movió.

Es su turno.\r\n" );

// ahora es el turno de este jugador miTurno = true; } // fin de else if else MostrarMensaje( mensaje + "\r\n" ); // muestra el mensaje } // fin del método ProcesarMensaje // envía al servidor el número del cuadro en el que se hizo clic public void EnviarCuadradoClic( int ubicacion ) { // si es el movimiento del jugador actual justo ahora if ( miTurno ) { // envía la ubicación del movimiento al servidor escritor.Write( ubicacion ); // si es ahora el turno del otro jugador miTurno = false; } // fin de if } // fin del método EnviarCuadradoClic // propiedad de sólo escritura para el cuadro actual public Cuadro CuadroActual { set { cuadroActual = value;

Figura 23.6 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 4 de 6).

945

946

Capítulo 23 Redes: sockets basados en flujos y datagramas

237 } // fin de set 238 } // fin de la propiedad CuadroActual 239 } // fin de la clase ClienteTresEnRayaForm Al principio del juego. a)

b)

Después que el jugador X hace el primer movimiento. c)

d)

Después que el jugador O hace el segundo movimiento. e)

f)

Figura 23.6 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 5 de 6).

23.8 Juego de Tres en raya cliente/servidor mediante el uso de un servidor con...

Después que el jugador X hace el movimiento final. g)

h)

La salida del servidor de Tres en raya, proveniente de las interacciones con los clientes. i) salida del servidor después de (a,b) salida del servidor después de (c,d) salida del servidor después de (e,f) salida del servidor después de (g,h)

Figura 23.6 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 6 de 6). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

// Fig. 23.7: Cuadro.cs // Un Cuadro en el tablero de Tres en raya. using System.Windows.Forms; // la representación de un cuadro en una cuadrícula de tres en raya public class Cuadro { private Panel panel; // panel de la GUI que representa este Cuadro private char marca; // la marca del jugador en este Cuadro (si la hay) private int ubicacion; // ubicacion en el tablero de este Cuadro // constructor public Cuadro( Panel nuevoPanel, char nuevaMarca, int nuevaUbicacion ) { panel = nuevoPanel; marca = nuevaMarca; ubicacion = nuevaUbicacion; } // fin del constructor

Figura 23.7 | La clase Cuadro. (Parte 1 de 2).

947

948

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50

Capítulo 23 Redes: sockets basados en flujos y datagramas

// propiedad PanelCuadros; el panel que representa el cuadro public Panel PanelCuadros { get { return panel; } // fin de get } // fin de la propiedad PanelCuadros // propiedad Marca; la marca en el cuadro public char Marca { get { return marca; } // fin de get set { marca = value; } // fin de set } // fin de la propiedad Marca // propiedad Ubicacion; la ubicación del cuadro en el tablero public int Ubicacion { get { return ubicacion; } // fin de get } // fin de la propiedad Ubicacion } // fin de la clase Cuadro

Figura 23.7 | La clase Cuadro. (Parte 2 de 2). la posición en el tablero en la que se debe colocar la marca. El manejador de eventos Load de ClienteTres50) y obtiene una referencia al objeto líneas 56-57 inician un subproceso para leer los mensajes enviados del servidor al cliente. El servidor pasa los mensajes (por ejemplo, si cada movimiento es válido) al método ProcesarMensaje (líneas 179-215). Si el mensaje indica que un movimiento es válido (línea 184), el cliente establece su Marca en el cuadro actual (el cuadro en el que el usuario hizo clic) y vuelve a pintar el tablero. Si el mensaje indica que un movimiento es inválido (línea 190), el cliente notifica al usuario que haga clic en un cuadro distinto. Si el mensaje indica que el oponente hizo un movimiento (línea 197), la línea 200 lee un int del servidor, especificando en qué parte del tablero debe el cliente colocar la Marca del oponente. ClienteTresEnRayaForm incluye un par delegate/método para permitir que los subprocesos modifiquen la propiedad Text de idLabel (líneas 97-113), así como los métodos DisplayDelegate y MostrarMensaje para modificar la propiedad Text de mostrarTextBox (líneas 77-93). EnRayaForm (líneas 30-58) abre una conexión con el servidor (línea NetworkStream asociado de la conexión, de TcpClient (línea 51). Las

23.9 Control WebBrowser Con la FCL 2.0, Microsoft introdujo el control WebBrowser, que permite a las aplicaciones incorporar herramientas de exploración Web. El control cuenta con métodos para navegar en páginas Web y mantiene su propio historial de los sitios Web visitados. También genera eventos, a medida que el usuario interactúa con el contenido que se muestra en el control, por lo que su aplicación puede responder a eventos como cuando el usuario hace clic en los vínculos que aparecen en el contenido. La figura 23.8 demuestra las capacidades del control WebBrowser. La clase ExploradorForm proporciona la funcionalidad básica de un explorador Web, ya que permite al usuario navegar hacia un URL, desplazarse hacia atrás y hacia delante por el historial de sitios visitados y volver a cargar la página Web actual.

23.9 Control WebBrowser

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

// Fig. 23.8: Explorador.cs // Ejemplo del control WebBrowser. using System; using System.Windows.Forms; public partial class ExploradorForm : Form { public ExploradorForm() { InitializeComponent(); } // fin del constructor // navega una página hacia atrás private void atrasButton_Click( object sender, EventArgs e ) { webBrowser.GoBack(); } // fin del método atrasButton_Click // navega una página hacia delante private void adelanteButton_Click( object sender, EventArgs e ) { webBrowser.GoForward(); } // fin del método adelanteButton_Click // deja de cargar la página actual private void detenerButton_Click( object sender, EventArgs e ) { webBrowser.Stop(); } // fin del método detenerButton_Click // vuelve a cargar la página actual private void actualizarButton_Click( object sender, EventArgs e ) { webBrowser.Refresh(); } // fin del método actualizarButton_Click // navega a la página inicial del usuario private void inicioButton_Click( object sender, EventArgs e ) { webBrowser.GoHome(); } // fin del método inicioButton_Click // si el usuario oprimió Intro, navega al URL especificado private void navegacionTextBox_KeyDown( object sender, KeyEventArgs e ) { if ( e.KeyCode == Keys.Enter ) webBrowser.Navigate( navegacionTextBox.Text ); } // fin del método navegacionTextBox_KeyDown // habilita detenerButton mientras la página actual se está cargando private void webBrowser_Navigating( object sender, WebBrowserNavigatingEventArgs e ) { detenerButton.Enabled = true; } // fin del método webBrowser_Navigating // actualiza el texto de estado private void webBrowser_StatusTextChanged( object sender,

Figura 23.8 | Ejemplo del control WebBrowser. (Parte 1 de 2).

949

950

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97

Capítulo 23 Redes: sockets basados en flujos y datagramas

EventArgs e ) { estadoTextBox.Text = webBrowser.StatusText; } // fin del método webBrowser_StatusTextChanged // actualiza el control ProgressBar con base en el porcentaje descargado de la página private void webBrowser_ProgressChanged( object sender, WebBrowserProgressChangedEventArgs e ) { paginaProgressBar.Value = ( int ) ( ( 100 * e.CurrentProgress ) / e.MaximumProgress ); } // fin del método webBrowser_ProgressChanged // actualiza los controles del explorador web en forma apropiada private void webBrowser_DocumentCompleted( object sender, WebBrowserDocumentCompletedEventArgs e ) { // establece el texto en navegacionTextBox al URL de la página actual navegacionTextBox.Text = webBrowser.Url.ToString(); // habilita o deshabilita los controles atrasButton y adelanteButton atrasButton.Enabled = webBrowser.CanGoBack; adelanteButton.Enabled = webBrowser.CanGoForward; // deshabilita detenerButton detenerButton.Enabled = false; // borra el control pagionaProgressBar paginaProgressBar.Value = 0; } // fin del método webBrowser_DocumentCompleted // actualiza el título del Explorador private void webBrowser_DocumentTitleChanged( object sender, EventArgs e ) { this.Text = webBrowser.DocumentTitle + " - Explorador"; } // fin del método webBrowser_DocumentTitleChanged } // fin de la clase ExploradorForm

Figura 23.8 | Ejemplo del control WebBrowser. (Parte 2 de 2). Las líneas 14-41 definen cinco manejadores de eventos Click, uno para cada uno de los cinco controles de navegación que aparecen en la parte superior del formulario. Cada manejador de eventos llama a su método correspondiente de WebBrowser. El método GoBack de WebBrowser (línea 16) hace que el control navegue de vuelta a la página anterior en el historial de navegación. El método GoForward (línea 22) hace que

Button

23.10 .NET Remoting

951

el control navegue hacia delante a la siguiente página en el historial de navegación. El método Stop (línea 28) hace que el control deje de cargar la página actual. El método Refresh (línea 34) hace que el control vuelva a cargar la página actual. El método GoHome (línea 40) hace que el control navegue a la página inicial del usuario, según esté definida en las opciones de Internet Explorer (bajo Herramientas > Opciones de Internet… en la sección Página de inicio). El control TextBox a la derecha de los botones de navegación permite al usuario escribir el URL de un sitio Web para explorarlo. Cuando el usuario escribe cada pulsación de tecla en el control TextBox, se ejecuta el manejador de eventos en las líneas 44-49. Si la tecla oprimida es Intro, la línea 48 llama al método Navigate de WebBrowser para recuperar el documento en el URL especificado. El control WebBrowser genera un evento Navigating cuando empieza a cargar una nueva página. Cuando esto ocurre, se ejecuta el manejador de eventos en las líneas 52-56 y la línea 55 habilita el control detenerBoton, para que el usuario pueda cancelar la carga de la página Web. Por lo general, un usuario puede ver el estado del proceso de carga de una página Web en la parte inferior de la ventana del explorador. Por esta razón, incluimos un control TextBox (llamado estadoTextBox) y un control ProgressBar (llamado paginaProgressBar) en la parte inferior de nuestro formulario. El control WebBrowser genera un evento StatusTextChanged cuando cambia la propiedad StatusText de WebBrowser. El manejador de eventos para este evento (líneas 59-63) asigna el nuevo contenido de la propiedad StatusText del control a la propiedad Text de estadoTextBox (línea 62), para que el usuario pueda supervisar los mensajes de estado de WebBrowser. El control genera un evento ProgressChanged cuando se actualiza el progreso de carga de la página en el control WebBrowser. El manejador de eventos ProgressChanged (líneas 66-71) actualiza la propiedad Value de paginaProgressBar (líneas 69-70), para reflejar cuánto porcentaje del documento actual se ha cargado. Cuando el control WebBrowser termina de cargar un documento, genera un evento DocumentCompleted. Esto ejecuta el manejador de eventos en las líneas 74-89. La línea 78 actualiza el contenido de navegacionTextBox, de manera que muestre el URL de la página actual que está cargada (la propiedad Url de WebBrowser). Esto es muy importante si el usuario navega a otra página Web haciendo clic en un vínculo de la página existente. Las líneas 81-82 utilizan las propiedades CanGoBack y CanGoForward para determinar si los botones atrás y adelante deben habilitarse o deshabilitarse. Como ahora el documento está cargado, la línea 85 deshabilita el control detenerButton. La línea 88 establece la propiedad Value de paginaProgressBar a 0, para indicar que no se está cargando ningún contenido en ese momento. Las líneas 92-96 definen un manejador de eventos para el evento DocumentTitleChanged, el cual se produce cuando se carga un nuevo documento en el control WebBrowser. La línea 95 establece la propiedad Text de ExploradorForm (que se muestra en la barra de título del formulario) con base en el título del documento actual (DocumentTitle) de WebBrowser.

23.10 .NET Remoting

El .NET Framework proporciona una tecnología de computación distribuida llamada .NET Remoting, la cual permite a un programa acceder a los objetos en otra máquina, a través de una red. El término .NET Remoting es similar en concepto a RMI (invocación de métodos remotos) en Java y RPC (llamada a procedimientos remotos) en los lenguajes de programación por procedimientos. La tecnología .NET Remoting es también similar a los servicios Web (capítulo 22), con unas cuantas diferencias clave. Con los servicios Web, una aplicación cliente se comunica con un servicio Web hospedado por un servidor Web. El cliente y el servicio Web pueden estar escritos en cualquier lenguaje, siempre y cuando puedan transmitir mensajes en SOAP. Con .NET Remoting, una aplicación cliente se comunica con una aplicación servidor, y tanto el cliente como el servidor deben estar escritos en lenguajes .NET. Mediante el uso de .NET Remoting, un cliente y un servidor pueden comunicarse a través de llamadas a métodos y pueden transmitirse objetos entre las aplicaciones; a este proceso se le conoce como cálculo de referencias (marshaling ) de los objetos.

Canales

El cliente y el servidor pueden comunicarse entre sí a través de canales. Por lo general, los canales utilizan el protocolo HTTP o TCP para transmitir mensajes. La ventaja de un canal HTTP es que comúnmente los firewalls permiten el uso de conexiones HTTP de manera predeterminada, mientras que generalmente bloquean las conexiones TCP desconocidas. La ventaja de un canal TCP es que tiene un mejor rendimiento que un canal HTTP. En una

952

Capítulo 23 Redes: sockets basados en flujos y datagramas

aplicación de .NET Remoting, el cliente y el servidor crean un canal cada uno, y ambos canales deben usar el mismo protocolo para comunicarse entre sí. En nuestro ejemplo, utilizamos canales HTTP.

Cálculo de referencias (Marshaling)

Existen dos maneras de calcular las referencias de un objeto: por valor y por referencia. El cálculo de referencias por valor requiere que el objeto sea serializable; esto es, que sea capaz de representarse como un mensaje con formato, que pueda enviarse de una aplicación a otra a través de un canal. El extremo receptor del canal deserializa el objeto para obtener una copia del objeto original. Para permitir la serialización y deserialización de un objeto, su clase debe declararse con el atributo [ Serializable ] o debe implementar la interfaz ISerializable. El cálculo de referencias por referencia requiere que la clase del objeto extienda a la clase MarshalByRefObject del espacio de nombres System. Un objeto cuyas referencias se calculan por referencia se conoce como objeto remoto, y su clase como clase remota. Cuando se calculan las referencias de un objeto por referencia, el objeto en sí no se transmite. En vez de ello, se crean dos objetos proxy: un proxy transparente y un proxy real. El proxy transparente proporciona todos los servicios public de un objeto remoto. Por lo general, un cliente llama a los métodos y las propiedades del proxy transparente como si fuera el objeto remoto. Después, el proxy transparente llama al método Invoke del proxy real. Esto envía el mensaje apropiado del canal del cliente al canal del servidor. El servidor recibe este mensaje y lleva a cabo la llamada al método especificado, o accede a la propiedad especificada en el objeto actual, que reside en el servidor. En nuestro ejemplo, calculamos las referencias de un objeto remoto por referencia.

Aplicación de información del clima mediante el uso de .NET Remoting Ahora presentaremos un ejemplo de .NET Remoting que descarga la información sobre el clima Traveler’s Forecast del sitio Web del Servicio meteorológico nacional: http://iwin.nws.noaa.gov/iwin/us/traveler.html

[Nota: Cuando desarrollamos este ejemplo, el Servicio meteorológico nacional indicó que la información que se proporciona en la página Web Traveler’s Forecast se proporcionará mediante un servicio Web en un futuro cercano. La información que utilizamos en este ejemplo depende directamente del formato de la página Web Traveler’s Forecast. Si tiene problemas al ejecutar este ejemplo, consulte la página de FAQs en www.deitel.com/faq.html. Este problema potencial demuestra un beneficio en cuanto al uso de los servicios Web o .NET Remoting para implementar aplicaciones de computación distribuidas que pueden cambiar en un futuro. La acción de separar la parte del servidor de la aplicación, que depende del formato de un origen de datos externo, de la parte del cliente de la aplicación, permite actualizar la implementación del servidor sin requerir cambios en el cliente.] Nuestra aplicación de .NET Remoting consiste en cinco componentes: 1. La clase serializable ClimaCiudad, que representa el reporte del clima para una ciudad. 2. La interfaz Reporte, que declara una propiedad Reportes que el objeto cuyas referencias se calcularon proporciona a una aplicación cliente, para obtener una colección de objetos ClimaCiudad. 3. La clase remota InfoReporte, que extiende a la clase MarshalByRefObject, implementa a la interfaz Reporte y se instanciará sólo en el servidor. 4. Una aplicación ServidorClima que establece un canal servidor y hace que la clase InfoReporte esté disponible en un URI (Identificador uniforme de recursos) específico. 5. Una aplicación

ClienteClima que establece un ServidorClima para obtener el reporte del clima

canal cliente y solicita un objeto del día.

InfoReporte

del

Clase ClimaCiudad La clase ClimaCiudad (figura 23.9) contiene información meteorológica para una ciudad. La clase ClimaCiudad se declara en el espacio de nombres Clima (línea 5) para que pueda reutilizarse, y se publicará en el archivo de biblioteca de clases Clima.dll, al cual deben hacer referencia las aplicaciones cliente y servidor. Por esta razón, es conveniente colocar esta clase (y la interfaz Reporte de la figura 23.10) en un proyecto de biblioteca de clases. La clase ClimaCiudad se declara con el atributo Serializable (línea 7), el cual indica que se pueden calcular las

23.10 .NET Remoting

953

referencias de un objeto de la clase ClimaCiudad por valor. Esto es necesario, ya que la propiedad Reportes del objeto InfoReportes devolverá los objetos ClimaCiudad, y deben calcularse las referencias por valor de los mismos valores de retorno de los métodos y las propiedades declarados por una clase remota, del servidor al cliente. (Las referencias a los valores de los argumentos en las llamadas a los métodos también se calculan por valor del cliente al servidor.) Así, cuando el cliente llama a un descriptor de acceso get que devuelve objetos ClimaCiudad, el canal servidor serializará los objetos ClimaCiudad en un mensaje que el canal cliente pueda deserializar, para crear copias de los objetos ClimaCiudad originales. La clase ClimaCiudad también implementa la interfaz IComparable (línea 8), de manera que pueda ordenarse alfabéticamente un arreglo ArrayList de objetos Clima-Ciudad.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

// Fig. 23.9: ClimaCiudad.cs // Clase que representa la información del clima para una ciudad. using System; namespace Clima { [ Serializable ] public class ClimaCiudad : IComparable { private string nombreCiudad; private string descripcion; private string temperatura; public ClimaCiudad( string ciudad, string informacion, string grados ) { nombreCiudad = ciudad; descripcion = informacion; temperatura = grados; } // fin del constructor // propiedad de sólo lectura que obtiene el nombre de la ciudad public string NombreCiudad { get { return nombreCiudad; } // fin de get } // fin de la propiedad NombreCiudad // propiedad de sólo lectura que obtiene la descripción del clima de la ciudad public string Descripcion { get { return descripcion; } // fin de get } // fin de la propiedad Descripcion // propiedad de sólo lectura que obtiene la temperatura de la ciudad public string Temperatura { get { return temperatura; } // fin de get

Figura 23.9 | La clase ClimaCiudad. (Parte 1 de 2).

954

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

Capítulo 23 Redes: sockets basados en flujos y datagramas

} // fin de la propiedad Temperatura // implementación del método CompareTo para alfabetizar public int CompareTo( object other ) { return string.Compare( NombreCiudad, ( ( ClimaCiudad ) other ).NombreCiudad ); } // fin del método Compare // devuelve representación string de este objeto ClimaCiudad // (utilizado para mostrar el reporte del clima en la consola del servidor) public override string ToString() { return nombreCiudad + " | " + temperatura + " | " + descripcion; } // fin del método ToString } // fin de la clase ClimaCiudad } // fin del espacio de nombres Clima

Figura 23.9 | La clase ClimaCiudad. (Parte 2 de 2). ClimaCiudad contiene tres variables de instancia (líneas 10-12) para almacenar el nombre de la ciudad, las temperaturas máxima/mínima y la descripción de la condición del tiempo. El constructor de ClimaCiudad (líneas 14-20) inicializa las tres variables de instancia. Las líneas 23-47 declaran tres propiedades de sólo lectura, que permiten obtener los valores de las tres variables de instancia. ClimaCiudad implementa a IComparable, por lo que debe declarar un método llamado CompareTo que recibe una referencia object y devuelve un int (líneas 50-54). Además, queremos alfabetizar los objetos ClimaCiudad con base en los nombres de sus ciudades (nombreCiudad), por lo que CompareTo llama al método string Compare con los nombres de las ciudades de los dos objetos ClimaCiudad. La clase ClimaCiudad también redefine el método ToString para mostrar información para esta ciudad (líneas 58-61). La aplicación servidor utiliza el método ToString para mostrar la información del clima obtenida de la página Web Traveler’s Forecast en la consola.

Interfaz Reporte La figura 23.10 muestra el código para la interfaz Reporte. Esta interfaz también se declara en el espacio de nombres Clima (línea 7) y se incluye con la clase ClimaCiudad en el archivo de biblioteca de clases Clima.dll, por lo que puede usarse tanto en la aplicación cliente como en la aplicación servidor. Reporte declara una propiedad de sólo lectura (líneas 11-14), con un descriptor de acceso get que devuelve un arreglo ArrayList de objetos

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 23.10: Reporte.cs // Interfaz que define una propiedad para obtener // la información en un reporte meteorológico. using System; using System.Collections; namespace Clima { public interface Reporte { ArrayList Reportes { get; } // fin de la propiedad Reportes } // fin de la interfaz Reporte } // fin del espacio de nombres Clima

Figura 23.10 | La interfaz Reporte en el espacio de nombres Clima.

23.10 .NET Remoting

955

ClimaCiudad.

La aplicación cliente utiliza esta propiedad para obtener la información en el reporte del clima: el nombre de cada ciudad, la temperatura máxima/mínima y la condición climatológica.

Clase InfoReporte La clase remota InfoReporte (figura 23.11) implementa a la interfaz Reporte (línea 10) del espacio de nombres Clima (especificado por la directiva using en la línea 8). InfoReporte también extiende a la clase base MarshalByRefObject. La clase InfoReporte es parte de la aplicación remota ServidorClima y no está directamente disponible para la aplicación cliente.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

// Fig. 23.11: InfoReporte.cs // Clase que implementa a la interfaz Reporte, obtiene // y devuelve datos acerca del clima using System; using System.Collections; using System.IO; using System.Net; using Clima; public class InfoReporte : MarshalByRefObject, Reporte { private ArrayList listaCiudades; // ciudades, temperaturas, descripciones public InfoReporte() { listaCiudades = new ArrayList(); // crea cliente Web para obtener acceso a la página Web WebClient miCliente = new WebClient(); // obtiene StreamReader de respuesta, para poder leer la página StreamReader entrada = new StreamReader( miCliente.OpenRead( "http://iwin.nws.noaa.gov/iwin/us/traveler.html" ) ); string separador1 = "TAV12"; // indica el primer lote de ciudades string separador2 = "TAV13"; // indica el segundo lote de ciudades // localiza separador1 en la página Web while ( !entrada.ReadLine().StartsWith( separador1 ) ); // no hace nada LeerCiudades( entrada ); // lee el primer lote de ciudades // localiza separador2 en la página Web while ( !entrada.ReadLine().StartsWith( separador2 ) ); // no hace nada LeerCiudades( entrada ); // lee el segundo lote de ciudades listaCiudades.Sort(); // ordena alfabéticamente la lista de ciudades entrada.Close(); // cierra StreamReader al servidor NWS // muestra los datos en el lado servidor Console.WriteLine( "Datos del sitio Web NWS:" ); foreach ( ClimaCiudad ciudad in listaCiudades ) { Console.WriteLine( ciudad ); } // fin de foreach } // fin del constructor

Figura 23.11 | El cálculo de las referencias en la clase InfoReporte, que implementa a la interfaz Reporte, es por referencia. (Parte 1 de 2).

956

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89

Capítulo 23 Redes: sockets basados en flujos y datagramas

// método utilitario que lee un lote de ciudades private void LeerCiudades( StreamReader entrada ) { // formato de día y formato de noche string formatoDia = "CITY WEA MAX/MIN WEA MAX/MIN"; string formatoNoche = "CITY WEA MIN/MAX WEA MIN/MAX"; string lineaEntrada = ""; // localiza el encabezado en donde empieza la información meteorológica do { lineaEntrada = entrada.ReadLine(); } while ( !lineaEntrada.Equals( formatoDia ) && !lineaEntrada.Equals( formatoNoche ) ); lineaEntrada = entrada.ReadLine(); // obtiene los datos de la primera ciudad // mientras haya más ciudades qué leer while ( lineaEntrada.Length > 28 ) { // crea objeto ClimaCiudad para la ciudad ClimaCiudad clima = new ClimaCiudad( lineaEntrada.Substring( 0, 16 ), lineaEntrada.Substring( 16, 7 ), lineaEntrada.Substring( 23, 7 ) ); listaCiudades.Add( clima ); // lo agrega a ArrayList lineaEntrada = entrada.ReadLine(); // obtiene los datos de la siguiente ciudad } // fin de while } // fin del método LeerCiudades // propiedad para obtener los reportes meteorológicos de las ciudades public ArrayList Reportes { get { return listaCiudades; } // fin de get } // fin de la propiedad Reportes } // fin de la clase InfoReporte

Figura 23.11 | El cálculo de las referencias en la clase InfoReporte, que implementa a la interfaz Reporte, es por referencia. (Parte 2 de 2). Las líneas 14-46 declaran el constructor InfoReporte. La línea 19 crea un objeto WebClient (espacio de nombres System.Net) para interactuar con un origen de datos especificado por un URL; en este caso, el URL para la página Traveler’s Forecast del NWS (http://iwin.nws.noaa.gov/iwin/us/traveler.html). Las líneas 22-23 llaman al método OpenRead de WebClient, el cual devuelve un objeto Stream que el programa puede usar para leer datos que contengan la información climatológica del URL especificado. Este objeto Stream se utiliza para crear un objeto StreamReader, por lo que el programa puede leer el marcado HTML de la página Web, línea por línea. La sección de la página Web en la que estamos interesados consiste en dos lotes de ciudades: de Albany hasta Reno, y de Salt Lake City hasta Washington, D.C. El primer lote ocurre en una sección que empieza con la cadena "TAV12", mientras que el segundo lote ocurre en una sección que empieza con la cadena "TAV13". Declaramos las variables separador1 y separador2 para que almacenen estas cadenas. La línea 29 lee el marcado de

23.10 .NET Remoting

957

HTML una línea a la vez, hasta encontrar "TAV12". Una vez que se llega a "TAV12", el programa llama al método utilitario LeerCiudades para leer un lote de ciudades en el objeto ArrayList listaCiudades. A continuación, la línea 33 lee el marcado de HTML una línea a la vez, hasta encontrar "TAV13", y la línea 34 hace otra llamada al método LeerCiudades para leer el segundo lote de ciudades. La línea 36 llama al método Sort de la clase ArrayList para ordenar los objetos ClimaCiudad en forma alfabética, por nombre de ciudad. La línea 37 cierra la conexión de StreamReader con el sitio Web. Las líneas 42-45 imprimen en pantalla la información del clima para cada ciudad, en la pantalla de la consola de la aplicación servidor. Las líneas 49-79 declaran el método utilitario LeerCiudades, el cual recibe un objeto StreamReader y lee la información para cada ciudad, crea un objeto ClimaCiudad para esto y coloca el objeto ClimaCiudad en listaCiudades. La instrucción do…while (líneas 59-63) continúa leyendo la página, una línea a la vez, hasta encontrar la línea del encabezado que empieza la tabla de pronósticos del clima. Esta línea empieza con formatoDia (líneas 52-53), indicando el encabezado para la información en horas diurnas, o con formatoNoche (líneas 54-55), indicando el encabezado para la información en horas nocturnas. Debido a que la línea podría estar en cualquier formato con base en la hora del día, la condición de continuación de ciclo comprueba ambas opciones. La línea 65 lee la siguiente línea de la página Web, que es la primera línea que contiene información sobre la temperatura. La instrucción while (líneas 68-78) crea un nuevo objeto ClimaCiudad para representar a la ciudad actual. Analiza el objeto string que contiene los datos actuales del clima, separando el nombre de la ciudad, la condición climatológica y la temperatura. El objeto ClimaCiudad se agrega a listaCiudades. Después se lee la siguiente línea de la página y se almacena en lineaEntrada para la siguiente iteración. Este proceso continúa mientras la longitud del objeto string que se lea de la página Web sea mayor de 28 (las líneas que contienen los datos del clima son mayores de 28 caracteres). La primera línea menor que esta cantidad indica el final de la sección de pronósticos en la página Web. La propiedad de sólo lectura Reportes (líneas 82-88) implementa la propiedad Reportes de la interfaz Reporte para devolver listaCiudades. La aplicación cliente llamará en forma remota a esta propiedad para obtener el reporte climatológico del día.

Clase ServidorClima La figura 23.12 contiene el código para la aplicación servidor. Las directivas using en las líneas 5-7 especifican los espacios de nombres de .NET Remoting System.Runtime.Remoting, System.Runtime.Remoting.Channels y System.Runtime.Remoting.Channels.Http. Los primeros dos espacios de nombres son requeridos para .NET Remoting, y el tercero es requerido para los canales HTTP. El espacio de nombres System.Runtime.Remoting. Channels.Http requiere que el proyecto haga referencia al ensamblado System.Runtime.Remoting, el cual puede encontrarse bajo la ficha .NET en el menú Agregar referencias. La directiva using en la línea 8 especifica el espacio de nombres Clima, que contiene la interfaz Reporte. Recuerde agregar una referencia a Clima.dll en este proyecto.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

// Fig. 23.12: ServidorClima.cs // Aplicación servidor que utiliza .NET Remoting para enviar // la información de un reporte meteorológico a un cliente using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using Clima; class ServidorClima { static void Main( string[] args ) { // establece canal HTTP HttpChannel canal = new HttpChannel( 50000 );

Figura 23.12 | La clase ServidorClima expone a la clase remota InfoReporte. (Parte 1 de 2).

958

16 17 18 19 20 21 22 23 24 25 26

Capítulo 23 Redes: sockets basados en flujos y datagramas

ChannelServices.RegisterChannel( canal, false ); // registra la clase InfoReporte RemotingConfiguration.RegisterWellKnownServiceType( typeof( InfoReporte ), "Reporte", WellKnownObjectMode.Singleton ); Console.WriteLine( "Oprima Intro para cerrar el servidor." ); Console.ReadLine(); } // fin de Main } // fin de la clase ServidorClima

Figura 23.12 | La clase ServidorClima expone a la clase remota InfoReporte. (Parte 2 de 2). Las líneas 15-16 en Main registran un canal HTTP en el equipo actual, en el puerto 50000. Éste es el número de puerto que los clientes utilizarán para conectarse al ServidorClima en forma remota. El argumento false en la línea 16 indica que no deseamos habilitar la seguridad, lo cual está más allá del alcance de esta introducción. Las líneas 19-21 registran el tipo de la clase InfoReporte en el URI “Reporte” como una clase remota Singleton. Si una clase remota se registra como Singleton, se creará un objeto remoto cuando el primer cliente solicite esa clase remota, y ese objeto remoto dará servicio a todos los clientes. El modo alternativo es SingleCall, en donde se crea un objeto remoto para cada llamada individual al método remoto en la clase remota. [Nota: Un objeto remoto Singleton no tiene un tiempo de vida infinito; será candidato a la recolección de basura después de estar inactivo durante 5 minutos. Se creará un nuevo objeto remoto Singleton si otro cliente solicita uno posteriormente.] La clase remota InfoReporte está ahora disponible para los clientes en el URI “http://DireccionIP:50000/Reporte”, en donde DireccionIP es la dirección IP de la computadora en la que se está ejecutando el servidor. El canal permanece abierto mientras que la aplicación siga ejecutándose, por lo que la línea 24 espera a que el usuario que ejecuta la aplicación servidor oprima Intro antes de terminar la aplicación.

Clase ClienteClimaForm La clase ClienteClimaForm (figura 23.13) es una aplicación Windows que utiliza .NET Remoting para obtener información climatológica de ServidorClima, y muestra la información en forma gráfica, fácil de leer. La GUI contiene 43 controles Label; uno para cada ciudad en la página Web Traveler’s Forecast. Todos los controles Label están colocados en un Panel con una barra de desplazamiento vertical. Cada control Label muestra la información del clima para una ciudad. Las líneas 7-9 son directivas using para los espacios de nombres requeridos para utilizar .NET Remoting. Para este proyecto, debe agregar referencias al ensamblado System.Runtime.Remoting y al archivo Clima.dll que creamos antes.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 23.13: ClienteClima.cs // Cliente que utiliza .NET Remoting para obtener un reporte meteorológico. using System; using System.Collections; using System.Drawing; using System.Windows.Forms; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using Clima; public partial class ClienteClimaForm : Form { public ClienteClimaForm()

Figura 23.13 | La aplicación ClienteClima accede a un objeto InfoReporte en forma remota y muestra el reporte climatológico. (Parte 1 de 3).

23.10 .NET Remoting

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72

959

{ InitializeComponent(); } // fin del constructor // obtiene los datos del clima private void ClienteClimaForm_Load( object sender, EventArgs e ) { // establece canal HTTP, no necesita proporcionar un número de puerto HttpChannel canal = new HttpChannel(); ChannelServices.RegisterChannel( canal, false ); // obtiene un proxy para un objeto que implementa a la interfaz Reporte Reporte info = ( Reporte ) RemotingServices.Connect( typeof( Reporte ), "http://localhost:50000/Reporte" ); // obtiene un ArrayList de objetos ClimaCiudad ArrayList ciudades = info.Reportes; // crea arreglo y lo llena con todos los controles Label Label[] etiquetasCiudades = new Label[ 43 ]; int contadorEtiquetas = 0; foreach ( Control control in mostrarPanel.Controls ) { if ( control is Label ) { etiquetasCiudades[ contadorEtiquetas ] = ( Label ) control; ++contadorEtiquetas; // incrementa contador de etiquetas } // fin de if } // fin de foreach // crea Hashtable y la llena con todas las condiciones del clima Hashtable clima = new Hashtable(); clima.Add( "SUNNY", "sunny" ); clima.Add( "PTCLDY", "pcloudy" ); clima.Add( "CLOUDY", "mcloudy" ); clima.Add( "MOCLDY", "mcloudy" ); clima.Add( "TSTRMS", "rain" ); clima.Add( "RAIN", "rain" ); clima.Add( "SNOW", "snow" ); clima.Add( "VRYHOT", "vryhot" ); clima.Add( "FAIR", "fair" ); clima.Add( "RNSNOW", "rnsnow" ); clima.Add( "SHWRS", "showers" ); clima.Add( "WINDY", "windy" ); clima.Add( "NOINFO", "noinfo" ); clima.Add( "MISG", "noinfo" ); clima.Add( "DRZL", "rain" ); clima.Add( "HAZE", "noinfo" ); clima.Add( "SMOKE", "mcloudy" ); clima.Add( "SNOWSHWRS", "snow" ); clima.Add( "FLRRYS", "snow" ); clima.Add( "FOG", "noinfo" ); // crea la fuente para la salida de texto Font fuente = new Font( "Courier New", 8, FontStyle.Bold ); // para cada ciudad

Figura 23.13 | La aplicación ClienteClima accede a un objeto InfoReporte en forma remota y muestra el reporte climatológico. (Parte 2 de 3).

960

73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93

Capítulo 23 Redes: sockets basados en flujos y datagramas

for ( int i = 0; i < ciudades.Count; i++ ) { // usa el arreglo etiquetasCiudades para buscar la siguiente etiqueta Label ciudadActual = etiquetasCiudades[ i ]; // usa el objeto ArrayList ciudades para buscar el siguiente objeto ClimaCiudad ClimaCiudad ciudad = ( ClimaCiudad ) ciudades[ i ]; // establece la imagen de la etiqueta actual a la imagen // que corresponde a la condición climatológica de la ciudad // busca el nombre de imagen correcto en Hashtable clima ciudadActual.Image = new Bitmap( @"images\" + clima[ ciudad.Descripcion.Trim() ] + ".png" ); ciudadActual.Font = fuente; // establece la fuente de la etiqueta ciudadActual.ForeColor = Color.White; // establece color de texto de la etiqueta // establece texto de etiqueta al nombre de ciudad y temperatura ciudadActual.Text = "\r\n " + ciudad.NombreCiudad + ciudad.Temperatura; } // fin de for } // fin del método ClienteClimaForm_Load } // fin de la clase ClienteClimaForm

Figura 23.13 | La aplicación ClienteClima accede a un objeto InfoReporte en forma remota y muestra el reporte climatológico. (Parte 3 de 3). El método ClienteClimaForm_Load (líneas 20-92) obtiene la información del clima cuando se carga esta aplicación Windows. La línea 23 crea un canal HTTP sin especificar un número de puerto. Esto hace que el constructor de HttpChannel seleccione cualquier número de puerto disponible en la computadora cliente. No es necesario un número de puerto específico, ya que esta aplicación no tiene sus propios clientes que necesitan conocer el número de puerto de antemano. La línea 24 registra el canal en la computadora cliente. Esto permite que el servidor envíe información de vuelta al cliente. Las líneas 27-28 declaran una variable Reporte y le asignan un proxy para un objeto Reporte instanciado por el servidor. Este proxy permite que el cliente llame en forma remota a las propiedades de InfoReporte, redirigiendo al servidor las llamadas a los métodos. El método Connect de RemotingServices se conecta al servidor y devuelve una referencia al proxy para el objeto Reporte. Tenga en cuenta que estamos ejecutando el cliente y el servidor en la misma computadora, por lo que usamos localhost en el URL que representa a la aplicación servidor. Para conectarse a un ServidorClima en una computadora distinta, debe modificar este URL de manera acorde. La línea 31 obtiene el arreglo ArrayList de los objetos ClimaCiudad generados por el constructor de InfoReporte (líneas 14-46 de la figura 23.11). La variable ciudades ahora hace

23.11 Conclusión

961

referencia a un arreglo ArrayList de objetos ClimaCiudad que contienen la información obtenida de la página Web Traveler’s Forecast. Debido a que la aplicación presenta los datos climatológicos para demasiadas ciudades, debemos establecer una manera de organizar la información en los controles Label y asegurar que cada descripción del clima esté acompañada por una imagen apropiada. El programa usa un arreglo para almacenar todos los controles Label y un objeto Hashtable (del cual hablaremos más en el capítulo 26, Colecciones) para almacenar las descripciones climatológicas y los nombres de sus imágenes correspondientes. Un objeto Hashtable almacena pares clave-valor, en los que tanto la clave como el valor pueden ser cualquier tipo de objeto. El método Add agrega pares clave-valor a un objeto Hashtable. La clase también proporciona un indexador para regresar el valor para una clave particular en el objeto Hashtable. La línea 34 crea un arreglo de referencias Label, y las líneas 35-44 colocan en el arreglo los controles Label que creamos en el diseñador, para que pueda accederse a ellos mediante la programación, para mostrar información climatológica para ciudades individuales. La línea 47 crea el objeto Hashtable clima para almacenar pares de condiciones climatológicas y los nombres para las imágenes asociadas con esas condiciones. Observe que el nombre de una descripción climatológica dada no necesariamente corresponde al nombre del archivo PNG que contiene la imagen correcta. Por ejemplo, las condiciones “TSTRMS” y “RAIN” utilizan el archivo de imagen rain.png. Las líneas 73-91 establecen cada control Label, de manera que contenga el nombre de una ciudad, la temperatura actual y una imagen correspondiente a las condiciones climatológicas para esa ciudad. La línea 76 obtiene el control Label que mostrará la información climatológica para la siguiente ciudad. La línea 79 utiliza el arreglo ArrayList llamado ciudades para obtener el objeto ClimaCiudad que contiene la información climatológica para la ciudad. Las líneas 84-85 establecen la imagen del control Label con base en la imagen PNG que corresponde a las condiciones climatológicas de la ciudad. Esto se hace eliminando cualquier espacio en la cadena de descripción, mediante una llamada al método string Trim y se obtiene el nombre de la imagen PNG del objeto Hashtable clima. Las líneas 86-87 establecen las propiedades de Label para lograr el efecto visual que se puede ver en las pantallas de resultados de ejemplo. La línea 90 establece la propiedad Text para mostrar el nombre de la ciudad y las temperaturas máxima/mínima. [Nota: Para preservar la distribución de la ventana de la aplicación cliente, establecemos las propiedades MaximumSize y MinimumSize del formulario Windows Forms al mismo valor, para que el usuario no pueda cambiar el tamaño de la ventana.]

Recursos Web para .NET Remoting En esta sección vimos una introducción básica a la tecnología .NET Remoting. Hay mucho más que ver en cuanto a esta poderosa herramienta del .NET Framework. Los siguientes sitios Web proporcionan información adicional para los lectores que deseen investigar más sobre estas herramientas. Además, si busca la frase “.NET Remoting” en la mayoría de los motores de búsqueda, podrá obtener muchos recursos adicionales. msdn.microsoft.com/library/en-us/cpguide/html/cpconaccessingobjectsinotherap-plicationdomainsusingnetremoting.asp

La Guía del desarrollador de .NET Framework en el sitio Web de MSDN proporciona información detallada acerca de .NET Remoting, incluyendo artículos que hablan sobre la elección entre ASP.NET y .NET Remoting, las generalidades acerca de .NET Remoting, técnicas avanzadas de .NET Remoting y ejemplos. msdn.microsoft.com/library/en-us/dndotnet/html/introremoting.asp

Este sitio ofrece una visión general sobre las herramientas de .NET Remoting. search.microsoft.com/search/results.aspx?qu=.net+remoting Esta búsqueda en microsoft.com proporciona vínculos a muchos artículos y recursos sobre .NET Remoting.

23.11 Conclusión En este capítulo presentamos las técnicas de redes orientadas a las conexiones y sin conexión. Aprendió que Internet es una red “desconfiable” que simplemente transmite paquetes de datos. Hablamos sobre dos protocolos para transmitir paquetes a través de Internet: el Protocolo de control de transmisión (TCP) y el Protocolo de datagramas de usuario (UDP). Aprendió que TCP es un protocolo de comunicación orientado a la conexión, el cual garantiza que los paquetes enviados lleguen al receptor destinado sin daños y en la secuencia correcta. También aprendió que UDP se utiliza por lo general en aplicaciones orientadas al rendimiento, ya que incurre en una mínima sobrecarga para la comunicación entre aplicaciones. Presentamos algunas de las herramientas de C# para implementar comunicaciones

962

Capítulo 23 Redes: sockets basados en flujos y datagramas

con TCP y UDP. Le mostramos cómo crear una aplicación de chat cliente/servidor simple, mediante el uso de sockets de flujo. Después le mostramos cómo enviar datagramas entre un cliente y un servidor. También vio un servidor de Tres en raya con subprocesamiento múltiple, el cual permite que dos clientes se conecten en forma simultánea al servidor para jugar Tres en raya, uno contra el otro. Le presentamos el nuevo control WebBrowser, que le permite agregar herramientas de exploración Web a sus aplicaciones Windows. Por último, demostramos la tecnología .NET Remoting, la cual permite que una aplicación cliente acceda en forma remota a las propiedades y métodos de un objeto instanciado por una aplicación servidor. En el capítulo 24, Estructuras de datos, aprenderá acerca de las estructuras dinámicas de datos que pueden crecer o reducirse en tiempo de ejecución.

24

Estructuras de datos

OBJETIVOS En este capítulo aprenderá lo siguiente: Q

A formar estructuras de datos enlazadas utilizando referencias, clases autorreferenciadas y recursividad.

Q

Cómo las conversiones boxing y unboxing permiten usar valores de tipos simples en donde se esperan objetos object en un programa.

Q

A crear y manipular estructuras dinámicas de datos como listas enlazadas, colas, pilas y árboles binarios.

Q

Comprenderá varias aplicaciones importantes de las estructuras de datos enlazadas.

Q

A crear estructuras de datos reutilizables con clases, herencia y composición.

De muchas cosas a las que estoy atado, no he podido liberarme; y muchas de las que me he liberado, han vuelto a mí. —Lee Wilson Dodd

‘¿Podría caminar un poco más rápido?’ dijo una merluza a un caracol. ‘Hay una marsopa acercándose mucho a nosotros y está pisándome la cola.’ —Lewis Carroll

Siempre hay lugar en la cima. —Daniel Webster

Empujen; sigan moviéndose. —Thomas Morton

Creo que nunca veré a un poema tan encantador como un árbol. —Joyce Kilmer

Pla n g e ne r a l

964

Capítulo 24 Estructuras de datos

24.1 24.2 24.3 24.4 24.5 24.6 24.7

Introducción Tipos struct simples, boxing y unboxing Clases autorreferenciadas Listas enlazadas Pilas Colas Árboles 24.7.1 Árbol binario de búsqueda de valores enteros 24.7.2 Árbol binario de búsqueda de objetos IComparable 24.8 Conclusión

24.1 Introducción

Éste es el primero de tres capítulos que hablarán sobre las estructuras de datos; las que hemos estudiado hasta ahora son de tamaño fijo, como los arreglos unidimensionales y bidimensionales. En este capítulo presentaremos las estructuras de datos dinámicas, que crecen y se reducen en tiempo de ejecución. Las listas enlazadas son colecciones de elementos de datos “alineados en una fila”; los usuarios pueden insertar y eliminar elementos en cualquier parte de una lista enlazada. Las pilas son importantes en los compiladores y sistemas operativos; pueden insertarse y eliminarse elementos sólo en un extremo de una pila: su parte superior. Las colas representan líneas de espera; se insertan elementos en la parte final (conocida como rabo) de una cola y se eliminan elementos de su parte inicial (conocida como cabeza). Los árboles binarios facilitan la búsqueda y ordenamiento de los datos de alta velocidad, la eliminación eficiente de elementos de datos duplicados, la representación de directorios del sistema de archivos y la compilación de expresiones en lenguaje máquina. Estas estructuras de datos también tienen muchas otras aplicaciones interesantes. Hablaremos sobre cada uno de estos tipos de estructuras de datos e implementaremos programas para crearlas y manipularlas. Utilizaremos clases, herencia y composición para crear y empaquetar estas estructuras de datos, para reutilizarlas y darles mantenimiento. En el capítulo 25 presentaremos los genéricos, que le permiten declarar estructuras de datos que pueden adaptarse en forma automática para contener datos de cualquier tipo. En el capítulo 26, Colecciones, hablaremos sobre las clases predefinidas de C# que implementan varias estructuras de datos. Los ejemplos que presentamos en este capítulo son programas prácticos que pueden utilizarse en cursos más avanzados y en aplicaciones industriales. Los programas se enfocan en la manipulación de referencias. Los ejercicios ofrecen una vasta colección de aplicaciones útiles.

24.2 Tipos struct simples, boxing y unboxing Las estructuras de datos que veremos en este capítulo almacenan referencias object. Sin embargo, como veremos en breve, podemos almacenar valores tanto de tipo simple como de tipo por referencia en estas estructuras de datos. En esta sección veremos el mecanismo que permite manipular los valores de tipos simples como si fueran objetos.

Tipos struct simples Cada tipo simple (véase el apéndice L, Los tipos simples) tiene un tipo struct correspondiente en el espacio de nombres System, que declara al tipo simple. Estos tipos struct se llaman Boolean, Byte, SByte, Char, Decimal, Double, Single, Int32, UInt32, Int64, UInt64, Int16 y UInt16. Los tipos que se declaran con la palabra clave struct son implícitamente tipos por valor. En realidad, los tipos simples son alias para sus tipos struct correspondientes, por lo que una variable de un tipo simple puede declararse usando ya sea la palabra clave para ese tipo simple, o el nombre del tipo struct; por ejemplo, int e Int32 son indistintos. Los métodos relacionados con un tipo simple se encuentran en el tipo struct correspondiente (por ejemplo, el método Parse, que convierte un objeto string en un valor int, se encuentra en el tipo struct Int32). En la documentación podrá consultar el tipo struct correspondiente y ver los métodos disponibles para manipular valores de ese tipo.

24.3 Clases autorreferenciadas

965

Conversiones boxing y unboxing Todos los tipos struct simples heredan de la clase ValueType en el espacio de nombres System. La clase ValueType hereda de la clase object. Por ende, cualquier valor de tipo simple puede asignarse a una variable object; a esto se le conoce como conversión boxing. En una conversión boxing, el valor de tipo simple se copia en un objeto, para que pueda manipularse como un object. Las conversiones boxing pueden realizarse en forma explícita o implícita, como se muestra en las siguientes instrucciones: int i = 5; // crea un valor int object objeto1 = ( object ) i; // conversión boxing explícita del valor int object objeto2 = i; // conversión boxing implícita del valor int

Después de ejecutar el código anterior, tanto objeto1 como objeto2 se refieren a dos objetos distintos que contienen una copia del valor entero en la variable int i. Una conversión unboxing puede usarse para convertir en forma explícita una referencia object a un valor simple, como se muestra en la siguiente instrucción: int int1 = ( int ) objeto1; // conversión unboxing explícita del valor int

Si se trata de realizar una conversión unboxing explícita de una referencia object que no se refiera al tipo de valor simple correcto, se produce una excepción InvalidCastException. En el capítulo 25, Genéricos, y en el capítulo 26, Colecciones, hablaremos sobre los genéricos y las colecciones genéricas en C#. Como veremos pronto, los genéricos eliminan la sobrecarga de las conversiones boxing y unboxing, al permitirnos crear y utilizar colecciones de tipos de valores específicos.

24.3 Clases autorreferenciadas

Una clase autorreferenciada contiene un miembro de referencia que hace referencia a un objeto del mismo tipo de clase. Por ejemplo, la declaración de la clase en la figura 24.1 define la estructura de una clase autorreferenciada, llamada Nodo. Este tipo tiene dos variables de instancia private: datos de tipo entero y la referencia Nodo llamada siguiente. El miembro siguiente hace referencia a un objeto de tipo Nodo, un objeto del mismo tipo que se está declarando aquí; es por ello que se utiliza el término “clase autorreferenciada”. El miembro siguiente es un enlace (es decir, siguiente puede usarse para “enlazar” un objeto de tipo Nodo con otro objeto del mismo tipo). La clase Nodo también tiene dos propiedades: una para la variable de instancia datos (llamada Datos) y otra para la variable de instancia siguiente (llamada Siguiente). Los objetos autorreferenciados pueden enlazarse entre sí para formar estructuras de datos útiles, como listas, colas, pilas y árboles. La figura 24.2 ilustra dos objetos autorreferenciados, enlazados entre sí para formar una lista enlazada. Una barra diagonal inversa (que representa una referencia null) se coloca en el miembro de enlace del segundo objeto autorreferenciado para indicar que el enlace no hace referencia a otro objeto. La barra 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 24.1: Fig24_01.cs // Una clase autorreferenciada. class Nodo { private int datos; // almacena datos enteros private Nodo siguiente; // almacena referencia al siguiente Nodo public Node( int valorDatos ) { // cuerpo del constructor } // fin del constructor public int Datos { get {

Figura 24.1 | Declaración de la clase Nodo autorreferenciada. (Parte 1 de 2).

966

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

Capítulo 24 Estructuras de datos

// cuerpo de get } // fin de get set { // cuerpo de set } // fin de set } // fin de la propiedad Datos public Nodo Siguiente { get { // cuerpo de get } // fin de get set { // cuerpo de set } // fin de set } // fin de la propiedad Siguiente } // fin de la clase Nodo

Figura 24.1 | Declaración de la clase Nodo autorreferenciada. (Parte 2 de 2).

15

10

Figura 24.2 | Objetos de una clase autorreferenciada, enlazados entre sí. diagonal inversa es para fines de ilustración; no corresponde al carácter barra diagonal inversa en C#. Por lo general, una referencia null indica el final de una estructura de datos.

Error común de programación 24.1 Si no se establece el enlace en el último nodo de una lista a null, se produce un error lógico.

Para crear y mantener estructuras dinámicas de datos se requiere la asignación dinámica de memoria: la habilidad para que un programa obtenga más espacio de memoria en tiempo de ejecución, para almacenar nuevos nodos y para liberar el espacio que ya no se necesita. En la sección 9.9 vimos que los programas en C# no liberan explícitamente la memoria asignada en forma dinámica. En vez de ello, C# realiza la recolección automática de basura. El operador new es esencial para la asignación dinámica de memoria. Este operador recibe como operando el tipo del objeto que se asignará en forma dinámica y devuelve una referencia a un objeto de ese tipo. Por ejemplo, la instrucción Nodo nodoParaAgregar = new Nodo( 10 );

asigna la cantidad apropiada de memoria para almacenar un Nodo, y almacena una referencia a este objeto en nodoParaAgregar. Si no hay memoria disponible, new lanza una excepción OutOfMemoryException. El argumento 10 del constructor especifica los datos del objeto Nodo. Las siguientes secciones hablan sobre listas, pilas, colas y árboles. Estas estructuras de datos se crean y se mantienen con la asignación dinámica de memoria y las clases autorreferenciadas.

Buena práctica de programación 24.1 Al crear un gran número de objetos, considere evaluar la probabilidad de que se lance una excepción OutOfMemoryRealice un procesamiento de errores apropiado si no se asigna la memoria solicitada.

Exception.

24.4 Listas enlazadas

967

24.4 Listas enlazadas

Una lista enlazada es una colección lineal (es decir, una secuencia) de objetos de una clase autorreferenciada, conocidos como nodos, que están conectados por enlaces de referencia; es por ello que se utiliza el término lista “enlazada”. Un programa accede a una lista enlazada a través de una referencia al primer nodo en la lista. El programa accede a cada nodo subsiguiente a través del miembro de referencia de enlace almacenado en el nodo anterior. Por convención, la referencia de enlace en el último nodo de una lista se establece en null para indicar el final de la lista. Los datos se almacenan en forma dinámica en una lista enlazada; el programa crea cada nodo según sea necesario. Un nodo puede contener datos de cualquier tipo, incluyendo referencias a objetos de otras clases. Las pilas y las colas son también estructuras de datos lineales; de hecho, son versiones restringidas de las listas enlazadas. Los árboles son estructuras de datos no lineales. Pueden almacenarse listas de datos en los arreglos, pero las listas enlazadas ofrecen varias ventajas. Una lista enlazada es apropiada cuando el número de elementos de datos que se van a representar en la estructura de datos es impredecible. A diferencia de una lista enlazada, el tamaño de un arreglo convencional en C# no puede alterarse, ya que el tamaño del arreglo se fija al momento de crearlo. Los arreglos convencionales pueden llenarse, pero las listas enlazadas se llenan sólo cuando el sistema no tiene la memoria suficiente para satisfacer las solicitudes de asignación dinámica de memoria.

Tip de rendimiento 24.1 Un arreglo puede declararse de manera que contenga más elementos que el número de elementos esperados, pero esto desperdicia memoria. Las listas enlazadas proporcionan una mejor utilización de memoria en estas situaciones, ya que pueden crecer y reducirse en tiempo de ejecución.

Tip de rendimiento 24.2 Después de localizar el punto de inserción para un nuevo elemento en una lista enlazada ordenada, la inserción de un elemento es rápida; sólo hay que modificar dos referencias. Todos los nodos existentes permanecen en sus posiciones actuales en memoria.

Los programadores pueden mantener listas enlazadas en orden con sólo insertar cada nuevo elemento en el punto apropiado de la lista (claro que lleva tiempo localizar el punto de inserción apropiado). Los elementos existentes en la lista no necesitan moverse.

Tip de rendimiento 24.3 Los elementos de un arreglo se almacenan en posiciones contiguas de memoria, para permitir el acceso inmediato a cualquiera de sus elementos; la dirección de cualquier elemento puede calcularse directamente con base en su índice. Las listas enlazadas no permiten dicho acceso inmediato a sus elementos; para acceder a uno de ellos se tiene que recorrer la lista desde su parte inicial.

Por lo general, las listas enlazadas no se almacenan en posiciones contiguas en memoria. En vez de ello, los nodos son adyacentes en forma lógica. La figura 24.3 ilustra una lista enlazada con varios nodos.

primerPtr

H

ultimoPtr

D

Figura 24.3 | Representación gráfica de una lista enlazada.

...

Q

968

Capítulo 24 Estructuras de datos

Tip de rendimiento 24.4 El uso de estructuras de datos enlazadas y la asignación dinámica de memoria (en vez de arreglos) para las estructuras de datos que crecen y se reducen en tiempo de ejecución puede ahorrar memoria. Sin embargo, tenga en cuenta que los enlaces de referencia ocupan espacio, y la asignación dinámica de memoria incurre en la sobrecarga relacionada con las llamadas a métodos.

Implementación de la lista enlazada El programa de las figuras 24.4 y 24.5 utiliza un objeto de la clase Lista para manipular una lista de tipos de objetos varios. El método Main de la clase PruebaLista (figura 24.5) crea una lista de objetos, inserta objetos al principio de la lista mediante el método InsertarAlFrente de Lista, inserta objetos al final de la lista mediante el método InsertarAlFinal de Lista, elimina objetos de la parte frontal de la lista usando el método EliminarDelFrente de Lista y elimina objetos de la parte final de la lista usando el método EliminarDelFinal de Lista. Después de cada operación de inserción y eliminación, el programa invoca al método Imprimir de Lista para mostrar su contenido actual. Si se trata de eliminar un elemento de una lista vacía, ocurre una excepción ExcepcionListaVacia. A continuación presentamos una discusión detallada del programa.

Tip de rendimiento 24.5 Las operaciones de inserción y eliminación en un arreglo almacenado pueden llevar tiempo; todos los elementos que van después del elemento insertado o eliminado deben desplazarse en forma apropiada.

El programa consiste en cuatro clases: NodoLista (figura 24.4, líneas 8-49), Lista (líneas 52-165), ExcepcionListaVacia (líneas 168-174) y PruebaLista (figura 24.5). Las clases en la figura 24.4 crean una biblioteca de listas enlazadas (definida en el espacio de nombres BibliotecaListasEnlazadas) que puede reutilizarse durante todo el capítulo. Debe colocar el código de la figura 24.4 en su propio proyecto de biblioteca de clases, como se describió en la sección 9.14. En cada objeto Lista hay encapsulada una lista enlazada de objetos NodoLista. La clase NodoLista (figura 24.4, líneas 8-49) contiene dos variables de instancia: datos y siguiente. Los datos miembro pueden hacer referencia a cualquier objeto. [Nota: Por lo general, una estructura de datos contendrá datos de un tipo solamente, o datos de cualquier tipo derivado de un tipo base. En este ejemplo, utilizamos datos de varios tipos derivados de object, para demostrar que nuestra clase Lista puede almacenar datos de cualquier tipo. El miembro siguiente almacena una referencia al siguiente objeto NodoLista en la lista enlazada. Los constructores de NodoLista (líneas 15-18 y 22-26) nos permiten inicializar un objeto NodoLista que se colocará al final de una Lista, o antes de un NodoLista específico en una Lista, respectivamente. Una Lista accede a las variables miembro de NodoLista a través de las propiedades Siguiente (líneas 29-39) y Datos (líneas 42-48), respectivamente. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

// Fig. 24.4: BibliotecaListasEnlazadas.cs // Declaraciones de las clases NodoLista y Lista. using System; namespace BibliotecaListasEnlazadas { // clase para representar un nodo en una lista class NodoLista { private object datos; // almacena los datos para este nodo private NodoLista siguiente; // almacena una referencia al siguiente nodo // constructor para crear un NodoLista que haga referencia a valorDatos // y sea el último nodo en la lista public NodoLista( object valorDatos ) : this( valorDatos, null ) { } // fin del constructor predeterminado

Figura 24.4 | Las clases NodoLista, Lista y ExcepcionListaVacia. (Parte 1 de 4).

24.4 Listas enlazadas

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77

// constructor para crear un NodoLista que haga referencia a valorDatos // y se refiera al siguiente NodoLista en la Lista public NodoLista( object valorDatos, NodoLista siguienteNodo ) { datos = valorDatos; siguiente = siguienteNodo; } // fin del constructor // propiedad Siguiente public NodoLista Siguiente { get { return siguiente; } // fin de get set { siguiente = value; } // fin de set } // fin de la propiedad Siguiente // propiedad Datos public object Datos { get { return datos; } // fin de get } // fin de la propiedad Datos } // fin de la clase NodoLista // declaración de la clase Lista public class Lista { private NodoLista primerNodo; private NodoLista ultimoNodo; private string nombre; // cadena a mostrar, tal como "lista" // construye Lista vacía con el nombre especificado public Lista( string nombreLista ) { nombre = nombreLista; primerNodo = ultimoNodo = null; } // fin del constructor // construye Lista vacía con "lista" como su nombre public Lista() : this( "lista" ) { } // fin del constructor predeterminado // Inserta objeto al frente de la Lista. Si está vacía, // primerNodo y ultimoNodo harán referencia al mismo objeto. // En caso contrario, primerNodo hace referencia al nuevo nodo. public void InsertarAlFrente( object insertarElemento ) { if ( EstaVacia() ) primerNodo = ultimoNodo = new NodoLista( insertarElemento );

Figura 24.4 | Las clases NodoLista, Lista y ExcepcionListaVacia. (Parte 2 de 4).

969

970

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135

Capítulo 24 Estructuras de datos

else primerNodo = new NodoLista( insertarElemento, primerNodo ); } // fin del método InsertarAlFrente // Insert objeto al final de la Lista. Si está vacía, // primerNodo y ultimoNodo harán referencia al mismo objeto. // En caso contrario, la propiedad Siguiente de ultimoNodo hace referencia al nuevo nodo. public void InsertarAlFinal( object insertarElemento ) { if ( EstaVacia() ) primerNodo = ultimoNodo = new NodoLista( insertarElemento ); else ultimoNodo = ultimoNodo.Siguiente = new NodoLista( insertarElemento ); } // fin del método InsertarAlFinal // elimina el primer nodo de la Lista public object EliminarDelFrente() { if ( EstaVacia() ) throw new ExcepcionListaVacia( nombre ); object eliminarElemento = primerNodo.Datos; // recupera los datos // restablece las referencias primerNodo y ultimoNodo if ( primerNodo == ultimoNodo ) primerNodo = ultimoNodo = null; else primerNodo = primerNodo.Siguiente; return eliminarElemento; // devuelve los datos eliminados } // fin del método EliminarDelFrente // elimina el último nodo de la Lista public object EliminarDelFinal() { if ( EstaVacia() ) throw new ExcepcionListaVacia( nombre ); object eliminarElemento = ultimoNodo.Datos; // obtiene los datos // restablece las referencias primerNodo y ultimoNodos if ( primerNodo == ultimoNodo ) primerNodo = ultimoNodo = null; else { NodoLista actual = primerNodo; // itera mientras el nodo actual no sea el ultimoNodo while ( actual.Siguiente != ultimoNodo ) actual = actual.Siguiente; // avanza al siguiente nodo // actual es el nuevo ultimoNodo ultimoNodo = actual; actual.Siguiente = null; } // fin de else return eliminarElemento; // devuelve los datos eliminados } // fin del método EliminarDelFinal

Figura 24.4 | Las clases NodoLista, Lista y ExcepcionListaVacia. (Parte 3 de 4).

24.4 Listas enlazadas

971

136 137 // devuelve verdadero si la Lista está vacía 138 public bool EstaVacia() 139 { 140 return primerNodo == null; 141 } // fin del método EstaVacia 142 143 // imprime en pantalla el contenido de la Lista 144 public void Imprimir() 145 { 146 if ( EstaVacia() ) 147 { 148 Console.WriteLine( nombre + " Vacía" ); 149 return; 150 } // fin de if 151 152 Console.Write( "La " + nombre + " es: " ); 153 154 NodoLista actual = primerNodo; 155 156 // imprime los datos del nodo actual mientras no esté al final de la lista 157 while ( actual != null ) 158 { 159 Console.Write( actual.Datos + " " ); 160 actual = actual.Siguiente; 161 } // fin de while 162 163 Console.WriteLine( "\n" ); 164 } // fin del método Imprimir 165 } // fin de la clase Lista 166 167 // declaración de la clase ExcepcionListaVacia 168 public class ExcepcionListaVacia : ApplicationException 169 { 170 public ExcepcionListaVacia( string nombre ) 171 : base( "La " + nombre + " está vacía" ) 172 { 173 } // fin del constructor 174 } // fin de la clase ExcepcionListaVacia 175 } // fin del espacio de nombres BibliotecaListasEnlazadas

Figura 24.4 | Las clases NodoLista, Lista y ExcepcionListaVacia. (Parte 4 de 4).

La clase Lista (líneas 52-165) contiene las variables de instancia private primerNodo (una referencia al primer NodoLista en una Lista) y ultimoNodo (una referencia al último NodoLista en una Lista). Los constructores (líneas 59-63 y 66-69) inicializan ambas referencias a null y nos permiten especificar el nombre de la Lista, para fines de visualizar los resultados en pantalla. InsertarAlFrente (líneas 74-80), InsertarAlFinal (líneas 85-91), EliminarDelFrente (líneas 94-108) y EliminarDelFinal (líneas 111-135) son los métodos principales de la clase Lista. El método EstaVacia (líneas 138-141) es un método predicado, el cual determina si la lista está vacía (es decir, la referencia al primer nodo de la lista es null). Por lo general, los métodos predicados evalúan una condición y no modifican el objeto desde el que se llaman. Si la lista está vacía, el método EstaVacia devuelve true; en caso contrario, devuelve false. El método Imprimir (líneas 144-164) muestra el contenido de la lista. Después de la figura 24.5 hablaremos con detalle sobre los métodos de la clase Lista. La clase ExcepcionListaVacia (líneas 168-174) define una clase de excepción que utilizamos para indicar operaciones ilegales en una Lista vacía. La clase PruebaLista (figura 24.5) utiliza la biblioteca de listas enlazadas para crear y manipular una lista enlazada. [Nota: En el proyecto que contiene la figura 24.5, debe agregar una referencia a la biblioteca de clases que

972

Capítulo 24 Estructuras de datos

contiene las clases de la figura 24.4. Si utiliza nuestro ejemplo existente, tal vez necesite actualizar esta referencia.] La línea 11 crea un nuevo objeto Lista y lo asigna a la variable lista. Las líneas 14-17 crean datos para agregarlos a la lista. Las líneas 20-27 utilizan los métodos de inserción de Lista para insertar estos valores y utilizar el método Imprimir de Lista para mostrar en pantalla el contenido de lista, después de cada inserción. Observe que se llevan a cabo conversiones boxing en los valores de las variables de tipos simples de las líneas 20, 22 y 24, en donde se esperan referencias object. El código dentro del bloque try (líneas 33-50) elimina objetos a través de los métodos de eliminación de Lista, imprime en pantalla cada objeto eliminado e imprime en pantalla el contenido de lista después de cada eliminación. Si hay un intento de eliminar un objeto de una lista vacía, la instrucción catch en las líneas 51-54 atrapa la ExcepcionListaVacia y muestra un mensaje de error.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 23 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

// Fig. 24.5: PruebaLista.cs // Prueba de la clase Lista. using System; using BibliotecaListasEnlazadas; // clase para probar la funcionalidad de la clase Lista class PruebaLista { static void Main( string[] args ) { Lista lista = new Lista(); // crea el contenedor Lista // crea los datos para almacenar en la Lista bool unBooleano = true; char unCaracter = '$'; int unEntero = 34567; string unaCadena = "hola"; // usa los métodos de inserción de Lista lista.InsertarAlFrente( unBooleano ); lista.Imprimir(); lista.InsertarAlFrente( unCaracter ); lista.Imprimir(); lista.InsertarAlFinal( unEntero ); lista.Imprimir(); lista.InsertarAlFinal( unaCadena ); lista.Imprimir(); // usa los métodos de eliminación de Lista object objetoEliminado; // elimina los datos de la lista e imprime después de cada eliminación try { objetoEliminado = lista.EliminarDelFrente(); Console.WriteLine( objetoEliminado + " eliminado" ); lista.Imprimir(); objetoEliminado = lista.EliminarDelFrente(); Console.WriteLine( objetoEliminado + " eliminado" ); lista.Imprimir(); objetoEliminado = lista.EliminarDelFinal(); Console.WriteLine( objetoEliminado + " eliminado" ); lista.Imprimir();

Figura 24.5 | Demostración de una lista enlazada. (Parte 1 de 2).

24.4 Listas enlazadas

46 47 48 49 50 51 52 53 54 55 56

973

objetoEliminado = lista.EliminarDelFinal(); Console.WriteLine( objetoEliminado + " eliminado" ); lista.Imprimir(); } // fin de try catch ( ExcepcionListaVacia excepcionListaVacia ) { Console.Error.WriteLine( "\n" + excepcionListaVacia ); } // fin de catch } // fin del método Main } // fin de la clase PruebaLista

La lista es: True La lista es: $ True La lista es: $ True 34567 La lista es: $ True 34567 hola $ eliminado La lista es: True 34567 hola True eliminado La lista es: 34567 hola hola eliminado La lista es: 34567 34567 eliminado lista Vacía

Figura 24.5 | Demostración de una lista enlazada. (Parte 2 de 2).

Método InsertarAlFrente En las siguientes páginas hablaremos sobre cada uno de los métodos de la clase Lista con detalle. El método InsertarAlFrente (figura 24.4, líneas 74-80) coloca un nuevo nodo en la parte frontal de la lista. El método consiste en tres pasos: 1. Llama a EstaVacio para determinar si la lista está vacía (línea 76). 2. Si la lista está vacía, establece primerNodo y ultimoNodo para que hagan referencia a un nuevo NodoLista, inicializado con insertarElemento (línea 77). El constructor NodoLista en las líneas 15-18 de la figura 24.4 llama al constructor de NodoLista en las líneas 22-26, el cual establece la variable de instancia datos para que haga referencia al objeto object que se pasa como primer argumento, y establece la referencia siguiente en null. 3. Si la lista no está vacía, el nuevo nodo se “enlaza” a la lista, estableciendo primerNodo para que haga referencia a un nuevo objeto NodoLista, inicializado con insertarElemento y primerNodo (línea 79). Cuando se ejecuta el constructor de NodoLista (líneas 22-26), establece la variable de instancia datos para que haga referencia al objeto object que se pasa como primer argumento, y realiza la inserción estableciendo la referencia siguiente al NodoLista que se pasa como segundo argumento. En la figura 24.6, la parte (a) muestra una lista y un nuevo nodo durante la operación InsertarAlFrente y antes de que se enlace el nuevo nodo a la lista. Las líneas punteadas y las flechas en la parte (b) ilustran el paso 3 de la operación InsertarAlFrente, la cual permite que el nodo que contiene 12 se convierta en el nuevo frente de la lista.

974

Capítulo 24 Estructuras de datos

V primerNodo 7

11

nuevo NodoLista 12

W primerNodo 7

11

nuevo NodoLista 12

Figura 24.6 | La operación InsertarAlFrente.

Método InsertarAlFinal El método InsertarAlFinal (figura 24.4, líneas 85-91) coloca un nuevo nodo en la parte final de la lista. El método consiste en tres pasos: 1. Llama a EstaVacia para determinar si la lista está vacía (línea 87). 2. Si la lista está vacía, establece primerNodo y ultimoNodo para que hagan referencia a un nuevo NodoLista inicializado con insertarElemento (línea 88). El constructor de NodoLista en las líneas 15-18 llama al constructor de NodoLista en las líneas 22-26, el cual establece la variable de instancia datos para que haga referencia al objeto object que se pasa como primer argumento, y establece la referencia siguiente a null. 3. Si la lista no está vacía, enlaza el nuevo nodo a la lista, estableciendo ultimoNodo y ultimoNodo. siguiente para que haga referencia a un nuevo objeto NodoLista, inicializado con insertarElemento (línea 90). Cuando el constructor de NodoLista (líneas 15-18) se ejecuta, hace una llamada al constructor en las líneas 22-26, el cual establece la variable de instancia datos para que haga referencia al objeto object que se pasa como argumento, y establece la referencia siguiente a null. En la figura 24.7, la parte (a) muestra una lista y un nuevo nodo durante la operación InsertarAlFinal, antes de que se haya enlazado el nuevo nodo a la lista. Las líneas punteadas y las flechas en la parte (b) ilustran el paso 3 del método InsertarAlFinal, el cual permite que se agregue un nuevo nodo al final de una lista que no esté vacía.

Método EliminarDelFrente El método EliminarDelFrente (figura 24.4, líneas 94-108) elimina el nodo frontal de la lista y devuelve una referencia a los datos eliminados. El método lanza una ExcepcionListaVacia (línea 97) si el programador trata de eliminar un nodo de una lista vacía. En caso contrario, el método devuelve una referencia a los datos eliminados. Después de determinar si una Lista no está vacía, el método consiste en cuatro pasos para eliminar el primer nodo: 1. Asigna primerNodo.Datos (los datos que se van a eliminar de la lista) a la variable eliminarElemento (línea 99). 2. Si los objetos a los que primerNodo y ultimoNodo hacen referencia son el mismo objeto, la lista sólo tiene un elemento, por lo que el método establece primerNodo y ultimoNodo a null (línea 103) para eliminar el nodo de la lista (dejando la lista vacía). 3. Si la lista tiene más de un nodo, el método deja la referencia ultimoNodo como está, y asigna primerNodo.Siguiente a primerNodo (línea 105). Así, primerNodo hace referencia al nodo que anteriormente era el segundo nodo en la lista.

24.4 Listas enlazadas

V

primerNodo

12

W

ultimoNodo

7

primerNodo

12

11

ultimoNodo

7

11

975

nuevo NodoLista

5

nuevo NodoLista

5

Figura 24.7 | La operación InsertarAlFinal. 4. Devuelve la referencia eliminarElemento (línea 107). En la figura 24.8, la parte (a) ilustra una lista antes de una operación de eliminación. Las líneas punteadas y las flechas en la parte (b) muestran las manipulaciones de las referencias.

Método EliminarDelFinal El método EliminarDelFinal (figura 24.4, líneas 111-135) elimina el último nodo de una lista y devuelve una referencia a los datos eliminados. El método lanza una ExcepcionListaVacia (línea 114) si el programa intenta eliminar un nodo de una lista vacía. El método consiste en varios pasos: 1. Asigna ultimoNodo.Datos (los datos que se van a eliminar de la lista) a la variable eliminarElemento (línea 116). 2. Si primerNodo y ultimoNodo hacen referencia al mismo objeto (línea 119), la lista sólo tiene un elemento, por lo que el método establece primerNodo y ultimoNodo a null (línea 120) para eliminar ese nodo de la lista (dejándola vacía). 3. Si la lista tiene más de un nodo, crea la variable NodoLista actual y le asigna primerNodo (línea 123).

V primerNodo

12

ultimoNodo

7

11

W primerNodo

12

5

ultimoNodo

7

Figura 24.8 | La operación EliminarDelFrente.

11

5

976

Capítulo 24 Estructuras de datos 4. Ahora recorre la lista con actual, hasta que hace referencia al nodo que está antes del último nodo. El ciclo while (líneas 126-127) asigna actual.Siguiente a actual, siempre y cuando actual.Siguiente no sea igual a ultimoNodo. 5. Después de localizar el antepenúltimo nodo, asigna actual a ultimoNodo (línea 130) para actualizar el nodo que esté al último en la lista. 6. Establece actual.Siguiente a null (línea 131) para eliminar el último nodo de la lista y terminarla en el nodo actual. 7. Devuelve la referencia eliminarElemento (línea 134).

En la figura 24.9, la parte (a) ilustra una lista antes de una operación de eliminación. Las líneas punteadas y las flechas en la parte (b) muestran las manipulaciones de las referencias.

Método Imprimir El método Imprimir (figura 24.4, líneas 144-164) determina primero si la lista está vacía (línea 146). Si es así, Imprimir muestra un objeto string que consiste en el nombre de la lista y la cadena "vacía ", y después devuelve el control al método que hizo la llamada. En caso contrario, Imprimir muestra en pantalla los datos en la lista. El método imprime un objeto string que consiste en la cadena "La ", el nombre de la lista y la cadena " es: ". Después, la línea 154 crea la variable NodoLista actual y la inicializa con primerNodo. Mientras que actual no sea null, significa que hay más elementos en la lista. Por lo tanto, el método muestra actual.Datos (línea 159) y después asigna actual.Siguiente a actual (línea 160), para avanzar al siguiente nodo en la lista.

Listas lineales y circulares, de enlace simple y doblemente enlazadas

El tipo de lista enlazada que hemos visto es una lista de enlace simple: la lista comienza con una referencia al primer nodo, y cada nodo contiene una referencia al siguiente nodo “en secuencia”. Esta lista termina con un nodo cuyo miembro de referencia tiene el valor null. Una lista de enlace simple puede recorrerse sólo en una dirección. Una lista circular de enlace simple (figura 24.10) empieza con una referencia al primer nodo, y cada nodo contiene una referencia al siguiente nodo. El “último nodo” no contiene una referencia null; en vez de ello, la referencia en el último nodo apunta de vuelta al primer nodo, con lo cual se cierra el “círculo”. Una lista doblemente enlazada (figura 24.11) permite realizar recorridos tanto hacia delante como hacia atrás. A menudo, dicha lista se implementa con dos “referencias iniciales”: una que hace referencia al primer elemento de la lista, para permitir un recorrido desde la parte inicial hasta la parte final, y una que hace referencia al último elemento, para permitir un recorrido desde la parte final hasta la parte inicial. Cada nodo tiene tanto

V

primerNodo

12

W

actual

ultimoNodo

7

primerNodo

12

11

actual

7

Figura 24.9 | La operación EliminarDelFinal.

5

ultimoNodo

11

5

24.5 Pilas

977

primerNodo

12

7

11

5

Figura 24.10 | Lista circular de enlace simple.

ultimoNodo

primerNodo

12

7

11

5

Figura 24.11 | Lista doblemente enlazada. una referencia hacia delante al siguiente nodo en la lista, como una referencia hacia atrás al nodo anterior en la lista. Por ejemplo, si su lista contiene un directorio telefónico alfabetizado, la búsqueda de alguien cuyo nombre empiece con una letra cerca de la parte inicial del alfabeto podría empezar desde la parte inicial de la lista. La búsqueda de alguien cuyo nombre empiece con una letra cerca del final del alfabeto podría empezar desde la parte final de la lista. En una lista circular doblemente enlazada (figura 24.12), la referencia hacia delante del último nodo se refiere al primer nodo, y la referencia hacia atrás del primer nodo se refiere al último nodo, con lo cual se cierra el “círculo”.

24.5 Pilas

Una pila es una versión restringida de una lista enlazada; una pila recibe nuevos nodos y libera nodos sólo desde su parte superior. Por esta razón, a una pila se le conoce como estructura de datos UEPS (último en entrar, primero en salir). Las operaciones principales para manipular una pila son push (meter) y pop (sacar). La operación meter agrega un nuevo nodo a la parte superior de la pila. La operación sacar elimina un nodo de la parte superior de la pila, y devuelve el elemento de datos del nodo que se extrajo.

ultimoNodo

primerNodo

12

7

Figura 24.12 | Lista circular, doblemente enlazada.

11

5

978

Capítulo 24 Estructuras de datos

Las pilas tienen muchas aplicaciones interesantes. Por ejemplo, cuando un programa llama a un método, éste debe saber cómo regresar al programa que lo llamó, por lo que la dirección de retorno se mete en la pila de llamadas a métodos. Si ocurre una serie de llamadas a métodos, los valores de retorno sucesivos se meten en la pila, en el orden “último en entrar, primero en salir”, de manera que cada método pueda regresar al objeto que lo llamó. Las pilas soportan las llamadas recursivas a métodos, de la misma forma que soportan las llamadas convencionales no recursivas a los métodos. El espacio de nombres System.Collections contiene la clase Stack para implementar y manipular pilas que puedan crecer y reducirse durante la ejecución de un programa. En los capítulos 25 y 26 hablaremos sobre la clase Stack. En nuestro siguiente ejemplo, aprovecharemos la estrecha relación entre las listas y las pilas para implementar una clase de pila, mediante la reutilización de una clase de lista. Mostraremos dos formas de reutilización. Primero implementaremos la clase de pila; para ello heredaremos de la clase Lista de la figura 24.4. Después implementaremos una clase de pila que funciona en forma idéntica a través de la composición, incluyendo un objeto Lista como miembro private de una clase de pila.

Clase de pila que hereda de Lista El programa de las figuras 24.13 y 24.14 crea una clase de pila; para ello hereda de la clase Lista de la figura 24.4 (línea 9). Queremos que la pila tenga los métodos Push, Pop, EstaVacia e Imprimir. En esencia, éstos son los métodos InsertarAlFrente, EliminarDelFrente, EstaVacia e Imprimir de la clase Lista. Desde luego que la clase Lista contiene otros métodos (como InsertarAlFinal y EliminarDelFinal) que preferimos no hacer accesibles a través de la interfaz public de la pila. Es importante recordar que todos los métodos en la interfaz public de la clase Lista son también métodos public de la clase derivada HerenciaPila (figura 24.13).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

// Fig. 24.13: BibliotecaHerenciaPila.cs // Implementación de una pila mediante la herencia de la clase Lista. using System; using BibliotecaListasEnlazadas; namespace BibliotecaHerenciaPila { // la clase HerenciaPila hereda las herramientas de la clase Lista public class HerenciaPila : Lista { // pasa el nombre "pila" al constructor de Lista public HerenciaPila() : base( "pila" ) { } // fin del constructor // coloca valorDatos en la parte superior de la pila, insertando // valorDatos al frente de la lista enlazada public void Push( object valorDatos ) { InsertarAlFrente(valorDatos); } // fin del método Push // elimina el elemento de la parte superior de la pila; para ello elimina // el elemento que está al frente de la lista enlazada public object Pop() { return EliminarDelFrente(); } // fin del método Pop } // fin de la clase HerenciaPila } // fin del espacio de nombres BibliotecaHerenciaPila

Figura 24.13 |

HerenciaPila

extiende a la clase Lista.

24.5 Pilas

979

La implementación de cada método HerenciaPila llama al método de Lista apropiado; el método Push llama a InsertarAlFrente, el método Pop llama a EliminarDelFrente. La clase HerenciaPila no define los métodos EstaVacia e Imprimir, ya que hereda estos métodos de la clase Lista en su interfaz public. Observe que la clase HerenciaPila utiliza el espacio de nombres BibliotecaListasEnlazadas (figura 24.4); por ende, la biblioteca de clases que define a HerenciaPila debe tener una referencia a la biblioteca de clases BibliotecaListasEnlazadas. El método Main de PruebaHerenciaPila (figura 24.14) utiliza la clase HerenciaPila para crear una pila de objetos object llamada pila (línea 12). Las líneas 15-18 definen cuatro valores que se meterán en la pila y se sacarán de ella. El programa mete a la pila (líneas 21, 23, 25 y 27) un valor bool que contiene true, un char que contiene '$', un int que contiene 34567 y un string que contiene "hola". Un ciclo while infinito (líneas 33-38) saca los elementos de la pila. Cuando está vacía, el método Pop lanza una ExcepcionListaVacia y el programa muestra el rastreo de pila de la excepción, el cual muestra la pila del programa en ejecución al momento en que ocurrió la excepción. El programa utiliza el método Imprimir (que HerenciaPila hereda de la clase Lista) para imprimir en pantalla el contenido de la pila después de cada operación. Observe que la clase PruebaHerenciaPila utiliza el espacio de nombres BibliotecaListasEnlazadas (figura 24.4) y el espacio de nombres BibliotecaHerenciaPila (figura 24.13); por ende, la solución para la clase PruebaHerenciaPila debe tener referencias a ambas bibliotecas de clases.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

// Fig. 24.14: PruebaHerenciaPila.cs // Prueba de la clase HerenciaPila. using System; using BibliotecaHerenciaPila; using BibliotecaListasEnlazadas; // demuestra la funcionalidad de la clase HerenciaPila class PruebaHerenciaPila { static void Main( string[] args ) { HerenciaPila pila = new HerenciaPila(); // crea objetos para almacenarlos en la pila bool unBooleano = true; char unCaracter = '$'; int unEntero = 34567; string unaCadena = "hola"; // usa el método Push para agregar elementos a la pila pila.Push( unBooleano ); pila.Imprimir(); pila.Push( unCaracter ); pila.Imprimir(); pila.Push( unEntero ); pila.Imprimir(); pila.Push( unaCadena ); pila.Imprimir(); // elimina elementos de la pila try { while ( true ) { object objetoEliminado = pila.Pop(); Console.WriteLine( objetoEliminado + " se sacó" ); pila.Imprimir();

Figura 24.14 | Uso de la clase HerenciaPila. (Parte 1 de 2).

980

38 39 40 41 42 43 44 45 46

Capítulo 24 Estructuras de datos

} // fin de while } // fin de try catch ( ExcepcionListaVacia excepcionListaVacia ) { // si ocurre una excepción, imprime el rastreo de pila Console.Error.WriteLine( excepcionListaVacia.StackTrace ); } // fin de catch } // fin de Main } // fin de la clase PruebaHerenciaPila

La pila es: True La pila es: $ True La pila es: 34567 $ True La pila es: hola 34567 $ True hola se sacó La pila es: 34567 $ True 34567 se sacó La pila es: $ True $ se sacó La pila es: True True se sacó pila Vacía en BibliotecaListasEnlazadas.Lista.EliminarDelFrente() en BibliotecaHerenciaPila.HerenciaPila.Pop() en PruebaHerenciaPila.Main(String[] args) en C:\MisProyectos\Fig24_14\PruebaHerenciaPila\ PruebaHerenciaPila.cs:línea 35

Figura 24.14 | Uso de la clase HerenciaPila. (Parte 2 de 2).

Clase de pila que contiene una referencia a una Lista Otra manera de implementar una clase de pila es reutilizando una clase de lista a través de la composición. La clase en la figura 24.15 utiliza un objeto private de la clase Lista (línea 11) en la declaración de la clase ComposicionPila. La composición nos permite ocultar los métodos de la clase Lista que no deben estar en nuestra interfaz public de la pila, proporcionando los métodos de la interfaz public sólo a los métodos requeridos de Lista. Para implementar cada uno de los métodos de la pila, esta clase delega su trabajo a un método apropiado de Lista. Los métodos de ComposicionPila llaman a los métodos InsertarAlFrente, EliminarDelFrente, EstaVacia e Imprimir de Lista. En este ejemplo no mostramos la clase PruebaComposicionPila, ya que la única diferencia en este ejemplo es que modificamos el nombre de la clase de pila, de HerenciaPila a ComposicionPila. Si ejecuta la aplicación desde el código, podrá ver que los resultados son idénticos.

24.6 Colas La cola es otra estructura de datos de uso común. Una cola es similar a una fila para pagar en un supermercado; el cajero atiende primero a la persona que está al principio de la fila. Otros clientes entran a la fila sólo por su parte final y esperan a que se les atienda. Los nodos de una cola se eliminan sólo desde el principio (o frente) de la misma y se insertan sólo al final de ésta. Por esta razón, una cola es una estructura de datos PEPS ( primero en entrar, primero en salir ). Las operaciones para insertar y eliminar se conocen como enqueue (agregar a la cola) y dequeue (retirar de la cola).

24.6 Colas

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

981

// Fig. 24.15: BibliotecaComposicionPila.cs // Declaración de ComposicionPila con un objeto Lista compuesto. using System; using BibliotecaListasEnlazadas; namespace BibliotecaComposicionPila { // la clase ComposicionPila encapsula las herramientas de Lista public class ComposicionPila { private Lista pila; // construye una pila vacía public ComposicionPila() { pila = new Lista( "pila" ); } // fin del constructor // agrega un objeto a la pila public void Push( object valorDatos ) { pila.InsertarAlFrente( valorDatos ); } // fin del método Push // elimina un objeto de la pila public object Pop() { return pila.EliminarDelFrente(); } // fin del método Pop // determina si la pila está vacía public bool EstaVacia() { return pila.EstaVacia(); } // fin del método EstaVacia // imprime el contenido de la pila public void Imprimir() { pila.Imprimir(); } // fin del método Imprimir } // fin de la clase ComposicionPila } // fin del espacio de nombres BibliotecaComposicionPila

Figura 24.15 | La clase ComposicionPila encapsula la funcionalidad de la clase Lista. Las colas tienen muchas aplicaciones en los sistemas computacionales. La mayoría de las computadoras tienen sólo un procesador, por lo que sólo pueden atender una aplicación a la vez. Cada aplicación que requiere tiempo del procesador se coloca en una cola. La aplicación al frente de la cola es la siguiente que recibe atención. Cada aplicación avanza en forma gradual al frente de la cola, a medida que las aplicaciones al frente reciben atención. Las colas también se utilizan para dar soporte al uso de la cola de impresión. Por ejemplo, una sola impresora puede compartirse entre todos los usuarios de la red. Muchos usuarios pueden enviar trabajos a la impresora, incluso cuando ésta ya se encuentre ocupada. Estos trabajos de impresión se colocan en una cola hasta que la impresora esté disponible. Un programa conocido como spooler administra la cola para asegurarse que, a medida que se complete cada trabajo de impresión, se envíe el siguiente trabajo a la impresora. En las redes de computación, los paquetes de información también esperan en colas. Cada vez que un paquete llega a un nodo de la red, debe enrutarse hacia el siguiente nodo de la red a través de la ruta hacia el destino

982

Capítulo 24 Estructuras de datos

final del paquete. El nodo enrutador envía un paquete a la vez, por lo que los paquetes adicionales se ponen en una cola hasta que el enrutador pueda enviarlos. Un servidor de archivos en una red computacional se encarga de las solicitudes de acceso a los archivos de muchos clientes distribuidos en la red. Los servidores tienen una capacidad limitada para dar servicio a las solicitudes de los clientes. Cuando se excede esa capacidad, las solicitudes de los clientes esperan en colas.

Clase de pila que hereda de Lista El programa de las figuras 24.16 y 24.17 crea una clase de cola, heredando de una clase de lista. Queremos que la clase HerenciaCola (figura 24.16) tenga los métodos Enqueue, Dequeue, EstaVacia e Imprimir. En esencia, éstos son los métodos InsertarAlFinal, EliminarDelFrente, EstaVacia e Imprimir de la clase Lista. Desde luego que la clase de lista contiene otros métodos (como InsertarAlFrente y EliminarDelFinal) que es preferible no hacer accesibles a través de la interfaz public de la clase de cola. Recuerde que todos los métodos en la interfaz public de la clase Lista son también métodos public de la clase derivada HerenciaCola. La implementación de cada método HerenciaCola llama al método de Lista apropiado; el método Enqueue llama a InsertarAlFinal y el método Dequeue llama a EliminarDelFrente. Las llamadas a EstaVacia y a Imprimir invocan las versiones de la clase base que se heredaron de la clase Lista a la interfaz public de HerenciaCola. Observe que la clase HerenciaCola usa el espacio de nombres BibliotecaListasEnlazadas (figura 24.4); por ende, la biblioteca de clases para HerenciaCola debe tener una referencia a la biblioteca de clases BibliotecaListasEnlazadas. El método Main de la clase PruebaHerenciaCola (figura 24.17) crea un objeto HerenciaCola llamado cola. Las líneas 15-18 definen cuatro valores que se meten y se sacan de la cola. El programa mete a la cola (líneas 21, 23, 25 y 27) un bool que contiene true, un char que contiene '$', un int que contiene 34567 y un

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

// Fig. 24.16: BibliotecaHerenciaCola.cs // Implementación de una cola, heredando de la clase Lista. using System; using BibliotecaListasEnlazadas; namespace BibliotecaHerenciaCola { // la clase HerenciaCola hereda las herramientas de Lista public class HerenciaCola : Lista { // pasa el nombre "cola" al constructor de Lista public HerenciaCola() : base( "cola" ) { } // fin del constructor // coloca valorDatos al final de la cola, insertando // valorDatos al final de la lista enlazada public void Enqueue( object valorDatos ) { InsertarAlFinal( valorDatos ); } // fin del método Enqueue // elimina elemento al principio de la cola, eliminando // el elemento al principio de la lista enlazada public object Dequeue() { return EliminarDelFrente(); } // fin del método Dequeue } // fin de la clase HerenciaPila } // fin del espacio de nombres BibliotecaHerenciaCola

Figura 24.16 | La clase HerenciaCola extiende a la clase Lista.

24.6 Colas

983

string que contiene "hola". Observe que la clase PruebaHerenciaCola utiliza el espacio de nombres BibliotecaListasEnlazadas y el espacio de nombres BibliotecaHerenciaCola; por ende, la solución para la clase PruebaHerenciaCola debe tener referencias a ambas bibliotecas de clases. Un ciclo while infinito (líneas 36-41) saca los elementos de la cola en orden PEPS. Cuando no hay más objetos qué sacar, el método Dequeue lanza una ExcepcionListaVacia, y el programa muestra el rastreo de pila de

la excepción, que a su vez muestra la pila de ejecución del programa, en el momento en que ocurrió la excepción. El programa utiliza el método Imprimir (heredado de la clase Lista) para imprimir en pantalla el contenido de la cola, después de cada operación. Observe que la clase PruebaHerenciaCola utiliza el espacio de nombres BibliotecaListasEnlazadas (figura 24.4) y el espacio de nombres BibliotecaHerenciaCola (figura 24.16); por ende, la solución para la clase PruebaHerenciaCola debe tener referencias a ambas bibliotecas de clases.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

// Fig. 24.17: PruebaCola.cs // Prueba de la clase HerenciaCola. using System; using BibliotecaHerenciaCola; using BibliotecaListasEnlazadas; // demuestra la funcionalidad de la clase HerenciaCola class PruebaCola { static void Main( string[] args ) { HerenciaCola cola = new HerenciaCola(); // crea objetos para almacenarlos en la cola bool unBooleano = true; char unCaracter = '$'; int unEntero = 34567; string unaCadena = "hola"; // usa el método Enqueue para agregar elementos a la cola cola.Enqueue( unBooleano ); cola.Imprimir(); cola.Enqueue( unCaracter ); cola.Imprimir(); cola.Enqueue( unEntero ); cola.Imprimir(); cola.Enqueue( unaCadena ); cola.Imprimir(); // usa el método Dequeue para eliminar elementos de la cola object objetoEliminado = null; // elimina elementos de la cola try { while ( true ) { objetoEliminado = cola.Dequeue(); Console.WriteLine( objetoEliminado + " se quitó de la cola" ); cola.Imprimir(); } // fin de while } // fin de try catch ( ExcepcionListaVacia excepcionListaVacia ) { // si ocurre una excepción, imprime el rastreo de pila

Figura 24.17 | Cola creada por herencia. (Parte 1 de 2).

984

46 47 48 49

Capítulo 24 Estructuras de datos

Console.Error.WriteLine( excepcionListaVacia.StackTrace ); } // fin de catch } // fin del método Main } // fin de la clase PruebaCola

La cola es: True La cola es: True $ La cola es: True $ 34567 La cola es: True $ 34567 hola True se quitó de la cola La cola es: $ 34567 hola $ se quitó de la cola La cola es: 34567 hola 34567 se quitó de la cola La cola es: hola hola se quitó de la cola cola Vacía en BibliotecaListasEnlazadas.Lista.EliminarDelFrente() en BibliotecaHerenciaCola.HerenciaCola.Dequeue() en PruebaCola.Main(String[] args) en C:\MisProyectos\Fig24_17\PruebaCola\PruebaCola.cs:línea 38

Figura 24.17 | Cola creada por herencia. (Parte 2 de 2).

24.7 Árboles

Las listas enlazadas, las pilas y las colas son estructuras de datos lineales (es decir, secuencias). Un árbol es una estructura de datos bidimensional no lineal, con propiedades especiales. Los nodos de un árbol contienen dos o más enlaces.

Terminología básica

En esta sección hablaremos sobre los árboles binarios (figura 24.18): árboles cuyos nodos contienen dos enlaces (de los cuales uno, ambos o ninguno pueden ser null). El nodo raíz es el primer nodo en un árbol. Cada enlace en el nodo raíz hace referencia a un hijo. El hijo izquierdo es el primer nodo en el subárbol izquierdo, y el hijo derecho es el primer nodo en el subárbol derecho. Los hijos de un nodo específico se llaman hermanos. Un nodo sin hijos se llama nodo hoja. Por lo general, los científicos computacionales dibujan los árboles partiendo desde el nodo raíz, hacia abajo; exactamente lo opuesto a la manera en que crecen la mayoría de los árboles reales.

Error común de programación 24.2 Si no se establecen en null los enlaces en los nodos hoja de un árbol, se produce un error lógico.

Árbol binario de búsqueda

En nuestro ejemplo del árbol binario, creamos un árbol binario especial, conocido como árbol binario de búsqueda. Un árbol binario de búsqueda (sin valores duplicados en los nodos) tiene la característica de que los valores en cualquier subárbol izquierdo son menores que el valor en el nodo padre del subárbol, y los valores en cualquier subárbol derecho son mayores que el valor en el nodo padre del subárbol. La figura 24.19 muestra un árbol binario de búsqueda con 9 valores enteros. Hay que tener en cuenta que la figura del árbol binario de búsqueda que corresponde a un conjunto de datos puede depender del orden en el que se insertan los valores en el árbol.

24.7 Árboles

985

GZ[ZgZcX^VVacdYdgV†o

B

CdYdgV†o HjW{gWda ^ofj^ZgYdYZa cdYdgV†oB

A

D

HjW{gWda YZgZX]dYZa cdYdgV†oB

C

Figura 24.18 | Representación gráfica de un árbol binario. 47 25 11

77 43 31

44

65 68

Figura 24.19 | Árbol binario de búsqueda que contiene 9 valores.

24.7.1 Árbol binario de búsqueda de valores enteros La aplicación de las figuras 24.20 y 24.21 crea un árbol binario de búsqueda de enteros y lo recorre (es decir, avanza por todos sus nodos) de tres formas: mediante los recorridos recursivos inorden, preorden y postorden. El programa genera 10 números aleatorios e inserta cada uno de ellos en el árbol. La figura 24.20 define la clase Arbol en el espacio de nombres BibliotecaArbolesBinarios para fines de reutilizarla en posteriores ejemplos. La figura 24.21 define la clase PruebaArbol para demostrar la funcionalidad de la clase Arbol. El método Main de la clase PruebaArbol crea una instancia de un objeto Arbol vacío, después genera al azar 10 enteros e inserta cada valor en el árbol binario mediante una llamada al método InsertarNodo de la clase Arbol. Después el programa realiza los recorridos preorden, inorden y postorden del árbol. En breve hablaremos sobre estos recorridos. La clase NodoArbol (líneas 8-81 de la figura 24.20) es una clase autorreferenciada que contiene tres miembros de datos private: nodoIzquierdo y nodoDerecho de tipo NodoArbol, y datos de tipo int. Al principio, todo NodoArbol es un nodo hoja, por lo que el constructor (líneas 15-19) inicializa las referencias nodoIzquierdo y nodoDerecho a null. Las propiedades NodoIzquierdo (líneas 22-32), Datos (líneas 35-45) y NodoDerecho (líneas 48-58) proporcionan acceso a los miembros de datos private de un NodoLista. En breve hablaremos sobre el método Insertar de NodoArbol (líneas 62-80). La clase Arbol (líneas 84-170 de la figura 24.20) manipula objetos de la clase NodoArbol. La clase Arbol tiene una raiz de datos private (línea 86): una referencia al nodo raíz del árbol. La clase contiene el método public InsertarNodo (líneas 97-103) para insertar un nuevo nodo en el árbol, y los métodos public RecorridoPreorden (líneas 106-109), RecorridoInorden (líneas 128-131) y RecorridoPostorden (líneas 150-153) para iniciar los recorridos del árbol. Cada uno de estos métodos llama a un método utilitario recursivo separado para realizar las operaciones de recorrido en la representación interna del árbol. El constructor de Arbol (líneas 89-92) inicializa raiz a null para indicar que al principio el árbol está vacío. El método InsertarNodo de Arbol (líneas 97-103) determina primero si el árbol está vacío. De ser así, la línea 100 asigna un nuevo NodoArbol, inicializa el nodo con el entero que se insertará en el árbol y asigna ese nuevo nodo a raiz. Si el árbol no está vacío, InsertarNodo llama al método Insertar de NodoArbol (líneas 62-80), el cuál determina en forma recursiva la ubicación para el nuevo nodo en el árbol, e inserta el nodo en esa ubicación. En un árbol binario, un nodo puede insertarse sólo como un nodo hoja.

986

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Capítulo 24 Estructuras de datos

// Fig. 24.20: BibliotecaArbolesBinarios.cs // Declaración de las clases NodoArbol y Arbol. using System; namespace BibliotecaArbolesBinarios { // declaración de la clase NodoArbol class NodoArbol { private NodoArbol nodoIzquierdo; // enlace al hijo izquierdo private int datos; // datos almacenados en el nodo private NodoArbol nodoDerecho; // enlace al hijo derecho // inicializa datos del nodo y lo convierte en nodo hoja public NodoArbol( int datosNodo ) { datos = datosNodo; nodoIzquierdo = nodoDerecho = null; // el nodo no tiene hijos } // fin del constructor // propiedad NodoIzquierdo public NodoArbol NodoIzquierdo { get { return nodoIzquierdo; } // fin de get set { nodoIzquierdo = value; } // fin de set } // fin de la propiedad NodoIzquierdo // propiedad Datos public int Datos { get { return datos; } // fin de get set { datos = value; } // fin de set } // fin de la propiedad Datos // propiedad NodoDerecho public NodoArbol NodoDerecho { get { return nodoDerecho; } // fin de get set { nodoDerecho = value; } // fin de set } // fin de la propiedad NodoDerecho

Figura 24.20 | Las clases NodoArbol y Arbol para un árbol binario de búsqueda. (Parte 1 de 3).

24.7 Árboles

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118

// inserta NodoArbol en el Arbol que contiene nodos; // ignora los valores duplicados public void Insertar( int valorInsercion ) { if ( valorInsercion < datos ) // inserta en el subárbol izquierdo { // inserta nuevo NodoArbol if ( nodoIzquierdo == null ) nodoIzquierdo = new NodoArbol( valorInsercion ); else // continúa recorriendo el subárbol izquierdo nodoIzquierdo.Insertar( valorInsercion ); } // fin de if else if ( valorInsercion > datos ) // inserta en el subárbol derecho { // inserta nuevo NodoArbol if ( nodoDerecho == null ) nodoDerecho = new NodoArbol( valorInsercion ); else // continua recorriendo el subárbol derecho nodoDerecho.Insertar( valorInsercion ); } // fin de else if } // fin del método Insertar } // fin de la clase NodoArbol // declaración de la clase Arbol public class Arbol { private NodoArbol raiz; // construye un Arbol de enteros vacío public Arbol() { raiz = null; } // fin del constructor // Inserta un nuevo nodo en el árbol binario de búsqueda. // Si el nodo raíz es null, crea el nodo raíz aquí. // En cualquier otro caso, llama al método Insertar de la clase NodoArbol. public void InsertarNodo( int valorInsercion ) { if ( raiz == null ) raiz = new NodoArbol( valorInsercion ); else raiz.Insertar( valorInsercion ); } // fin del método InsertarNodo // empieza el recorrido preorden public void RecorridoPreorden() { AyudantePreorden( raiz ); } // fin del método RecorridoPreorden // método recursivo para realizar el recorrido preorden private void AyudantePreorden( NodoArbol nodo ) { if ( nodo == null ) return; // imprime los datos del nodo Console.Write( nodo.Datos + " " );

Figura 24.20 | Las clases NodoArbol y Arbol para un árbol binario de búsqueda. (Parte 2 de 3).

987

988

Capítulo 24 Estructuras de datos

119 120 // recorre el subárbol izquierdo 121 AyudantePreorden( nodo.NodoIzquierdo ); 122 123 // recorre el subárbol derecho 124 AyudantePreorden( nodo.NodoDerecho ); 125 } // fin del método AyudantePreorden 126 127 // empieza recorrido inorden 128 public void RecorridoInorden() 129 { 130 AyudanteInorden( raiz ); 131 } // fin del método RecorridoInorden 132 133 // método recursivo para realizar el recorrido inorden 134 private void AyudanteInorden( NodoArbol nodo ) 135 { 136 if ( nodo == null ) 137 return; 138 139 // recorre el subárbol izquierdo 140 AyudanteInorden( nodo.NodoIzquierdo ); 141 142 // imprime datos del nodo 143 Console.Write( nodo.Datos + " " ); 144 145 // recorre el subárbol derecho 146 AyudanteInorden( nodo.NodoDerecho ); 147 } // fin del método AyudanteInorden 148 149 // empieza recorrido postorden 150 public void RecorridoPostorden() 151 { 152 AyudantePostorden( raiz ); 153 } // fin del método RecorridoPostorden 154 155 // método recursivo para realizar el recorrido postorden 156 private void AyudantePostorden( NodoArbol nodo ) 157 { 158 if ( nodo == null ) 159 return; 160 161 // recorre el subárbol izquierdo 162 AyudantePostorden( nodo.NodoIzquierdo ); 163 164 // recorre el subárbol derecho 165 AyudantePostorden( nodo.NodoDerecho ); 166 167 // imprime datos del nodo 168 Console.Write( nodo.Datos + " " ); 169 } // fin del método AyudantePostorden 170 } // fin de la clase Arbol 171 } // fin del espacio de nombres BibliotecaArbolesBinarios

Figura 24.20 | Las clases NodoArbol y Arbol para un árbol binario de búsqueda. (Parte 3 de 3). El método Insertar de NodoArbol compara el valor a insertar con el valor de datos en el nodo raiz. Si el valor a insertar es menor que los datos del nodo raíz, el programa determina si el subárbol izquierdo está vacío (línea 67). De ser así, la línea 68 asigna un nuevo NodoArbol, lo inicializa con el entero que se insertará y asigna el nuevo nodo a la referencia nodoIzquierdo. En caso contrario, la línea 70 hace una llamada recursiva a Insertar

24.7 Árboles

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

989

// Fig. 24.21: PruebaArbol.cs // Este programa prueba la clase Arbol. using System; using BibliotecaArbolesBinarios; // declaración de la clase PruebaArbol public class PruebaArbol { // prueba la clase Arbol static void Main( string[] args ) { Arbol arbol = new Arbol(); int valorInsercion; Console.WriteLine( "Insertando valores: " ); Random aleatorio = new Random(); // inserta 10 enteros aleatorios del 0-99 en el árbol for ( int i = 1; i 1000. Para buscar en un árbol binario de búsqueda de 1,000,000 elementos (estrechamente empaquetado) se requieren a lo más 20 comparaciones, ya que 220 > 1,000,000.

Generalidades acerca de los ejercicios con árboles binarios Los ejercicios del capítulo presentan algoritmos para otras operaciones con árboles binarios, como eliminar un elemento de un árbol binario y realizar un recorrido en orden de niveles de un árbol binario. El recorrido en orden de niveles de un árbol binario visita los nodos del árbol fila por fila, empezando en el nivel del nodo raíz. En cada nivel del árbol, un recorrido por orden de nivel visita los nodos de izquierda a derecha.

24.7.2 Árbol binario de búsqueda de objetos IComparable El ejemplo del árbol binario en la sección 24.7.1 funciona bien cuando todos los datos son de tipo int. Suponga que desea manipular un árbol binario de valores enteros dobles. Podría reescribir las clases NodoArbol y Arbol con distintos nombres, y personalizarlas para manipular valores double. De manera similar, para cada tipo de datos podría crear versiones personalizadas de la clases NodoArbol y Arbol. Esto resulta en la proliferación de código, que se volvería difícil de manejar y mantener. Lo ideal sería definir la funcionalidad de un árbol binario una sola vez, y reutilizar esa funcionalidad para muchos tipos de datos. Los lenguajes como Java™ y C# cuentan con herramientas de polimorfismo, las cuales permiten manipular todos los objetos de manera uniforme. Mediante el uso de dichas herramientas, podemos diseñar una estructura de datos más flexible. La nueva versión de C#, C# 2.0, proporciona estas herramientas mediante los genéricos (capítulo 25). En nuestro siguiente ejemplo, aprovecharemos las herramientas polimórficas de C#, implementando clases NodoArbol y Arbol para manipular objetos de cualquier tipo que implemente a la interfaz IComparable (espacio de nombres System). Es imperativo poder comparar los objetos almacenados en una búsqueda binaria, para poder determinar la ruta al punto de inserción de un nuevo nodo. Las clases que implementan a IComparable definen el método CompareTo, que compara el objeto que invocó al método con el objeto que recibe ese método como argumento. El método devuelve un valor int menor que cero si el objeto que hizo la llamada es menor que el objeto argumento, cero si los objetos son iguales y un valor positivo si el objeto que hizo la llamada es más grande que el objeto argumento. Además, tanto el objeto que hizo la llamada como los objetos argumentos deben ser del mismo tipo; en caso contrario, el método lanza una excepción ArgumentException. El programa de las figuras 24.23 y 24.24 agrega características al programa de la sección 24.7.1 para manipular objetos IComparable. Una restricción en las nuevas versiones de las clases NodoArbol y Arbol en la figura 24.23 es que cada objeto Arbol puede contener objetos de sólo un tipo de datos (por ejemplo, que todos sean

992

Capítulo 24 Estructuras de datos

string o que todos sean double). Si un programa trata de insertar varios tipos de datos en el mismo objeArbol, se producirán excepciones ArgumentException. Sólo modificamos seis líneas de código en la clase NodoArbol (líneas 12, 16, 36, 63, 65 y 73) y una línea de código en la clase Arbol (línea 98) para habilitar el procesamiento de objetos IComparable. Con la excepción de las líneas 65 y 73, todas las demás modificaciones simplemente sustituyen el tipo int con el tipo IComparable. Las líneas 65 y 73 anteriormente usaban los operadores < y > para comparar el valor a insertar con el valor en un nodo dado. Ahora estas líneas comparan objetos IComparable mediante el método CompareTo de la interfaz, y después evalúan el valor de retorno del método

to

para determinar si es menor que cero (el objeto que hizo la llamada es menor que el objeto argumento) o mayor que cero (el objeto que hizo la llamada es mayor que el objeto argumento). [Nota: si esta clase se hubiera escrito con genéricos, el tipo de datos, int o IComparable podría haberse sustituido en tiempo de compilación por cualquier otro tipo que implemente los operadores y métodos necesarios.]

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

// Fig. 24.23: BibliotecaArbolesBinarios2.cs // Declaración de las clases NodoArbol y Arbol para // objetos IComparable. using System; namespace BibliotecaArbolesBinarios2 { // declaración de la clase NodoArbol class NodoArbol { private NodoArbol nodoIzquierdo; // enlace al hijo izquierdo private IComparable datos; // datos almacenados en el nodo private NodoArbol nodoDerecho; // enlace al subárbol derecho // inicializa los datos en este nodo y lo convierte en nodo hoja public NodoArbol( IComparable datosNodo ) { datos = datosNodo; nodoIzquierdo = nodoDerecho = null; // el nodo no tiene hijos } // fin del constructor // propiedad NodoIzquierdo public NodoArbol NodoIzquierdo { get { return nodoIzquierdo; } // fin de get set { nodoIzquierdo = value; } // fin de set } // fin de la propiedad NodoIzquierdo // propiedad Datos public IComparable Datos { get { return datos; } // fin de get set { datos = value;

Figura 24.23 | Las clases NodoArbol y Arbol para manipular objetos IComparable. (Parte 1 de 4).

24.7 Árboles

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103

} // fin de set } // fin de la propiedad Datos // propiedad NodoDerecho public NodoArbol NodoDerecho { get { return nodoDerecho; } // fin de get set { nodoDerecho = value; } // fin de set } // fin de la propiedad NodoDerecho // inserta NodoArbol en Arbol que contiene nodos; // ignora los valores duplicados public void Insertar( IComparable valorInsercion ) { if ( valorInsercion.CompareTo( datos ) < 0 ) { // inserta en subárbol izquierdo if ( nodoIzquierdo == null ) nodoIzquierdo = new NodoArbol( valorInsercion ); else // continúa recorriendo el subárbol izquierdo nodoIzquierdo.Insertar( valorInsercion ); } // fin de if else if ( valorInsercion.CompareTo( datos ) > 0 ) { // inserta en subárbol derecho if ( nodoDerecho == null ) nodoDerecho = new NodoArbol( valorInsercion ); else // continúa recorriendo el subárbol derecho nodoDerecho.Insertar( valorInsercion ); } // fin de else if } // fin del método Insertar } // fin de la clase NodoArbol // declaración de la clase Arbol public class Arbol { private NodoArbol raiz; // construye un Arbol de enteros vacío public Arbol() { raiz = null; } // fin del constructor // Inserta un nuevo nodo en el árbol binario de búsqueda. // Si el nodo raíz es null, lo crea aquí. // En cualquier otro caso, llama al método Insertar de la clase NodoArbol. public void InsertarNodo( IComparable valorInsercion ) { if ( raiz == null ) raiz = new NodoArbol( valorInsercion ); else raiz.Insertar( valorInsercion );

Figura 24.23 | Las clases NodoArbol y Arbol para manipular objetos IComparable. (Parte 2 de 4).

993

994

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162

Capítulo 24 Estructuras de datos

} // fin del método InsertarNodo // empieza recorrido preorden public void RecorridoPreorden() { AyudantePreorden( raiz ); } // fin del método RecorridoPreorden // método recursivo para realizar el recorrido preorden private void AyudantePreorden( NodoArbol nodo ) { if ( nodo == null ) return; // imprime datos del nodo Console.Write( nodo.Datos + " " ); // recorre subárbol izquierdo AyudantePreorden( nodo.NodoIzquierdo ); // recorre subárbol derecho AyudantePreorden( nodo.NodoDerecho ); } // fin del método AyudantePreorden // empieza recorrido inorden public void RecorridoInorden() { AyudanteInorden( raiz ); } // fin del método RecorridoInorden // método recursivo para realizar recorrido inorden private void AyudanteInorden( NodoArbol nodo ) { if ( nodo == null ) return; // recorre subárbol izquierdo AyudanteInorden( nodo.NodoIzquierdo ); // imprime datos del nodo Console.Write( nodo.Datos + " " ); // recorre subárbol derecho AyudanteInorden( nodo.NodoDerecho ); } // fin del método AyudanteInorden // empieza recorrido postorden public void RecorridoPostorden() { AyudantePostorden( raiz ); } // fin del método RecorridoPostorden // método recursivo para realizar recorrido postorden private void AyudantePostorden( NodoArbol nodo ) { if ( nodo == null ) return; // recorre subárbol izquierdo

Figura 24.23 | Las clases NodoArbol y Arbol para manipular objetos IComparable. (Parte 3 de 4).

24.7 Árboles

995

163 AyudantePostorden( nodo.NodoIzquierdo ); 164 165 // recorre subárbol derecho 166 AyudantePostorden( nodo.NodoDerecho ); 167 168 // imprime datos del nodo 169 Console.Write( nodo.Datos + " " ); 170 } // fin del método AyudantePostorden 171 172 } // fin de la clase Arbol 173 } // fin del espacio de nombres BibliotecaArbolesBinarios2

Figura 24.23 | Las clases NodoArbol y Arbol para manipular objetos IComparable. (Parte 4 de 4). La clase PruebaArbol (figura 24.24) crea tres objetos Arbol para almacenar valores int, double y string; y el .NET Framework los define como tipos IComparable. El programa llena los árboles con los valores en los arreglos arregloInt (línea 12), arregloDouble (líneas 13-14) y arregloString (líneas 15-16), respectivamente. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

// Fig. 24.24: PruebaArbol.cs // Este programa prueba la clase Arbol. using System; using BibliotecaArbolesBinarios2; // declaración de la clase PruebaArbol public class PruebaArbol { // prueba la clase Arbol static void Main( string[] args ) { int[] arregloInt = { 8, 2, 4, 3, 1, 7, 5, 6 }; double[] arregloDouble = { 8.8, 2.2, 4.4, 3.3, 1.1, 7.7, 5.5, 6.6 }; string[] arregloString = { "ocho", "dos", "cuatro", "tres", "uno", "siete", "cinco", "seis" }; // crea Arbol int Arbol arbolInt = new Arbol(); llenarArbol( arregloInt, arbolInt, "arbolInt" ); recorrerArbol( arbolInt, "arbolInt" ); // crea Arbol double Arbol arbolDouble = new Arbol(); llenarArbol( arregloDouble, arbolDouble, "arbolDouble" ); recorrerArbol( arbolDouble, "arbolDouble" ); // crea Arbol string Arbol arbolString = new Arbol(); llenarArbol( arregloString, arbolString, "arbolString" ); recorrerArbol( arbolString, "arbolString" ); } // fin de Main // llena el Arbol con elementos del arreglo static void llenarArbol( Array arreglo, Arbol arbol, string nombre ) { Console.WriteLine( "\n\n\nInsertando en " + nombre + ":" );

Figura 24.24 | Demostración de la clase Arbol con objetos IComparable. (Parte 1 de 2).

996

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

Capítulo 24 Estructuras de datos

foreach ( IComparable datos in arreglo ) { Console.Write( datos + " " ); arbol.InsertarNodo( datos ); } // fin de foreach } // fin del método llenarArbol // realiza los recorridos static void recorrerArbol( Arbol arbol, string tipoArbol ) { // realiza recorrido preorden del árbol Console.WriteLine( "\n\nRecorrido preorden de " + tipoArbol ); arbol.RecorridoPreorden(); // realiza recorrido inorden del árbol Console.WriteLine( "\n\nRecorrido inorden de " + tipoArbol ); arbol.RecorridoInorden(); // realiza recorrido postorden del árbol Console.WriteLine( "\n\nRecorrido postorden de " + tipoArbol ); arbol.RecorridoPostorden(); } // fin del método recorrerArbol } // fin de la clase PruebaArbol

Insertando en arbolInt: 8 2 4 3 1 7 5 6 Recorrido preorden de arbolInt 8 2 1 4 3 7 5 6 Recorrido inorden de arbolInt 1 2 3 4 5 6 7 8 Recorrido postorden de arbolInt 1 3 6 5 7 4 2 8 Insertando en arbolDouble: 8.8 2.2 4.4 3.3 1.1 7.7 5.5 6.6 Recorrido preorden de arbolDouble 8.8 2.2 1.1 4.4 3.3 7.7 5.5 6.6 Recorrido inorden de arbolDouble 1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 Recorrido postorden de arbolDouble 1.1 3.3 6.6 5.5 7.7 4.4 2.2 8.8 Insertando en arbolString: ocho dos cuatro tres uno siete cinco seis Recorrido preorden de arbolString ocho dos cuatro cinco tres siete seis uno Recorrido inorden de arbolString ocho cinco cuatro uno siete seis tres dos Recorrido postorden de arbolString cinco seis siete uno tres cuatro dos ocho

Figura 24.24 | Demostración de la clase Arbol con objetos IComparable. (Parte 2 de 2).

24.8 Conclusión

997

El método llenarArbol (líneas 35-44) recibe como argumentos un objeto Array que contiene los valores inicializadores para el Arbol, un Arbol en el que se colocarán los elementos y un string que representa el nombre del Arbol; después inserta cada elemento del objeto Array en el Arbol. El método recorrerArbol (líneas 47-60) recibe como argumentos un Arbol y un string que representa el nombre del Arbol; después imprime en pantalla los recorridos preorden, inorden y postorden del Arbol. Observe que el recorrido inorden de cada Arbol imprime en pantalla los datos en orden, sin importar el tipo de datos almacenado en el Arbol. Nuestra implementación polimórfica de la clase Arbol invoca al método CompareTo del tipo de datos apropiado para determinar la ruta hacia el punto de inserción de cada valor, usando las reglas de inserción estándar para árboles binarios de búsqueda. Además, observe que el Arbol de objetos string aparece en orden alfabético.

24.8 Conclusión En este capítulo aprendió que los tipos simples son tipos struct por valor, pero de todas formas pueden usarse en cualquier parte en la que se esperen objetos object en un programa, debido a las conversiones boxing y unboxing. Aprendió que las listas enlazadas son colecciones de elementos de datos que se “enlazan entre sí en una cadena”. También aprendió que un programa puede realizar inserciones y eliminaciones en cualquier parte de una lista enlazada (aunque nuestra implementación sólo realiza inserciones y eliminaciones en los extremos de la lista). También demostramos que las estructuras de datos tipo pila y cola son versiones restringidas de las listas. Para las pilas, vimos que las inserciones y eliminaciones se realizan sólo en la parte superior; es por ello que las pilas se conocen como estructuras de datos UEPS (último en entrar, primero en salir). Para las colas, que representan filas de espera, vimos que las inserciones se realizan en la parte final y las eliminaciones en la parte inicial; es por ello que las colas se conocen como estructuras de datos PEPS (primero en entrar, primero en salir). Después presentamos la estructura de datos tipo árbol binario. Vimos un árbol binario de búsqueda que facilita las búsquedas de alta velocidad y la ordenación de los datos, además de una eficiente eliminación de valores duplicados. En el siguiente capítulo presentaremos los genéricos, que le permiten declarar una familia de clases y métodos que implementan la misma funcionalidad sobre cualquier tipo.

25

Genéricos

…nuestra individualidad especial, según se distingue desde nuestra genérica humanidad. —Oliver Wendell Holmes, Sr.

OBJETIVOS En este capítulo aprenderá lo siguiente: Q

Crear métodos genéricos que realicen tareas idénticas con argumentos de distintos tipos.

Q

Crear una clase Pila genérica que pueda usarse para almacenar objetos de cualquier tipo de clase o de interfaz.

Q

Comprender cómo sobrecargar métodos genéricos con métodos no genéricos, o con otros métodos genéricos.

Q

Comprender la restricción new() de un parámetro de tipo.

Q

Aplicar múltiples restricciones a un parámetro de tipo.

Q

Comprender la relación entre los genéricos y la herencia.

Todo hombre de genio ve el mundo desde un ángulo distinto al de sus semejantes. —Havelock Ellis

Nacido bajo una ley, hacia otro límite. —Lord Brooke

Pla n g e ne r a l

1000 Capítulo 25 Genéricos

25.1 25.2 25.3 25.4 25.5 25.6 25.7 25.8

Introducción Motivación para los métodos genéricos Implementación de un método genérico Restricciones de los tipos Sobrecarga de métodos genéricos Clases genéricas Observaciones acerca de los genéricos y la herencia Conclusión

25.1 Introducción En el capítulo 24 presentamos estructuras de datos que almacenan y manipulan referencias object. Podríamos almacenar cualquier objeto object en nuestras estructuras de datos. Un aspecto inconveniente de almacenar referencias object se produce cuando se obtienen de una colección. Por lo general, una aplicación necesita procesar tipos específicos de objetos. Como resultado, las referencias object que se obtienen de una colección generalmente necesitan de una conversión descendente a un tipo apropiado, para permitir que la aplicación procese los objetos en forma correcta. Además, se deben realizar conversiones boxing con los datos de tipos de valores (por ejemplo, int y double) para manipularlos con referencias object, lo cual incrementa la sobrecarga de procesar tales datos. Aparte de eso, si se procesan todos los datos como de tipo object, se limita la habilidad del compilador de C# para realizar la comprobación de tipos. Aunque podemos crear fácilmente estructuras de datos para manipular cualquier tipo de datos como objetos object (como hicimos en el capítulo 24), sería bueno si pudiéramos detectar los conflictos de tipo en tiempo de compilación; a esto se le conoce como seguridad de tipos en tiempo de ejecución. Por ejemplo, si una Pila sólo debe almacenar valores int, al tratar de meter un string en esa Pila se produciría un error en tiempo de compilación. De manera similar, un método Ordenar debería ser capaz de comparar elementos en los que se garantice que todos son del mismo tipo. Si creamos versiones de tipo específico de la clase Pila y del método Ordenar, el compilador C# sin duda podría hacer valer la seguridad de los tipos en tiempo de compilación. Sin embargo, esto requeriría de la creación de muchas copias del mismo código básico. En este capítulo hablaremos sobre una de las características más recientes de C#: los genéricos, que proporcionan los medios para crear los modelos generales antes mencionados. Los métodos genéricos nos permiten especificar, mediante la declaración de un solo método, un conjunto de métodos relacionados. Las clases genéricas nos permiten especificar, mediante la declaración de una sola clase, un conjunto de clases relacionadas. Asimismo, las interfaces genéricas nos permiten especificar, mediante la declaración de una sola interfaz, un conjunto de interfaces relacionadas. Los genéricos proporcionan seguridad de tipos en tiempo de compilación. [Nota: También podemos implementar tipos struct y delegate genéricos. Para más información, consulte la especificación del lenguaje C#.] Podemos escribir un método genérico para ordenar un arreglo de objetos y después invocar al método genérico por separado con un arreglo int, un arreglo double, un arreglo string y así sucesivamente, para ordenar cada tipo distinto de arreglo. El compilador realiza la comprobación de tipos para asegurar que el arreglo que se pasa al método de ordenamiento contenga sólo elementos del mismo tipo. Podemos escribir una sola clase Pila genérica que manipule una pila de objetos, y después crear instancias de objetos Stack para una pila de valores int, una pila de valores double, una pila de valores string, y así sucesivamente. El compilador realiza la comprobación de tipos para asegurar que la Pila almacene sólo elementos del mismo tipo. En este capítulo presentaremos ejemplos de clases y métodos genéricos. También consideraremos las relaciones entre los genéricos y otras características de C#, como la sobrecarga y la herencia. El capítulo 26, Colecciones, habla sobre las clases de colecciones genéricas y no genéricas del .NET Framework. Una colección es una estructura de datos que mantiene un grupo de objetos o valores relacionados. Las clases de colecciones del .NET Framework utilizan genéricos para permitirnos especificar los tipos exactos de objetos que debe almacenar una colección específica.

25.2 Motivación para los métodos genéricos 1001

25.2 Motivación para los métodos genéricos A menudo, los métodos sobrecargados se utilizan para realizar operaciones similares con distintos tipos de datos. Para motivar los métodos genéricos, empecemos con un ejemplo (figura 25.1) que contiene tres métodos ImprimirArreglo sobrecargados (líneas 23-29, 32-38 y 41-47). Estos métodos muestran los elementos de un arreglo int, un arreglo double y un arreglo char, respectivamente. Pronto reimplementaremos este programa de una manera más concisa y elegante, mediante el uso de un solo método genérico.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

// Fig. 25.1: MetodosSobrecargados.cs // Uso de métodos sobrecargados para imprimir arreglos de distintos tipos. using System; class MetodosSobrecargados { static void Main( string[] args ) { // crea arreglos de valores int, double y char int[] arregloInt = { 1, 2, 3, 4, 5, 6 }; double[] arregloDouble = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7 }; char[] arregloChar = { 'H', 'O', 'L', 'A' }; Console.WriteLine( "El arreglo arregloInt contiene:" ); ImprimirArreglo( arregloInt ); // pasa un arreglo int como argumento Console.WriteLine( "El arreglo arregloDouble contiene:" ); ImprimirArreglo( arregloDouble ); // pasa un arreglo double como argumento Console.WriteLine( "El arreglo arregloChar contiene:" ); ImprimirArreglo( arregloChar ); // pasa un arreglo char como argumento } // fin de Main // imprime en pantalla el arreglo int static void ImprimirArreglo( int[] arregloEntrada ) { foreach ( int elemento in arregloEntrada ) Console.Write( elemento + " " ); Console.WriteLine( "\n" ); } // fin del método ImprimirArreglo // imprime en pantalla el arreglo double static void ImprimirArreglo( double[] arregloEntrada ) { foreach ( double elemento in arregloEntrada ) Console.Write( elemento + " " ); Console.WriteLine( "\n" ); } // fin del método ImprimirArreglo // imprime en pantalla el arreglo char static void ImprimirArreglo( char[] arregloEntrada ) { foreach ( char elemento in arregloEntrada ) Console.Write( elemento + " " ); Console.WriteLine( "\n" ); } // fin del método ImprimirArreglo } // fin de la clase MetodosSobrecargados

Figura 25.1 | Mostrar arreglos de distintos tipos mediante el uso de los métodos sobrecargados. (Parte 1 de 2).

1002 Capítulo 25 Genéricos

El arreglo arregloInt contiene: 1 2 3 4 5 6 El arreglo arregloDouble contiene: 1.1 2.2 3.3 4.4 5.5 6.6 7.7 El arreglo arregloChar contiene: H O L A

Figura 25.1 | Mostrar arreglos de distintos tipos mediante el uso de los métodos sobrecargados. (Parte 2 de 2).

El programa empieza declarando e inicializando tres arreglos: un arreglo int de seis elementos llamado arregloInt (línea 10), un arreglo double de siete elementos llamado arregloDouble (línea 11) y un arreglo char de cuatro elementos llamado arregloChar (línea 12). Después, las líneas 14-19 imprimen los arreglos en pantalla. Cuando el compilador encuentra la llamada a un método, trata de localizar la declaración de un método con el mismo nombre y los mismos parámetros que coincidan con los tipos de argumentos en la llamada al método. En este ejemplo, cada llamada a ImprimirArreglo coincide exactamente con una de las declaraciones del método ImprimirArreglo. Por ejemplo, la línea 15 llama a ImprimirArreglo con arregloInt como su argumento. En tiempo de compilación, el compilador determina el tipo del argumento arregloInt (es decir, int[]), trata de localizar un método llamado ImprimirArreglo que especifique un solo parámetro int[] (lo encuentra en las líneas 23-29) y establece una llamada a ese método. De manera similar, cuando el compilador encuentra la llamada a ImprimirArreglo en la línea 17, determina el tipo del argumento arregloDouble (es decir, double[]), después trata de localizar un método llamado ImprimirArreglo que especifique un solo parámetro double[] (lo encuentra en las líneas 32-38) y establece una llamada a ese método. Por último, cuando el compilador encuentra la llamada a ImprimirArreglo en la línea 19, determina el tipo del argumento arregloChar (es decir, char[]), después trata de localizar un método llamado ImprimirArreglo que especifique un solo parámetro char[] (lo encuentra en las líneas 41-47) y establece una llamada a ese método. Estudie cada uno de los métodos ImprimirArreglo. Observe que el tipo de elementos del arreglo (int, double o char) aparece en dos ubicaciones en cada método: el encabezado del mismo (líneas 23, 32 y 41) y el encabezado de la instrucción foreach (líneas 25, 34 y 43). Si sustituyéramos los tipos de los elementos en cada método con un nombre genérico (elegimos E para representar el tipo “elemento”), entonces los tres métodos se verían como el de la figura 25.2. Parece ser que, si podemos sustituir el tipo del elemento arreglo en cada uno de los tres métodos con un solo “parámetro de tipo genérico”, entonces podemos declarar un método ImprimirArreglo que pueda mostrar los elementos de cualquier arreglo. El método en la figura 25.2 no compila, ya que su sintaxis no es correcta. En la figura 25.3 declaramos un método ImprimirArreglo genérico con la sintaxis apropiada.

25.3 Implementación de un método genérico Si las operaciones realizadas por varios métodos sobrecargados son idénticas para cada tipo de argumento, los métodos sobrecargados pueden codificarse en forma más compacta y conveniente, usando un método genérico.

1 2 3 4 5 6 7

static void ImprimirArreglo( E[] arregloEntrada ) { foreach ( E elemento in arregloEntrada ) Console.Write( elemento + " " ); Console.WriteLine( "\n" ); } // fin del método ImprimirArreglo

Figura 25.2 | Método ImprimirArreglo en el que los nombres de los tipos se sustituyen por convención con el nombre genérico E.

25.3 Implementación de un método genérico 1003 Usted puede escribir una sola declaración de un método genérico, que pueda llamarse en distintos momentos, con argumentos de tipos diferentes. Con base en los tipos de los argumentos que se pasan al método genérico, el compilador maneja cada llamada al método de manera apropiada. La figura 25.3 reimplementa la aplicación de la figura 25.1, usando un método ImprimirArreglo genérico (líneas 24-30). Observe que las llamadas al método ImprimirArreglo en las líneas 16, 18 y 20 son idénticas a las de la figura 25.1, los resultados de las dos aplicaciones son idénticos y el código en la figura 25.3 tiene 17 líneas menos que el código en la figura 25.1. Como se ilustra en la figura 25.3, los genéricos nos permiten crear y evaluar nuestro código una vez, para después utilizar ese código con muchos tipos de datos distintos. Esto demuestra el poder expresivo de los genéricos. La línea 24 empieza la declaración del método ImprimirArreglo. Todas las declaraciones de métodos genéricos tienen una lista de parámetros de tipo delimitada entre los signos < y > (< E > en este ejemplo) que va después del nombre del método. Cada lista de parámetros de tipo contiene uno o más parámetros de tipo, separados por comas. Un parámetro de tipo es un identificador que se utiliza en vez de los nombres reales de los tipos. En la declaración de un método genérico, los parámetros de tipo pueden usarse para declarar el tipo de valor de retorno,

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

// Fig. 25.3: MetodoGenerico.cs // Uso de métodos sobrecargados para imprimir arreglos de distintos tipos. using System; using System.Collections.Generic; class MetodoGenerico { static void Main( string[] args ) { // crea arreglos de tipo int, double and char int[] arregloInt = { 1, 2, 3, 4, 5, 6 }; double[] arregloDouble = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7 }; char[] arregloChar = { 'H', 'O', 'L', 'A' }; Console.WriteLine( "El arreglo arregloInt contiene:" ); ImprimirArreglo( arregloInt ); // pasa un arreglo int como argumento Console.WriteLine( "El arreglo arregloDouble contiene:" ); ImprimirArreglo( arregloDouble ); // pasa un arreglo double como argumento Console.WriteLine( "El arreglo arregloChar contiene:" ); ImprimirArreglo( arregloChar ); // pasa un arreglo char como argumento } // fin de Main // imprime un pantalla un arreglo de todos los tipos static void ImprimirArreglo< E >( E[] arregloEntrada ) { foreach ( E element in arregloEntrada ) Console.Write( element + " " ); Console.WriteLine( "\n" ); } // fin del método ImprimirArreglo } // fin de la clase MetodoGenerico

El arreglo arregloInt contiene: 1 2 3 4 5 6 El arreglo arregloDouble contiene: 1.1 2.2 3.3 4.4 5.5 6.6 7.7 El arreglo arregloChar contiene: H O L A

Figura 25.3 | Impresión de los elementos de un arreglo, mediante el uso del método genérico ImprimirArreglo.

1004 Capítulo 25 Genéricos los tipos de los parámetros y los tipos de las variables locales; los parámetros de tipo actúan como receptáculos para los tipos de los argumentos que se pasan al método genérico. El cuerpo de un método genérico se declara de igual forma que con cualquier otro método. Debemos tener en cuenta que los nombres de los parámetros de tipo esparcidos en la declaración del método deben coincidir con los que se declaran en la lista de parámetros de tipo. Por ejemplo, la línea 26 declara a elemento en la instrucción foreach con el tipo E, que coincide con el parámetro de tipo (E) declarado en la línea 24. Además, un parámetro de tipo puede declararse sólo una vez en la lista de parámetros de tipo, pero puede aparecer más de una vez en la lista de parámetros del método. Los nombres de los parámetros de tipo no necesitan ser únicos entre varios métodos genéricos distintos.

Error común de programación 25.1 Si olvida incluir la lista de parámetros de tipo al declarar un método genérico, el compilador no reconocerá los nombres de los parámetros de tipo cuando los encuentre en un método. Esto produce errores de compilación.

La lista de parámetros de tipo de ImprimirArreglo (línea 24) declara el parámetro de tipo E como receptáculo para el tipo de elemento de arreglo que ImprimirArreglo mostrará en pantalla. Observe que E aparece en la lista de parámetros como el tipo de elemento de arreglo (línea 24). El encabezado de la instrucción foreach (línea 26) también utiliza a E como el tipo de elemento. Éstas son las mismas dos ubicaciones en donde los métodos ImprimirArreglo sobrecargados de la figura 25.1 especificaron int, double o char como el tipo de elemento del arreglo. El resto de ImprimirArreglo es idéntico a la versión que se presenta en la figura 25.1.

Buena práctica de programación 25.1 Se recomienda que los parámetros de tipo se especifiquen como letras mayúsculas individuales. Por lo general, un parámetro de tipo que representa el tipo de un elemento en un arreglo (o en otra colección) se llama E, por “elemento”, o T por “tipo”.

Al igual que en la figura 25.1, el programa de la figura 25.3 empieza por declarar e inicializar el arreglo int de seis elementos llamado arregloInt (línea 11), el arreglo double de siete elementos llamado arregloDouble (línea 12) y el arreglo char de cuatro elementos llamado arregloChar (línea 13). Después se imprime cada arreglo, llamando a ImprimirArreglo (líneas 16, 18 y 20); una vez con el argumento arregloInt, otra con el argumento arregloDouble y la última con el argumento arregloChar. Cuando el compilador encuentra la llamada a un método como en la línea 16, analiza el conjunto de métodos (tanto genéricos como no genéricos) que podrían coincidir con la llamada al método, buscando un método que coincida con la llamada en forma exacta. Si no hay coincidencias exactas, el compilador elije el método que coincida mejor. Si no hay métodos que coincidan, o si hay más de una mejor coincidencia, el compilador genera un error. En la sección 14.5.5.1 de la Especificación del lenguaje C# de Ecma encontrará los detalles completos acerca de la resolución de llamadas a métodos: www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf

o también en la sección 20.9.7 de la Especificación 2.0 del lenguaje C# de Microsoft: msdn.microsoft.com/vcsharp/programming/language

En el caso de la línea 16, el compilador determina que ocurre una coincidencia exacta si el parámetro de tipo E en las líneas 24 y 26 de la declaración del método ImprimirArreglo se sustituye con el tipo de los elementos en el argumento arregloInt de la llamada al método (es decir, int). Después, el compilador establece una llamada a ImprimirArreglo con int como el argumento de tipo para el parámetro de tipo E. A esto se le conoce como el proceso de inferencia de tipos. El mismo proceso se repite para las llamadas al método ImprimirArreglo en las líneas 18 y 20.

Error común de programación 25.2 Si el compilador no puede encontrar una sola declaración de un método no genérico o genérico que sea la mejor coincidencia para la llamada a un método, o si hay varias mejores coincidencias, se produce un error de compilación.

25.4 Restricciones de los tipos 1005 También podemos usar argumentos de tipo explícito para indicar el tipo exacto que debe utilizarse para llamar a una función genérica. Por ejemplo, la línea 16 podría escribirse así: ImprimirArreglo< int >( arregloInt ); // pasa un arreglo int como argumento

La llamada al método anterior proporciona en forma explicita el argumento de tipo (int) que debe utilizarse para sustituir el parámetro de tipo E en las líneas 24 y 26 de la declaración del método ImprimirArreglo. El compilador también determina si las operaciones realizadas en los parámetros de tipo del método pueden aplicarse a los elementos del tipo almacenado en el argumento tipo arreglo. La única operación que se realiza con los elementos tipo arreglo en este ejemplo es imprimir en pantalla la representación string de esos elementos. La línea 27 realiza una conversión implícita para cada elemento del arreglo de tipo de valor, y una llamada implícita a ToString en cada elemento del arreglo de tipo de referencia. Como todos los objetos tienen un método ToString, el compilador queda satisfecho de que la línea 27 realice una operación válida para cualquier elemento del arreglo. Al declarar a ImprimirArreglo como método genérico en la figura 25.3, eliminamos la necesidad de los métodos sobrecargados de la figura 25.1, lo cual nos ahorra 17 líneas de código y se crea un método reutilizable que puede imprimir las representaciones de cadena de los elementos en cualquier arreglo, no sólo en arreglos de elementos int, double o char.

25.4 Restricciones de los tipos En esta sección presentaremos un método genérico llamado Maximo, que determina y devuelve el mayor de sus tres argumentos (todos del mismo tipo). El método genérico en este ejemplo utiliza el parámetro de tipo para declarar tanto el tipo de valor de retorno del método, como sus parámetros. Por lo general, al comparar valores para determinar cuál es mayor, se utiliza el operador >. No obstante, este operador no está sobrecargado para utilizarse con cualquier tipo integrado en la FCL, o que pueda definirse mediante la extensión de esos tipos. El código genérico se restringe a realizar operaciones cuyo funcionamiento con todos los tipos posibles esté garantizado. Por ende, una expresión como variable1

Es posible comparar dos objetos del mismo tipo, si ese tipo implementa a la interfaz genérica IComparable< T > (del espacio de nombres System). Un beneficio de implementar esta interfaz es que se pueden utilizar los objetos IComparable< T > con los métodos de ordenamiento y búsqueda de las clases en el espacio de nombres System. Collections.Generic; en el capítulo 26, Colecciones, hablaremos sobre esos métodos. Todas las estructuras en la FCL que corresponden a los tipos simples implementan esta interfaz. Por ejemplo, la estructura para el tipo simple double es Double, y la estructura para el tipo simple int es Int32; tanto Double como Int32 implementan a la interfaz IComparable. Los tipos que implementan a IComparable< T > deben declarar un método CompareTo para comparar objetos. Por ejemplo, si tenemos dos objetos int llamados int1 e int2, pueden compararse con la siguiente expresión: int1.CompareTo( int2 )

El método CompareTo debe devolver 0 si los objetos son iguales, un número negativo si int1 es menor que int2, o un entero positivo si int1 es mayor que int2. Es responsabilidad del programador que declara a un tipo que implementa a la interfaz IComparable< T > declarar el método CompareTo, de tal forma que compare el contenido de dos objetos de ese tipo y devuelva el resultado apropiado.

Especificación de las restricciones de los tipos Aun cuando pueden compararse objetos IComparable, no pueden usarse con el código genérico de manera predeterminada, ya que no todos los tipos implementan a la interfaz IComparable< T >. No obstante, podemos restringir los tipos que pueden usarse con un método o clase genéricos para asegurar que cumplan con ciertos requerimientos. Esta característica, conocida como restricción de tipos, restringe el tipo del argumento que se

1006 Capítulo 25 Genéricos proporciona a un parámetro de tipo específico. La figura 25.4 declara el método Maximo (líneas 20-33) con una restricción de tipo que requiere que cada uno de los argumentos del método sea de tipo IComparable< T >. Esta restricción es importante, ya que no todos los objetos pueden compararse. Sin embargo, se garantiza que todos los objetos IComparable< T > tienen un método CompareTo que puede utilizarse en el método Maximo para determinar el mayor de sus tres argumentos. El método genérico Maximo utiliza el parámetro de tipo T como el tipo de valor de retorno del método (línea 20), como el tipo de los parámetros x, y y z del método (línea 20) y como el tipo de la variable local max (línea 22). La cláusula where del método Maximo (después de la lista de parámetros en la línea 20) especifica la restricción de tipos para el parámetro de tipo T. En este caso, la cláusula where T : IComparable< T > indica que este método requiere que los argumentos de tipo implementen a la interfaz IComparable< T >. Si no se especifica una restricción de tipo, la predeterminada es object. C# cuenta con varios tipos de restricciones de tipos. Una restricción de clase indica que el argumento de tipo debe ser un objeto de una clase base específica, o de una de sus subclases. Una restricción de interfaz indica que la clase del argumento de tipo debe implementar una interfaz específica. La restricción de tipo en la línea 20 es una

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

// Fig. 25.4: PruebaMaximo.cs // El método genérico Maximo devuelve el mayor de los tres objetos. using System; class PruebaMaximo { static void Main( string[] args ) { Console.WriteLine( "El máximo de {0}, {1} 3, 4, 5, Maximo( 3, 4, 5 ) ); Console.WriteLine( "El máximo de {0}, {1} 6.6, 8.8, 7.7, Maximo( 6.6, 8.8, 7.7 ) Console.WriteLine( "El máximo de {0}, {1} "pera", "manzana", "naranja", Maximo( “pera”, "manzana", "naranja" ) } // fin de Main

y {2} es {3}\n", y {2} es {3}\n", ); y {2} es {3}\n", );

// la función genérica determina el // mayor de los tres objetos IComparable static T Maximo< T >( T x, T y, T z ) where T : IComparable < T > { T max = x; // supone que al principio x es el mayor // compara y con max if ( y.CompareTo( max ) > 0 ) max = y; // y es el mayor hasta ahora // compara z con max if ( z.CompareTo( max ) > 0 ) max = z; // z es el mayor return max; // devuelve el objeto mayor } // fin del método Maximo } // fin de la clase PruebaMaximo

El máximo de 3, 4 y 5 es 5 El máximo de 6.6, 8.8 y 7.7 es 8.8 El máximo de pera, manzana y naranja es pera

Figura 25.4 | El método genérico Maximo con una restricción de tipos en su parámetro de tipo.

25.6 Clases genéricas 1007 restricción de interfaz, ya que IComparable< T > es una interfaz. Podemos especificar que el argumento de tipo debe ser un tipo de referencia o un tipo de valor, mediante el uso de la restricción de tipo de referencia (class) o la restricción de tipo de valor (struct), respectivamente. Por último, podemos especificar una restricción de constructor [new()] para indicar que el código genérico puede usar el operador new para crear nuevos objetos del tipo representado por el parámetro de tipo. Si se especifica un parámetro de tipo con una restricción de constructor, la clase del argumento de tipo debe proporcionar a public un constructor sin parámetros o predeterminado, para asegurar que puedan crearse objetos de la clase sin pasar argumentos al constructor; en caso contrario, se produce un error de compilación. Es posible aplicar restricciones múltiples a un parámetro de tipo. Para ello, simplemente se proporciona una lista de restricciones separadas por comas en la cláusula where. Si tiene una restricción de clase, de tipo de referencia o de tipo de valor, debe listarse primero; sólo uno de estos tipos de restricciones pueden usarse para cada parámetro de tipo. Las restricciones de interfaz (en caso de haber) se listan después. La restricción de constructor se lista al último (si la hay).

Análisis del código El método Maximo supone que su primer argumento (x) es el mayor y lo asigna a la variable local max (línea 22). A continuación, la instrucción if en las líneas 25-26 determinan si y es mayor que max. La condición invoca al método CompareTo de y con la expresión y.CompareTo( max ). Si y es mayor que max, entonces y se asigna a la variable max (línea 26). De manera similar, la instrucción en las líneas 29-30 determina si z es mayor que max. Si esto es cierto, la línea 30 asigna z a max. Después, la línea 32 devuelve max al método que hizo la llamada. En Main (líneas 7-16), la línea 10 llama a Maximo con los enteros 3, 4 y 5. El método genérico Maximo es una coincidencia para esta llamada, pero sus argumentos deben implementar la interfaz IComparable< T >, parea asegurar que puedan compararse. El tipo int es un sinónimo para struct Int32, que implementa a la interfaz IComparable< int >. Por ende, los valores int (y otros tipos simples) son argumentos válidos para el método Maximo. La línea 12 pasa tres argumentos double a Maximo. De nuevo, esto se permite debido a que double es sinónimo del tipo struct Double, que implementa a IComparable< double >. La línea 15 pasa a Maximo tres objetos string, que son también objetos IComparable< string >. Observe que colocamos de manera intencional el valor más grande en una posición distinta en cada llamada al método (líneas 10, 12 y 15) para mostrar que el método genérico siempre encuentra el valor máximo, sin importar su posición en la lista de argumentos y sin importar el argumento de tipo inferido.

25.5 Sobrecarga de métodos genéricos

Un método genérico puede sobrecargarse. Una clase puede proporcionar dos o más métodos genéricos con el mismo nombre, pero con distintos parámetros para los métodos. Por ejemplo, podríamos proporcionar una segunda versión del método genérico ImprimirArreglo (figura 25.3) con los parámetros adicionales indiceBajo e indiceAlto, para especificar la porción del arreglo que se desea imprimir. Un método genérico también puede sobrecargarse mediante otro método genérico con el mismo nombre y con un número distinto de parámetros de tipo, o mediante un método genérico con distintos números de parámetros de tipo y de parámetros del método. Un método genérico puede sobrecargarse mediante métodos no genéricos que tengan el mismo nombre del método y el mismo número de parámetros. Cuando el compilador encuentra una llamada a un método, busca la declaración del método que coincida con más precisión con el nombre del método y los tipos de los argumentos especificados en la llamada. Por ejemplo, el método genérico ImprimirArreglo de la figura 25.3 podría sobrecargarse con una versión específica para objetos string, que imprima en pantalla estos objetos en formato tabular ordenado. Si el compilador no puede hacer que coincida la llamada a un método con un método no genérico o genérico, o si hay ambigüedad debido a varias posibles coincidencias, el compilador genera un error. Los métodos genéricos también pueden sobrecargarse mediante métodos no genéricos que tengan el mismo nombre del método, pero un número distinto de parámetros.

25.6 Clases genéricas El concepto de una estructura de datos (por ejemplo, una pila), que contiene elementos de datos, puede comprenderse sin importar el tipo de elemento que manipule. Una clase genérica proporciona el medio para describir

1008 Capítulo 25 Genéricos una clase en forma independiente de su tipo. Así, podemos crear instancias de objetos específicos para el tipo de la clase genérica. Esta capacidad es una oportunidad para la reutilización de software. Una vez que se tiene una clase genérica, se puede usar una notación simple y concisa para indicar el (los) tipo(s) actual(es) que debe(n) usarse en vez del (los) parámetro(s) de tipo de la clase. En tiempo de compilación, el compilador se encarga de la seguridad de los tipos de nuestro código, y el sistema en tiempo de ejecución sustituye los parámetros de tipo con argumentos reales, para que nuestro código cliente interactúe con la clase genérica. Por ejemplo, una clase genérica Pila podría ser la base para crear muchas clases de Pila (por ejemplo, “Pila de double”, “Pila de int”, “Pila de char”, “Pila de Empleado”). La figura 25.5 presenta la declaración de una clase Pila genérica. La declaración de una clase genérica es similar a la de una clase no genérica, con la excepción de que el nombre de la clase va seguido de una lista de parámetros de tipo (línea 5) y, en este caso, una restricción en su parámetro de tipo (que veremos en breve). El parámetro de tipo E representa el tipo de elemento que la Pila manipulará. Al igual que con los métodos genéricos, la lista de parámetros de tipo de una clase genérica puede tener uno o más parámetros de tipo, separados por comas. El parámetro de tipo E se utiliza a lo largo de la declaración de la clase Pila (figura 25.5) para representar el tipo de elemento. La clase Pila declara la variable elementos como un arreglo de tipo E (línea 8). Este arreglo (creado en la línea 20 o 22) almacena los elementos de la Pila. [Nota: Este ejemplo implementa una Pila como un arreglo. Como vimos en el capítulo 24, Estructuras de datos, es común que los objetos Pila se implementen también como listas enlazadas.]

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

// Fig. 25.5: Pila.cs // La clase genérica Pila using System; class Pila< E > { private int superior; // ubicación del elemento superior private E[] elementos; // arreglo que almacena elementos de la Pila // el constructor sin parámetros crea una Pila del tamaño predeterminado public Pila() : this( 10 ) // tamaño predeterminado de la pila { // constructor vacío; llama al constructor de la línea 18 para realizar la inicialización } // fin el constructor de Pila // el constructor crea una Pila con el número especificado de elementos public Pila( int tamanioPila ) { if ( tamanioPila > 0 ) // valida tamanioPila elementos = new E[ tamanioPila ]; // crea elementos tamanioPila else elementos = new E[ 10 ]; // crea 10 elementos superior = -1; // al principio, la Pila está vacía } // fin del constructor de Pila // mete (push) un elemento en la Pila; si tiene éxito, devuelve true // en caso contrario, lanza ExcepcionPilaLlena public void Push( E meterValor ) { if ( superior == elementos.Length - 1 ) // la Pila está llena throw new ExcepcionPilaLlena( String.Format( "La pila está llena, no se puede meter {0}", meterValor ) ); superior++; // incrementa elemento superior elementos[ superior ] = meterValor; // coloca meterValor en la Pila

Figura 25.5 | Declaración de la clase genérica Pila. (Parte 1 de 2).

25.6 Clases genéricas 1009

37 38 39 40 41 42 43 44 45 46 47 48 49

} // fin del método Push // devuelve el elementos superior si no está vacía // en cualquier otro caso lanza ExcepcionPilaVacia public E Pop() { if ( superior == -1 ) // la pila está vacía throw new ExcepcionPilaVacia( "La pila está vacía, no se puede sacar" ); superior--; // decrementa elemento superior return elementos[ superior + 1 ]; // devuelve valor del elemento superior } // fin del método Pop } // fin de la clase Pila

Figura 25.5 | Declaración de la clase genérica Pila. (Parte 2 de 2). La clase Pila tiene dos constructores. El constructor sin parámetros (líneas 11-14) pasa el tamaño de pila predeterminado (10) al constructor de un argumento, usando la sintaxis this (línea 11) para invocar a otro constructor en la misma clase. El constructor de un argumento (líneas 17-25) valida el argumento tamanioPila y crea un arreglo con el tamanioPila especificado (si es mayor que 0) o un arreglo de 10 elementos, en caso contrario. El método Push (líneas 29-37) determina primero si se está tratando de meter un elemento en una Pila llena. Si es así, las líneas 32-33 lanzan una ExcepcionPilaLlena (declarada en la figura 25.6). Si la Pila no está llena, la línea 35 incrementa el contador superior para indicar la nueva posición superior, y la línea 36 coloca el argumento en esa ubicación del arreglo elementos. El método Pop (líneas 41-48) determina primero si se está tratando de sacar un elemento de una Pila vacía. Si es así, la línea 44 lanza una ExcepcionPilaVacia (declarada en la figura 25.7). En caso contrario, la línea 46 decrementa el contador superior para indicar la nueva posición superior, y la línea 47 devuelve el elemento superior original de la Pila. Las clases ExcepcionPilaLlena (figura 25.6) y ExcepcionPilaVacia (figura 25.7) proporcionan cada una un constructor sin parámetros y un constructor con un argumento de las clases de excepciones (como vimos en la sección 12.8). El constructor sin parámetros establece el mensaje de error predeterminado, y el constructor con un argumento establece un mensaje de error personalizado. Al igual que con los métodos genéricos, cuando se compila una clase genérica el compilador realiza la comprobación de tipos en los parámetros de tipo de la clase, para asegurar que puedan utilizarse con el código en la 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

// Fig. 25.6: ExcepcionPilaLlena.cs // Indica que una pila está llena. using System; class ExcepcionPilaLlena : ApplicationException { // constructor sin parámetros public ExcepcionPilaLlena() : base( "La pila está llena" ) { // constructor vacío } // fin del constructor de ExcepcionPilaLlena // constructor con un parámetro public ExcepcionPilaLlena( string exception ) : base( exception ) { // constructor vacío } // fin del constructor de ExcepcionPilaLlena } // fin de la clase ExcepcionPilaLlena

Figura 25.6 | Declaración de la clase ExcepcionPilaLlena.

1010 Capítulo 25 Genéricos

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

// Fig. 25.7: ExcepcionPilaVacia.cs // Indica que una pila está vacía. using System; class ExcepcionPilaVacia : ApplicationException { // constructor sin parámetros public ExcepcionPilaVacia() : base( "La pila está vacía" ) { // constructor vacío } // fin del constructor de ExcepcionPilaVacia // constructor con un parámetro public ExcepcionPilaVacia( string exception ) : base( exception ) { // constructor vacío } // fin del constructor de ExcepcionPilaVacia } // fin de la clase ExcepcionPilaVacia

Figura 25.7 | Declaración de la clase ExcepcionPilaVacia. clase genérica. Las restricciones determinan las operaciones que pueden realizarse con los parámetros de tipo. El sistema en tiempo de ejecución sustituye los parámetros de tipo con los verdaderos tipos en tiempo de ejecución. Para la clase Pila (figura 25.5) no se especifica una restricción de tipos, por lo que se utiliza la restricción de tipo predeterminada, object. El alcance del parámetro de tipo de una clase genérica es toda la clase. Ahora consideraremos una aplicación (figura 25.8) que utiliza la clase genérica Pila. Las líneas 13-14 declaran variables de tipo Pila< double > (se pronuncia como “Pila de elementos double”) y Pila< int > (se pronuncia como “Pila de elementos int”). Los tipos double e int son los argumentos de tipo de la Pila. El compilador sustituye los parámetros de tipo en la clase genérica, para que el compilador pueda realizar la comprobación de tipos. El método Main crea instancias de objetos pilaDouble con un tamaño de 5 (línea 18) y pilaInt con un tamaño de 10 (línea 19), después llama a los métodos PruebaPushDouble (líneas 28-48), PruebaPopDouble (líneas 51-73), PruebaPushInt (líneas 76-96) y PruebaPopInt (líneas 99-121) para manipular los dos objetos Pila en este ejemplo.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

// Fig. 25.8: PruebaPila.cs // Programa de prueba de la clase genérica Pila. using System; class PruebaPila { // crea arreglos con elementos double e int static double[] elementosDouble = new double[]{ 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 }; static int[] elementosInt = new int[]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; static Pila< double > pilaDouble; // pila que almacena objetos double static Pila< int > pilaInt; // pila que almacena objetos int static void Main( string[] args ) { pilaDouble = new Pila< double >( 5 ); // Pila de valores double pilaInt = new Pila< int >( 10 ); // Pila de valores int

Figura 25.8 | Programa de prueba de la clase genérica Pila. (Parte 1 de 4).

25.6 Clases genéricas 1011

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

PruebaPushDouble(); // mete valores double en pilaDouble PruebaPopDouble(); // saca valores double de pilaDouble PruebaPushInt(); // mete valores int en pilaInt PruebaPopInt(); // saca valores int de pilaInt } // fin de Main // prueba el método Push con pilaDouble static void PruebaPushDouble() { // mete elementos en la pila try { Console.WriteLine( "\nMetiendo elementos en pilaDouble" ); // mete elementos en la pila foreach ( double elemento in elementosDouble ) { Console.Write( "{0:F1} ", elemento ); pilaDouble.Push( elemento ); // mete elemento en pilaDouble } // fin de foreach } // fin de try catch ( ExcepcionPilaLlena exception ) { Console.Error.WriteLine(); Console.Error.WriteLine( "Mensaje: " + exception.Message ); Console.Error.WriteLine( exception.StackTrace ); } // fin de catch } // fin del método PruebaPushDouble // prueba el método Pop con pilaDouble static void PruebaPopDouble() { // saca elementos de la pila try { Console.WriteLine( "\nSacando elementos de pilaDouble" ); double sacarValor; // almacena elemento eliminado de la pila // elimina todos los elementos de la Pila while ( true ) { sacarValor = pilaDouble.Pop(); // saca elemento de pilaDouble Console.Write( "{0:F1} ", sacarValor ); } // fin de while } // fin de try catch ( ExcepcionPilaVacia exception ) { Console.Error.WriteLine(); Console.Error.WriteLine( "Mensaje: " + exception.Message ); Console.Error.WriteLine( exception.StackTrace ); } // fin de catch } // fin del método PruebaPopDouble // prueba el método Push con pilaInt static void PruebaPushInt() { // mete elementos en la pila try

Figura 25.8 | Programa de prueba de la clase genérica Pila. (Parte 2 de 4).

1012 Capítulo 25 Genéricos

80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122

{ Console.WriteLine( "\nMetiendo elementos en pilaInt" ); // mete elementos en la pila foreach ( int elemento in elementosInt ) { Console.Write( "{0} ", elemento ); pilaInt.Push( elemento ); // mete elemento en pilaInt } // fin de foreach } // fin de try catch ( ExcepcionPilaLlena exception ) { Console.Error.WriteLine(); Console.Error.WriteLine( "Mensaje: " + exception.Message ); Console.Error.WriteLine( exception.StackTrace ); } // fin de catch } // fin del método PruebaPushInt // prueba el método Pop con pilaInt static void PruebaPopInt() { // saca elementos de la pila try { Console.WriteLine( "\nSacando elementos depilaIntk" ); int sacarValor; // almacena elemento eliminado de la pila // elimina todos los elementos de la Pila while ( true ) { sacarValor = pilaInt.Pop(); // saca elemento de pilaInt Console.Write( "{0} ", sacarValor ); } // fin de while } // fin de try catch ( ExcepcionPilaVacia exception ) { Console.Error.WriteLine(); Console.Error.WriteLine( "Mensaje: " + exception.Message ); Console.Error.WriteLine( exception.StackTrace ); } // fin de catch } // fin del método PruebaPopInt } // fin de la clase PruebaPila

Metiendo elementos en pilaDouble 1.1 2.2 3.3 4.4 5.5 6.6 Mensaje: La pila está llena, no se puede meter 6.6 en Pila`1.Push(E meterValor) en C:\MisProyectos\Fig25_05_08\Pila\Pila.cs:línea 32 en PruebaPila.PruebaPushDouble() en C:\MisProyectos\Fig25_05_08\Pila\PruebaPila.cs:línea 39 Sacando elementos de pilaDouble 5.5 4.4 3.3 2.2 1.1 Mensaje: La pila está vacía, no se puede sacar en Pila`1.Pop() en C:\MisProyectos\Fig25_05_08\Pila\Pila.cs:línea 44 en PruebaPila.PruebaPopDouble() en C:\MisProyectos\Fig25_05_08\Pila\PruebaPila.cs:línea 63

Figura 25.8 | Programa de prueba de la clase genérica Pila. (Parte 3 de 4).

25.6 Clases genéricas 1013

Metiendo elementos en pilaInt 1 2 3 4 5 6 7 8 9 10 11 Mensaje: La pila está llena, no se puede meter 11 en Pila`1.Push(E meterValor) en C:\MisProyectos\Fig25_05_08\Pila\Pila.cs:línea 32 en PruebaPila.PruebaPushInt() en C:\MisProyectos\Fig25_05_08\Pila\PruebaPila.cs:línea 87 Sacando elementos de pilaInt 10 9 8 7 6 5 4 3 2 1 Mensaje: La pila está vacía, no se puede sacar en Pila`1.Pop() en C:\MisProyectos\Fig25_05_08\Pila\Pila.cs:línea 44 en PruebaPila.PruebaPopInt() en C:\MisProyectos\Fig25_05_08\Pila\PruebaPila.cs:línea 111

Figura 25.8 | Programa de prueba de la clase genérica Pila. (Parte 4 de 4). El método PruebaPushDouble (líneas 28-48) invoca al método Push para colocar en pilaDouble los valores double 1.1, 2.2, 3.3, 4.4 y 5.5 almacenados en el arreglo elementosDouble. La instrucción for termina cuando el programa de prueba trata de meter un sexto valor en pilaDouble (la cual está llena, ya que pilaDouble sólo puede almacenar cinco elementos). En este caso, el método lanza una ExcepcionPilaLlena (figura 25.6) para indicar que la Pila está llena.Las líneas 42-47 atrapan esta excepción e imprimen el mensaje y la información de rastreo de pila. Este rastreo de pila indica la excepción que ocurrió y muestra que el método Push de Pila generó la excepción en las líneas 34-35 del archivo Pila.cs (figura 25.5). El rastreo también muestra que el método PruebaPushDouble de PruebaPila hizo una llamada al método Push en la línea 39 de PruebaPila.cs. Esta información nos permite determinar los métodos que estaban en la pila de llamadas a métodos en el momento en que ocurrió la excepción. Debido a que este programa atrapa la excepción, el entorno en tiempo de ejecución de C# considera que se ha manejado la excepción y el programa puede continuar su ejecución. El método PruebaPopDouble (líneas 51-73) invoca al método Pop de Pila en un ciclo while infinito para eliminar todos los valores de la pila. Observe en los resultados que los valores se sacan en el orden “último en entrar, primero en salir”; es evidente que ésta es la característica que define a las pilas. El ciclo while (líneas 61-65) continúa hasta que la pila está vacía. Al tratar de sacar un elemento de la pila vacía, se produce una ExcepcionPilaVacia. Esto hace que el programa proceda al bloque catch (líneas 67-72) y maneje la excepción, para que pueda continuar su ejecución. Cuando el programa de prueba trata de sacar un sexto valor, la pilaDouble está vacía, por lo que el método Pop lanza una ExcepcionPilaVacia. El método PruebaPushInt (líneas 76-96) invoca al método Push de Pila para colocar valores en pilaInt hasta que se llene. El método PruebaPopInt (líneas 99-121) invoca al método Pop de Pila para eliminar valores de pilaInt hasta que se vacíe. Una vez más, observe que los valores se sacan en el orden “último en entrar, primero en salir”.

Creación de métodos genéricos para probar la clase Pila


Observe que el código en los métodos PruebaPushDouble y PruebaPushInt es casi idéntico para meter valores en una Pila< double > o en una Pila< int >, respectivamente. De manera similar, el código en los métodos PruebaPopDouble y PruebaPopInt es casi idéntico para sacar valores de una Pila< double > o de una Pila < int >, respectivamente. Esto presenta otra oportunidad para usar los métodos genéricos. La figura 25.9 declara el método genérico PruebaPush (líneas 32-53) para realizar las mismas tareas que PruebaPushDouble y PruebaPushInt en la figura 25.8; esto es, meter los valores en una Pila< E >. De manera similar, el método genérico PruebaPop (líneas 55-77) realiza las mismas tareas que PruebaPopDouble y PruebaPopInt en la figura 25.8; esto es, sacar valores de una Pila< E >. Observe que los resultados de la figura 25.9 coinciden en forma precisa con los resultados de la figura 25.8. El método Main (líneas 17-30) crea los objetos Pila< double > (línea 19) y Pila< int > (línea 20). Las líneas 23-29 invocan a los métodos genéricos PruebaPush y PruebaPop para probar los objetos Pila. El método genérico PruebaPush (líneas 32-53) utiliza el parámetro de tipo E (que se especifica en la línea 32) para representar el tipo de datos almacenado en la Pila. El método genérico recibe tres argumentos: un string

1014 Capítulo 25 Genéricos

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

// Fig. 25.9: PruebaPila.cs // Programa de prueba de la clase genérica Pila. using System; using System.Collections.Generic; class PruebaPila { // crea arreglos de elementos double e int static double[] elementosDouble = new double[]{ 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 }; static int[] elementosInt = new int[]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; static Pila< double >pilaDouble; // pila que almacena objetos double static Pila< int >pilaInt; // pila que almacena objetos int static void Main( string[] args ) { pilaDouble = new Pila< double >( 5 ); // Pila de elementos double pilaInt = new Pila< int >( 10 ); // Pila de elementos int // mete elementos double en pilaDouble PruebaPush( "pilaDouble", pilaDouble, elementosDouble ); // saca elementos de pilaDouble PruebaPop( "pilaDouble", pilaDouble ); // mete elementos int en pilaInt PruebaPush( "pilaInt", pilaInt, elementosInt ); // saca elementos int de pilaInt PruebaPop( "pilaInt", pilaInt ); } // fin de Main static void PruebaPush< E >( string nombre, Pila< E > pila, IEnumerable< E > elementos ) { // mete elementos en la pila try { Console.WriteLine( "\nMetiendo elementos en " + nombre ); // mete elementos en la pila foreach ( E elemento in elementos ) { Console.Write( "{0} ", elemento ); pila.Push( elemento ); // mete elemento en la pila } // fin de foreach } // fin de try catch ( ExcepcionPilaLlena excepcion ) { Console.Error.WriteLine(); Console.Error.WriteLine( "Mensaje: " + excepcion.Message ); Console.Error.WriteLine( excepcion.StackTrace ); } // fin de catch } // fin del método PruebaPush static void PruebaPop< E >( string nombre, Pila< E > pila ) { // saca elementos de la pila try {

Figura 25.9 | Paso de una Pila de tipo genérico a un método genérico. (Parte 1 de 2).

25.6 Clases genéricas 1015

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78

Console.WriteLine( "\nSacando elementos de " + nombre ); E sacarValor; // almacena elemento eliminado de la pila // elimina todos los elementos de la Pila while ( true ) { sacarValor = pila.Pop(); // saca elemento de la pila Console.Write( "{0} ", sacarValor ); } // fin de while } // fin de try catch ( ExcepcionPilaVacia excepcion ) { Console.Error.WriteLine(); Console.Error.WriteLine( "Mensaje: " + excepcion.Message ); Console.Error.WriteLine( excepcion.StackTrace ); } // fin de catch } // fin de PruebaPop } // fin de la clase PruebaPila

Metiendo elementos en pilaDouble 1.1 2.2 3.3 4.4 5.5 6.6 Mensaje: La pila está llena, no se puede meter 6.6 en Pila`1.Push(E meterValor) en C:\MisProyectos\Fig25_09\Pila\Pila.cs:línea 32 en PruebaPila.PruebaPush[E](String nombre, Pila`1 pila, IEnumerable`1 elementos) en C:\MisProyectos\Fig25_09\Pila\PruebaPila.cs:línea 44 Sacando elementos de pilaDouble 5.5 4.4 3.3 2.2 1.1 Mensaje: La pila está vacía, no se pueden sacar elementos en Pila`1.Pop() en C:\MisProyectos\Fig25_09\Pila\Pila.cs:línea 44 en PruebaPila.PruebaPop[E](String nombre, Pila`1 pila) en C:\MisProyectos\Fig25_09\Pila\PruebaPila.cs:línea 67 Metiendo elementos en pilaInt 1 2 3 4 5 6 7 8 9 10 11 Mensaje: La pila está llena, no se puede meter 11 en Pila`1.Push(E meterValor) en C:\MisProyectos\Fig25_09\Pila\Pila.cs:línea 32 en PruebaPila.PruebaPush[E](String nombre, Pila`1 pila, IEnumerable`1 elementos) en C:\MisProyectos\Fig25_09\Pila\PruebaPila.cs:línea 44 Sacando elementos de pilaInt 10 9 8 7 6 5 4 3 2 1 Mensaje: La pila está vacía, no se pueden sacar elementos en Pila`1.Pop() en C:\MisProyectos\Fig25_09\Pila\Pila.cs:línea 44 en PruebaPila.PruebaPop[E](String nombre, Pila`1 pila) en C:\MisProyectos\Fig25_09\Pila\PruebaPila.cs:línea 67

Figura 25.9 | Paso de una Pila de tipo genérico a un método genérico. (Parte 2 de 2). que representa el nombre del objeto Pila para fines de imprimirlo en pantalla, un objeto de tipo Pila< E > y un objeto IEnumerable< E >; el tipo de elementos que se meterán en Pila< E >. Observe que el compilador hace cumplir la consistencia entre el tipo de la Pila y los elementos que se meterán a la misma cuando se invoque a Push, que es el argumento de tipo de la llamada al método genérico. El método genérico PruebaPop (líneas 55-77) recibe dos argumentos: un string que representa el nombre del objeto Pila para fines de imprimirlo en pantalla, y un objeto de tipo Pila< E >. Observe que el parámetro de tipo E que se utiliza en los métodos

1016 Capítulo 25 Genéricos PruebaPush y PruebaPop tiene una restricción new, ya que ambos métodos reciben un objeto de tipo Pila< E > y el parámetro de tipo en la declaración genérica de la clase Pila tiene una restricción new (línea 5, figura 25.5). Esto asegura que los objetos que manipularán los métodos PruebaPush y PruebaPop puedan usarse con la clase genérica Pila, la cual requiere que los objetos almacenados en la Pila tengan un constructor public predeterminado o sin parámetros.

25.7 Observaciones acerca de los genéricos y la herencia Los genéricos pueden utilizarse con la herencia de varias formas: • Una clase genérica puede derivarse de una clase no genérica. Por ejemplo, la clase object (que no es una clase genérica) es una clase base directa o indirecta de todas las clases genéricas. • Una clase genérica puede derivarse de otra clase genérica. En el capítulo 24 vimos que la clase Pila no genérica (figura 24.12) hereda de la clase Lista no genérica (figura 24.5). También podemos crear una clase Pila genérica si heredamos de una clase Lista genérica. • Una clase no genérica puede derivarse de una clase genérica. Por ejemplo, podemos implementar una clase ListaDirecciones no genérica que herede de una clase Lista genérica, y que sólo almacene objetos Direccion.

25.8 Conclusión En este capítulo se presentaron los genéricos: una de las herramientas más recientes de C#. Hablamos acerca de cómo los genéricos imponen la seguridad de tipos en tiempo de comprobación, mediante la comprobación de la discordancia de tipos en tiempo de comprobación. Aprendió que el compilador permite que el código genérico se compile sólo si todas las operaciones realizadas con los parámetros de tipo en el código genérico están soportadas por todos los tipos que podrían utilizarse con ese código genérico. También aprendió a declarar métodos y clases genéricas, mediante el uso de parámetros de tipo. Demostramos cómo utilizar una restricción de tipo para especificar los requerimientos de un parámetro de tipo; un componente clave de la seguridad de tipos en tiempo de ejecución. Hablamos sobre varios tipos de restricciones de tipos, incluyendo las restricciones de tipos de referencia, restricciones de tipos de valor, restricciones de clase, restricciones de interfaz y restricciones de constructor. Aprendió que una restricción de constructor indica que el argumento de tipo debe proporcionar un constructor public sin parámetros o predeterminado, de manera que los objetos de ese tipo puedan crearse con new. También hablamos sobre cómo implementar restricciones múltiples de tipos para un parámetro de tipo. Le mostramos cómo los genéricos mejoran la reutilización de código. Por último, mencionamos varias formas de usar los genéricos en la herencia. En el siguiente capítulo demostraremos las clases, interfaces y algoritmos de colecciones de la FCL de .NET. Las clases de colecciones son estructuras de datos preconstruidas que usted puede reutilizar en sus aplicaciones, lo cual le ahorra tiempo. En el siguiente capítulo presentaremos las colecciones genéricas y las colecciones no genéricas, que son más antiguas.

26

Colecciones

¡Las figuras que puede contener un contenedor brillante! —Theodore Roethke

OBJETIVOS En este capítulo aprenderá lo siguiente:

La sabiduría se adquiere no por la edad, sino por la capacidad.

Q

Acerca de las colecciones genéricas y no genéricas que proporciona el .NET Framework.

—Titus Maccius Plautus

Q

A utilizar los métodos static de la clase Array para manipular arreglos.

Es un acertijo envuelto en un misterio dentro de un enigma.

Q

A utilizar enumeradores para “recorrer” una colección.

—Winston Churchill

Q

A utilizar la instrucción foreach con las colecciones de .NET.

Q

A utilizar las clases de colección no genéricas ArrayList, Stack y Hashtable.

Q

A utilizar las clases de colección genéricas SortedDictionary y LinkedList.

Q

A utilizar envolturas de sincronización para que las colecciones sean seguras en aplicaciones con subprocesamiento múltiple.

Creo que es la colección más extraordinaria de talento, de conocimiento humano, que haya sido recopilada en la Casa Blanca; con la posible excepción de cuando Thomas Jefferson cenó solo. —John F. Kennedy

Pla n g e ne r a l

1018 Capítulo 26 Colecciones

26.1 26.2 26.3 26.4

Introducción Generalidades acerca de las colecciones La clase Array y los enumeradores Colecciones no genéricas 26.4.1 La clase ArrayList 26.4.2 La clase Stack 26.4.3 La clase Hashtable 26.5 Colecciones genéricas 26.5.1 La clase genérica SortedDictionary 26.5.2 La clase genérica LinkedList 26.6 Colecciones sincronizadas 26.7 Conclusión

26.1 Introducción En el capítulo 24 vimos cómo crear y manipular estructuras de datos. La discusión fue de “bajo nivel”, en cuanto a que creamos de la manera difícil cada elemento de cada estructura de datos en forma dinámica mediante new, y modificamos las estructuras de datos manipulando directamente sus elementos y referencias a otros elementos. En este capítulo consideraremos las clases de estructuras de datos pre-empaquetadas que proporciona el .NET Framework. Estas clases se conocen como clases de colección, ya que almacenan colecciones de datos. Cada instancia de una de estas clases es una colección de elementos. Algunos ejemplos de colecciones son las cartas que utilizamos en un juego de cartas, las canciones almacenadas en su computadora, los registros de bienes raíces en su registro local de escrituras (que asignan números de libro y de página con los propietarios de los terrenos y casas), y los jugadores en su equipo deportivo favorito. Las clases de colección permiten a los programadores almacenar conjuntos de elementos mediante el uso de estructuras de datos existentes, sin preocuparse por la forma en que se implementan. Éste es un excelente ejemplo de reutilización de código. Los programadores pueden escribir código con más rapidez y esperar un excelente rendimiento, maximizando la velocidad de ejecución y minimizando el consumo de memoria. En este capítulo hablaremos sobre las interfaces de colección que listan las capacidades de cada tipo de colección, las clases de implementación y los enumeradores que se utilizan para “recorrer” las colecciones. El .NET Framework proporciona tres espacios de nombres dedicados a las colecciones. El espacio de nombres System.Collections contiene colecciones que almacenan referencias a objetos de la clase object. El espacio de nombres System.Collections.Generic más reciente contiene clases genéricas para almacenar colecciones de tipos especificados. El espacio de nombres System.Collections.Specialized más reciente contiene varias colecciones que soportan tipos específicos, como string y bit. Si desea aprender más acerca de este espacio de nombres, visite el sitio http://msdn.microsoft.com/library/spa/default.asp?url=/library/SPA/ cpref/html/frlrfsystemcollectionsspecialized.asp. Las colecciones en estos espacios de nombres proporcionan componentes estandarizados y reutilizables; no necesita escribir sus propias clases de colecciones. Estas colecciones están escritas para reutilizarse ampliamente. Están optimizadas para ejecutarse con rapidez y para un uso eficiente de la memoria. A medida que se desarrollen nuevas estructuras de datos y algoritmos que se adapten a este marco de trabajo, una gran base de programadores ya estarán familiarizados con las interfaces y algoritmos implementados por esas estructuras de datos.

26.2 Generalidades acerca de las colecciones Todas las clases de colección en el .NET Framework implementan cierta combinación de las interfaces de colección. Estas interfaces declaran las operaciones que se van a realizar de manera genérica en varios tipos de colecciones. La figura 26.1 lista algunas de las interfaces de las colecciones del .NET Framework. Todas las interfaces en la figura 26.1 se declaran en el espacio de nombres System.Collections y tienen equivalentes genéricas en el espacio de nombres System.Collections.Generic. Las implementaciones de estas interfaces se proporcionan

26.2 Generalidades acerca de las colecciones 1019

Interfaz

Descripción

ICollection

La interfaz raíz en la jerarquía de colecciones, a partir de la cual heredan las interfaces IList e IDictionary. Contiene una propiedad Count para determinar el tamaño de una colección, y un método CopyTo para copiar el contenido de una colección a un arreglo tradicional. Una colección ordenada que puede manipularse como un arreglo. Proporciona un indexador para acceder a los elementos con un índice int. También tiene métodos para buscar en una colección y modificarla, incluyendo Add, Remove, Contains e IndexOf. Una colección de valores, indexados por un objeto “clave” arbitrario. Proporciona un indexador para acceder a los elementos con un índice object y métodos para modificar la colección (por ejemplo, Add, Remove). La propiedad Keys de IDictionary contiene los objetos object que se utilizan como índices, y la propiedad Values contiene todos los objetos object almacenados. Un objeto que puede enumerarse. Esta interfaz contiene exactamente un método llamado GetEnumerator, el cual devuelve un objeto IEnumerator (que veremos en la sección 26.3). ICollection implementa a IEnumerable, por lo que todas las clases de colección implementan a IEnumerable, ya sea en forma directa o indirecta.

IList

IDictionary

IEnumerable

Figura 26.1 | Algunas interfaces de colección comunes. dentro del marco de trabajo. Los programadores también pueden proporcionar implementaciones específicas a sus propios requerimientos. En versiones anteriores de C#, el .NET Framework proporcionaba principalmente las clases de colección en los espacios de nombres System.Collections y System.Collections.Specialized. Estas clases almacenaban y manipulaban referencias object. Podíamos almacenar cualquier object en una colección. Un aspecto inconveniente en cuanto a ordenar referencias object ocurre cuando se obtienen de una colección. Por lo general, una aplicación necesita procesar tipos específicos de objetos. Como resultado, las referencias object que se obtienen de una colección necesitan comúnmente de una conversión descendente a un tipo apropiado, para que la aplicación pueda procesar los objetos en forma correcta. El .NET Framework 2.0 ahora incluye también el espacio de nombres System.Collections.Generic, el cual utiliza las capacidades de los genéricos que presentamos en el capítulo 25. Muchas de estas nuevas clases son simplemente contrapartes genéricos de las clases en el espacio de nombres System.Collections. Esto significa que puede especificar el tipo exacto que se almacenará en una colección. También obtiene los beneficios de la comprobación de tipos en tiempo de compilación; el compilador asegura que se estén utilizando los tipos apropiados con su colección y, en caso contrario, genera mensajes de error en tiempo de compilación. Además, una vez que se especifica el tipo almacenado en una colección, cualquier elemento que se obtenga de ésta tendrá el tipo correcto. Esto elimina la necesidad de conversiones de tipo explícitas, que pueden lanzar excepciones InvalidCastException en tiempo de ejecución si el objeto referenciado no es del tipo apropiado. Esto también elimina la sobrecarga de la conversión explícita, con lo cual se mejora la eficiencia. En este capítulo mostraremos seis clases de colección: Array, ArrayList, Stack, Hashtable, SortedDictionary genérica y LinkedList genérica, más las herramientas de arreglos integradas. El espacio de nombres System.Collections proporciona varias estructuras de datos más, incluyendo BitArray (una colección de valores verdadero/falso), Queue y SortedList (una colección de pares clave/valor que se ordenan por clave y se puede acceder a ellos ya sea por clave, o por índice). La figura 26.2 muestra un resumen de muchas de las clases de colección. También hablaremos sobre la interfaz IEnumerator. Las clases de colección pueden crear enumeradores para que los programadores puedan recorrer las colecciones. Aunque estos enumeradores tienen distintas implementaciones, todos implementan la interfaz IEnumerator, por lo que pueden procesarse mediante el polimorfismo. Como veremos pronto, la instrucción foreach es simplemente una notación conveniente para usar un enumerador. En la siguiente sección, empezaremos nuestra discusión con un análisis de los enumeradores y las herramientas de las colecciones para la manipulación de arreglos.

1020 Capítulo 26 Colecciones

Clase

Implementa a

Descripción

IList

La clase base de todos los arreglos convencionales. Vea la sección 26.3.

Espacio de nombres System : Array

Espacio de nombres System.Collections : ArrayList

IList

Imita los arreglos convencionales, pero crece o se reduce según sea necesario para adaptarse al número de elementos. Vea la sección 26.4.1.

BitArray

ICollection

Un arreglo de elementos bool que hace un uso eficiente de la memoria.

Hashtable

IDictionary

Una colección desordenada de pares clavevalor, a los que se puede acceder por la clave. Vea la sección 26.4.3.

Queue

ICollection

Una colección PEPS (primero en entrar, primero en salir). Vea la sección 24.6, Colas.

SortedList

IDictionary

Una clase Hashtable genérica que ordena los datos por claves, a la cual se puede acceder ya sea por clave o por índice.

Stack

ICollection

Una colección UEPS (último en entrar, primero en salir). Vea la sección 26.4.2.

Espacio de nombres System.Collections.Generic : Dictionary< K, E >

IDictionary< K, E >

Una colección genérica desordenada de pares clave-valor, a la cual se puede acceder por clave.

LinkedList< E >

ICollection< E >

Una lista doblemente enlazada. Vea la sección 26.5.2.

List< E >

IList< E >

Una clase ArrayList genérica.

Queue< E >

ICollection< E >

Una clase Queue genérica.

SortedDictionary< K, E >

IDictionary< K, E >

Una clase Dictionary que ordena los datos por las claves en un árbol binario. Vea la sección 26.5.1.

SortedList< K, E >

IDictionary< K, E >

Una clase SortedList genérica.

Stack< E >

ICollection< E >

Una clase Stack genérica.

[Nota: Todas las clases de colección implementan de forma directa o indirecta a ICollection y a IEnumerable (o a las interfaces genéricas equivalentes ICollection< E > y IEnumerable< E > para las colecciones genéricas).]

Figura 26.2 | Algunas clases de colección del .NET Framework.

26.3 La clase Array y los enumeradores El capítulo 8 presentó las herramientas básicas de procesamiento de arreglos. Todos los arreglos heredan en forma implícita de la clase base abstract Array (espacio de nombres System), el cual define la propiedad Length, que especifica el número de elementos en el arreglo. Además, la clase Array proporciona métodos static, los cuales tienen algoritmos para procesar arreglos. Por lo general, la clase Array sobrecarga estos métodos; por ejemplo, el método Reverse de Array puede invertir el orden de los elementos en todo un arreglo completo, o

26.3 La clase Array y los enumeradores 1021 puede invertir los elementos en un rango especificado de elementos en un arreglo. Para una lista completa de los métodos static de la clase Array, visite la página: msdn2.microsoft.com/es-es/library/system.array(VS.80).aspx

La figura 26.3 demuestra varios métodos static de la clase Array.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

// Fig. 26.3: UsoDeArray.cs // Métodos static de la clase Array para manipulaciones de arreglos comunes. using System; using System.Collections; // demuestra los algoritmos de la clase Array public class UsoDeArray { private static int[] valoresInt = { 1, 2, 3, 4, 5, 6 }; private static double[] valoresDouble = { 8.4, 9.3, 0.2, 7.9, 3.4 }; private static int[] copiaValoresInt; // el método Main demuestra los métodos de la clase Array public static void Main( string[] args ) { copiaValoresInt = new int[ valoresInt.Length ]; // los valores predeterminados son ceros Console.WriteLine( "Valores iniciales del arreglo:\n" ); ImprimirArreglos(); // imprime el contenido inicial del arreglo // ordena valoresDouble Array.Sort( valoresDouble ); // copia valoresInt a copiaValoresInt Array.Copy( valoresInt, copiaValoresInt, valoresInt.Length ); Console.WriteLine( "\nValores del arreglo después de Sort y Copy:\n" ); ImprimirArreglos(); // imprime el contenido del arreglo Console.WriteLine(); // busca el 5 en valoresInt int resultado = Array.BinarySearch( valoresInt, 5 ); if ( resultado >= 0 ) Console.WriteLine( "Se encontró el 5 en el elemento {0} de valoresInt", resultado ); else Console.WriteLine( "No se encontró el 5 en valoresInt" ); // busca el 8763 en valoresInt resultado = Array.BinarySearch( valoresInt, 8763 ); if ( resultado >= 0 ) Console.WriteLine( "Se encontró el 8763 en el elemento {0} de valoresInt", resultado ); else Console.WriteLine( "No se encontró el 8763 en valoresInt" ); } // fin del método Main // imprime el contenido del arreglo con enumeradores private static void ImprimirArreglos()

Figura 26.3 | Clase Array que se utiliza para realizar muchas manipulaciones comunes de arreglos. (Parte 1 de 2).

1022 Capítulo 26 Colecciones

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75

{ Console.Write( "valoresDouble: " ); // itera a través del arreglo double con un enumerador IEnumerator enumerador = valoresDouble.GetEnumerator(); while ( enumerador.MoveNext() ) Console.Write( enumerador.Current + " " ); Console.Write( "\nvaloresInt: " ); // itera a través del arreglo int con un enumerador enumerador = valoresInt.GetEnumerator(); while ( enumerador.MoveNext() ) Console.Write( enumerador.Current + " " ); Console.Write( "\ncopiaValoresInt: " ); // itera a través del segundo arreglo int con una instrucción foreach foreach ( int elemento in copiaValoresInt ) Console.Write( elemento + " " ); Console.WriteLine(); } // fin del método ImprimirArreglos } // fin de la clase UsoDeArray

Valores iniciales del arreglo: valoresDouble: 8.4 9.3 0.2 7.9 3.4 valoresInt: 1 2 3 4 5 6 copiaValoresInt: 0 0 0 0 0 0 Valores del arreglo después de Sort y Copy: valoresDouble: 0.2 3.4 7.9 8.4 9.3 valoresInt: 1 2 3 4 5 6 copiaValoresInt: 1 2 3 4 5 6 Se encontró el 5 en el elemento 4 de valoresInt No se encontró el 8763 en valoresInt

Figura 26.3 | Clase Array que se utiliza para realizar muchas manipulaciones comunes de arreglos. (Parte 2 de 2).

Las directivas using en las líneas 3-4 incluyen los espacios de nombres System (para las clases Array y y System.Collections (para la interfaz IEnumerator, que veremos en breve). Las referencias a los ensamblados para estos espacios de nombres se incluyen de manera implícita en cada aplicación, por lo que no es necesario agregar nuevas referencias al archivo de proyecto. Nuestra clase de prueba declara tres variables tipo arreglo static (líneas 9-11). Las primeras dos líneas inicializan valoresInt y valoresDouble con un arreglo int y un double, respectivamente. El fin de la variable estática copiaValoresInt es demostrar el método Copy de Array, por lo que se deja con el valor predeterminado null; todavía no se refiere a un arreglo. La línea 16 inicializa copiaValoresInt con un arreglo int que tiene la misma longitud que el arreglo valoresInt. La línea 19 llama al método ImprimirArreglos (líneas 49-74) para imprimir en pantalla el contenido inicial de los tres arreglos. En breve hablaremos sobre el método ImprimirArreglos. De los resultados de la figura 26.3 podemos ver que cada elemento del arreglo copiaValoresInt se inicializa con el valor predeterminado 0. Console)

26.3 La clase Array y los enumeradores 1023 La línea 22 utiliza el método static Sort de Array para ordenar el arreglo valoresDouble. Cuando este método regresa, el arreglo contiene sus elementos originales, ordenados en forma ascendente. La línea 25 utiliza el método static Copy de Array para copiar elementos del arreglo valoresInt al arreglo copiaValoresInt. El primer argumento es el arreglo a copiar (valoresInt), el segundo argumento es el arreglo de destino (copiaValoresInt) y el tercer argumento es un int que representa el número de elementos a copiar (en este caso, valoresInt.Length especifica a todos los elementos). Las líneas 32 y 40 invocan al método static BinarySearch de Array para realizar búsquedas binarias en el arreglo valoresInt. El método BinarySearch recibe el arreglo ordenado en el que se realizará la búsqueda, y la clave que debe buscar. El método devuelve el índice en el arreglo en donde encuentra la clave (o un número negativo si no la encontró). Observe que BinarySearch asume que recibe un arreglo ordenado. Su comportamiento con un arreglo desordenado es impredecible.

Error común de programación 26.1 Pasar un arreglo desordenado a BinarySearch es un error lógico; el valor que se devuelve no está definido.

El método ImprimirArreglos (líneas 49-74) utiliza los métodos de la clase Array para iterar a través de cada arreglo. En la línea 54, el método GetEnumerator obtiene un enumerador para el arreglo valoresInt. Recuerde que Array implementa a la interfaz IEnumerable. Todos los arreglos heredan implícitamente de Array, por lo que los tipos de arreglos int[] y double[] implementan el método GetEnumerator de la interfaz IEnumerator, el cual devuelve un enumerador que puede iterar a través de la colección. La interfaz IEnumerator (que todos los enumeradores implementan) define los métodos MoveNext y Reset, junto con la propiedad Current. MoveNext desplaza el enumerador hasta el siguiente elemento en la colección. La primera llamada a MoveNext posiciona el enumerador en el primer elemento de la colección. MoveNext devuelve true si hay por lo menos un elemento más en la colección; en caso contrario, el método devuelve false. El método Reset posiciona el enumerador antes del primer elemento de la colección. Los métodos MoveNext y Reset lanzan una excepción InvalidOperationException si el contenido de la colección se modifica en cualquier forma, antes de crear el enumerador. La propiedad Current devuelve el objeto en la ubicación actual en la colección.

Error común de programación 26.2 Si se modifica una colección después de crear un enumerador para esa colección, el enumerador se vuelve inválido de inmediato; cualquier método que se llame con el enumerador después de este punto lanzará excepciones InvalidOperationException. Por esta razón, se dice que los enumeradores son de “falla rápida”.

Cuando el método GetEnumerator devuelve un enumerador en la línea 54, al principio se posiciona antes del primer elemento en el arreglo valoresDouble. Después, cuando la línea 56 llama a MoveNext en la primera iteración del ciclo while, el enumerador avanza al primer elemento en valoresDouble. La instrucción while en las líneas 56-57 itera a través de cada elemento, hasta que el enumerador pasa el final de valoresDouble y MoveNext devuelve false. En cada iteración, utilizamos la propiedad Current del enumerador para obtener e imprimir en pantalla el elemento actual del arreglo. Las líneas 62-65 iteran a través del arreglo valoresInt. Observe que ImprimirArreglos se llama dos veces (líneas 19 y 28), por lo que GetEnumerator se llama dos veces en valoresDouble. El método GetEnumerator (líneas 54 y 62) siempre devuelve un enumerador que se posiciona antes del primer elemento. Observe además que la propiedad Current de IEnumerator es de sólo lectura. Los enumeradores no pueden utilizarse para modificar el contenido de las colecciones, sólo para obtenerlo. Las líneas 70-71 utilizan una instrucción foreach para iterar a través de los elementos de la colección como un enumerador. De hecho, esta instrucción se comporta de la misma forma que un enumerador. Ambos iteran a través de los elementos de un arreglo, uno por uno, en un orden bien definido. Ninguno le permite modificar los elementos durante la iteración. Ésta no es una coincidencia. La instrucción foreach obtiene de manera implícita un enumerador a través del método GetEnumerator, y utiliza el método MoveNext y la propiedad Current del enumerador para recorrer la colección, de la misma forma que se hizo explícitamente en las líneas 54-57. Por esta razón, podemos utilizar la instrucción foreach para iterar a través de cualquier colección que implemente la interfaz IEnumerable, y no sólo arreglos. En la siguiente sección mostraremos esta funcionalidad, cuando hablemos sobre la clase ArrayList.

1024 Capítulo 26 Colecciones Otros métodos static de Array incluyen a Clear (para establecer un rango de elementos a 0 o null), CreateInstance (para crear un nuevo arreglo de un tipo especificado), IndexOf (para localizar la primera ocurrencia de un objeto en un arreglo, o en una porción del mismo), LastIndexOf (para localizar la última ocurrencia de un objeto en un arreglo, o en una porción del mismo) y Reverse (para invertir el contenido de un arreglo, o de una porción del mismo).

26.4 Colecciones no genéricas El espacio de nombres System.Collections en la Biblioteca de clases del .NET Framework es la fuente principal de las colecciones no genéricas. Estas clases proporcionan implementaciones estándar de muchas de las estructuras de datos que vimos en el capítulo 24, con colecciones que almacenan referencias de tipo object. En esta sección, mostraremos las clases ArrayList, Stack y Hashtable.

26.4.1 La clase ArrayList En la mayoría de los lenguajes de programación, los arreglos convencionales tienen un tamaño fijo; no pueden cambiarse en forma dinámica para conformarse a los requerimientos de memoria en tiempo de ejecución de una aplicación. En algunas aplicaciones, esta limitación del tamaño fijo presenta un problema para los programadores, ya que deben elegir entre usar arreglos de tamaño fijo que sean lo bastante grandes como para almacenar el máximo número de elementos que pueda requerir la aplicación, o utilizar estructuras de datos dinámicas que puedan aumentar y reducir la cantidad de memoria requerida para almacenar datos, en respuesta a los requerimientos cambiantes de una aplicación en tiempo de ejecución. La colección ArrayList del .NET Framework imita la funcionalidad de los arreglos convencionales y proporciona la capacidad de modificar el tamaño de la colección en forma dinámica, a través de sus métodos. En cualquier momento dado, un objeto ArrayList contiene un cierto número de elementos menos que, o iguales a, su capacidad: el número de elementos reservados en un momento dado para el objeto ArrayList. Una aplicación puede manipular la capacidad mediante la propiedad Capacity de ArrayList. Si un objeto ArrayList necesita crecer, duplica su capacidad de manera predeterminada.

Tip de rendimiento 26.1 Al igual que con las listas enlazadas, insertar elementos adicionales en un objeto ArrayList cuyo tamaño actual sea menor que su capacidad es una operación rápida.

Tip de rendimiento 26.2 Insertar un elemento en un objeto ArrayList que necesite crecer más para acomodarlo es una operación lenta. Un objeto ArrayList que se encuentra al límite de su capacidad debe reasignar su memoria y los valores existentes que están copiados en la misma.

Tip de rendimiento 26.3 Si el almacenamiento es de máxima importancia, use el método TrimToSize de la clase ArrayList para recortar un objeto ArrayList a su tamaño exacto. Esto optimizará el uso de la memoria del objeto ArrayList. Pero debe tener cuidado; si la aplicación necesita insertar elementos, el proceso será más lento debido a que el objeto ArrayList debe crecer en forma dinámica (el recorte no deja espacio para que crezca).

Tip de rendimiento 26.4 Tal vez parezca que el incremento predeterminado de la capacidad de un objeto ArrayList (duplicar su tamaño) desperdicia espacio de almacenamiento, pero duplicar el tamaño es una manera eficiente de que un objeto ArrayList crezca con rapidez al tamaño más apropiado. Éste es un uso mucho más eficiente de tiempo que aumentar el tamaño del objeto ArrayList un elemento a la vez, en respuesta a las operaciones de inserción.

Los objetos ArrayList almacenan referencias a objetos de la clase object. Todas las clases se derivan de la clase object, por lo que un objeto ArrayList puede contener objetos de cualquier tipo. La figura 26.4 lista algunos métodos y propiedades útiles de la clase ArrayList.

26.4 Colecciones no genéricas 1025

Método o propiedad Add Capacity Clear Contains Count IndexOf Insert Remove RemoveAt RemoveRange Sort TrimToSize

Descripción Agrega un objeto object al objeto ArrayList y devuelve un int que especifica el índice en el que se agregó el objeto object. Propiedad que obtiene y establece el número de elementos para los que se reserva espacio en un momento dado, dentro del objeto ArrayList. Elimina todos los elementos del objeto ArrayList. Devuelve true si el objeto object especificado está en el objeto ArrayList; en caso contrario, devuelve false. Propiedad de sólo lectura que obtiene el número de elementos almacenados en el objeto ArrayList. Devuelve el índice de la primera ocurrencia del objeto object especificado en el objeto ArrayList. Inserta un objeto object en el índice especificado. Elimina la primera ocurrencia del objeto object especificado. Elimina un objeto en el índice especificado. Elimina un número especificado de elementos, empezando en un índice especificado en el objeto ArrayList. Ordena el objeto ArrayList. Establece la capacidad del objeto ArrayList al número de elementos que contiene el objeto ArrayList en un momento dado (Count).

Figura 26.4 | Algunos métodos y propiedades de la clase ArrayList. La figura 26.5 demuestra el uso de la clase ArrayList y varios de sus métodos. La clase ArrayList pertenece al espacio de nombres System.Collections (línea 4). Las líneas 8-11 declaran dos arreglos de objetos string (colores y eliminarColores), que utilizaremos para llenar dos objetos ArrayList. En la sección 9.11 vimos que las constantes deben inicializarse en tiempo de compilación, pero las variables de sólo lectura (readonly) pueden inicializarse en tiempo de ejecución. Los arreglos son objetos creados en tiempo de ejecución, por lo que declaramos a colores y a eliminarColores con readonly (no const) para que no se puedan modificar. Cuando la aplicación empieza a ejecutarse, creamos un objeto ArrayList con una capacidad inicial de un elemento, y la almacenamos en la variable lista (línea 16). La instrucción foreach en las líneas 20-21 agrega los cinco elementos del arreglo colores a lista, a través del método Add de ArrayList, por lo que lista crece para dar cabida a estos nuevos elementos. La línea 25 utiliza el constructor sobrecargado de ArrayList para crear un nuevo objeto ArrayList, el cual se inicializa con el contenido del arreglo eliminarColores y después lo asigna a la variable eliminarLista. Este constructor puede inicializar el contenido de un objeto ArrayList con los elementos de cualquier objeto ICollection que se le pase como parámetro. Muchas de las clases de colección tienen un constructor similar. Observe que la llamada al constructor en la línea 25 realiza la tarea de las líneas 20-21. La línea 28 llama al método MostrarInformacion (líneas 38-55) para imprimir en pantalla el contenido de la lista. Este método utiliza una instrucción foreach para recorrer los elementos de un objeto ArrayList. Como vimos en la sección 26.3, la instrucción foreach es una abreviación conveniente para llamar al método GetEnumerator de ArrayList y utilizar un enumerador para recorrer los elementos de la colección. Además, debemos usar una variable de iteración de tipo object, ya que la clase ArrayList no es genérica y almacena referencias a objetos object. Utilizamos las propiedades Count y Capacity en la línea 46 para mostrar el número actual de elementos y el máximo número de elementos que pueden almacenarse sin asignar más memoria al objeto ArrayList. Los resultados de la figura 26.5 indican que el objeto ArrayList tiene capacidad de 8; recuerde que un objeto ArrayList duplica su capacidad cuando necesita más espacio.

1026 Capítulo 26 Colecciones

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

// Fig. 26.5: PruebaArrayList.cs // Uso de la clase ArrayList. using System; using System.Collections; public class PruebaArrayList { private static readonly string[] colores = { "MAGENTA", "ROJO", "BLANCO", "AZUL", "CYAN" }; private static readonly string[] eliminarColores = { "ROJO", "BLANCO", "AZUL" }; // crea un objeto ArrayList, le agrega los colores y lo manipula public static void Main( string[] args ) { ArrayList lista = new ArrayList( 1 ); // capacidad inicial de 1 // agrega los elementos del arreglo colores // al objeto ArrayList lista foreach ( string color in colores ) lista.Add( color ); // agrega el color al objeto ArrayList lista // agrega los elementos en el arreglo eliminarColores al // objeto ArrayList eliminarLista con el constructor de ArrayList ArrayList eliminarLista = new ArrayList( eliminarColores ); Console.WriteLine( "ArrayList: " ); MostrarInformacion( lista ); // imprime la lista en pantalla // elimina del objeto ArrayList lista los colores en eliminarLista EliminarColores( lista, eliminarLista ); Console.WriteLine( "\nArrayList después de llamar a EliminarColores: " ); MostrarInformacion( lista ); // imprime en pantalla el contenido de lista } // fin del método Main // muestra información sobre el contenido de un arreglo llamado lista private static void MostrarInformacion( ArrayList arregloLista ) { // itera a través del arreglo lista con una instrucción foreach foreach ( object elemento in arregloLista ) Console.Write( "{0} ", elemento ); // invoca a ToString // muestra el tamaño y la capacidad Console.WriteLine( "\nTamaño = {0}; Capacidad = {1}", arregloLista.Count, arregloLista.Capacity ); int index = arregloLista.IndexOf( "AZUL" ); if ( index != -1 ) Console.WriteLine( "El arreglo lista contiene AZUL en el índice {0}.", index ); else Console.WriteLine( "El arreglo lista no contiene AZUL." ); } // fin del método MostrarInformacion // elimina de primeraLista los colores especificados en segundaLista private static void EliminarColores( ArrayList primeraLista, ArrayList segundaLista )

Figura 26.5 | Uso de la clase ArrayList. (Parte 1 de 2).

26.4 Colecciones no genéricas 1027

60 61 62 63 64 65

{ // itera a través del segundo objeto ArrayList como si fuera un arreglo for ( int cuenta = 0; cuenta < segundaLista.Count; cuenta++ ) primeraLista.Remove( segundaLista[ cuenta ] ); } // fin del método EliminarColores } // fin de la clase PruebaArrayList

ArrayList: MAGENTA ROJO BLANCO AZUL CYAN Tamaño = 5; Capacidad = 8 El arreglo lista contiene AZUL en el índice 3. ArrayList después de llamar a EliminarColores: MAGENTA CYAN Tamaño = 2; Capacidad = 8 El arreglo lista no contiene AZUL.

Figura 26.5 | Uso de la clase ArrayList. (Parte 2 de 2).

En la línea 48, invocamos al método IndexOf para determinar la posición del objeto string "AZUL" en y almacenar el resultado en la variable local index. El método IndexOf devuelve -1 si no se encuentra el elemento. La instrucción if en las líneas 50-54 comprueba si indice es -1 para determinar si arregloLista contiene "AZUL". Si es así, se imprime su índice en pantalla. La clase ArrayList también cuenta con el método Contains, el cual simplemente devuelve true si un objeto se encuentra en el objeto ArrayList, y false en caso contrario. El método Contains es más conveniente si no necesitamos el índice del elemento. arregloLista

Tip de rendimiento 26.5 Los métodos IndexOf y Contains de ArrayList realizan una búsqueda lineal, la cual es una operación costosa para objetos ArrayList extensos. Si el objeto ArrayList está ordenado, use el método BinarySearch para realizar una búsqueda más eficiente. Este método devuelve el índice del elemento, o un número negativo si no se encontró ese elemento.

Una vez que el método MostrarInformacion regresa, llamamos al método EliminarColores (líneas 58-64) con los dos objetos ArrayList. La instrucción for en las líneas 62-63 itera a través del arreglo ArrayList segundaLista. La línea 63 utiliza un indexador para acceder a un elemento del objeto ArrayList; para ello, coloca después del nombre de la referencia ArrayList dos corchetes ([]) que contienen el índice deseado del elemento. Si el índice especificado no es mayor que 0 y menor que el número de elementos almacenados actualmente en el objeto ArrayList (que se especifican mediante la propiedad Count de ArrayList), se produce una excepción ArgumentOutOfRangeException. Utilizamos el indexador para obtener cada uno de los elementos de segundaLista y después eliminamos cada uno de primeraLista mediante el método Remove. Este método elimina un elemento especificado de un objeto ArrayList, para lo cual realiza una búsqueda lineal y elimina (sólo) la primera ocurrencia del objeto especificado. Todos los elementos subsiguientes se desplazan hacia el inicio del objeto ArrayList, para llenar la posición vacía. Después de la llamada a EliminarColores, la línea 34 vuelve a imprimir en pantalla el contenido de lista, confirmando que se eliminaron los elementos de eliminarLista.

26.4.2 La clase Stack La clase Stack implementa a una estructura de datos tipo pila, y proporciona gran parte de la funcionalidad que definimos en nuestra propia implementación en la sección 24.3. Puede regresar a esa sección para recordar los conceptos de la estructura de datos tipo pila. En la figura 24.11 creamos una aplicación de prueba para demostrar la estructura de datos HerenciaPila que desarrollamos. En la figura 26.6 adaptamos la figura 24.14 para demostrar la clase de colección Stack del .NET Framework.

1028 Capítulo 26 Colecciones La directiva using en la línea 4 nos permite utilizar la clase Stack con su nombre no calificado del espacio de nombres System.Collections. La línea 10 crea un objeto Stack con la capacidad inicial predeterminada (10 elementos). Como es de esperarse, la clase Stack tiene los métodos Push y Pop para realizar las operaciones básicas de una pila. El método Push recibe un objeto object como argumento y lo inserta en la parte superior del objeto Stack. Si el número de elementos en el objeto Stack (la propiedad Count) es igual a la capacidad al momento de la operación Push, el objeto Stack crece para dar cabida a más objetos object. Las líneas 19-26 usan el método Push para agregar cuatro elementos (bool, char, int y string) a la pila e invocar al método ImprimirPila (líneas 50-64) después de cada operación Push, para imprimir en pantalla el contenido de la pila. Observe que esta clase Stack no genérica sólo puede almacenar referencias a objetos object, por lo que aplica una conversión boxing a cada uno de los elementos de tipo de valor (bool, char e int) de manera implícita, antes de agregarlos al objeto Stack. (El espacio de nombres System.Collections.Generic proporciona una clase Stack genérica, que tiene muchos de los mismos métodos y propiedades que se utilizan en la figura 26.6.) El método ImprimirPila (líneas 50-64) usa la propiedad Count de Stack (que se implementó para cumplir con los requisitos de la interfaz ICollection) para obtener el número de elementos en pila. Si la pila no está vacía (es decir, si Count no es igual a 0), utilizamos una instrucción foreach para iterar a través de la pila e imprimir su contenido, invocando en forma implícita al método ToString de cada elemento. La instrucción foreach invoca en forma implícita al método GetEnumerator de Pila, que podríamos haber llamado explícitamente para recorrer la pila a través de un enumerador. El método Peek devuelve el valor del elemento superior de la pila, pero no elimina ese elemento de la misma. Utilizamos Peek en la línea 30 para obtener el objeto superior del objeto Stack, y después imprimimos en pantalla ese objeto, invocando en forma implícita al método ToString del mismo. Si el objeto Stack está vacío cuando la aplicación llama a Peek, se produce una excepción InvalidOperation. (No necesitamos un bloque de manejo de excepciones, ya que sabemos que aquí la pila no está vacía.)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

// Fig. 26.6: PruebaStack.cs // Demostración de la clase Stack. using System; using System.Collections; public class PruebaStack { public static void Main( string[] args ) { Stack pila = new Stack(); // capacidad predeterminada de 10 // crea objetos para almacenarlos en la pila bool unBooleano = true; char unCaracter = '$'; int unEntero = 34567; string unaCadena = "hola"; // usa el método Push para agregar elementos a (la parte superior de) la pila pila.Push( unBooleano ); ImprimirStack( pila ); pila.Push( unCaracter ); ImprimirStack( pila ); pila.Push( unEntero ); ImprimirStack( pila ); pila.Push( unaCadena ); ImprimirStack( pila ); // comprueba el elemento superior de la pila Console.WriteLine( "El elemento superior de la pila es {0}\n",

Figura 26.6 | Demostración de la clase Stack. (Parte 1 de 2).

26.4 Colecciones no genéricas 1029

30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65

pila.Peek() ); // elimina elementos de la pila try { while ( true ) { object objetoEliminado = pila.Pop(); Console.WriteLine( "Se sacó " + objetoEliminado ); ImprimirStack( pila ); } // fin de while } // fin de try catch ( InvalidOperationException excepcion ) { // si ocurre una excepción, imprime el rastreo de pila Console.Error.WriteLine( excepcion ); } // fin de catch } // fin de Main // imprime el contenido de una pila private static void ImprimirStack( Stack pila ) { if ( pila.Count == 0 ) Console.WriteLine( "La pila está vacía\n" ); // la pila está vacía else { Console.Write( "La pila es: " ); // itera a través de la pila mediante una instrucción foreach foreach ( object elemento in pila ) Console.Write( "{0} ", elemento ); // invoca a ToString Console.WriteLine( "\n" ); } // fin de else } // fin del método ImprimirStack } // fin de la clase PruebaStack

La pila es: True La pila es: $ True La pila es: 34567 $ True La pila es: hola 34567 $ True El elemento superior de la pila es hola Se sacó hola La pila es: 34567 $ True Se sacó 34567 La pila es: $ True Se sacó $ La pila es: True Se sacó True La pila está vacía System.InvalidOperationException: Pila vacía. en System.Collections.Stack.Pop() en PruebaStack.Main(String[] args) en C:\MisProyectos\ fig26_06\PruebaStack\PruebaStack.cs:línea 37

Figura 26.6 | Demostración de la clase Stack. (Parte 2 de 2).

1030 Capítulo 26 Colecciones El método Pop no recibe argumentos; elimina y devuelve el objeto que se encuentra en ese momento en la parte superior del objeto Stack. Un ciclo infinito (líneas 35-40) saca objetos de la pila y los imprime en pantalla, hasta que la pila se vacía. Cuando la aplicación llama a Pop con la pila vacía, se lanza una excepción InvalidOperationException. El bloque catch (líneas 42-46) imprime la excepción en forma implícita, invocando al método ToString de InvalidOperationException para obtener un mensaje de error y el rastreo de pila.

Error común de programación 26.3 Al tratar de utilizar los métodos Peek o Pop con un objeto Stack vacío (una pila cuya propiedad Count es 0) se produce una excepción InvalidOperationException.

Aunque la figura 26.6 no lo demuestra, la clase Stack también tiene el método Contains, que devuelve true si el objeto Stack contiene el objeto especificado, y false en caso contrario.

26.4.3 La clase Hashtable Cuando una aplicación crea objetos de tipos nuevos o ya existentes, necesita administrarlos en forma eficiente. Esto incluye los procesos de ordenar y obtener los objetos. Las acciones de ordenar y obtener información con los arreglos son eficientes si algún aspecto de nuestros datos coincide directamente con el valor clave, y si esas claves son únicas y están estrechamente empaquetadas. Si tenemos 100 empleados con números de Seguro Social de nueve dígitos, y queremos almacenar y recuperar los datos de los empleados mediante el uso del número de Seguro Social como clave, por lo general se requeriría un arreglo con 999,999,999, ya que hay 999,999,999 números únicos de nueve dígitos. Si tenemos un arreglo de ese tamaño, podríamos tener un rendimiento muy alto al ordenar y obtener los registros de los empleados con sólo utilizar el número de Seguro Social como índice para el arreglo, pero sería un gran desperdicio de memoria. Muchas aplicaciones tienen este problema; o las claves son del tipo incorrecto (es decir, no son números no negativos) o son del tipo correcto, pero están muy esparcidas a lo largo de un extenso rango. Lo que se necesita es un esquema de alta velocidad para convertir claves como los números de Seguro Social y los números de pieza en los inventarios en índices únicos para el arreglo. Así, cuando una aplicación necesite almacenar algo, el esquema podría convertir la clave de la aplicación rápidamente en un índice, y el registro de información podría almacenarse en esa ubicación del arreglo. Para obtener datos se hace lo mismo: una vez que la aplicación tiene una clave para la que desea obtener el registro de datos, simplemente aplica la conversión a la clave, lo cual produce el subíndice del arreglo en donde residen los datos, y recupera esos datos. El esquema que describimos aquí es la base de una técnica conocida como hashing, en la que almacenamos datos en una estructura de datos conocida como tabla de hash. ¿Por qué el nombre? Porque cuando convertimos una clave en un subíndice de un arreglo, literalmente revolvemos los bits, haciendo un “picadillo (hash)” del número. En realidad, este número no tiene un significado real más allá de su utilidad para almacenar y obtener este registro de datos específico. Hay un ligero defecto en este esquema cuando ocurren conflictos (es decir, dos claves distintas se asignan a la misma celda, o elemento, en el arreglo). Como no podemos ordenar dos registros de datos distintos en el mismo espacio, necesitamos encontrar un lugar alternativo para todos los registros más allá del primero que se asignen a un subíndice particular en el arreglo. Un esquema para hacer esto es reasignar la transformación de hashing a la clave, para proporcionar una siguiente celda candidata en el arreglo. El proceso de hashing está diseñado para ser aleatorio, por lo que se supone que con sólo unos cuantos valores de hash se encontrará una celda disponible. Otro de los esquemas utiliza un valor de hash para localizar la primera celda candidata. Si la celda está ocupada, se realiza una búsqueda lineal en las celdas subsiguientes hasta encontrar una celda disponible. Para obtener datos se realiza el mismo procedimiento: se obtiene un valor de hash para la clave, y la celda resultante se comprueba para determinar si contiene los datos deseados. Si es así, la búsqueda termina. Si no, se realiza una búsqueda lineal en las celdas subsiguientes hasta que se encuentran los datos deseados. La solución más popular para los conflictos de tablas hash es hacer que cada celda de la tabla sea un “recipiente” hash; por lo general, una lista enlazada de todos los pares clave-valor que se asocian a esa celda. Ésta es la solución que implementa la clase Hashtable del .NET Framework. El factor de carga afecta al rendimiento de los esquemas de hash. El factor de carga es la proporción del número de objetos almacenados en la tabla hash en relación con el número total de celdas de la tabla hash. A medida que esta proporción aumenta, la probabilidad de conflictos tiende a incrementarse.

26.4 Colecciones no genéricas 1031

Tip de rendimiento 26.6 El factor de carga en una tabla hash es un ejemplo clásico de una concesión entre espacio/tiempo: al incrementar el factor de carga, obtenemos un mejor uso de la memoria, pero la aplicación se ejecuta con más lentitud debido al aumento en los conflictos de hash. Al reducir el factor de carga obtenemos una mayor velocidad en la aplicación, debido a que se reducen los conflictos de hash, pero obtenemos un uso más deficiente de la memoria, ya que una gran parte de la tabla hash permanece vacía.

Los estudiantes de ciencias computacionales estudian los esquemas de hash en cursos llamados “Estructuras de datos” y “Algoritmos”. Como reconocimiento al valor del uso de hash, el .NET Framework proporciona la clase Hashtable para que los programadores puedan emplear con facilidad los valores de hash en sus aplicaciones. Este concepto es en extremo importante, en relación con nuestro estudio de la programación orientada a objetos. Las clases encapsulan y ocultan la complejidad (es decir, los detalles de implementación) y ofrecen interfaces amigables para los usuarios. El proceso de diseñar clases para que realicen lo anterior con eficiencia es una de las habilidades más preciadas en el campo de la programación orientada a objetos. Una función hash realiza un cálculo que determina en qué parte de la tabla hash se van a colocar datos. La función hash se aplica a la clave en un par clave-valor de objetos. La clase Hashtable puede aceptar cualquier objeto como clave. Por esta razón, la clase object define el método GetHashCode, el cual heredan todos los objetos. La mayoría de las clases que son candidatas a utilizarse como claves en una tabla hash, redefinen este método para proporcionar uno que realice cálculos eficientes de código hash para un tipo específico. Por ejemplo, un objeto string tiene un cálculo de código hash basado en el contenido del string. La figura 26.7 utiliza un objeto Hashtable para contar el número de ocurrencias de cada palabra en un objeto string. Las líneas 4-6 contienen directivas using para los espacios de nombres System (para la clase Console), System.Text.RegularExpressions (para la clase Regex, que vimos en el capítulo 16, Cadenas, caracteres y

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

// Fig. 26.7: PruebaHashtable.cs // Aplicación que cuenta el número de ocurrencias de cada palabra // en una cadena y las almacena en una tabla hash. using System; using System.Text.RegularExpressions; using System.Collections; public class PruebaHashtable { public static void Main( string[] args ) { // crea una tabla hash con base en la entrada del usuario Hashtable tabla = RecolectarPalabras(); // muestra el contenido de la tabla hash MostrarHashtable( tabla ); } // fin del método Main // crea una tabla hash a partir de la entrada del usuario private static Hashtable RecolectarPalabras() { Hashtable tabla = new Hashtable(); // crea una nueva tabla hash Console.WriteLine( "Escriba una cadena: " ); // pide la entrada al usuario string entrada = Console.ReadLine(); // obtiene la entrada // divide el texto de entrada en símbolos (tokens) string[] palabras = Regex.Split( entrada, @"\s+" );

Figura 26.7 | Aplicación que cuenta el número de ocurrencias de cada palabra en un string y los almacena en una tabla hash. (Parte 1 de 2).

1032 Capítulo 26 Colecciones

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

// procesamiento de las palabras de entrada foreach ( string palabra in palabras ) { string clavePalabra = palabra.ToLower(); // obtiene palabra en minúsculas // si la tabla hash contiene la palabra if ( tabla.ContainsKey( clavePalabra ) ) { tabla[ clavePalabra ] = ( ( int ) tabla[ clavePalabra ] ) + 1; } // fin de if else // agrega nueva palabra con una cuenta de 1 a la tabla hash tabla.Add( clavePalabra, 1 ); } // fin de foreach return tabla; } // fin del método RecolectarPalabras // muestra el contenido de la tabla hash private static void MostrarHashtable( Hashtable tabla ) { Console.WriteLine( "\nEl objeto Hashtable contiene:\n{0,-12}{1,-12}", "Clave:", "Valor:" ); // genera la salida para cada clave en la tabla hash // al iterar a través de la propeidad Keys con una instrucción foreach foreach ( object clave in tabla.Keys ) Console.WriteLine( "{0,-12}{1,-12}", clave, tabla[ clave ] ); Console.WriteLine( "\ntamaño: {0}", tabla.Count ); } // fin del método MostrarHashtable } // fin de la clase PruebaHashtable

Escriba una cadena: Tan quieto como un barco pintado en un oceano pintado El objeto Hashtable contiene: Clave: Valor: barco 1 un 2 en 1 como 1 oceano 1 quieto 1 pintado 2 tan 1 tamaño: 8

Figura 26.7 | Aplicación que cuenta el número de ocurrencias de cada palabra en un string y los almacena en una tabla hash. (Parte 2 de 2). expresiones regulares) y System.Collections (para la clase Hashtable). La clase PruebaHashtable declara tres métodos static. El método RecolectarPalabras (líneas 20-46) recibe como entrada un objeto string y devuelve un objeto Hashtable en el que cada valor almacena el número de veces que aparece esa palabra en el objeto string, y la palabra se utiliza para la clave. El método MostrarHashtable (líneas 49-60) muestra el objeto Hashtable que recibe en formato de columnas. El método Main (líneas 10-17) simplemente invoca a Recolectar-

26.4 Colecciones no genéricas 1033 Palabras (línea 13) y después pasa el objeto Hashtable devuelto por RecolectarPalabras a MostrarHashtable en la línea 16. El método RecolectarPalabras (líneas 20-46) empieza inicializando la variable local tabla con un nuevo objeto Hashtable (línea 22), el cual tiene una capacidad inicial predeterminada de 0 elementos y un factor de carga máximo predeterminado de 1.0. Cuando el número de elementos en el objeto Hashtable se vuelve mayor que el número de celdas multiplicado por el factor de carga, la capacidad se incrementa en forma automática. (Este detalle de implementación es invisible para los clientes de la clase.) Las líneas 24-25 piden al usuario que introduzca una cadena de caracteres. En la línea 28 utilizamos el método static Split de la clase Regex para dividir el objeto string, con base en sus caracteres de espacio en blanco. Esto crea un arreglo de “palabras”, que después almacenamos en la variable local palabras. La instrucción foreach en las líneas 31-43 itera a través de cada elemento del arreglo palabras. Cada palabra se convierte en minúsculas con el método string ToLower y después se almacena en la variable clavePalabra (línea 33). Después, la línea 36 llama al método ContainsKey de Hashtable para determinar si la palabra está en la tabla hash (y, por ende, tuvo una ocurrencia anterior en el objeto string). Si el objeto Hashtable no contiene una entrada para la palabra, la línea 42 utiliza el método Add de Hashtable para crear una nueva entrada en la tabla hash, con la palabra en minúsculas como la clave y un objeto que contiene 1 como el valor. Observe que la conversión boxing automática se produce cuando la aplicación pasa el entero 1 al método Add, ya que la tabla hash almacena tanto la clave como el valor en referencias al tipo object.

Error común de programación 26.4 Al utilizar el método Add para agregar una clave que ya exista en la tabla hash, se produce una excepción ArgumentException.

Si la palabra ya es una clave en la tabla hash, la línea 38 usa el indexador de Hashtable para obtener y establecer el valor asociado de la clave (la cuenta de palabras) en la tabla hash. Primero realizamos una conversión descendente sobre el valor obtenido por el descriptor de acceso get, de un object a un int. Esto provoca una conversión unboxing del valor, para poder incrementarlo en 1. Después, cuando utilizamos el descriptor de acceso set del indexador para asignar el valor asociado de la clave, se vuelve a aplicar una conversión boxing al valor incrementado, para que pueda almacenarse en la tabla hash. Observe que al invocar el descriptor de acceso get de un indexador Hashtable con una clave que no existe en la tabla hash, se obtiene una referencia null. Al utilizar el descriptor de acceso set con una clave que no existe en la tabla hash se crea una nueva entrada, como si se hubiera utilizado el método Add. La línea 45 devuelve la tabla hash al método Main, que a su vez la pasa al método MostrarTablahash (líneas 49-60) para mostrar todas las entradas. Este método utiliza la propiedad de sólo lectura llamada Keys (línea 56) para obtener un objeto ICollection que contenga todas las claves. Como ICollection extiende a IEnumerable, podemos usar esta colección en la instrucción foreach de las líneas 56-57 para iterar a través de las claves de la tabla hash. Este ciclo accede a cada clave con su valor, y los imprime en pantalla mediante el uso de la variable de iteración y el descriptor de acceso get de tabla. Cada clave y su valor se muestran en una anchura de campo de –12. La anchura del campo negativo indica que la salida se justifica a la izquierda. Como una tabla hash no está ordenada, los pares clave-valor no se muestran en ningún orden específico. La línea 59 utiliza la propiedad Count de Hashtable para obtener el número de pares clave-valor en el objeto Hashtable. Las líneas 56-57 podrían haber utilizado también la instrucción foreach con el propio objeto Hashtable, en vez de utilizar la propiedad Keys. Si utiliza una instrucción foreach con un objeto Hashtable, la variable de iteración será de tipo DictionaryEntry. El enumerador de Hashtable (o de cualquier otra clase que implemente a IDictionary) utiliza la estructura DictionaryEntry para almacenar pares clave-valor. Esta estructura proporciona las propiedades Key y Value para obtener la clave y el valor del elemento actual. Si no necesita la clave, la clase Hashtable también proporciona una propiedad Values de sólo lectura, la cual obtiene un objeto ICollection de todos los valores almacenados en el objeto Hashtable. Podemos usar esta propiedad para iterar a través de los valores almacenados en el objeto Hashtable, sin necesidad de preocuparnos por ver en dónde se almacenan.

Problemas con las colecciones no genéricas En la aplicación de conteo de palabras de la figura 26.7, nuestro objeto Hashtable almacena sus claves y datos como referencias object, aun cuando sólo almacenamos claves string y valores int por convención. Esto

1034 Capítulo 26 Colecciones produce como resultado un código extraño. Por ejemplo, en la línea 38 nos vemos forzados a realizar conversiones unboxing y boxing en los datos int almacenados en el objeto Hashtable, cada vez que se incrementa la cuenta para una clave específica. Esto es ineficiente. En la línea 56 ocurre un problema similar: la variable de iteración de la instrucción foreach es una referencia object. Si necesitamos usar cualquiera de sus métodos específicos para string, necesitamos una conversión descendente explícita. Esto puede provocar errores sutiles. Suponga que decidimos mejorar la legibilidad de la figura 26.7, usando el descriptor de acceso set del indexador en vez del método Add para agregar un par clave/valor en la línea 42, pero accidentalmente escribimos: tabla[ clavePalabra ] = clavePalabra; // inicializa a 1

Esta instrucción crea una nueva entrada con una clave string y un valor string, en vez de un valor int de 1. Aunque la aplicación se compilará en forma correcta, es evidente que esto es incorrecto. Si una palabra aparece dos veces, la línea 38 tratará de realizar una conversión descendente sobre este valor string, para convertirlo en un int, con lo cual se producirá una excepción InvalidCastException en tiempo de ejecución. El error que aparece en tiempo de ejecución indicará que el problema está en la línea 38, en donde ocurrió la excepción, no en la línea 42. Esto hace que el error sea más difícil de encontrar y depurar, especialmente en aplicaciones de software extensas, en donde la excepción puede ocurrir en un archivo distinto; e incluso hasta en un ensamblado distinto. En el capítulo 25 presentamos los genéricos. En las siguientes dos secciones, veremos cómo utilizar colecciones genéricas.

26.5 Colecciones genéricas El espacio de nombres System.Collections.Generic en la FCL es una nueva adición para C# 2.0. Este espacio de nombres contiene clases genéricas que nos permiten crear colecciones de tipos específicos. Como vimos en la figura 26.2, muchas de las clases son simplemente versiones genéricas de colecciones no genéricas. Hay un par de clases que implementan nuevas estructuras de datos. En esta sección mostraremos las colecciones genéricas SortedDictionary y LinkedList.

26.5.1 La clase genérica SortedDictionary

Un diccionario es el término general para una colección de pares clave-valor. Una tabla hash es una manera de implementar un diccionario. El .NET Framework proporciona varias implementaciones de diccionarios, tanto genéricos como no genéricos (todos los cuales implementan a la interfaz IDictionary en la figura 26.1). La aplicación de la figura 26.8 es una modificación de la figura 26.7, que utiliza la clase genérica SortedDictionary. La clase genérica SortedDictionary no utiliza una tabla hash, sino que almacena sus pares clave-valor en un árbol binario de búsqueda. (En la sección 24.5 hablamos con detalle sobre los árboles binarios.) Como el nombre de la clase lo sugiere, las entradas en SortedDictionary se ordenan en el árbol por clave. Cuando la clave implementa a la interfaz genérica IComparable, SortedDictionary utiliza los resultados del método CompareTo de IComparable para ordenar las claves. Observe que, a pesar de estos detalles de implementación, usamos los mismos métodos public, propiedades e indexadores con las clases Hashtable y SortedDictionary, de la misma forma. De hecho, con la excepción de la sintaxis genérica específica, la figura 26.8 tiene una apariencia asombrosamente similar a la figura 26.7. Esto es lo bello de la programación orientada a objetos. La línea 6 contiene una directiva using parea el espacio de nombres System.Collections.Generic, el cual contiene la clase SortedDictionary. La clase genérica SortedDictionary recibe dos argumentos de tipo; el primero especifica el tipo de clave (por ejemplo, string) y el segundo especifica el tipo de valor (por decir, int). Lo único que hicimos fue sustituir la palabra Hashtable en la línea 13 y en las líneas 23-24 con SortedDictionary< string, int > para crear un diccionario de valores int con claves string. Ahora, el compilador puede comprobar y notificarnos si tratamos de almacenar un objeto del tipo incorrecto en el diccionario. Además, como el compilador ahora sabe que la estructura de datos contiene valores int, ya no hay necesidad de la conversión descendente en la línea 40. Esto permite que la línea 40 utilice la notación de incremento prefijo (++), mucho más precisa. Éstas son las únicas modificaciones que se realizaron en los métodos Main y RecolectarPalabras. El método estático MostrarDiccionario (líneas 51-63) se modificó para ser completamente genérico. Recibe los parámetros de tipo K y V. Estos parámetros se utilizan en la línea 52 para indicar que MostrarDiccionario recibe un objeto SortedDictionary con claves de tipo K y valores de tipo V. Utilizamos el parámetro de tipo K

26.5 Colecciones genéricas 1035

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57

// Fig. 26.8: PruebaSortedDictionary.cs // Aplicación que cuenta el número de ocurrencias de cada palabra en una cadena // y las almacena en un diccionario ordenado genérico. using System; using System.Text.RegularExpressions; using System.Collections.Generic; public class PruebaSortedDictionary { public static void Main( string[] args ) { // crea el diccionario ordenado, con base en la entrada del usuario SortedDictionary< string, int > diccionario = RecolectarPalabras(); // muestra el contenido del diccionario ordenado MostrarDiccionario( diccionario ); } // fin del método Main // crea un diccionario ordenado a partir de la entrada del usuario private static SortedDictionary< string, int > RecolectarPalabras() { // crea un nuevo diccionario ordenado SortedDictionary< string, int > diccionario = new SortedDictionary< string, int >(); Console.WriteLine( "Escriba una cadena: " ); // pide la entrada al usuario string entrada = Console.ReadLine(); // obtiene la entrada // divide el texto de entrada en símbolos (tokens) string[] palabras = Regex.Split( entrada, @"\s+" ); // procesamiento de las palabras de entrada foreach ( string palabra in palabras ) { string clavePalabra = palabra.ToLower(); // obtiene palabra en minúsculas // si el diccionario contiene la palabra if ( diccionario.ContainsKey( clavePalabra ) ) { diccionario[ clavePalabra ]++; } // fin de if else // agrega una nueva palabra con una cuenta de 1 al diccionario diccionario.Add( clavePalabra, 1 ); } // fin de foreach return diccionario; } // fin del método RecolectarPalabras // muestra el contenido del diccionario private static void MostrarDiccionario< K, V >( SortedDictionary< K, V > diccionario ) { Console.WriteLine( "\nEl diccionario ordenado contiene:\n{0,-12}{1,-12}", "Clave:", "Valor:" ); // genera resultados para cada clave en el diccionario ordenado,

Figura 26.8 | Aplicación que cuenta el número de ocurrencias de cada palabra en un objeto string, y las almacena en un diccionario ordenado genérico. (Parte 1 de 2).

1036 Capítulo 26 Colecciones

58 59 60 61 62 63 64

// mediante una iteración a través de la propiedad Keys con una instrucción foreach foreach ( K clave in diccionario.Keys ) Console.WriteLine( "{0,-12}{1,-12}", clave, diccionario[ clave ] ); Console.WriteLine( "\ntamaño: {0}", diccionario.Count ); } // fin del método MostrarDiccionario } // fin de la clase PruebaSortedDictionary

Escriba una cadena: Somos pocos, somos felices y pocos, somos banda de hermanos El diccionario Clave: banda de felices hermanos pocos, somos y

ordenado contiene: Valor: 1 1 1 1 2 3 1

tamaño: 7

Figura 26.8 | Aplicación que cuenta el número de ocurrencias de cada palabra en un objeto string, y las almacena en un diccionario ordenado genérico. (Parte 2 de 2). otra vez en la línea 59 como el tipo de la clave de iteración. Este uso de los genéricos es un maravilloso ejemplo de reutilización de código. Si decidimos modificar la aplicación para que cuente el número de veces que aparece cada carácter en la cadena, el método MostrarDictionary podría recibir un argumento de tipo SortedDictionary < char, int > sin necesidad de modificación.

Tip de rendimiento 26.7 Como la clase SortedDictionary mantiene sus elementos ordenados en un árbol binario, para obtener o insertar un par clave-valor se requiere un tiempo definido por O( log n), que es rápido en comparación con el proceso de realizar una búsqueda lineal y después la inserción.

Error común de programación 26.5 Al invocar el descriptor de acceso get de un indexador SortedDictionary con una clave que no exista en la colección, se produce una excepción KeyNotFoundException. Este comportamiento es distinto al del descriptor de acceso get del indexador de Hashtable, que devolvería null.

26.5.2 La clase genérica LinkedList En el capítulo 24, empezamos nuestra discusión sobre las estructuras de datos con el concepto de una lista enlazada, y terminamos nuestra discusión con la clase genérica LinkedList del .NET Framework. La clase LinkedList es una lista doblemente enlazada; podemos navegar por la lista hacia delante y hacia atrás, con nodos de la clase genérica LinkedListNode. Cada nodo contiene la propiedad Value, y las propiedades de sólo lectura Previous y Next. El tipo de la propiedad Value coincide con el parámetro de tipo de LinkedList, ya que contiene los datos almacenados en el nodo. La propiedad Previous obtiene una referencia al nodo anterior en la lista enlazada (o null si el nodo es el primero de la lista). De manera similar, la propiedad Next obtiene una referencia a la subsiguiente referencia en la lista enlazada (o null si el nodo es el último de la lista). En la figura 26.9 mostramos unas cuantas manipulaciones de las listas enlazadas. La directiva using en la línea 4 nos permite utilizar la clase LinkedList por su nombre no calificado. Las líneas 16-23 crean los objetos LinkedList lista1 y lista2 de objetos string, y las llenan con el contenido

26.5 Colecciones genéricas 1037

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

// Fig. 26.9: PruebaLinkedList.cs // Uso de objetos LinkedList. using System; using System.Collections.Generic; public class PruebaLinkedList { private static readonly string[] colores = { "negro", "amarillo", "verde", "azul", "violeta", "plata" }; private static readonly string[] colores2 = { "oro", "blanco", "café", "azul", "gris" }; // establece y manipula objetos LinkedList public static void Main( string[] args ) { LinkedList< string > lista1 = new LinkedList< string >(); // agrega elementos a la primera lista enlazada foreach ( string color in colores ) lista1.AddLast( color ); // agrega elementos a la segunda lista enlazada a través del constructor LinkedList< string > lista2 = new LinkedList< string >( colores2 ); Concatenar( lista1, lista2 ); // concatena lista2 en lista1 ImprimirLista( lista1 ); // imprime los elementos de lista1 Console.WriteLine( "\nConvirtiendo las cadenas en lista1 a mayúsculas\n" ); CadenasAMayusculas( lista1 ); // convierte a cadena en mayúsculas ImprimirLista( lista1 ); // imprime los elementos de lista1 Console.WriteLine( "\nEliminando las cadenas entre NEGRO y CAFÉ\n" ); EliminarElementosEntre( lista1, "NEGRO", "CAFÉ" ); ImprimirLista( lista1 ); // imprime los elementos de lista1 ImprimirListaInversa( lista1 ); // imprime la lista en orden inverso } // fin del método Main // imprime el contenido de la lista private static void ImprimirLista< E >( LinkedList< E > lista ) { Console.WriteLine( "Lista enlazada: " ); foreach ( E valor in lista ) Console.Write( "{0} ", valor ); Console.WriteLine(); } // fin del método ImprimirLista // concatena la segunda lista al final de la primera lista private static void Concatenar< E >( LinkedList< E > lista1, LinkedList< E > lista2 ) { // concatena las listas copiando los valores de los elementos // en orden, desde la segunda lista hasta la primera lista foreach ( E valor in lista2 ) lista1.AddLast( valor ); // agrega un nuevo nodo } // fin del método Concatenar

Figura 26.9 | Uso de objetos LinkedList. (Parte 1 de 3).

1038 Capítulo 26 Colecciones

60 // localiza los objetos string y los convierte a mayúsculas 61 private static void CadenasAMayusculas( LinkedList< string > lista ) 62 { 63 // itera a través de la lista utilizando los nodos 64 LinkedListNode< string > nodoActual = lista.First; 65 66 while ( nodoActual != null ) 67 { 68 string color = nodoActual.Value; // obtiene el valor en el nodo 69 nodoActual.Value = color.ToUpper(); // convierte a mayúsculas 70 71 nodoActual = nodoActual.Next; // obtiene el siguiente nodo 72 } // fin de while 73 } // fin del método CadenasAMayusculas 74 75 // elimina los elementos de la lista entre dos elementos dados 76 private static void EliminarElementosEntre< E >( LinkedList< E > lista, 77 E elementoInicial, E elementoFinal ) 78 { 79 // obtiene los nodos que corresponden a los elementos inicial y final 80 LinkedListNode< E > nodoActual = lista.Find( elementoInicial ); 81 LinkedListNode< E > nodoFinal = lista.Find( elementoFinal ); 82 83 // elimina los elementos que van después del elemento inicial 84 // hasta encontrar el elemento final, o el final de la lista enlazada 85 while ( ( nodoActual.Next != null ) && 86 ( nodoActual.Next != nodoFinal ) ) 87 { 88 lista.Remove( nodoActual.Next ); // elimina el siguiente nodo 89 } // fin de while 90 } // fin del método EliminarElementosEntre 91 92 // imprime la lista al revés 93 private static void ImprimirListaInversa< E >( LinkedList< E > lista ) 94 { 95 Console.WriteLine( "Lista invertida:" ); 96 97 // itera a través de la lista, utilizando los nodos 98 LinkedListNode< E > nodoActual = lista.Last; 99 100 while ( nodoActual != null ) 101 { 102 Console.Write( "{0} ", nodoActual.Value ); 103 nodoActual = nodoActual.Previous; // obtiene el nodo anterior 104 } // fin de while 105 106 Console.WriteLine(); 107 } // fin del método ImprimirListaInversa 108 } // fin de la clase PruebaLinkedList Lista enlazada: negro amarillo verde azul violeta plata oro blanco café azul gris Convirtiendo las cadenas en lista1 a mayúsculas Lista enlazada: NEGRO AMARILLO VERDE AZUL VIOLETA PLATA ORO BLANCO CAFÉ AZUL GRIS Eliminando las cadenas entre NEGRO y CAFÉ

Figura 26.9 | Uso de objetos LinkedList. (Parte 2 de 3).

26.5 Colecciones genéricas 1039

Lista enlazada: NEGRO CAFÉ AZUL GRIS Lista invertida: GRIS AZUL CAFÉ NEGRO

Figura 26.9 | Uso de objetos LinkedList. (Parte 3 de 3). de los arreglos colores y colores2, respectivamente. Observe que LinkedList es una clase genérica que tiene un parámetro de tipo, para el cual especificamos el argumento de tipo string en este ejemplo (líneas 16 y 23). Mostraremos dos formas de llenar las listas. En las líneas 19-20, utilizamos la instrucción foreach y el método AddLast para llenar lista1. El método AddLast crea un nuevo objeto LinkedListNode (con el valor dado disponible a través de la propiedad Value), y adjunta este nodo al final de la lista. La línea 23 invoca el constructor que recibe un parámetro IEnumerable< string >. Todos los arreglos heredan en forma implícita de las interfaces genéricas IList e IEnumerable con el tipo del arreglo como el argumento de tipo, por lo que el arreglo string colores2 implementa a IEnumerable< string >. El parámetro de tipo de este objeto genérico IEnumerable coincide con el parámetro de tipo del objeto LinkedList genérico. La llamada a este constructor copia el contenido del arreglo colores2 a lista2. La línea 25 llama al método genérico Concatenar (líneas 51-58) para adjuntar todos los elementos de lista2 al final de lista1. La línea 26 llama al método ImprimirLista (líneas 40-48) para imprimir en pantalla el contenido de lista1. La línea 29 llama al método CadenasAMayusculas (líneas 61-73) para convertir cada elemento string a mayúsculas, y después la línea 30 llama a ImprimirLista de nuevo para mostrar los objetos string modificados. La línea 33 llama al método EliminarElementosEntre (líneas 76-90) para eliminar los elementos entre "NEGRO" y "CAFÉ", pero sin incluirlos. La línea 35 imprime en pantalla la lista otra vez, y después la línea 36 invoca al método ImprimirListaInvertida (líneas 93-107) para imprimir la lista en orden inverso. El método genérico Concatenar (líneas 51-58) itera a través de lista2 con una instrucción foreach y llama al método AddLast para adjuntar cada valor al final de lista1. El enumerador de la clase LinkedList itera a través de los valores de los nodos, y no de los nodos en sí, por lo que la variable de iteración tiene el tipo E. Observe que esto crea un nuevo nodo en lista1 para cada nodo en lista2. Un objeto LinkedListNode no puede ser miembro de más de un objeto LinkedList. Cualquier intento por agregar un nodo de un objeto LinkedList a otro genera una excepción InvalidOperationException. Si desea que los mismos datos pertenezcan a más de un objeto LinkedList, debe crear una copia del nodo para cada lista. El método genérico ImprimirLista (líneas 40-48) utiliza de manera similar una instrucción foreach para iterar a través de los valores en un objeto LinkedList, y los imprime en pantalla. El método CadenasAMayusculas (líneas 61-73) recibe una lista enlazada de objetos string y convierte cada valor string a mayúsculas. Este método sustituye los objetos string almacenados en la lista, por lo que no podemos utilizar un enumerador (a través de una instrucción foreach) como en los dos métodos anteriores. En vez de ello, obtenemos el primer objeto LinkedListNode a través de la propiedad First (línea 64) y utilizamos una instrucción while para iterar a través de la lista (líneas 66-72). Cada iteración de la instrucción while obtiene y actualiza el contenido de nodoActual mediante la propiedad Value, usando el método string ToUpper para crear una versión en mayúsculas del objeto string color. Al final de cada iteración, desplazamos el nodo actual al siguiente nodo en la lista, asignando nodoActual al nodo que se obtiene mediante su propia propiedad Next (línea 71). La propiedad Next del último nodo de la lista obtiene null, por lo que cuando la instrucción while itera más allá del final de la lista, el ciclo termina. Observe que no tiene sentido declarar CadenasAMayusculas como un método genérico, ya que utiliza los métodos específicos de string de los valores en los nodos. Los métodos ImprimirLista (líneas 40-48) y Concatenar (líneas 51-58) no necesitan utilizar métodos específicos de string, por lo que pueden declararse con parámetros de tipo genéricos para promover la máxima reutilización de código. El método genérico EliminarElementosEntre (líneas 76-90) elimina un rango de elementos entre dos nodos. Las líneas 80-81 obtienen los dos nodos “límite” del rango, usando el método Find. Este método realiza una búsqueda lineal en la lista, y devuelve el primer nodo que contenga un valor igual al argumento que se pasó. El método Find devuelve null si no encuentra el valor. Almacenamos el nodo que va antes del rango en la variable local nodoActual, y el nodo que va después del rango en nodoFinal. La instrucción while en las líneas 85-89 elimina todos los elementos entre nodoActual y nodoFinal. En cada iteración del ciclo, eliminamos el nodo que va después de nodoActual, invocando al método Remove (línea 88).

1040 Capítulo 26 Colecciones Este método recibe un objeto LinkedListNode, extrae ese nodo del objeto LinkedList y corrige las referencias de los nodos circundantes. Después de la llamada a Remove, la propiedad Next de nodoActual obtiene ahora el nodo que va después del nodo que se acaba de eliminar, y la propiedad Previous de ese nodo ahora obtiene nodoActual. La instrucción while continúa iterando hasta que no quedan nodos entre nodoActual y nodoFinal, o hasta que nodoActual es el último nodo de la lista. (También existe una versión sobrecargada del método Remove, que realiza una búsqueda lineal para el valor especificado y elimina el primer nodo en la lista que lo contiene.) El método ImprimirListaInversa (líneas 93-107) imprime la lista al revés, navegando por los nodos en forma manual. La línea 98 obtiene el último elemento de la lista a través de la propiedad Last y lo almacena en nodoActual. La instrucción while en las líneas 100-104 itera a través de la lista al revés, desplazando la referencia nodoActual al nodo anterior al final de cada iteración, y después se sale cuando avanzamos más allá del principio de la lista. Observe la similitud entre este código y las líneas 64-72, en donde se itera a través de la lista desde el principio hasta el final.

26.6 Colecciones sincronizadas En el capítulo 15 hablamos sobre el subprocesamiento múltiple. La mayoría de las colecciones no genéricas carecen de sincronización de manera predeterminada, por lo que pueden operar con eficiencia cuando no se requiere el subprocesamiento múltiple. Sin embargo, como no están sincronizadas, el acceso concurrente a una colección por parte de varios subprocesos podría ocasionar errores. Para evitar problemas potenciales con el subprocesamiento, se utilizan contenedores de sincronización para muchas de las colecciones a las que varios subprocesos podrían acceder. Un objeto contenedor recibe llamadas a métodos, agrega la sincronización de subprocesos (para evitar el acceso concurrente a la colección) y transfiere las llamadas al objeto de la colección dentro del contenedor. La mayoría de las clases de colección no genéricas en el .NET Framework contienen el método static Synchronized, el cual devuelve un objeto contenedor sincronizado para el objeto especificado. Por ejemplo, el siguiente código crea un objeto ArrayList sincronizado: ArrayList listaNoSegura = new ArrayList(); ArrayList listaSeguraSubproceso = ArrayList.Synchronized( listaNoSegura );

Las colecciones en el .NET Framework no proporcionan contenedores para un rendimiento seguro con varios subprocesos. Muchas de las colecciones genéricas son intrínsicamente seguras para los subprocesos en operaciones de lectura, pero no de escritura, consulte la documentación de esa clase en la referencia a las bibliotecas de clases del .NET Framework. Recuerde que cuando se modifica una colección, cualquier enumerador devuelto previamente por el método GetEnumerator se invalida y lanza una excepción si se invocan sus métodos. Si utiliza un enumerador o una instrucción foreach en una aplicación con subprocesamiento múltiple, emplee la palabra clave lock para evitar que otros subprocesos usen la colección o una instrucción try para atrapar la excepción InvalidOperationException.

26.7 Conclusión En este capítulo se presentaron las clases de colección del .NET Framework. Aprendió acerca de la jerarquía de interfaces que implementan muchas de las clases de colección. Vio cómo utilizar la clase Array para realizar manipulaciones de arreglos. Aprendió que los espacios de nombres System.Collections y System.Collections. Generic contienen muchas clases de colección no genéricas y genéricas, respectivamente. Presentamos las clases no genéricas ArrayList, Stack y Hashtable, así como las clases genéricas SortedDictionary y LinkedList. También aprendió a utilizar enumeradores para recorrer estas estructuras de datos y obtener su contenido. Demostramos el uso de la instrucción foreach con muchas de las clases de la FCL, y explicamos que esto funciona mediante el uso de enumeradores “detrás de las cámaras” para recorrer las colecciones. Por último, hablamos sobre algunas de las cuestiones que se deben considerar al utilizar colecciones en aplicaciones con subprocesamiento múltiple.

Las páginas 1041 a 1156, que corresponden a los apéndices de este libro, se encuentran en el CD-ROM que acompaña al libro.

A

Tabla de precedencia de los operadores

A.1 Precedencia de los operadores Los operadores se muestran en orden decreciente de precedencia, de arriba hacia abajo; cada nivel de precedencia se separa por una línea negra horizontal (figura A.1). La asociatividad de los operadores se muestra en la columna derecha. Operador

Tipo

Asociatividad

.

acceso a miembro

de izquierda a derecha

()

llamada a método

[]

acceso a elemento

++

unario de postincremento

--

unario de postdecremento

new

creación de objetos

typeof

obtener el objeto System.Type para un tipo

sizeof

obtener el tamaño en bytes de un tipo

checked

evaluación comprobada

unchecked

evaluación no comprobada

+

unario de suma

-

unario de resta

!

negación lógica

~

complemento a nivel de bits

++

unario de preincremento

--

unario de predecremento

(type)

conversión

Figura A.1 | Tabla de precedencia de los operadores. (Parte 1 de 2).

de derecha a izquierda

1042 Apéndice A Tabla de precedencia de los operadores

Operador

Tipo

Asociatividad

*

multiplicación

de izquierda a derecha

/

división

%

residuo

+

suma

-

resta

>>

desplazamiento a la derecha

mayor que

=

mayor o igual que

is

comparación de tipos

as

conversión de tipos

!=

no es igual a

==

es igual a

&

AND lógico

de izquierda a derecha

^

XOR lógico

de izquierda a derecha

|

OR lógico

de izquierda a derecha

&&

AND condicional

de izquierda a derecha

||

OR condicional

de izquierda a derecha

??

asignación de null

de derecha a izquierda

?:

condicional

de derecha a izquierda

=

asignación

de derecha a izquierda

*=

asignación, multiplicación

/=

asignación, división

%=

asignación, residuo

+=

asignación, suma

-=

asignación, resta

=

asignación, desplazamiento a la derecha

&=

asignación, AND lógico

^=

asignación, XOR lógico

|=

asignación, OR lógico

Figura A.1 | Tabla de precedencia de los operadores. (Parte 2 de 2).

de izquierda a derecha

de izquierda a derecha

de izquierda a derecha

de izquierda a derecha

B

Sistemas numéricos

He aquí sólo los números ratificados. —William Shakespeare

OBJETIVOS En este apéndice aprenderá lo siguiente: Q

Comprender los conceptos acerca de los sistemas numéricos como base, valor posicional y valor simbólico.

Q

Trabajar con los números representados en los sistemas numéricos binario, octal y hexadecimal.

Q

Abreviar los números binarios como octales o hexadecimales.

Q

Convertir los números octales y hexadecimales en binarios.

Q

Realizar conversiones hacia y desde números decimales y sus equivalentes en binario, octal y hexadecimal.

Q

Comprender el funcionamiento de la aritmética binaria y la manera en que se representan los números binarios negativos, utilizando la notación de complemento a dos.

La naturaleza tiene un cierto tipo de sistema de coordenadas aritméticas-geométricas, ya que cuenta con todo tipo de modelos. Lo que experimentamos de la naturaleza está en los modelos, y todos los modelos de la naturaleza son tan bellos. Se me ocurrió que el sistema de la naturaleza debe ser una verdadera belleza, porque en la química encontramos que las asociaciones se encuentran siempre en hermosos números enteros; no hay fracciones. —Richard Buckminster Fuller

Pla n g e ne r a l

1044 Apéndice B Sistemas numéricos

B.1 B.2 B.3 B.4 B.5 B.6

Introducción Abreviatura de los números binarios como números octales y hexadecimales Conversión de números octales y hexadecimales a binarios Conversión de un número binario, octal o hexadecimal a decimal Conversión de un número decimal a binario, octal o hexadecimal Números binarios negativos: notación de complemento a dos

B.1 Introducción En este apéndice presentaremos los sistemas numéricos clave que utilizan los programadores, especialmente cuando trabajan en proyectos de software que requieren de una estrecha interacción con el hardware a nivel de máquina. Entre los proyectos de este tipo están los sistemas operativos, el software de redes computacionales, los compiladores, sistemas de bases de datos y aplicaciones que requieren de un alto rendimiento. Cuando escribimos un entero, como 227 o −63, en un programa, se asume que el número está en el sistema numérico decimal (base 10). Los dígitos en el sistema numérico decimal son 0, 1, 2, 3, 4, 5, 6, 7, 8 y 9. El dígito más bajo es el 0 y el más alto es el 9 (uno menos que la base, 10). En su interior, las computadoras utilizan el sistema numérico binario (base 2). Este sistema numérico sólo tiene dos dígitos: 0 y 1. El dígito más bajo es el 0 y el más alto es el 1 (uno menos que la base, 2). Como veremos, los números binarios tienden a ser mucho más extensos que sus equivalentes decimales. Los programadores que trabajan con lenguajes ensambladores y en lenguajes de alto nivel como C#, que les permiten llegar hasta el nivel de máquina, encuentran que es complicado trabajar con números binarios. Por eso existen otros dos sistemas numéricos, el octal (base 8) y el hexadecimal (base 16), que son populares debido a que permiten abreviar los números binarios de una manera conveniente. En el sistema numérico octal, los dígitos utilizados son del 0 al 7. Debido a que tanto el sistema numérico binario como el octal tienen menos dígitos que el sistema numérico decimal, sus dígitos son los mismos que sus correspondientes en decimal. El sistema numérico hexadecimal presenta un problema, ya que requiere de dieciséis dígitos: el dígito más bajo es 0 y el más alto tiene un valor equivalente al 15 decimal (uno menos que la base, 16). Por convención utilizamos las letras de la A a la F para representar los dígitos hexadecimales que corresponden a los valores decimales del 10 al 15. Por lo tanto, en hexadecimal podemos tener números como el 876, que consisten solamente de dígitos similares a los decimales; números como 8A55F que consisten de dígitos y letras; y números como FFE que consisten solamente de letras. En ocasiones un número hexadecimal puede coincidir con una palabra común como FACE o FEED (en inglés); esto puede parecer extraño para los programadores acostumbrados a trabajar con números. Los dígitos de los sistemas numéricos binario, octal, decimal y hexadecimal se sintetizan en las figuras B.1 y B.2. Cada uno de estos sistemas numéricos utilizan la notación posicional: cada posición en la que se escribe un dígito tiene un valor posicional distinto. Por ejemplo, en el número decimal 937 (el 9, el 3 y el 7 se conocen como valores simbólicos) decimos que el 7 se escribe en la posición de las unidades; el 3, en la de las decenas; y el 9, en la de las centenas. Observe que cada una de estas posiciones es una potencia de la base (10) y que estas potencias empiezan en 0 y aumentan de 1 en 1 a medida que nos desplazamos hacia la izquierda por el número (figura B.3). Para números decimales más extensos, las siguientes posiciones a la izquierda serían: de millares (10 a la tercera potencia), de decenas de millares (10 a la cuarta potencia), de centenas de millares (10 a la quinta potencia), de los millones (10 a la sexta potencia), de decenas de millones (10 a la séptima potencia), y así sucesivamente. En el número binario 101 decimos que el 1 más a la derecha se escribe en la posición de los unos; el 0, en la posición de los dos; y el 1 de más a la izquierda, en la posición de los cuatros. Observe que cada una de estas posiciones es una potencia de la base (2) y que estas potencias empiezan en 0 y aumentan de 1 en 1 a medida que nos desplazamos hacia la izquierda por el número (figura B.4). Por lo tanto, 101 = 1 * 22 + 0 * 21 + 1 * 20 = 4 + 0 + 1 = 5. Para números binarios más extensos, las siguientes posiciones a la izquierda serían la posición de los ochos (2 a la tercera potencia), la posición de los dieciséis (2 a la cuarta potencia), la de los treinta y dos (2 a la quinta potencia), la de los sesenta y cuatros (2 a la sexta potencia), y así sucesivamente. En el número octal 425, decimos que el 5 se escribe en la posición de los unos; el 2, en la posición de los ochos; y el 4, en la posición de los sesenta y cuatros. Observe que cada una de estas posiciones es una potencia

B.1 Introducción 1045

Dígito binario

Dígito octal

Dígito decimal

Dígito hexadecimal

0

0

0

0

1

1

1

1

2

2

2

3

3

3

4

4

4

5

5

5

6

6

6

7

7

7

8

8

9

9 A B C D E F

(valor de 10 en decimal) (valor de 11 en decimal) (valor de 12 en decimal) (valor de 13 en decimal) (valor de 14 en decimal) (valor de 15 en decimal)

Figura B.1 | Dígitos de los sistemas numéricos binario, octal, decimal y hexadecimal. Atributo

Binario

Octal

Decimal

Hexadecimal

Base Dígito más bajo Dígito más alto

2

8

10

16

0

0

0

0

1

7

9

F

Figura B.2 | Comparación de los sistemas binario, octal, decimal y hexadecimal. Valores posicionales en el sistema numérico decimal Dígito decimal Nombre de la posición Valor posicional Valor posicional como potencia de la base (10)

9

3

7

Centenas

Decenas

Unidades

100

10

1

102

101

100

Figura B.3 | Valores posicionales en el sistema numérico decimal. de la base (8) y que empiezan en 0 y aumentan de 1 en 1 a medida que nos desplazamos hacia la izquierda por el número (figura B.5). Para números octales más extensos, las siguientes posiciones a la izquierda sería la posición de los quinientos doces (8 a la tercera potencia), la de los cuatro mil noventa y seis (8 a la cuarta potencia), la de los treinta y dos mil setecientos sesenta y ochos (8 a la quinta potencia), y así sucesivamente. En el número hexadecimal 3DA, decimos que la A se escribe en la posición de los unos; la D, en la posición de los dieciséis; y el 3, en la posición de los doscientos cincuenta y seis. Observe que cada una de estas posiciones es una potencia de la base (16) y que estas potencias empiezan en 0 y aumentan de 1 en 1 a medida que nos desplazamos hacia la izquierda por el número (figura B.6).

1046 Apéndice B Sistemas numéricos

Valores posicionales en el sistema numérico binario Dígito binario Nombre de la posición Valor posicional Valor posicional como potencia de la base (2)

1

0

1

Cuatros

Dos

Unos

4

2

1

22

21

20

Figura B.4 | Valores posicionales en el sistema numérico binario. Valores posicionales en el sistema numérico octal Dígito octal Nombre de la posición Valor posicional Valor posicional como potencia de la base (8)

4

2

5

Sesenta y cuatros

Ochos

Unos

64

8

1

82

81

80

Figura B.5 | Valores posicionales en el sistema numérico octal. Valores posicionales en el sistema numérico hexadecimal Dígito hexadecimal Nombre de la posición

3

D

A

Doscientos cincuenta y seis

Dieciséis

Unos

Valor posicional Valor posicional como potencia de la base (16)

256

16

1

162

161

160

Figura B.6 | Valores posicionales en el sistema numérico hexadecimal. Para números hexadecimales más extensos, las siguientes posiciones a la izquierda serían la posición de los cuatro mil noventa y seis (16 a la tercera potencia), la posición de los sesenta y cinco mil quinientos treinta y seis (16 a la cuarta potencia), y así sucesivamente.

B.2 Abreviatura de los números binarios como números octales y hexadecimales En computación, el uso principal de los números octales y hexadecimales es para abreviar representaciones binarias demasiado extensas. La figura B.7 muestra que los números binarios extensos pueden expresarse más concisamente en sistemas numéricos con bases mayores que en el sistema numérico binario. Una relación especialmente importante que tienen tanto el sistema numérico octal como el hexadecimal con el sistema binario es que las bases de los sistemas octal y hexadecimal (8 y 16, respectivamente) son potencias de la base del sistema numérico binario (base 2). Considere el siguiente número binario de 12 dígitos y sus equivalentes en octal y hexadecimal. Vea si puede determinar cómo esta relación hace que sea conveniente abreviar los números binarios en octal o hexadecimal. La respuesta sigue después de los números. Número binario

Equivalente en octal

Equivalente en hexadecimal

100011010001

4321

8D1

B.3 Conversión de números octales y hexadecimales a binarios 1047

Número decimal

Representación binaria

Representación octal

Representación hexadecimal

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

0 1 10 11 100 101 110 111 1000 1001 1010 1011 1100 1101 1110 1111 10000

0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20

0 1 2 3 4 5 6 7 8 9 A B C D E F 10

Figura B.7 | Equivalentes en decimal, binario, octal y hexadecimal. Para convertir fácilmente el número binario en octal, sólo divida el número binario de 12 dígitos en grupos de tres bits consecutivos y escriba esos grupos por encima de los dígitos correspondientes del número octal, como se muestra a continuación: 100 4

011 3

010 2

001 1

Observe que el dígito octal que escribió debajo de cada grupo de tres bits corresponde precisamente al equivalente octal de ese número binario de 3 dígitos que se muestra en la figura B.7. El mismo tipo de relación puede observarse al convertir números de binario a hexadecimal. Divida el número binario de 12 dígitos en grupos de cuatro bits consecutivos y escriba esos grupos por encima de los dígitos correspondientes del número hexadecimal, como se muestra a continuación: 1000 8

1101 D

0001 1

Observe que el dígito hexadecimal que escribió debajo de cada grupo de cuatro bits corresponde precisamente al equivalente hexadecimal de ese número binario de 4 dígitos que se muestra en la figura B.7.

B.3 Conversión de números octales y hexadecimales a binarios En la sección anterior vimos cómo convertir números binarios a sus equivalentes en octal y hexadecimal, formando grupos de dígitos binarios y simplemente volviéndolos a escribir como sus valores equivalentes en dígitos octales o hexadecimales. Este proceso puede utilizarse en forma inversa para producir el equivalente en binario de un número octal o hexadecimal. Por ejemplo, el número octal 653 se convierte en binario simplemente escribiendo el 6 como su equivalente binario de 3 dígitos 110, el 5 como su equivalente binario de 3 dígitos 101 y el 3 como su equivalente binario de 3 dígitos 011 para formar el número binario de 9 dígitos 110101011.

1048 Apéndice B Sistemas numéricos El número hexadecimal FAD5 se convierte en binario simplemente escribiendo la F como su equivalente binario de 4 dígitos 1111, la A como su equivalente binario de 4 dígitos 1010, la D como su equivalente binario de 4 dígitos 1101 y el 5 como su equivalente binario de 4 dígitos 0101, para formar el número binario de 16 dígitos 1111101011010101.

B.4 Conversión de un número binario, octal o hexadecimal a decimal Como estamos acostumbrados a trabajar con el sistema decimal, a menudo es conveniente convertir un número binario, octal o hexadecimal en decimal para tener una idea de lo que “realmente” vale el número. Nuestros diagramas en la sección B.1 expresan los valores posicionales en decimal. Para convertir un número en decimal desde otra base, multiplique el equivalente en decimal de cada dígito por su valor posicional y sume estos productos. Por ejemplo, el número binario 110101 se convierte en el número 53 decimal, como se muestra en la figura B.8. Para convertir el número 7614 octal en el número 3980 decimal utilizamos la misma técnica, esta vez utilizando los valores posicionales apropiados para el sistema octal, como se muestra en la figura B.9. Para convertir el número AD3B hexadecimal en el número 44347 decimal utilizamos la misma técnica, esta vez empleando los valores posicionales apropiados para el sistema hexadecimal, como se muestra en la figura B.10.

B.5 Conversión de un número decimal a binario, octal o hexadecimal Las conversiones de la última sección siguen naturalmente las convenciones de la notación posicional. Las conversiones de decimal a binario, octal o hexadecimal también siguen estas convenciones. Conversión de un número binario en decimal Valores posicionales: Valores simbólicos: Productos: Suma:

32

16

8

4

2

1

1

1

0

1

0

1

1*32=32

1*16=16

0*8=0

1*4=4

0*2=0

1*1=1

= 32 + 16 + 0 + 4 + 0 + 1 = 53

Figura B.8 | Conversión de un número binario en decimal. Conversión de un número octal en decimal Valores posicionales: Valores simbólicos: Productos: Suma:

512

64

8

1

7

6

1

4

7*512=3584

6*64=384

1*8=8

4*1=4

= 3584 + 384 + 8 + 4 = 3980

Figura B.9 | Conversión de un número octal en decimal. Conversión de un número hexadecimal en decimal Valores posicionales: Valores simbólicos: Productos: Suma:

4096

256

16

1

A

D

3

B

A*4096=40960

D*256=3328

3*16=48

B*1=11

= 40960 + 3328 + 48 + 11 = 44347

Figura B.10 | Conversión de un número hexadecimal en decimal.

B.5 Conversión de un número decimal a binario, octal o hexadecimal 1049 Suponga que queremos convertir el número 57 decimal en binario. Empezamos escribiendo los valores posicionales de las columnas de derecha a izquierda, hasta llegar a una columna cuyo valor posicional sea mayor que el número decimal. Como no necesitamos esa columna, podemos descartarla. Por lo tanto, primero escribimos: Valores posicionales:

64

32

16

8

4

2

1

Luego descartamos la columna con el valor posicional de 64, dejando: Valores posicionales:

32

16

8

4

2

1

A continuación, empezamos a trabajar desde la columna más a la izquierda y nos vamos desplazando hacia la derecha. Dividimos 57 entre 32 y observamos que hay un 32 en 57, con un residuo de 25, por lo que escribimos 1 en la columna de los 32. Dividimos 25 entre 16 y observamos que hay un 16 en 25, con un residuo de 9, por lo que escribimos 1 en la columna de los 16. Dividimos 9 entre 8 y observamos que hay un 8 en 9 con un residuo de 1. Las siguientes dos columnas producen el cociente de cero cuando se divide 1 entre sus valores posicionales, por lo que escribimos 0 en las columnas de los 4 y de los 2. Por último, 1 entre 1 es 1, por lo que escribimos 1 en la columna de los 1. Esto nos da: Valores posicionales: Valores simbólicos:

32 1

16 1

8 1

4 0

2 0

1 1

y, por lo tanto, el 57 decimal es equivalente al 111001 binario. Para convertir el número decimal 103 en octal, empezamos por escribir los valores posicionales de las columnas hasta llegar a una columna cuyo valor posicional sea mayor que el número decimal. Como no necesitamos esa columna, podemos descartarla. Por lo tanto, primero escribimos: Valores posicionales:

512

64

8

1

Luego descartamos la columna con el valor posicional de 512, lo que nos da: Valores posicionales:

64

8

1

A continuación, empezamos a trabajar desde la columna más a la izquierda y nos vamos desplazando hacia la derecha. Dividimos 103 entre 64 y observamos que hay un 64 en 103 con un residuo de 39, por lo que escribimos 1 en la columna de los 64. Dividimos 39 entre 8 y observamos que el 8 cabe cuatro veces en 39 con un residuo de 7, por lo que escribimos 4 en la columna de los 8. Por último, dividimos 7 entre 1 y observamos que el 1 cabe siete veces en 7 y no hay residuo, por lo que escribimos 7 en la columna de los 1. Esto nos da: Valores posicionales: Valores simbólicos:

64 1

8 4

1 7

y, por lo tanto, el 103 decimal es equivalente al 147 octal. Para convertir el número decimal 375 en hexadecimal, empezamos por escribir los valores posicionales de las columnas hasta llegar a una columna cuyo valor posicional sea mayor que el número decimal. Como no necesitamos esa columna, podemos descartarla. Por consecuencia, primero escribimos: Valores posicionales:

4096 256

16

1

Luego descartamos la columna con el valor posicional de 4096, lo que nos da: Valores posicionales:

256

16

1

A continuación, empezamos a trabajar desde la columna más a la izquierda y nos vamos desplazando hacia la derecha. Dividimos 375 entre 256 y observamos que 256 cabe una vez en 375 con un residuo de 119, por lo que escribimos 1 en la columna de los 256. Dividimos 119 entre 16 y observamos que el 16 cabe siete veces en 119 con un residuo de 7, por lo que escribimos 7 en la columna de los 16. Por último, dividimos 7 entre 1 y observamos que el 1 cabe siete veces en 7 y no hay residuo, por lo que escribimos 7 en la columna de los 1. Esto produce: Valores posicionales: Valores simbólicos:

256 1

16 7

1 7

y, por lo tanto, el 375 decimal es equivalente al 177 hexadecimal.

1050 Apéndice B Sistemas numéricos

B.6 Números binarios negativos: notación de complemento a dos La discusión en este apéndice se ha enfocado hasta ahora en números positivos. En esta sección explicaremos cómo las computadoras representan números negativos mediante el uso de la notación de complementos a dos. Primero explicaremos cómo se forma el complemento a dos de un número binario y después mostraremos por qué representa el valor negativo de dicho número binario. Considere una máquina con enteros de 32 bits. Suponga que se ejecuta la siguiente instrucción: int valor = 13;

La representación en 32 bits de valor es: 00000000 00000000 00000000 00001101

Para formar el negativo de valor, primero formamos su complemento a uno aplicando el operador de complemento a nivel de bits de C# (~): complementoAUnoDeValor = ~valor;

Internamente, ~valor es ahora valor con cada uno de sus bits invertidos; los unos se convierten en ceros y los ceros en unos, como se muestra a continuación: valor: 00000000 00000000 00000000 00001101

(es decir, el complemento a uno de valor): 11111111 11111111 11111111 11110010 ~valor

Para formar el complemento a dos de valor, simplemente sumamos uno al complemento a uno de valor. Por lo tanto: El complemento a dos de valor es: 11111111 11111111 11111111 11110011

Ahora, si esto de hecho es igual a –13, deberíamos poder sumarlo al 13 binario y obtener como resultado 0. Comprobemos esto: 00000000 00000000 00000000 00001101 +11111111 11111111 11111111 11110011

------------------------------------------------------

00000000 00000000 00000000 00000000

El bit de acarreo que sale de la columna que está más a la izquierda se descarta y evidentemente obtenemos cero como resultado. Si sumamos el complemento a uno de un número a ese mismo número, todos los dígitos del resultado serían iguales a 1. La clave para obtener un resultado en el que todos los dígitos sean cero es que el complemento a dos es 1 más que el complemento a 1. La suma de 1 hace que el resultado de cada columna sea 0 y se acarrea un 1. El acarreo sigue desplazándose hacia la izquierda hasta que se descarta en el bit que está más a la izquierda, con lo que todos los dígitos del número resultante son iguales a cero. En realidad, las computadoras realizan una suma como: x = a - valor;

mediante la suma del complemento a dos de valor con a, como se muestra a continuación: x = a + (~valor + 1);

Suponga que a es 27 y que valor es 13 como en el ejemplo anterior. Si el complemento a dos de valor es en realidad el negativo de éste, entonces al sumar el complemento de dos de valor con a se produciría el resultado de 14. Comprobemos esto: a (es decir, 27) 00000000 00000000 00000000 00011011 + (~valor + 1) +11111111 11111111 11111111 11110011

------------------------------------------------------00000000 00000000 00000000 00001110

lo que ciertamente da como resultado 14.

C

Uso del depurador de Visual Studio® 2005 Estamos construidos para cometer errores, codificados por error. —Lewis Thomas

OBJETIVOS En este apéndice aprenderá lo siguiente:

Lo que anticipamos raras veces pasa; lo que menos esperamos es lo que generalmente ocurre.

Q

Utilizar el depurador para localizar y corregir errores lógicos en un programa.

—Benjamin Disraeli

Q

Utilizar puntos de interrupción para suspender la ejecución de un programa y poder examinar los valores de las variables.

Q

Establecer, deshabilitar y quitar puntos de interrupción.

Una cosa es mostrar a un hombre que está equivocado, y otra es ponerlo en posesión de la verdad.

Q

Usar el comando Continuar para continuar la ejecución desde un punto de interrupción.

Q

Usar la ventana Variables locales para ver y modificar los valores de las variables.

Q

Usar la ventana Inspección para evaluar expresiones.

Q

Usar los comandos Paso a paso por instrucciones, Paso a paso por procedimientos y Paso a paso para salir, para ejecutar un programa línea por línea.

Q

Usar las nuevas características Editar y Continuar de Visual Studio 2005, junto con la depuración Sólo mi código ( Just My Code™).

—John Locke

Puede correr, pero no puede ocultarse. —Joe Louis

Por lo tanto, yo también atraparé la mosca. —William Shakespeare

Pla n g e ne r a l

1052 Apéndice C Uso del depurador de Visual Studio® 2005

C.1 C.2 C.3 C.4

Introducción Los puntos de interrupción y el comando Continuar Las ventanas Variables locales e Inspección Control de la ejecución mediante los comandos Paso a paso por instrucciones, Paso a paso por procedimientos, Paso a paso para salir y Continuar C.5 Otras características C.5.1 Editar y Continuar C.5.2 Ayudante de excepciones C.5.3 Depuración Sólo mi código ( Just My Code™) C.5.4 Otras nuevas características del depurador C.6 Conclusión

C.1 Introducción En el capítulo 3 vimos que hay dos tipos de errores: los de compilación y los errores lógicos; y aprendimos a eliminar del código los errores de compilación. Los errores lógicos, también conocidos como bugs, no evitan que un programa se compile con éxito, pero pueden provocar resultados erróneos, o hacer que el programa termine antes de tiempo a la hora de ejecutarse. La mayoría de los distribuidores de compiladores como Microsoft, proporcionan una herramienta conocida como depurador, el cual le permite supervisar la ejecución de sus programas para localizar y eliminar los errores lógicos. Un programa debe compilarse con éxito para poder utilizarlo en el depurador; éste nos ayuda a analizar un programa mientras se ejecuta. El depurador nos permite suspender la ejecución de un programa, examinar y establecer los valores de las variables, y mucho más. En este apéndice presentaremos el depurador de Visual Studio, varias de sus herramientas de depuración y las nuevas características que se agregaron en Visual Studio 2005.

C.2 Los puntos de interrupción y el comando Continuar

Empezaremos por investigar los puntos de interrupción, que son marcadores que pueden establecerse en cualquier línea de código ejecutable. Cuando un programa en ejecución llega a un punto de interrupción, se pausa la ejecución y podemos examinar los valores de las variables, lo cual nos ayuda a determinar si existen o no errores lógicos. Por ejemplo, podemos examinar el valor de una variable que almacena el resultado de un cálculo para determinar si éste se realizó en forma correcta. También podemos examinar el valor de una expresión. Para ilustrar las características del depurador, utilizaremos el programa en las figuras C.1 y C.2, el cual crea y manipula un objeto de la clase Cuenta (figura C.1). Este ejemplo es similar al que vimos en el capítulo 4 (figuras 4.15 y 4.16). Por lo tanto, no utiliza las características que presentamos en capítulos posteriores, como el operador += y la instrucción if...else. La ejecución empieza en Main (líneas 8-29 de la figura C.2). La línea 10 crea un objeto Cuenta con un saldo inicial de $50.00. El constructor de Cuenta (líneas 10-13 de la figura C.1) acepta un argumento, el cual especifica el Saldo inicial de la Cuenta. Las líneas 13-14 de la figura C.2 muestran en pantalla el saldo inicial de la cuenta, usando la propiedad Saldo de Cuenta. La línea 16 declara e inicializa la variable local montoDeposito. Las líneas 19-20 piden al usuario que introduzca el montoDeposito. La línea 23 suma el depósito al saldo de la Cuenta, usando su método Acredita. Por último, las líneas 26-27 muestran el nuevo saldo. 1 2 3 4 5 6 7

// Fig. C.01: Cuenta.cs // La clase Cuenta con un constructor para // inicializar la variable de instancia saldo. public class Cuenta { private decimal saldo; // variable de instancia que almacena el saldo

Figura C.I | La clase Cuenta con un constructor para inicializar la variable de instancia saldo. (Parte 1 de 2).

C.2 Los puntos de interrupción y el comando Continuar 1053

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

// constructor public Cuenta( decimal saldoInicial ) { Saldo = saldoInicial; // establece el saldo usando la propiedad Saldo } // fin del constructor de Cuenta // acredita (suma) un monto a la cuenta public void Acredita( decimal monto ) { Saldo = Saldo + monto; // suma el monto al saldo } // fin del método Acredita // una propiedad para obtener y establecer el saldo de la cuenta public decimal Saldo { get { return saldo; } // fin de get set { // valida si el valor es mayor que 0; // si no lo es, saldo se establece al valor predeterminado de 0 if ( value > 0 ) saldo = value; if ( value Insertar punto de interrupción. Puede establecer todos los puntos de interrupción que desee. Establezca puntos de interrupción en las líneas 19, 23 y 28 de su código. Un círculo relleno aparece en la barra indicadora de margen en donde hizo clic y se resalta toda la instrucción de código completa, indicando que se han establecido puntos de interrupción (figura C.3). Cuando el programa se ejecuta, el depurador suspende la ejecución en cualquier línea que contenga un punto de interrupción. Después, el programa entra en modo de interrupción. Pueden establecerse puntos de interrupción antes de ejecutar un programa, en modo de interrupción y durante la ejecución.

Barra indicadora de margen

Puntos de interrupción

Figura C.3 | Establecimiento de puntos de interrupción.

C.2 Los puntos de interrupción y el comando Continuar 1055 2. Empezar el proceso de depuración. Después de establecer puntos de interrupción en el editor de código, seleccione Generar > Generar solución para compilar el programa y después seleccione Depurar > Iniciar depuración (u oprima F5 ) para empezar el proceso de depuración. Mientras se depura una aplicación de consola, aparece la ventana Símbolo del sistema (figura C.4), lo cual nos permite interactuar con el programa (entradas y salidas). 3. Examinar la ejecución del programa. La ejecución del programa se detiene en el primer punto de interrupción (línea 19), y el IDE se convierte en la ventana activa (figura C.5). La flecha amarilla a la izquierda de la línea 19 indica que esta línea contiene la siguiente instrucción a ejecutar. El IDE también resalta la línea. 4. Uso del comando Continue para continuar la ejecución. Para continuar la ejecución, seleccione Depurar > Continuar (u oprima F5 ). El comando Continuar ejecutará las instrucciones a partir del punto actual en el programa, hasta el siguiente punto de interrupción o el final de Main, lo que ocurra primero. El programa continúa su ejecución y se detiene para recibir la entrada en la línea 20. Escriba 49.99 en la ventana Símbolo del sistema como el monto a depositar. Cuando oprima Intro, el programa se ejecutará hasta detenerse en el siguiente punto de interrupción (línea 23). Observe que si coloca el puntero del ratón encima del nombre de la variable montoDeposito, su valor se muestra en un cuadro de Información rápida (figura C.6). Como veremos más adelante, este cuadro puede ayudarle a detectar errores lógicos en sus programas. 5. Continuar la ejecución del programa. Use el comando Depurar > Continuar para ejecutar la línea 23. El programa deberá mostrar el resultado de su cálculo (figura C.7). 6. Deshabilitar un punto de interrupción. Para deshabilitar un punto de interrupción, haga clic con el botón derecho en una línea de código en la que se haya establecido el punto de interrupción, y seleccione Punto de interrupción > Deshabilitar punto de interrupción. También puede hacer clic con el botón derecho del ratón en el mismo punto de interrupción y seleccionar Deshabilitar punto de interrupción. El punto de interrupción deshabilitado se indica mediante un círculo sin relleno (figura C.8); para

Figura C.4 | Ejecución del programa Cuenta.

Flecha amarilla

Siguiente instrucción a ejecutar

Figura C.5 | La ejecución del programa suspendida en el primer punto de interrupción.

1056 Apéndice C Uso del depurador de Visual Studio® 2005

Figura C.6 | El cuadro de Información rápida muestra el valor de la variable montoDeposito.

Figura C.7 | Salida del programa.

Punto de interrupción deshabilitado

Figura C.8 | Punto de interrupción deshabilitado. rehabilitar el punto de interrupción, haga clic con el ratón dentro del círculo sin relleno o haga clic con el botón derecho del ratón en la línea marcada por el círculo sin relleno (o el círculo en sí) y seleccione Punto de interrupción > Habilitar punto de interrupción. 7. Eliminar un punto de interrupción. Para eliminar un punto de interrupción que ya no necesite, haga clic con el botón derecho del ratón en la línea de código en la que se encuentra el punto de interrupción y seleccione Punto de interrupción > Eliminar punto de interrupción. También puede eliminar un punto de interrupción haciendo clic en el círculo que se encuentra en la barra indicadora de margen. 8. Terminar la ejecución del programa. Seleccione Depurar > Continuar para ejecutar el programa hasta que termine.

C.3 Las ventanas Variables locales e Inspección 1057

C.3 Las ventanas Variables locales e Inspección En la sección anterior aprendió que la característica Información rápida nos permite examinar el valor de una variable. En esta sección aprenderá a utilizar la ventana Variables locales para asignar nuevos valores a las variables mientras su programa se ejecuta. También utilizará la ventana Inspección para examinar los valores de las expresiones. 1. Insertar puntos de interrupción. Establezca un punto de interrupción en la línea 23 (figura C.9) del código fuente, haciendo clic con el botón izquierdo del ratón en la barra indicadora de margen a la izquierda de la línea 23. Use la misma técnica para establecer puntos de interrupción en las líneas 26 y 28 también. 2. Iniciar la depuración. Seleccione

Depurar > Iniciar depuración. Escriba 49.99 en el indicador Escriba el monto a depositar para cuenta 1: (figura C.10) y oprima Intro, para que el programa lea el valor que

acaba de introducir. El programa se ejecutará hasta el punto de interrupción en la línea 23. 3. Suspender la ejecución del programa. Cuando el programa llega a la línea 23, Visual Studio suspende la ejecución y coloca al programa en modo de interrupción (figura C.11). En este punto, la instrucción en la línea 20 (figura C.2) recibe el montoDeposito que usted escribió (49.99), la instrucción en las líneas 21-22 imprime en pantalla un mensaje indicando que el programa está sumando ese monto al saldo de cuenta1 y la instrucción en la línea 23 es la siguiente a ejecutar. 4. Examinar los datos. Una vez que el programa entra al modo de interrupción, puede explorar los valores de sus variables locales mediante la ventana Variables locales del depurador. Para ver esta ventana, seleccione Depurar > Ventanas > Variables locales. Haga clic en el cuadro con el signo positivo a la izquierda de cuenta1, en la columna Nombre de la ventana Variables locales (figura C.12). Esto le permite ver cada uno de los valores de la variable de instancia cuenta1 en forma individual, incluyendo el valor de saldo (50). Observe que la ventana Variables locales muestra las propiedades de una clase como datos, razón por la cual vemos la propiedad Saldo y la variable de instancia saldo en esta ventana. Además, también se muestran el valor actual de la variable local montoDeposito (49.99) y el parámetro args de Main. 5. Evaluar expresiones aritméticas y booleanas. Podemos evaluar expresiones aritméticas y booleanas usando la ventana Inspección. Seleccione Depurar > Ventanas > Inspección para mostrar esta ventana (figura C.13). En la primera fila de la columna Nombre (que al principio debe estar en blanco) escriba (montoDeposito + 10) * 5 y oprima Intro. A continuación aparecerá el valor 299.95 (figura C.13). En la siguiente fila de la

Figura C.9 | Establecer puntos de interrupción en las líneas 23 y 26.

Figura C.10 | Escribir el monto a depositar antes de llegar al punto de interrupción.

1058 Apéndice C Uso del depurador de Visual Studio® 2005

Figura C.11 | La ejecución del programa se detiene cuando el depurador llega al punto de interrupción en la línea 23.

Figura C.12 | Análisis de la variable montoDeposito.

Evaluación de una expresión aritmética

Evaluación de una expresión bool

Figura C.13 | Análisis de los valores de las expresiones. columna Nombre en la ventana Inspección, escriba montoDeposito == 200 y oprima Intro. Esta expresión determina si el valor que contiene montoDeposito es 200. Las expresiones que contienen el símbolo == son expresiones bool. El valor devuelto es false (figura C.13), ya que montoDeposito no contiene en ese momento el valor 200. 6. Continuar la ejecución. Seleccione Depurar > Continuar para continuar con la ejecución. La línea 23 se ejecuta, acreditando a la cuenta el monto de depósito, y el programa regresa al modo de interrupción en la línea 26. Seleccione Depurar > Ventanas > Variables locales. Ahora se mostrarán la variable de instancia saldo y la propiedad Saldo con sus valores actualizados (figura C.14). 7. Modificar los valores. Con base en el valor introducido por el usuario (49.99), el saldo de la cuenta producido por el programa debe ser $99.99. No obstante, puede utilizar la ventana Variables locales para modificar los valores de las variables durante la ejecución del programa. Esto puede ser valioso para

C.4 Control de la ejecución mediante los comandos Paso a paso por instrucciones... 1059

Valor actualizado de la variable saldo

Figura C.14 | Mostrar el valor de las variables locales. experimentar con distintos valores y para localizar errores lógicos en los programas. En la ventana Variables locales, haga clic en el campo Valor en la fila saldo para seleccionar el valor 99.99. Escriba 66.99 y oprima Intro. El depurador cambia el valor de saldo (y la propiedad Saldo también) y después muestra su nuevo valor en rojo (figura C.15). Ahora seleccione Depurar > Continuar para ejecutar las líneas 26-27. Observe que el nuevo valor de saldo se muestra en la ventana Símbolo del sistema. 8. Detener la sesión de depuración. Seleccione Depurar > Detener depuración. Elimine todos los puntos de interrupción.

C.4 Control de la ejecución mediante los comandos Paso a paso por instrucciones, Paso a paso por procedimientos, Paso a paso para salir y Continuar Algunas veces es necesario ejecutar un programa línea por línea, para buscar y corregir errores lógicos. El avance paso a paso a lo largo de una parte del programa puede ayudarnos a verificar que el código de un método se ejecute en forma correcta. Los comandos que usted aprenderá en esta sección le permitirán ejecutar un método línea por línea, ejecutar todas las instrucciones de un método o sólo las instrucciones restantes del mismo (si ya ha ejecutado algunas instrucciones dentro del método). 1. Insertar un punto de interrupción. Establezca un punto de interrupción en la línea 23, haciendo clic con el botón izquierdo en la barra indicadora de margen (figura C.16). 2. Iniciar el depurador. Seleccione Depurar > Iniciar depuración. Introduzca el valor 49.99 en el indicador Escriba el monto a depositar para cuenta1:. La ejecución del programa se detendrá cuando éste llegue al punto de interrupción en la línea 23. 3. Uso del comando Paso a paso por instrucciones. El comando Paso a paso por instrucciones ejecuta la siguiente instrucción en el programa (la línea resaltada de la figura C.17) y se detiene de inmediato. Si la instrucción a ejecutar es la llamada a un método, el control se transfiere al método que se llamó.

Valor modificado en el depurador

Figura C.15 | Modificar el valor de una variable.

1060 Apéndice C Uso del depurador de Visual Studio® 2005

Figura C.16 | Establecimiento de un punto de interrupción en el programa.

La siguiente instrucción a ejecutar es la llamada a un método

Figura C.17 | Uso del comando Paso a paso por instrucciones para ejecutar una instrucción. El comando Paso a paso por instrucciones nos permite seguir la ejecución en un método y confirmar su ejecución, al ejecutar en forma individual cada instrucción dentro del método. Seleccione Depurar > Paso a paso por instrucciones (u oprima F11) para entrar al método Acredita (figura C.18). 4. Uso del comando Paso a paso por procedimientos. Seleccione Depurar > Paso a paso por procedimientos para entrar al cuerpo del método Acredita (línea 17 en la figura C.18) y transferir el control a la línea 18 (figura C.19). El comando Paso a paso por instrucciones se comporta de manera similar al comando Paso a paso por instrucciones cuando la siguiente instrucción a ejecutar no contiene una llamada a un método ni accede a una propiedad. En el paso 10 verá la diferencia entre el comando Paso a paso por procedimientos y el comando Paso a paso por instrucciones.

Siguiente instrucción a ejecutar

Figura C.18 | Avance paso a paso por el método Acredita.

C.4 Control de la ejecución mediante los comandos Paso a paso por instrucciones... 1061

El control se transfiere a la siguiente instrucción

Figura C.19 | Avanzar paso a paso hasta una instrucción dentro del método Acredita. 5. Usar el comando Paso a paso para salir. Seleccione Depurar > Paso a paso para salir para ejecutar el resto de las instrucciones en el método y devolver el control al método que hizo la llamada. A menudo, en métodos extensos, es conveniente analizar unas cuantas líneas clave de código y después continuar depurando el código del método que hace la llamada. El comando Paso a paso para salir es útil para ejecutar el resto de un método y regresar al método que hizo la llamada. 6. Establecer un punto de interrupción. Establezca un punto de interrupción (figura C.20) en la línea 28 de la figura C.2. Utilizará este punto de interrupción en el siguiente paso. 7. Usar el comando Continuar. Seleccione Depurar > Continuar para ejecutar el programa hasta llegar al siguiente punto de interrupción en la línea 28. Esta característica ahorra tiempo cuando no deseamos avanzar línea por línea a través de muchas líneas de código, para llegar al siguiente punto de interrupción. 8. Detener el depurador. Seleccione Depurar > Detener depuración para terminar la sesión de depuración. 9. Iniciar el depurador. Antes de poder demostrar la siguiente característica del depurador, debe reiniciarlo. Inicie el depurador como en el paso 2, y escriba el mismo valor (49.99). A continuación, el depurador suspenderá la ejecución en la línea 23. 10. Usar el comando Paso a paso por procedimientos. Seleccione Depurar > Paso a paso por procedimientos (figura C.21). Recuerde que este comando se comporta de manera similar al comando Paso a paso por instrucciones, cuando la siguiente instrucción a ejecutar no contiene una llamada a un método. Si la siguiente instrucción a ejecutar contiene una llamada a un método, el método que recibe la llamada se ejecuta en su totalidad (sin pasar la ejecución a ninguna de las instrucciones que contiene, a menos que haya un punto de interrupción en el método), y la flecha avanza a la siguiente línea ejecutable (después

Figura C.20 | Establecimiento de un segundo punto de interrupción en el programa.

1062 Apéndice C Uso del depurador de Visual Studio® 2005

El método Acredita se ejecuta sin entrar paso a paso en él cuando se selecciona el comando Paso a paso por procedimientos.

Figura C.21 | Uso del comando Paso a paso por procedimientos del depurador. de la llamada al método) en el método actual. En este caso, el depurador ejecuta la línea 23, que se encuentra en Main (figura C.2). La línea 23 llama al método Acredita. Después, el depurador detiene la ejecución en la línea 26, la siguiente instrucción ejecutable. 11. Detener el depurador. Seleccione Depurar > Detener depuración. Elimina todos los puntos de interrupción restantes.

C.5 Otras características Visual Studio 2005 proporciona muchas características de depuración que simplifican el proceso de prueba y depuración. En esta sección, hablaremos sobre algunas de estas características.

C.5.1 Editar y Continuar

La característica Editar y continuar nos permite realizar modificaciones o cambios en el código en modo de depuración, y después continuar ejecutando el programa sin tener que recompilar el código. 1. Establecer un punto de interrupción. Establezca un punto de interrupción en la línea 19 en su ejemplo (figura C.22). 2. Iniciar el depurador. Seleccione Depurar > Iniciar depuración. Cuando empieza la ejecución, se muestra el saldo de cuenta1. El depurador entra al modo de interrupción cuando llega al punto de interrupción en la línea 19.

Figura C.22 | Establecimiento de un punto de interrupción en la línea 19.

C.5 Otras características 1063 3. Modificar el texto de información de entrada. Suponga que desea modificar el texto indicador de entrada, para proveer al usuario un rango de valores para la variable montoDeposito. En vez de detener el proceso de depuración, agregue el texto "(de $1 a $500):" al final del mensaje "Escriba el monto a depositar para cuenta1" en la línea 19, en la ventana de vista de código (figura C.23). Seleccione Depurar > Continuar. La aplicación le pedirá que introduzca los datos, usando el texto actualizado (figura C.24). En este ejemplo quisimos hacer una modificación en el texto para nuestro mensaje indicador de entrada antes de ejecutar la línea 19. No obstante, si desea realizar una modificación en una línea que ya se ejecutó, debe seleccionar una instrucción anterior en su código desde la que se va a continuar la ejecución. 1. Establecer un punto de interrupción. Establezca un punto de interrupción en la línea 21 (figura C.25). 2. Iniciar el depurador. Elimine el texto "(de $1 a $500)" que acaba de agregar en los pasos anteriores. Seleccione Depurar > Iniciar depuración. Al comenzar la ejecución, aparece el indicador Escriba el monto a depositar para cuenta 1:. Introduzca el valor 650 en el indicador (figura C.26). El depurador entrará en el modo de interrupción en la línea 21 (figura C.26).

Figura C.23 | Modificación del texto del indicador de entrada, mientras la aplicación se encuentra en modo de Depuración.

Figura C.24 | Indicador de entrada de la aplicación, que muestra el texto actualizado.

Figura C.25 | Establecimiento de un punto de interrupción en la línea 21.

1064 Apéndice C Uso del depurador de Visual Studio® 2005

Figura C.26 | Detener la ejecución en el punto de interrupción en la línea 21. 3. Modificar el texto del indicador de entrada. Digamos que una vez más desea modificar el texto del indicador de entrada, para proveer al usuario un rango de valores para la variable montoDeposito. Agregue el texto "(de $1 a $500):" al final del mensaje "Escriba el monto a depositar para cuenta1" en la línea 19, dentro de la ventana de vista de código. 4. Establecer la siguiente instrucción. Para que el programa actualice el texto del indicador de entrada en forma correcta, debe establecer el punto de ejecución en una línea anterior de código. Haga clic con el botón derecho del ratón en la línea 16 y seleccione la opción Establecer instrucción siguiente del menú que aparezca (figura C.27). 5. Seleccione Depurar > Continuar. La aplicación le pedirá de nuevo la entrada, usando el texto actualizado (figura C.28). 6. Detener el depurador. Seleccione Depurar > Detener depuración.

Figura C.27 | Establecer la siguiente instrucción a ejecutar.

C.5 Otras características 1065

Figura C.28 | La ejecución del programa continúa con el texto del indicador de entrada actualizado. Una vez que el programa empieza a ejecutarse, no se permiten ciertos tipos de modificaciones con la característica Editar y Continuar. Estas modificaciones incluyen cambiar los nombres de las clases, agregar o eliminar parámetros de los métodos, agregar campos public a una clase, y agregar o eliminar métodos. Si desea hacer una modificación específica a su programa, que no se permita durante el proceso de depuración, Visual Studio muestra un cuadro de diálogo como el de la figura C.29.

C.5.2 Ayudante de excepciones El Asistente de excepciones es otra nueva característica en Visual Studio 2005. Puede ejecutar un programa, seleccionando ya sea Depurar > Iniciar depuración o Depurar > Iniciar sin depurar. Si selecciona la opción Depurar > Iniciar depuración y el entorno en tiempo de ejecución detecta excepciones sin atrapar, la aplicación se detiene y aparece una ventana llamada Ayudante de excepciones, la cual indica en dónde ocurrió la excepción, el tipo de la misma y vínculos a información útil acerca de cómo manejar la excepción. En la sección 12.4.3 hablamos con detalle sobre el Ayudante de excepciones.

C.5.3 Depuración Sólo mi código ( Just My Code™) A lo largo de este libro hemos producido programas cada vez más robustos, que a menudo incluyen una combinación de código escrito por el programador y código generado por Visual Studio. El código generado por el IDE puede ser difícil de entender para los principiantes (e incluso hasta para los programadores experimentados); por fortuna, raras veces necesitará ver este código. Visual Studio 2005 cuenta con una nueva característica de depuración llamada Sólo mi código ( Just My Code™), la cual permite a los programadores probar y depurar sólo la parte del código que escribieron. Cuando se habilita esta opción, el depurador siempre avanza paso a paso, ignorando las llamadas a los métodos de las clases que usted no escribió. Puede modificar esta configuración en las opciones del depurador. Seleccione Herramientas > Opciones. En el cuadro de diálogo Opciones, seleccione la categoría Depuración para ver las herramientas de depuración y las opciones disponibles. Después haga clic en la casilla de verificación que aparezca enseguida de la opción Habilitar Sólo mi código (sólo administrado) (figura C.30), para habilitar o deshabilitar esta característica.

C.5.4 Otras nuevas características del depurador Todas las características que hemos visto hasta ahora en esta sección están disponibles en todas las versiones de Visual Studio, incluyendo Visual C# 2005 Express Edition. El depurador de Visual Studio 2005 ofrece nuevas características adicionales, como visualizadores, puntos de seguimiento y más, de las que encontrará más información en msdn2.microsoft.com/es-es/library/01xdt7cs(VS.80).aspx.

Figura C.29 | Cuadro de diálogo que indica que ciertas ediciones en el programa no se permiten durante su ejecución.

1066 Apéndice C Uso del depurador de Visual Studio® 2005

Figura C.30 | Habilitar la característica de depuración Sólo mi código en Visual Studio.

C.6 Conclusión En este apéndice aprendió a habilitar el depurador y establecer puntos de interrupción, para poder examinar su código y los resultados mientras se ejecuta un programa. Esta capacidad le permite localizar y corregir los errores lógicos en sus programas. También aprendió a continuar la ejecución después de que un programa suspende su ejecución en un punto de interrupción, y cómo deshabilitar y eliminar los puntos de interrupción. Le mostramos cómo utilizar las ventanas Inspección y Variables locales del depurador para evaluar expresiones aritméticas y booleanas. También demostramos cómo modificar el valor de una variable durante la ejecución del programa, para poder ver cómo los cambios en los valores afectan a sus resultados. Aprendió a utilizar el comando Paso a paso por instrucciones del depurador, para depurar métodos a los que se llama durante la ejecución de su programa. Vio cómo puede usarse el comando Paso a paso por procedimientos para ejecutar la llamada a un método sin detener el método al que se llamó. Utilizó el comando Paso a paso para salir para continuar la ejecución hasta el final del método actual. También aprendió que el comando Continuar continúa la ejecución hasta encontrar otro punto de interrupción o el fin del programa. Por último, hablamos sobre las nuevas características del depurador de Visual Studio 2005, incluyendo Editar y Continuar, el Ayudante de excepciones y la depuración Sólo mi código (Just My Code).

D

Conjunto de caracteres ASCII

0

1

2

3

4

5

6

7

8

9

0

nul

soh

stx

etx

eot

enq

ack

bel

bs

ht

1

nl

vt

ff

cr

so

si

dle

dc1

c2

dc3

2

dc4

nak

syn

etb

can

em

sub

esc

fs

gs

3

rs

us

sp

!

"

#

$

%

&



4

(

)

*

+

,

-

.

/

0

1

5

2

3

4

5

6

7

8

9

:

;

6




?

@

A

B

C

D

E

7

F

G

H

I

J

K

L

M

N

O

8

P

Q

R

S

T

U

V

W

X

Y

9

Z

[

\

]

^

_



a

b

c

10

d

e

f

g

h

i

j

k

l

m

11

n

o

p

q

r

s

t

u

v

w

12

x

y

z

{

|

}

~

del

Figura D.1 | El conjunto de caracteres ASCII. La columna izquierda de la tabla contiene los dígitos izquierdos de los equivalentes decimales (0-127) del código de caracteres, y los dígitos en la parte superior de la tabla son los dígitos derechos del código de caracteres. Por ejemplo, el código de carácter para la “F” es 70, mientras que el código de carácter para el “&” es 38. La mayoría de los usuarios de este libro estarán interesados en el conjunto de caracteres ASCII utilizado para representar los caracteres del idioma español en muchas computadoras. El conjunto de caracteres ASCII es un subconjunto del conjunto de caracteres Unicode utilizado por C# para representar caracteres de la mayoría de los lenguajes existentes en el mundo. Para obtener más información acerca del conjunto de caracteres Unicode, vea el apéndice E.

E

Unicode®

OBJETIVOS En este apéndice aprenderá lo siguiente: Q

Los fundamentos de Unicode.

Q

La misión del consorcio Unicode.

Q

La base del diseño de Unicode.

Q

Las tres formas de codificación de Unicode: UTF-8, UTF-16 y UTF-32.

Q

Los caracteres y glifos.

Q

Las ventajas y desventajas de usar Unicode.

Pla n g e ne r a l

1070 Apéndice E Unicode®

E.1 E.2 E.3 E.4 E.5 E.6

Introducción Formatos de transformación de Unicode Caracteres y glifos Ventajas y desventajas de Unicode Uso de Unicode Rangos de caracteres

E.1 Introducción

El uso de codificaciones de caracteres (es decir, valores numéricos asociados con caracteres) inconsistentes al desarrollar productos de software globales provoca graves problemas, ya que las computadoras procesan la información utilizando números. Por ejemplo, el carácter “a” se convierte en un valor numérico para que una computadora pueda manipular esa pieza de información. Muchos países y corporaciones han desarrollado sus propios sistemas de codificación, que son incompatibles con los sistemas de codificación de otros países y corporaciones. Por ejemplo, el sistema operativo Microsoft Windows asigna el valor 0xC0 al carácter “A con un acento g;rave”; mientras que el sistema operativo Apple Macintosh asigna ese mismo valor a un signo de interrogación de apertura. Esto produce una mala representación y la posible corrupción de los datos, ya que éstos no se procesan de la manera esperada. A falta de un estándar universal de codificación de caracteres implementado ampliamente, los desarrolladores de software de todo el mundo tuvieron que localizar sus productos extensivamente, antes de distribuirlos. La localización incluye la traducción del lenguaje y la adaptación cultural del contenido. El proceso de localización generalmente incluye modificaciones considerables al código fuente (como la conversión de valores numéricos y las suposiciones subyacentes por parte de los programadores), lo cual produce un aumento en costos y retrasos en la liberación del software. Por ejemplo, algunos programadores de habla inglesa podrían diseñar productos de software global suponiendo que un solo carácter puede ser representado por un byte. Sin embargo, cuando esos productos se localizan en mercados asiáticos las suposiciones del programador ya no son válidas, por lo cual la mayor parte del código (si no es que todo) necesita volver a escribirse. La localización es necesaria cada vez que se libera una versión. Para cuando el producto de software se localiza para un mercado específico ya está lista una nueva versión, que también necesita localizarse. Como resultado, es muy complicado y costoso producir y distribuir productos de software global en un mercado en el que no existe un estándar universal de codificación de caracteres. En respuesta a esta situación se creó el Estándar Unicode, que facilita la producción y distribución de software; este estándar resume una especificación para producir la codificación consistente de los caracteres y símbolos de todo el mundo. Los productos de software que se encargan del texto codificado en el estándar Unicode necesitan localizarse, pero este proceso es más simple y eficiente debido a que los valores numéricos no necesitan convertirse y las suposiciones de los programadores acerca de la codificación de caracteres son universales. El estándar Unicode es mantenido por una organización sin fines de lucro conocida como Consorcio Unicode, cuyos miembros incluyen a Apple, IBM, Microsoft, Oracle, Sun Microsystems, Sybase y muchos otros. Cuando el Consorcio ideó y desarrolló el estándar Unicode, querían un sistema de codificación que fuera universal, eficiente, uniforme y sin ambigüedades. Un sistema de codificación universal comprende a todos los caracteres que se utilizan comúnmente. Un sistema de codificación eficiente permite que los archivos de texto se analicen con facilidad. Un sistema de codificación uniforme asigna valores fijos a todos los caracteres. Un sistema de codificación sin ambigüedades representa a un carácter dado en una manera consistente. A estos cuatro términos se les conoce como la base del diseño del estándar Unicode.

E.2 Formatos de transformación de Unicode Aunque Unicode incorpora el conjunto de caracteres (es decir, una colección de caracteres) ASCII limitado, comprende un conjunto de caracteres más comprensivo. En ASCII, cada carácter está representado por un byte que contiene 0s y 1s. Un byte es capaz de almacenar los números binarios de 0 a 255. A cada carácter se le asigna un número entre 0 y 255, por lo cual los sistemas basados en ASCII sólo soportan 256 caracteres, una minúscula fracción de los caracteres existentes en el mundo. Unicode extiende el conjunto de caracteres ASCII al codificar la gran mayoría

E.3 Caracteres y glifos 1071 de los caracteres de todo el mundo. El estándar Unicode codifica a todos esos caracteres en un espacio numérico uniforme, de 0 a 10FFFF en hexadecimal. Una implementación expresará estos números en uno de varios formatos de transformación, seleccionando el que se adapte mejor a la aplicación específica en consideración. Hay tres de esos formatos en uso: UTF-8, UTF-16 y UTF-32, dependiendo del tamaño de las unidades (en bits) utilizadas. UTF-8, un formato de codificación de anchura variable, requiere de uno a cuatro bytes para expresar cada uno de los caracteres Unicode. Los datos de UTF-8 consisten en bytes de 8 bits (secuencias de uno, dos, tres o cuatro bytes, dependiendo del carácter que se vaya a codificar) y son bastante adecuados para los sistemas basados en ASCII, cuando predominan los caracteres de un byte (ASCII representa los caracteres como un byte). En la actualidad, UTF-8 se implementa ampliamente en sistemas UNIX y bases de datos. El formato de codificación UTF-16 de anchura variable expresa los caracteres Unicode en unidades de 16 bits (es decir, como dos bytes adyacentes, o un entero corto en muchos equipos). La mayoría de los caracteres de Unicode se expresan en una sola unidad de 16 bits. Sin embargo, los caracteres con valores por arriba de FFFF en hexadecimal se expresan mediante un par ordenado de unidades de 16 bits, conocidas como sustitutos. Los sustitutos son enteros de 16 bits en el rango de D800 a DFFF, que se utilizan únicamente para el propósito de “escapar” hacia caracteres con una numeración más alta. De esta forma pueden expresarse aproximadamente un millón de caracteres. Aunque un par de sustitutos requiere de 32 bits para representar caracteres, al utilizar estas unidades de 16 bits el uso del espacio se hace eficiente. Los sustitutos son caracteres raros en las implementacio nes actuales. Muchas implementaciones para el manejo de cadenas se escriben en términos de UTF-16. [Nota: los detalles y el código de ejemplo para el manejo de UTF-16 están disponibles en el sitio Web del consorcio Unicode, en www.unicode.org.] Las implementaciones que requieren de un uso considerable de caracteres raros o secuencias de comandos completas con una codificación por encima del FFFF hexadecimal deben usar UTF-32, un formato de codificación de 32 bits con anchura fija que, por lo general, requiere del doble de memoria que los caracteres codificados con UTF-16. La principal ventaja del formato de codificación UTF-32 de anchura fija es que expresa uniformemente a todos los caracteres, por lo que es fácil de manejar en los arreglos. La figura E.1 muestra las distintas formas en las que los tres formatos manejan la codificación de caracteres. Hay unos cuantos lineamientos que indican cuándo utilizar un formato de codificación específico. El mejor formato a utilizar depende de los sistemas computacionales y los protocolos de negocios, no de la información en sí. Generalmente debe utilizarse el formato de codificación UTF-8 cuando los sistemas computacionales y los protocolos de negocios requieren que la información se maneje en unidades de 8 bits, especialmente en los sistemas heredados que se están actualizando, ya que esto a menudo simplifica los cambios que deben realizarse en los programas existentes. Por esta razón, UTF-8 se ha convertido en el formato de codificación preferido en Internet. De la misma forma, UTF-16 es el formato de codificación preferido en aplicaciones para Microsoft Windows. Es probable que UTF-32 se utilice más ampliamente en el futuro, a medida que se codifiquen más caracteres con valores por encima del FFFF hexadecimal. Además, UTF-32 requiere de un manejo menos sofisticado que UTF-16, debido a la presencia de los pares de sustitutos.

E.3 Caracteres y glifos El Estándar Unicode consiste de caracteres, componentes escritos (es decir, alfabetos, números, signos de puntuación, acentos, etc.) que pueden ser representados por valores numéricos. Algunos ejemplos de caracteres son: U+0041, letra A mayúscula en latín. En la primera representación de caracteres, U+ yyyy es un valor de código, en

Carácter

UTF-8

UTF-16

UTF-32

Letra A mayúscula en latín

0x41

0x0041

0x00000041

Letra ALFA mayúscula en griego

0xCD 0x91

0x0391

0x00000391

Ideograma unificado CJK-4E95

0xE4 0xBA 0x95

0x4E95

0x00004E95

Letra A en cursiva antigua

0xF0 0x80 0x83 0x80

0xDC00 0xDF00

0x00010300

Figura E.I | La correlación entre los tres formatos de codificación.

1072 Apéndice E Unicode® donde U+ se refiere a los valores de código de Unicode, a diferencia de los demás valores hexadecimales. Las letras yyyy representan un número hexadecimal de cuatro dígitos, perteneciente a un carácter codificado. Los valores de códigos son combinaciones de bits que representan caracteres codificados. Los caracteres se representan utilizando glifos, varias figuras, tipos de letra y tamaños para mostrar caracteres. No hay valores de código para los glifos en el estándar Unicode. En la figura E.2 se muestran ejemplos de glifos. El estándar Unicode comprende los alfabetos, ideogramas, silabarios, signos de puntuación, signos diacríticos, operadores matemáticos, etc., que comprenden los lenguajes escritos y manuscritos del mundo. Un signo diacrítico es un signo especial que se agrega a un carácter para diferenciarlo de otra letra, o para indicar un acento (por ejemplo, en español se utiliza la tilde “~” por encima del carácter “n”). Actualmente, Unicode proporciona los valores de código para 94,140 representaciones de caracteres, con más de 880,000 valores de código reservados para una expansión en el futuro.

E.4 Ventajas y desventajas de Unicode El estándar Unicode tiene varias ventajas considerables que promueven su uso. Una es el impacto que tiene sobre el rendimiento de la economía internacional. Unicode estandariza los caracteres para los sistemas de escritura mundiales en un modelo uniforme que promueve la transferencia y la compartición de información. Los programas que se desarrollan usando este esquema mantienen su precisión, ya que cada carácter tiene una sola definición (es decir, a es siempre U+0061, % es siempre U+0025). Esto permite a las corporaciones administrar las altas demandas de los mercados internacionales, al procesar distintos sistemas de escritura al mismo tiempo. Además, los caracteres pueden administrarse en forma idéntica, evitando así la confusión ocasionada por las distintas arquitecturas de códigos de caracteres. Lo que es más, al administrar los datos de una manera consistente se elimina la corrupción de los mismos, ya que la información puede ordenarse, buscarse y manipularse utilizando un proceso consistente. Otra ventaja del estándar Unicode es la portabilidad (es decir, software que puede ejecutarse en computadoras distintas o con distintos sistemas operativos). La mayoría de los sistemas operativos, bases de datos, lenguajes de programación y navegadores Web soportan actualmente, o planean soportar, Unicode. Una desventaja del estándar Unicode es la cantidad de memoria que requieren UTF-16 y UTF-32. Los conjuntos de caracteres ASCII tienen una longitud de 8 bits, por lo que requieren de una menor capacidad de almacenamiento que el conjunto de caracteres Unicode predeterminado de 16 bits. Sin embargo, el conjunto de caracteres de doble byte (DBCS) y el conjunto de caracteres multibyte (MBCS) que codifican caracteres asiáticos (ideogramas) requieren de dos a cuatro bytes, respectivamente. En tales casos, pueden usarse los formatos de codificación UTF-16 o UTF-32 con pocas restricciones en cuanto a memoria y rendimiento.

E.5 Uso de Unicode Visual Studio utiliza la codificación UTF-16 de Unicode para representar a todos los caracteres. La figura E.3 usa C# para mostrar el texto “Bienvenido a Unicode” en ocho lenguajes: inglés, francés, alemán, japonés, portugués, ruso, español y chino tradicional. [Nota: el sitio Web del Consorcio Unicode contiene un vínculo a las tablas de caracteres en las que se enlistan los valores de código de Unicode de 16 bits.] El primer mensaje de bienvenida (líneas 19-23) contiene los códigos hexadecimales para el texto en inglés. La página Code Charts en el sitio Web del Consorcio Unicode contiene un documento que lista los valores de códigos para el bloque (o categoría) Basic Latin (latín básico), que incluye el alfabeto en inglés. Los códigos hexadecimales en las líneas 19-20 corresponden a “Bienvenido” y un carácter de espacio (\u0020). Los caracteres de Unicode en C# utilizan el formato \uyyyy, en donde yyyy representa la codificación Unicode hexadecimal. Por ejemplo, la letra “B” (de “Bienvenido”) se representa por el código \u0057. Los valores hexadecimales para la palabra “a” y un carácter de espacio aparecen en la línea 21, y la palabra “Unicode” en la línea 23. “Unicode” no está codificada, ya que es una marca registrada y no tiene traducción equivalente en la mayoría de los lenguajes.

Figura E.2 | Distintos glifos para la letra A.

E.5 Uso de Unicode 1073

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

// Fig. E.3: UnicodeForm.cs // Demostración de la codificación Unicode. using System; using System.Windows.Forms; namespace DemoUnicode { public partial class UnicodeForm : Form { public UnicodeForm() { InitializeComponent(); } // asigna cadenas Unicode a cada control Label private void UnicodeForm_Load( object sender, EventArgs e ) { // Inglés char[] ingles = { '\u0057', '\u0065', '\u006C', '\u0063', '\u006F', '\u006D', '\u0065', '\u0020', '\u0074', '\u006F', '\u0020' }; inglesLabel.Text = new string( ingles ) + "Unicode" + '\u0021'; // Francés char[] frances = { '\u0042', '\u0069', '\u0065', '\u006E', '\u0076', '\u0065', '\u006E', '\u0075', '\u0065', '\u0020', '\u0061', '\u0075', '\u0020' }; francesLabel.Text = new string( frances ) + "Unicode" + '\u0021'; // Alemán char[] aleman = { '\u0057', '\u0069', '\u006C', '\u006B', '\u006F', '\u006D', '\u006D', '\u0065', '\u006E', '\u0020', '\u007A', '\u0075', '\u0020' }; alemanLabel.Text = new string( aleman ) + "Unicode" + '\u0021'; // Japonés char[] japones = { '\u3078', '\u3087', '\u3045', '\u3053', '\u305D', '\u0021' }; japonesLabel.Text = "Unicode" + new string( japones ); // Portugués char[] portugues = { '\u0053', '\u0065', '\u006A', '\u0061', '\u0020', '\u0062', '\u0065', '\u006D', '\u0020', '\u0076', '\u0069', '\u006E', '\u0064', '\u006F', '\u0020', '\u0061', '\u0020' }; portuguesLabel.Text = new string( portugues ) + "Unicode" + '\u0021'; // Ruso char[] ruso = { '\u0414', '\u043E', '\u0431', '\u0440', '\u043E', '\u0020', '\u043F', '\u043E', '\u0436', '\u0430', '\u043B', '\u043E', '\u0432', '\u0430', '\u0442', '\u044A', '\u0020', '\u0432', '\u0020' }; rusoLabel.Text = new string( ruso ) + "Unicode" + '\u0021';

Figura E.3 | Aplicación Windows que demuestra la codificación Unicode. (Parte 1 de 2).

1074 Apéndice E Unicode®

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74

// Español char[] espaniol = { '\u0042', '\u0069', '\u0065', '\u006E', '\u0076', '\u0065', '\u006E', '\u0069', '\u0064', '\u006F', '\u0020', '\u0061', '\u0020' }; espaniolLabel.Text = new string( espaniol ) + "Unicode" + '\u0021'; // Chino simplificado char[] chino = { '\u6B22', '\u8FCE', '\u4F7F', '\u7528', '\u0020' }; chinoLabel.Text = new string( chino ) + "Unicode" + '\u0021'; } // fin del método UnicodeForm_Load } // fin de la clase UnicodeForm } // fin del espacio de nombres DemoUnicode

Figura E.3 | Aplicación Windows que demuestra la codificación Unicode. (Parte 2 de 2). El resto de los mensajes de bienvenida (líneas 26-71) contienen los códigos hexadecimales para los otros siete lenguajes. Los valores de código utilizados para el texto en francés, alemán, portugués y español se encuentran en el bloque Basic Latin, los valores de código utilizados para el texto en chino tradicional se encuentran en el bloque CJK Unified Ideographs, los valores de código usados para el texto en ruso se encuentran en el bloque Cyrillic y los valores de código usados para el texto en japonés se encuentran en el bloque Hiragana. [Nota: para representar los caracteres asiáticos en una aplicación Windows, necesita instalar los archivos de lenguaje apropiados en su computadora. Para ello, abra el cuadro de diálogo Configuración regional y de idioma del Panel de control (Inicio > Panel de control). Al final de la ficha Idiomas hay una lista de lenguajes. Seleccione las casillas de verificación Japonés y Chino tradicional, y haga clic en Aplicar. Siga las indicaciones del asistente de instalación para instalar los lenguajes. Para obtener ayuda adicional, visite www.unicode.org/help/display_ problems.html.

E.6 Rangos de caracteres El estándar Unicode asigna valores de código, que varían de 0000 (Latín básico) a E007F (Marcas), a los caracteres escritos de todo el mundo. Actualmente existen valores de código para 94,140 caracteres. Para simplificar la búsqueda de un carácter y su valor de código asociado, el estándar Unicode generalmente agrupa los valores de código por escritura y función (es decir, los caracteres en latín se agrupan en un bloque, los operadores matemáticos en otro bloque, etc.). Como regla, una escritura es un sistema de escritura individual que se utiliza para varios lenguajes (por ejemplo, la escritura del latín se utiliza para el inglés, francés, español, etc.). La página Code Charts (Tablas de códigos) en el sitio Web del Consorcio Unicode enlista todos los bloques definidos y sus respectivos valores de códigos. En la figura E.4 se enlistan algunos bloques (escrituras) del sitio Web y su rango de valores de código.

E.6 Rangos de caracteres 1075

Escritura

Rango de valores de código

Arábica Latín básico Bengalí (La India) Cherokee (nativo de América) Ideogramas Unificados CJK (Este de Asia) Cirílico (Rusia y Este de Europa) Etiope Griego Hangul Jamo (Corea) Hebreo Hiragana (Japón) Khmer (Cambodia) Lao (Laos) Mongol Myanmar Ogham (Irlanda) Runic (Alemania y Escandinavia) Sinhala (Sri Lanka) Telugu (La India) Thai

U+0600–U+06FF U+0000–U+007F U+0980–U+09FF U+13A0–U+13FF U+4E00–U+9FAF U+0400–U+04FF U+1200–U+137F U+0370–U+03FF U+1100–U+11FF U+0590–U+05FF U+3040–U+309F U+1780–U+17FF U+0E80–U+0EFF U+1800–U+18AF U+1000–U+109F U+1680–U+169F U+16A0–U+16FF U+0D80–U+0DFF U+0C00–U+0C7F U+0E00–U+0E7F

Figura E.4 | Algunos rangos de caracteres.

F

Introducción a XHTML: parte 1

Leer entre líneas fue más fácil que seguir el texto. —Henry James

OBJETIVOS En este apéndice aprenderá lo siguiente: Q

Comprender los componentes importantes de los documentos en XHTML.

Q

Utilizar el lenguaje XHTML para crear páginas Web.

Q

Añadir imágenes a páginas Web.

Q

Crear y utilizar los hipervínculos para navegar en las páginas Web.

Q

Realizar el marcado de listas de información.

Los pensamientos de alto nivel deben poseer un lenguaje de alto nivel. —Aristófanes

Pla n g e ne r a l

1078 Apéndice F Introducción a XHTML: parte 1

F.1 F.2 F.3 F.4 F.5 F.6 F.7 F.8 F.9 F.10 F.11

Introducción Edición de XHTML El primer ejemplo en XHMTL Servicio de validación de XHTML del W3C Encabezados Vínculos Imágenes Caracteres especiales y más saltos de línea Listas desordenadas Listas anidadas y ordenadas Recursos Web

F.1 Introducción Bienvenidos al mundo de oportunidades creadas por World Wide Web. Internet tiene ya tres décadas de edad pero no fue sino hasta que la Web se volvió popular en la década de 1990 que comenzó la explosión de oportunidades que aún estamos experimentando. Casi a diario ocurren nuevos y excitantes desarrollos; el ritmo de la innovación nunca ha sido compartido por ninguna otra tecnología. En este apéndice desarrollaremos nuestras propias páginas Web. A medida que progresemos en el libro, crearemos páginas Web cada vez más agradables y poderosas. En la parte final de este libro aprenderemos a crear aplicaciones completas basadas en la Web. En este apéndice comenzamos a desatar el poder del desarrollo de las aplicaciones basadas en Web con el lenguaje XHTML (Lenguaje de marcado de hipertexto extensible). En el siguiente apéndice introduciremos técnicas para XHTML más sofisticadas como las tablas, que son particularmente útiles para estructurar información que se obtiene de bases de datos (en otras palabras, el software que almacena conjuntos estructurados de datos) y las hojas de estilo en cascada (CSS ), que hacen que las páginas Web sean más agradables a la vista. A diferencia de los lenguajes de programación por procedimientos como C, Fortran, Cobol y Pascal, XHTML es un lenguaje de marcado que especifica el formato del texto que se presenta a través de un explorador Web como Internet Explorer de Microsoft o Netscape. Un aspecto importante a tomar en cuenta cuando utilizamos XHTML es la separación de la presentación de un documento (es decir, la apariencia del documento cuando se presenta a través de un explorador Web) de la estructura de la información de dicho documento. El lenguaje XHTML está basado en el lenguaje HTML (Lenguaje de marcado de hipertexto): una tecnología estandarizada por el Consorcio World Wide Web (W3C). En HTML era común especificar el contenido, la estructura y el formato de un documento. El formato puede especificar en dónde va a colocar el explorador un elemento en una página Web, o el tipo de letra y los colores utilizados para presentar un elemento. La versión 1.1 de XHTML (la versión más reciente del W3C de la Recomendación para XHTML, al momento de publicar este libro) sólo permite que el contenido y la estructura de un documento aparezcan en un documento en XHTML válido, y no su formato. Por lo general, dicho formato se especifica mediante las Hojas de estilo en cascada. Todos nuestros ejemplos en este apéndice se basan en la Recomendación para XHTML 1.1.

F.2 Edición de XHTML

En este apéndice escribiremos XHTML en su formato de código fuente. También crearemos documentos XHTML a través de un editor de texto (por ejemplo, Bloc de notas, Wordpad, vi, emacs) y los guardaremos con la extensión de archivo .html o htm.dany

Buena práctica de programación F.1 Asigne a sus documentos nombres de archivo que describan su función. Esta práctica le ayudará a identificar documentos con más rapidez. También ayuda a las personas que desean crear un vínculo a una página, al darles un nombre fácil de recordar. Por ejemplo, si está escribiendo un documento en XHTML que contiene información sobre productos, quizás sea conveniente llamarlo productos.html.

F.3 El primer ejemplo en XHTML 1079 Las computadoras que ejecutan software especializado, llamadas servidores Web, almacenan documentos en XHTML. Los clientes (en otras palabras, los exploradores Web) solicitan al servidor Web recursos específicos, tales como los documentos en XHTML. Por ejemplo, al escribir la dirección www.deitel.com/libros/descargas.html en el campo de dirección del explorador Web, se solicita el archivo descargas.html al servidor de Web que se ejecuta en www.deitel.com. Este documento está ubicado en el servidor, dentro de un directorio llamado libros. Por ahora, simplemente colocaremos los documentos en XHTML en nuestro equipo y los abriremos mediante Internet Explorer.

F.3 El primer ejemplo en XHTML En este apéndice y en el siguiente, presentaremos marcado de XHTML y proporcionaremos capturas de pantalla para mostrar cómo Internet Explorer representa (es decir, visualiza) el XHTML.1 Cada documento en XHTML que mostramos incluye números de línea para la conveniencia del lector. Estos números de línea no son parte de los documentos en XHTML. Nuestro primer ejemplo (figura F.1) es un documento en XHTML llamado principal.html, que presenta el mensaje “¡Bienvenido a XHTML!” en el explorador. La línea clave en el programa es la línea 14, la cual comunica al explorador que muestre el mensaje “¡Bienvenido a XHTML!”. Ahora examinemos cada línea del programa. Las líneas 1-3 son requeridas en los documentos XHTML para conformarse a la sintaxis apropiada de XHTML. Por ahora sólo copiaremos y pegaremos estas líneas en cada documento XHTML que vamos a crear. Las líneas 5-6 son comentarios de XHTML. Los creadores de documentos en XHTML agregan comentarios para mejorar la legibilidad del marcado y para describir el contenido de un documento. Los comentarios también ayudan a que otras personas puedan leer y comprender el marcado y el contenido de un documento en XHTML.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16



Cómo programar en Internet y WWW - Bienvenidos

¡Bienvenidos a XHTML!



Figura F.1 | Primer ejemplo en XHTML.

1.

Todos los ejemplos presentados en este libro están disponibles en www.deitel.com.

1080 Apéndice F Introducción a XHTML: parte 1 Los comentarios no requieren de ninguna acción del explorador Web, cuando el usuario carga el documento en XHTML para visualizarlo. Los comentarios en XHTML siempre comienzan con

Deitel

Prentice Hall

Yahoo!

USA Today



Figura F.5 | Creación de vínculos a otras páginas Web. (Parte 1 de 2).

F.6 Vínculos 1085

Figura F.5 | Creación de vínculos a otras páginas Web. (Parte 2 de 2). del explorador, para confirmar que son idénticas.) Si un servidor Web no puede localizar un documento solicitado, devuelve una indicación de error al explorador Web, y éste muestra una página Web que contiene un mensaje de error para el usuario. Las anclas pueden crear vínculos a direcciones de correo electrónico, utilizando un URL mailto:. Cuando alguien hace clic en este tipo de vínculo anclado, la mayoría de los exploradores ejecutan el programa para correo electrónico predeterminado (por ejemplo, Outlook Express), para permitir al usuario que escriba un mensaje de correo electrónico a la dirección vinculada. La figura F.6 muestra este tipo de ancla. Las líneas 17-19 contienen un vínculo a un correo electrónico. La forma del vínculo de correo electrónico es .... En este caso estamos creando un vínculo a la dirección de correo electrónico [email protected]. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18



Como programar en Internet y WWW - Pagina de Contactos

Mi direccion de correo electronico es

[email protected]

Figura F.6 | Vínculo a una dirección de correo electrónico. (Parte 1 de 2).

1086 Apéndice F Introducción a XHTML: parte 1

19 20 21 22 23 24

. Haga clic en la direccion y su explorador abrira un mensaje de correo electronico y me lo enviara a mi.



Figura F.6 | Vínculo a una dirección de correo electrónico. (Parte 2 de 2).

F.7 Imágenes Los ejemplos que hemos visto hasta ahora demuestran cómo marcar documentos que contienen sólo texto. Sin embargo, la mayoría de las páginas Web contienen tanto texto como imágenes. De hecho, las imágenes son una parte equivalente, sino es que esencial, del diseño de una página Web. Los tres formatos para imágenes más populares utilizados por los desarrolladores Web son el Formato de intercambio de gráficos (GIF), el formato del Grupo unido de expertos en fotografía (JPEG) y las imágenes de Gráficos de red portables (PNG). Los usuarios pueden crear imágenes utilizando aplicaciones de software especializadas, tales como Photoshop Elements 2.0 de Adobe (www.adobe.com), Fireworks de Macromedia (www.macromedia.com) y Paint Shop Pro de Jasc (www.jasc.com). También se pueden adquirir imágenes de varios sitios Web tales como la galería de imágenes de Yahoo! (gallery. yahoo.com). La figura F.7 demuestra cómo incorporar imágenes a las páginas Web.

1 2 3 4 5 6 7 8 9 10



Como programar en Internet y WWW - Bienvenidos

Figura F.7 | Imágenes en archivos de XHTML. (Parte 1 de 2).

F.7 Imágenes 1087

11 12 13 14 15 16 17 18 19 20 21 22



Figura F.7 | Imágenes en archivos de XHTML. (Parte 2 de 2). Las líneas 16-17 utilizan el elemento img para insertar una imagen en el documento. La ubicación del archivo de la imagen se especifica mediante el atributo src del elemento img. En este caso, la imagen está ubicada en el mismo directorio que este documento XHTML, así que sólo se requiere el nombre de archivo de la imagen. Los atributos opcionales width y height especifican la anchura y altura de la imagen, respectivamente. El autor del documento puede escalar la imagen al incrementar o decrementar los valores de sus atributos width y height. Si se omiten estos atributos, el explorador utiliza la anchura y altura actual de la imagen. Las imágenes se miden en píxeles (“elementos de imagen”), los cuales representan puntos de color en la pantalla. La imagen en la figura F.7 tiene 183 píxeles de anchura y 238 píxeles de altura.

Buena práctica de programación F.5 Siempre debemos incluir la anchura (width) y altura (height) de una imagen dentro de la etiqueta . Cuando el explorador cargue el archivo XHTML, sabrá inmediatamente a través de estos atributos cuánto espacio en la pantalla debe proporcionar para la imagen y la colocará adecuadamente en la página incluso antes de que se haya descargado a la imagen.

Tip de rendimiento F.1 Si incluimos los atributos de anchura (width) y altura (height) en una etiqueta , el explorador podría cargar y representar las páginas con más rapidez.

Error común de programación F.4 Si introducimos nuevas medidas para una imagen, de manera que cambien su proporción natural de anchura-altura, se distorsionará la apariencia de la imagen. Por ejemplo, si la imagen es de 200 píxeles de anchura y 100 píxeles de altura, siempre hay que asegurarnos de que cualquier medida nueva que se utilice tenga una proporción de anchuraaltura de 2:1.

1088 Apéndice F Introducción a XHTML: parte 1 Cada elemento img en un documento en XHTML posee un atributo alt. Si un explorador no puede presentar una imagen, presenta el valor del atributo alt. Tal vez el explorador no pueda presentar una imagen por distintas razones. Puede ser que no soporte las imágenes; tal es el caso de un explorador basado en texto (en otras palabras, un explorador que sólo puede mostrar texto), o tal vez el cliente haya deshabilitado la visualización de imágenes para reducir el tiempo de descarga. La figura F.7 muestra la forma en que Internet Explorer 6 presenta el valor del atributo alt, cuando un documento hace referencia a un archivo de imagen inexistente (jhtp.jpg). El atributo alt es importante para la creación de páginas Web accesibles a usuarios con discapacidad, en especial para aquéllos con discapacidad visual que utilizan exploradores basados en texto. A menudo, la gente discapacitada utiliza un software especializado llamado sintetizador de voz. Esta aplicación de software “dice” el valor del atributo alt para que el usuario sepa lo que el explorador está presentando. Algunos elementos de XHTML (llamados elementos vacíos) sólo contienen atributos y no poseen funciones para el marcado de texto (en otras palabras, el texto no está ubicado entre las etiquetas inicial y final). Los elementos vacíos (por ejemplo, img) deben terminarse ya sea utilizando el carácter de barra diagonal (/) dentro del signo mayor que (>) de la etiqueta inicial, o incluyendo la etiqueta final de manera explícita. Al utilizar el carácter de barra diagonal, agregamos un espacio antes del carácter para mejorar la legibilidad (como se muestra al final de las líneas 17 y 19). En vez de utilizar el carácter de barra diagonal, podríamos escribir las líneas 18-19 con una etiqueta de cierre, como se muestra a continuación:

Al utilizar imágenes como hipervínculos, los desarrolladores Web pueden crear páginas Web con gráficos que están vinculados a otros recursos. En la figura F.8 creamos seis diferentes hipervínculos de imágenes. Las líneas 17-20 crean un hipervínculo de imagen al anidar un elemento img en un elemento de ancla (a). El valor del atributo src del elemento img especifica que esta imagen (vinculos.jpg) está ubicada en un directorio llamado botones. El directorio botones y el documento en XHTML se encuentran en el mismo directorio. También se puede hacer referencia a las imágenes de otros documentos Web (después de obtener permiso del dueño del documento), asignando el nombre y la ubicación de la imagen al atributo src. Al hacer clic en un hipervínculo el usuario es llevado a la página Web especificada por el atributo href del elemento de ancla que enmarca al vínculo. En la línea 20 introducimos el elemento br, el cual la mayoría de los exploradores presentan como un salto de línea. Cualquier marcado o texto que siga después de un elemento br se presenta en la siguiente línea. Al igual que el elemento img, br es un ejemplo de un elemento vacío terminado por un carácter de barra diagonal. Hemos agregado un espacio antes del carácter de barra diagonal para mejorar la legibilidad. [Nota: los últimos dos hipervínculos de imágenes en la figura F.8 vinculan a documentos en XHTML (en otras palabras, tabla1.html y formulario.html) que presentamos como ejemplos en el apéndice G, y se incluyen en el directorio de ejemplos del apéndice G. Si hace clic en estos vínculos ahora se producirán errores.]

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16



Como programar en Internet y WWW - Barra de navegación



Figura F.8 | Imágenes como anclas de vínculos. (Parte 1 de 2).

F.7 Imágenes 1089

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49




Figura F.8 | Imágenes como anclas de vínculos. (Parte 2 de 2).

1090 Apéndice F Introducción a XHTML: parte 1

F.8 Caracteres especiales y más saltos de línea Cuando marcamos texto, ciertos caracteres o símbolos (por ejemplo,