Bases De Datos Con Visual Basic_nodrm

  • Uploaded by: Francisco Javier Estrada Ordónñez
  • 0
  • 0
  • March 2021
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Bases De Datos Con Visual Basic_nodrm as PDF for free.

More details

  • Words: 87,659
  • Pages: 297
Loading documents preview...
VisualBasic2005_Primeras.qxp

12/08/2007

13:42

PÆgina i

Bases de datos con Visual Basic

VisualBasic2005_Primeras.qxp

12/08/2007

13:42

PÆgina iii

Luis Durán

Bases de datos con Visual Basic

iv

Índice general

Título: Bases de datos con Visual Basic Autor: © Luis Durán

Editoriales: © MARCOMBO, EDICIONES TÉCNICAS 2007 MARCOMBO, S.A. Gran Via de les Corts Catalanes 594 08007 Barcelona (España) en coedición con: © ALFAOMEGA GRUPO EDITOR, S.A. 2007 C/ Pitágoras 1139 Colonia del Valle - 03100 México D.F. (México) Quedan rigurosamente prohibidas, sin la autorización escrita de los titulares del copyright, bajo las sanciones establecidas en las leyes, la reproducción total o parcial de esta obra por cualquier medio o procedimiento, incluidos la reprografía y el tratamiento informático, así como la distribución de ejemplares mediante alquiler o préstamo públicos. ISBN (por MARCOMBO): 978-84-267-1423-7 ISBN (por ALFAOMEGA GRUPO EDITOR): 978-970-15-1313-2 Impreso en Gráficas Díaz Tudurí D.L.: BI-

VisualBasic2005_Primeras.qxp

12/08/2007

13:42

PÆgina v

Índice general CAPÍTULO 1 Pasar de ADO a ADO.NET . . . . . . . . . . . . . . . . . . . . . . . . . .

1

1.1

Una nueva manera de acceder a los datos . . . . . . . . . . . . .

2

1.1.1 1.1.2

El namespace System.Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Proveedores de datos ADO.NET . . . . . . . . . . . . . . . . . . . . . . . . . .

2 4

1.2

Los objetos básicos de datos ADO.NET . . . . . . . . . . . . . . .

5

1.3

Creando objetos básicos de datos ADO.NET con SqlClient

SqlDataReaders con juegos de resultados múltiples . . . . . . . . . . . . XmlReaders con consulta FOR XML AUTO . . . . . . . . . . . . . . . . . . . Rellenar un DataGridView con un DataReader . . . . . . . . . . . . . . . Devolver una sola fila de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . Devolver un valor escalar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejecutar peticiones que no devuelven datos . . . . . . . . . . . . . . . . .

7

7 9 11 12 12 13

1.4

Aplicar transacciones para actualizar las tablas múltiples .

14

1.5

Utilizando clases de miembros OleDb, SqlXml, y Odbc . . .

17

1.6

Trabajando con datos DataReader y SqlResultSet tipificados . . . . . . . . . . . . . . . . . . . . . . . . . . . Objetos tipificados DataSet de ADO.NET . . . . . . . . . . . . . .

21 22

1.7 1.8 1.9 1.10 1.11

1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6

1.5.1 1.5.2 1.5.3

1.7.1

Substituir OleDb por objetos SqlClient . . . . . . . . . . . . . . . . . . . . . Cambiar SqlConnection y SqlCommand por SqlXmlCommand . . . . Probando el proveedor de datos Odbc . . . . . . . . . . . . . . . . . . . . .

Añadir un juego de datos tipificado desde un servidor SQL, fuente de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Añadir una DataGridView y BindingNavigator Controls Persistir y reabrir el juego de datos . . . . . . . . . . . . . . . . Cambiar de un DataViewGrid a un Details Form . . . . . . Añadir un control de vínculo de datos relacionado . . . .

. . . .

18 19 20

25

. . . .

29 31 32 33

CAPÍTULO 2 Las novedades de ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . .

37

2.1

37

Los objetos de formulario . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 2.1.2

Utilizar DbProviderFactories para crear proyectos con bases de datos agnósticas . . . . . . . . . . . . . . . . . . . . . . . . . . . Restablecer los esquemas de las tablas base . . . . . . . . . . . . . . . . .

38 39 v

VisualBasic2005_Primeras.qxp

12/08/2007

13:42

PÆgina vi

Bases de datos con Visual Basic

2.2

Comprobar las instancias de servidor SQL disponibles y los proveedores de datos ADO.NET 2.0 . . . . . . . . . . . . . 2.2.1

Entradas Batch en tablas de servidor SQL con el objeto SqlBulkCopy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Obtener las estadísticas de conexión del servidor SQL . . . . . . . . . .

46 50

Ejecutar comandos SQL de forma asincrónica . . . . . . . . . .

51

2.3.1 2.3.2 2.3.3 2.3.4

El modelo Polling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El módelo Callback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El modelo WaitAll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Crear tablas de datos independientes . . . . . . . . . . . . . . . . . . . . . .

52 54 57 60

Utilizar tipos Nullable que soporten valores DBNull . . . . . Utilizar objetos persistentes de formulario Windows de ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

65

2.2.2

2.3

2.4 2.5

2.5.1 2.5.2 2.5.3

2.6 2.7

44

Comparando los diseñadores de datos de ADO.NET 1.x y 2.0 . . . ADO.NET 1.x . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

68

69 70 70

Añadir los controles ADO.NET que faltan . . . . . . . . . . . . . Actualizar proyectos de 1.x con componentes de ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

72

Diseñar y mostrar informes con el control ReportViewer .

74

Capítulo 3 Concretando proyectos reales . . . . . . . . . . . . . . . . . . . . . . .

77

3.1

Establecer la arquitectura . . . . . . . . . . . . . . . . . . . . . . . . .

78

3.2

Las arquitecturas referenciales . . . . . . . . . . . . . . . . . . . . . .

79

3.3

Encontrar modelos para proyectos . . . . . . . . . . . . . . . . . .

80

3.3.1 3.3.2 3.3.3 3.3.4 3.3.5

Enterprise Solution Patterns Using Microsoft .NET . . . . . . . . . . . . . Data Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modelos de sistemas distribuidos . . . . . . . . . . . . . . . . . . . . . . . . . Modelos de integración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utilizar librerías de bloques de aplicaciones . . . . . . . . . . . . . . . . . .

81 82 82 82 83

El bloque de aplicación Data Access (Data Access Application Block) . . . . . . . . . . . . . . . . . . . . .

84

3.4.1 3.4.2 3.4.3

El archivo de configuración de datos . . . . . . . . . . . . . . . . . . . . . . . Código de restablecimiento de datos . . . . . . . . . . . . . . . . . . . . . . Código de actualización de datos . . . . . . . . . . . . . . . . . . . . . . . . .

85 86 87

El cliente DataAccessQuickStart . . . . . . . . . . . . . . . . . . . . .

89

2.7.1

2.8

3.4

3.5 vi

3.2.1 3.2.2 3.2.3

Añadir subformularios multinivel . . . . . . . . . . . . . . . . . . . . . . . . . .

Windows Server System Reference Architecture . . . . . . . . . . . . . . . Designando aplicaciones y servicios . . . . . . . . . . . . . . . . . . . . . . . Arquitecrura referencial para el desarrollo empresarial . . . . . . . . . .

71 72

79 80 80

VisualBasic2005_Primeras.qxp

12/08/2007

13:42

PÆgina vii

Índice general

3.6

Seguir las guías de diseño . . . . . . . . . . . . . . . . . . . . . . . . .

91

3.7

Aplicar las directrices de diseño para la biblioteca de clas (Apply Class Library Design Guidelines) . . . . . . . . .

97

3.7.1 3.7.2

Naming Guidelines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Class Member Usage Guidelines . . . . . . . . . . . . . . . . . . . . . . . . . .

97 98

Prepararse para la arquitectura orientada al servicio . . . .

98

3.8.1 3.8.2 3.8.3 3.8.4

99 100 101 103

3.8

3.9 3.10 3.11 3.12

3.6.1 3.6.2 3.6.3

La guía .NET Data Access Architecture Guide . . . . . . . . . . . . . . . . Mejorando el rendimiento y la escalabilidad de la aplicación .NET Diseñar componentes Data Tier y pasar datos por tier . . . . . . . . .

El camino a la Arquitectura orientada al servicio (SOA) . . . . . . . . . Implementar SOA con servicios Web . . . . . . . . . . . . . . . . . . . . . . . Garantizar total interoperabilidad del servicio Web . . . . . . . . . . . . Instalar y publicar el servicio Web DataSetWS . . . . . . . . . . . . . . . .

Use FxCop para validar el código del proyecto . . . . . . Automatizar Test-Driven Development . . . . . . . . . . . . . Ejecutar Best Practices Analyzer para SQL Server 2000 Applicar Best Practices específicas a los proyectos de ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

91 91 92

. . . 105 . . . 106 . . . 106 . . . 107

3.12.1 Use cadenas de conexión idénticas para las conexiones de bases de Pool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.12.2 Definir el tamaño del pool de conexión . . . . . . . . . . . . . . . . . . . . . 3.12.3 Guardar cadenas de conexión en archivos de configuración . . . . . 3.12.4 Encriptar cadenas de conexión que contienen nombres de usuario y contraseñas . . . . . . . . . . . . . . . . . . . . . . . . 3.12.5 Ejecutar el SQL Server Profiler para inspeccionar las consultas SQL y RPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.12.6 Evitar añadir instancias CommandBuilder en tiempo de ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.12.7 Sustituir las consultas SQL Batch por procedimientos almacenados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.12.8 Definir valores por defecto en los parámetros que no son necesarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.12.9 Utilizar sp_executesql y parámetros con nombre para reutilizar los Cached Query Plans . . . . . . . . . . . . . . . . . . . . . . . . . . 3.12.10Añadir columnas timestamp para el control de concurrencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.12.11Verificar registros en test de concurrencia . . . . . . . . . . . . . . . . . . . 3.12.12Evitar SqlExceptions con las validaciones del cliente . . . . . . . . . . .

datos 107 108 109 109 109 110 110 112 113 114 115 115

Capítulo 4 Programar TableAdapters, BindingSources y DataGridViews . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 4.1

Diseñar un formulario básico Customer-OrdersOrder Details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 vii

VisualBasic2005_Primeras.qxp

12/08/2007

13:42

PÆgina viii

Bases de datos con Visual Basic 4.1.1 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6

4.2

118 119 120 122 123 124

Añadir y dar formato a DataGridView . . . . . . . . . . . . . . . . 125 4.2.1 4.2.2 4.2.3 4.2.4

4.3

Reducir el tamaño del DataSet . . . . . . . . . . . . . . . . . . . . . . . . . . . Crear el origen de datos y añadir los controles . . . . . . . . . . . . . . . Añadir métodos FillBy para cada tabla de datos . . . . . . . . . . . . . . Modificar el código autogenerado para llenar los controles . . . . . . Llenar el cuadro combinado con valores CustomerID . . . . . . . . . . Limpiar la UI y el código . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dar formato a las columnas OrdersDataGridView . . . . . . . . . . . . . Añadir y dar formato a una columna calculada en Order_DetailsDataGridView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Añadir la columna Extended amount . . . . . . . . . . . . . . . . . . . . . . . Calcular y mostrar el valor Extended . . . . . . . . . . . . . . . . . . . . . . .

125 127 127 127

Proporcionar valores por defecto a los nuevo records . . . 129 4.3.2

Añadir valores por defecto en los registros de Order Details . . . . .

130

4.4 4.5 4.6

Manejar el evento DataErrors . . . . . . . . . . . . . . . . . . . . . . 131 Entrada de datos Streamline Heads-Down . . . . . . . . . . . . . 132 Migrar el UI a un formulario tabular . . . . . . . . . . . . . . . . . 134

4.7

Crear y vincular listas de consulta Lookup para valores de clave primaria . . . . . . . . . . . . . . . . . . . . . . . . . . 139

4.6.1 4.6.2 4.6.3

4.7.1 4.7.2 4.7.3 4.7.4 4.7.5 4.7.6

4.8

Crear un juego de datos lookup no tipificado y sus tablas de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rellenar el cuadro combinado cboCustomerID . . . . . . . . . . . . . . . Sustituir los cuadros de texto de DataGridView por cuadros combinados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Añadir código para poblar los cuadros combinados Employees y ShipVia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Remplazar los valores nulos por defecto en las filas nuevas . . . . . . Asociar cuadros combinados con cuadros de texto . . . . . . . . . . . .

135 136 137

139 141 141 142 143 144

Añadir un cuadro combinado que defina valores adicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146 4.8.1 4.8.2

4.9

Comprobar el proyecto OrdersByCustomersV3 . . . . . . . . . . . . . . . Fijar los valores por defecto que faltan al añadir filas con código . Editar un record DataGridView seleccionado en la segunda ficha .

Crear y vincular un DataView ordenado por ProductName . . . . . . Comprobar que no haya duplicados y actualizar la columna UnitPrice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

147 148

Añadir filas a las tablas lookup para entradas de nuevos Customers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 4.9.1 4.9.2

Añadir y vincular una BindingSource CustomerID . . . . . . . . . . . . . Comprobar la existencia de duplicados con un DataRowView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

151 152

4.10 Aplicar reglas de negocio a las ediciones . . . . . . . . . . . . . 153

viii

VisualBasic2005_Primeras.qxp

12/08/2007

13:42

PÆgina ix

Índice general

4.11 Guardar los cambios en las tablas base . . . . . . . . . . . . . . . 154 4.11.1 Mantener la integridad referencial . . . . . . . . . . . . . . . . . . . . . . . . 4.11.2 Crear y comprobar la función UpdateBaseTables . . . . . . . . . . . . . . 4.11.3 Entender la generación de cambios en tablas y las instrucciones para la actualización de las tablas base . . . . . . . . 4.11.4 Añadir la función UpdateBaseTables . . . . . . . . . . . . . . . . . . . . . . . 4.11.5 Operaciones previas de actualización . . . . . . . . . . . . . . . . . . . . . . 4.11.6 Invocar la función UpdateBaseTables . . . . . . . . . . . . . . . . . . . . . . . 4.11.7 Comprobar los valores CustomerID del servidor para evitar duplicados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

154 155

156 158 162 162 163

Capítulo 5 Añadir código para validar datos y gestionar la concurrencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 5.1

Validar las entradas de datos . . . . . . . . . . . . . . . . . . . . . . . 167 5.1.1 5.1.2 5.1.3 5.1.4

5.2

5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8

5.4

5.5

167 168 170 171

Gestionar las transgresiones de concurrencia . . . . . . . . . . 173 5.2.1

5.3

Validar cuadros de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Validar controles DataGridViews . . . . . . . . . . . . . . . . . . . . . . . . . . Capturar las violaciones de restricción de clave primera durante la entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Validar valores por defecto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Control de concurrencia y cambios de transacción en ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Propiedades ocultas de conexión y transacción . . . . . . . . . . . . . . . La propiedad ContinueUpdateOnError . . . . . . . . . . . . . . . . . . . . . Estrategias de control de concurrencia . . . . . . . . . . . . . . . . . . . . . Los "vínculos perdidos" en la gestión de la concurrencia . . . . . . . . Detectar los fallos de concurrencia en los registros hijo . . . . . . . . . Detectar otros conflictos potenciales de concurrencia . . . . . . . . . . Permitir a los usuarios re-crear los pedidos borrados . . . . . . . . . . .

174 175 176 176 177 178 179 181

Anticipar las transgresiones de restricción de clave primaria basada en valores . . . . . . . . . . . . . . . . . . . . 184 Manejar elegantemente los errores de concurrencia . . . . . 187 5.4.1 5.4.2

Obtener datos actuales del servidor . . . . . . . . . . . . . . . . . . . . . . . Restablecer y comparar los valores de celda del servidor y el cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

188

190

Trabajar con usuarios desconectados . . . . . . . . . . . . . . . . . 193 5.5.1 5.5.2

Crear y gestionar juegos de datos offline . . . . . . . . . . . . . . . . . . . . Activar el tratamiento de registros padre múltiples . . . . . . . . . . . .

194 195

Capítulo 6 La aplicación de técnicas avanzadas de los DataSets . . . . . 199 6.1

Aplicar transacciones a las actualizaciones de DataSets . . . 200 ix

VisualBasic2005_Primeras.qxp

12/08/2007

13:42

PÆgina x

Bases de datos con Visual Basic 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.1.6

6.2

209 210 210

Limitar el número de filas devueltas por las consultas TOP n . . . . . Añadir clases Partial para TableAdapters . . . . . . . . . . . . . . . . . . . .

213 214

Trabajar con imágenes en DataGridViews . . . . . . . . . . . . . 215 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5

6.5

Añadir una relación a SelectCommand . . . . . . . . . . . . . . . . . . . . . Añadir las columnas adjuntadas con relaciones al DataGridView . . Proporcionar los valores por defecto y columnas de sólo lectura . .

Mejorar el rendimiento reduciendo el tamaño de los juegos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 6.3.1 6.3.2

6.4

202 203 204 205 206 207

Añadir relaciones a los SelectCommand de la tabla de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208 6.2.1 6.2.2 6.2.3

6.3

Simplificar el listado con System.Transactions . . . . . . . . . . . . . . . . . Listar SqlDataAdapters en una transacción implícita . . . . . . . . . . . Autolistar SqlTableAdapters en una transacción implícita . . . . . . . . SQL Profiler para rastrear transacciones . . . . . . . . . . . . . . . . . . . . . Listar manualmente SqlTableAdapters en una transacción explícita Definir las opciones TransactionScope y Transaction . . . . . . . . . . .

Añadir columnas Image a los DataGridViews . . . . . . . . . . . . . . . . Manipular imágenes en DataGridView . . . . . . . . . . . . . . . . . . . . . Cambiar ImageLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Guardar una imagen seleccionada, mostrarla en un PictureBox y remplazarla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Evitar crear imágenes desde los campos de objeto OLE en Access

215 216 216 218 220

Editar documentos XML con DataSets yDataGridViews . . . 220 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.5.8 6.5.9 6.5.10

Adaptar un esquema XML existente para generar un DataSet . . . . Esquemas para documentos XML de jerarquía anidada . . . . . . . . . Un ejemplo de esquema anidado . . . . . . . . . . . . . . . . . . . . . . . . . La ventana Propiedades de las columnas . . . . . . . . . . . . . . . . . . . . Un esquema anidado con atributos . . . . . . . . . . . . . . . . . . . . . . . . Ejemplo de esquema anidado y "envuelto" (wrapped) . . . . . . . . . . Un ejemplo de esquema plano . . . . . . . . . . . . . . . . . . . . . . . . . . . Inferir un esquema XML para generar un juego de datos . . . . . . . Crear formularios de edición desde fuentes de datos XML . . . . . . . El proyecto de ejemplo EditNorthwindDS . . . . . . . . . . . . . . . . . .

221 222 224 227 227 228 230 233 235 235

Capítulo 7 Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . 239 7.1

Las nuevas características de ASP.NET 2.0 . . . . . . . . . . . . . 240 7.1.1 7.1.2

7.2 7.3

x

El modelo de compilación de ASP.NET . . . . . . . . . . . . . . . . . . . . . . Los nuevos controles (Data Controls) de ASP.NET 2.0 . . . . . . . . . .

242 244

Los controles DataSource . . . . . . . . . . . . . . . . . . . . . . . . . . 245 El control DataList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 7.3.1

SqlDataSources para controles vinculados . . . . . . . . . . . . . . . . . . .

247

VisualBasic2005_Primeras.qxp

12/08/2007

13:42

PÆgina xi

Índice general 7.3.2 7.3.3 7.3.4 7.3.5

7.4

251 252 254 257

El control FormView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259 7.4.1 7.4.2 7.4.3 7.4.4

7.5

Propiedades de control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Plantilla de datos vinculados y formateo de datos . . . . . . . . . . . . . Restricciones WHERE en el código fuente en los valores de controles vinculados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Editar ítems en listas de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . Paginar la fuente de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Remplazar los valores Null por texto específico de la columna . . . . Editar, añadir y borrar registros . . . . . . . . . . . . . . . . . . . . . . . . . . . Añadir botones de comando . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

259 260 262 263

El control GridView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264 7.5.1 7.5.2

Convertir campos BoundFields en campos EditItemTemplate . . . . Remplazar cuadros de texto por listas desplegables para la edición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

266 267

Capítulo 8 Aplicar técnicas avanzadas con ASP.NET 2.0 . . . . . . . . . . . . 271 8.1 8.2

Validar entradas en controles vinculados a datos . . . . . . . 271 Los controles de validación de ASP.NET 2.0 . . . . . . . . . . . . 272

8.3 8.4

Otras propiedades de validación compartidas . . . . . . . . . . 273 Validar ediciones en GridView . . . . . . . . . . . . . . . . . . . . . . 274

8.5

Validar entradas CustomerID con un control RegularExpressionValidator . . . . . . . . . . . . . . . . . . . . . . . . 276

8.2.1

8.4.1

8.5.1 8.5.2

8.6

8.9

Añadir un campo necesario de validación a un control GridView .

Comprobar los valores de EmployeeID con un control RangeValidator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aplicar RangeValidator y RegularExpressionValidator a las entradas de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

273

275

278 278

Impedir entradas ilógicas con un CompareValidator . . . . . 279 8.6.1

8.7 8.8

La nueva propiedad ValidationGroup . . . . . . . . . . . . . . . . . . . . . .

Añadir un control CustomValidator . . . . . . . . . . . . . . . . . . . . . . . .

280

Escribir un mensaje para el control Validation Summary . . 283 Validar ediciones de ProductID en el servidor Web . . . . . . 284 8.8.1

Test para descubrir valores duplicados de ProductID en el cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

284

Remplaar SqlDataSources por ObjectDataSources . . . . . . . 285 8.9.1 8.9.2

ObjectDataSources a partir de DataTables . . . . . . . . . . . . . . . . . . . Crear y asignar ObjectDataSources de un DataSet . . . . . . . . . . . . .

286 287

xi

VisualBasic2005_01.qxp

02/08/2007

16:10

PÆgina 1

CAPÍTULO 1

Pasar de ADO a ADO.NET Este capítulo es una introducción a ADO.NET 2.0 para los desarrolladores de Visual Basic 6 que han decidido aceptar lo inevitable y cambiar a Microsoft.NET Framework 2.0, Visual Studio 2005 (VS 2005) o VisualBasic Express (VBX), y Visual Basic 2005 (VB 2005), para utilizarlo como lenguaje de programación. Los ejemplos de código de ADO.NET 2.0 y los proyectos de código descritos en este capítulo implican los siguientes requisitos: )

)

)

)

) )

Experiencia en la programación de bases de datos con VB6, utilizando Data Environment Designer, y escritura de código para la creación y manipulación de los objetos ADODB Connection, Command, y Recordset, incluyendo los Recordsets offline y los controles vinculados. Comprensión básica de la organización y el uso de los namespace de .NET Framework y sus clases. Estar suficientemente familiarizado con el uso de VS 2005 IDE y la escritura de código VB 2005 para crear sencillos proyectos de formularios Windows (Windows Forms). Edición con Microsoft SQL Server 2000 o 2005 Developer, o superior; tener instalado MSDE 2000, o SQL Server Express (SQLX), o tener fácil acceso a ellos desde una red de trabajo. Access 2000 o posteriores versiones, para los ejemplos en Jet 4.0, es totalmente opcional. La base de datos de Northwind instalada o accesible desde un SQLServer. Saber trabajar con los documentos XML estándar y estar mínimamente familiarizado con los esquemas XML.

Si tiene experiencia con ADO.NET 1.x, puede ojear este capítulo para ver las nuevas características de ADO.NET 2.0 y seguir con el capítulo 2, que tratat las novedades de ADO.NET 2.0, para una información más detallada. Uno de los objetivos de Microsoft con VS 2005 es minimizar el trauma que puedan sufrir los programadores al cambiar de VB6 y VBA a .NET Framework 2.0 y VB 2005. Lo que veremos es si se incrementará la migración de programadores a VB 2005. Lo que hace falta para traer programadores profesionales de bases de datos con VB6 a esta tercera edición de .NET Framework y Visual Studios .NET es una mayor productividad en la programación, aplicación o escalabilidad, y resultados de los componentes y la reutilización del código. 1

VisualBasic2005_01.qxp

02/08/2007

16:10

PÆgina 2

Bases de datos con Visual Basic Este capítulo empieza mostrando las similitudes de los códigos VB6 y VBA para crear objetos ADODB y el código VB 2005 para generar los objetos básicos ADO.NET 2.0 –conexiones de bases de datos, comandos, y juegos de resultados de sólo lectura para los proyectos en formato Windows– las clases que proporcionan datos de Native ADO.NET, especialmente SqlClient para los servidores SQLServer, proporcionan un acceso a datos substancialmente mejor que ADODB y sus proveedores de datos OLE DB. Los demás apartados muestran diversas maneras de crear DataSets en ADO.NET utilizando las nuevas características de VS 2005 y los ayudantes que generan, de forma automática, los objetos básicos de lectura y escritura. Los DataSets demuestran la productividad mejorada de la programación para el acceso a datos y la contribución de ADO.NET 2.0 a la escalabilidad de la aplicación.

1.1 Una nueva manera de acceder a los datos Microsoft ha diseñado ADO.NET para maximizar la escalabilidad de los componentes .NET y las aplicaciones de Windows y la Web que trabajan con grandes cantidades de datos. La escalabilidad no es un factor decisivo cuando el proyecto con el que trabajamos sólo inlcuye algún formulario de Windows para los clientes y algunas tablas de capatación y actualización de los datos, todo ello en una sola base de datos. Sin embargo, las páginas Web muy visitadas deben ser capaces de procesar un número mucho mayor de datos, ya sea añadiendo más procesadores y más RAM a un solo servidor, o bien derivar el procesamiento añadiendo más servidores de aplicación para manejar la carga de datos a procesar. El código Managed ADO.NET es la clave para conseguir un proyecto .NET escalable que maneje gran cantidad de datos, ya que minimiza la duración y el número de conexiones de los servidores de bases de datos implicadas y utiliza un test de optimización para actualizar las tablas. La siguiente sección explica el papel de los namespace y los proveedores controlados de datos de ADO.NET 2.0, dos de los elementos básicos en las operaciones de acceso a datos de .NET 2.0.

1.1.1 El namespace System.Data El namespace System.Data de .NET Framework 2.0 contiene todos los namespace, clases, interfaces, enumeraciones y surrogados de ADO.NET 2.0. La siguiente figura muestra los namespace de System.Data en la pantalla del navegador de objetos. Los objetos de ADO.NET SqlConnection y SqlCommand corresponden a los objetos ADODB.Connection y ADODB.Command, pero sólo se pueden utilizar con bases de datos de servidores SQL. A continuación le mostramos las jerarquías de namespace de ADO.NET para los proveedores controlados de datos SqlConnection y SqlCommand; los namespace que son nuevos en ADO.NET 2.0 aparencen en negrita: System.Object System.MarshalByRefObject System.ComponentModel.Component System.Data.Common.DbConnection

2

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 3

Pasar de ADO a ADO.NET

VS 2005 no añade automáticamente una referencia al ensamblaje de System.Data.dll cuando se inicia un nuevo proyecto de formulario Windows. Si se crea una nueva fuente de datos con el Ayudante Data Source Configuration Wizard se añaden referencias a los namespace de System.Data y System.Xml. La sección "Añadir un DataSet por teclado desde una fuente de datos del servidor SQL", describe más adelante, en este capítulo, cómo usar el Ayudante Data Source Configuration Wizard System.Data.SqlClient.SqlConnectionSystem.Object System.MarshalByRefObject System.ComponentModel.Component System.Data.Common.DbCommand System.Data.SqlClient.SqlCommand

La tabla siguiente muestra una breve descripción de los namespace de System.Data que aparecen en la figura anterior. En la tabla se han ordenado según la jerarquía anterior. Namespace

Descrición

System.Object

Es la raíz del tipo de jerarquía de .NET Framework 2.0 (miembro de System).

System.MarshalByRefObject

Permite enviar los objetos de datos más allá de los límites del dominio de la aplicación (miembro de System).

System.ComponentModel

Soporta los objetos compartidos entre componentes y permite mejorar los tiempos de ejecución y de diseño de los componentes.

3

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 4

Bases de datos con Visual Basic Namespace

Descrición

System.Data

Proporciona las clases básicas, interfaces, enumeraciones y tratadores de eventos para todas las fuentes de datos soportadas, principalmente los datos relacionales y los archivos o corrientes XML.

System.Data.Common

Proporciona clases compartidas por todos los proveedores controlados de datos, por ejemplo los DbConnection y DbCommand de la lista anterior.

System.Data.Common.DbConnection

Proporciona clases heredables para los proveedores de datos específicos para tecnología y vendedores (nuevo en ADO.NET 2.0).

System.Data.Odbc, System.Data.OleDb, System.Data.OracleClient, system.Data.SqlClient System.Data. SqlCeClientlos

Namespace para los cinco proveedores controlados de datos incluidos en ADO.NET 2.0; la sección siguiente describe

System.Data.SqlTypes

Proporciona una clase para cada tipo de datos del SQLServer, incluido el nuevo tipo xml del SQLServer 2005; estas clases substituyen la enumeración genérica del tipo Db soportado por todos los proveedores.

System.XML

Incorpora la clase System.Xml.XmlDataDocument, que incluye objetos DataSet con los que se pueden procesar documentos XML estructurados.

Después de añadir una referencia de proyecto a System.Data.dll, se puede escribir mediante el teclado el namespace System.Data para eliminar los cualificadores y asegurar así un chequeo estricto de la escritura por teclado añadiendo las siguientes líneas al principio del código de clase: Option Explicit On Option Strict On Imports System.Data Imports System.Data.SqlClient Especificar Option Explicit On y Option Strict On en el cuadro de opciones de Projects and Solutions, la página de valores por defecto VB no asegura que otros programadores que trabajan con su mismo código tengan también los mismos valores por defecto. EScriba ImportsSystem.Data.OleDb en lugar de ImportsSystem.Data.SqlClient si está utilizando el proveedor de datos Ole Db.

1.1.2 Proveedores de datos ADO.NET Los proveedores controlados de datos ADO.NET y sus objetos de datos subyacentes forman la espina dorsal del acceso a los datos .NET. Los proveedores de datos son una capa abstracta de servicios y su concepto es similar al de la clase ADODB de objetos de datos ActiveX, la cual soporta únicamente proveedores de datos OLE DB. ADO.NET soporta muchos tipos diferentes de proveedores de datos gracias a los siguientes namespace de proveedores de datos: 4

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 5

Pasar de ADO a ADO.NET )

)

)

)

)

SqlClient: sus miembros proporcionan una conectividad de alto rendimiento a los servidores SQL 7.0, 2000, y 2005. El mayor rendimiento se produce gracias a la superación de la capa OLE DB y la comunicación con el protocolo TDS (Tabular Data Stream) original del servidor SQL. La mayoría de los ejemplos de este libro utilizan clases en el namespace SqlClient. SqlClientCe: incorpora características similares a las de SqlClient para los servidores SQL CE 3.0 y 2005 Mobile Edition. En este libro no se tratarán las versiones CE ni Mobile de los servidores SQL. OracleClient: sus miembros poseen una funcionalidad parecida a la de SqlClient para las bases de datos de Oracle 8i y 9i. Oracle ofrece Oracle Data Provider para .NET (ODP.NET) en substitución de OracleClient; ODP.NET también soporta Oracle 10g y versiones posteriores. Puede obtener más información sobre ODP .NET en la siguiente dirección http://otn.oracle.com/tech/windows/odpnet/. OleDb: sus miembros proporcionan una conexión directa con los proveedores de datos OLE DB basados en COM para bases y fuentes de datos distintos del SQLServer, SQLServer CE, y Oracle. Al crear un nuevo objeto OleDbConnection se puede elegir entre 19 proveedores OLE DB incorporados al programa. Algunos de los ejemplos de este libro utilizan el proveedor de datos Microsoft Jet 4.0 OLE DB Data Provider con el archivo Access 2000 o el archivo posterior Northwind.mdb. ADO.NET 2.0 no proporciona acceso al proveedor de Microsoft OLE DB para controladores ODBC. Odbc: sus miembros proporcionan conexión a las fuentes de datos heredadas que no tienen los proveedores de datos OLE DB. El namespace Odbc viene incluido en .NET Framework 2.0 para garantizar la compatibilidad hacie atrás con las aplicaciones de .NET Framework 1.x.

El namespace de cada proveedor cuenta con su propio juego de clases de objetos de datos. El proveedor que se elija, determinará el prefijo de los nombres de los objetos, como SqlConnection, SqlCeConnection, OracleConnection, u OleDbConnection.

1.2 Los objetos básicos de datos ADO.NET Este apartado define los objetos básicos de datos como tipos de acceso de datos en tiempo de ejecución, que tienen su contrapartida ADODB. ADO.NET 2.0 proporciona los siguientes objetos básicos de datos para la restauración de datos, actualizaciones, o ambos: )

Connection: estos objetos definen al proveedor de datos; la instancia de gestión de la base de datos, la base de datos, seguridad, credenciales y otras propiedades relacionadas con la conexión. El código VB 2005 para crear un elemento .NET Connection es muy similar al código VB6 necesario para crear un objeto ADODB.Connection. También se puede crear un objeto Connection nuevo, duradero (tiempo de diseño), pulsando con el botón derecho del ratón sobre Conexiones de datos, en el Explorador de servidores y seleccionar Agregar conexión para abrir el cuadro de diá5

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 6

Bases de datos con Visual Basic logo del mismo nombre. Otra alternativa es seleccionar Herramientas/Conectar con base de datos para abrir esa misma ventana. )

)

)

Command: estos objetos ejecutan sentencias batch SQL o almacenan procedimientos sobre una conexión abierta. Los objetos Command pueden devolver uno o más resultados, resultados de un juego de resultados, un solo valor escalar, un objeto XmlDataReader, o el valor RowsAffected para la actualización de tablas. A diferencia de los objetos de apertura ADODB.Recordset de una ADODB.Connection, el objeto Command de ADO.NET no es opcional. Los objetos Command soportan una colección opcional de objetos Parameter para ejecutar consultas parametrizadas o procedimientos almacenados. La relación entre parámetros y comandos ADODB y ADO.NET es idéntica. DataReader: estos objetos restablecen uno o más juegos de resultados de sólo lectura ejecutando las sentencias batch SQL o almacenando procedimientos. El código VB .NET para crear y ejecutar un DataReader desde un objeto Command en un objeto Connection es parecido al utilizado para crear el objeto por defacto, sin cursor, ADODB Recordset desde un objeto ADODB.Command. A diferencia del elemento por defecto, hacia delante, ADODB.Recordset, los juegos de resultados de DataReader no se pueden salvar en un archivo local y reabrirlo con un cursor del cliente (client-side) a través de los métodos Save y Open. XmlReader: estos objetos consumen streams que contienen documentos XML bien formados, como los producidos por las consultas FOR XML AUTO del servidor SQL o procedimientos almacenados, o columnas xml originales del SQL Server 2005. Los XmlReaders son el equivalente a un cursor de sólo lectura, hacia delante sobre el documento XML. Un objeto XmlReader corresponde al objeto ADODB.Stream devuelto por el proveedor SQLXML3.0 y el posterior SQLXMLOLEDB. SqlClient no soporta los cursores (navegables) bidireccionales. Microsoft añadió un objeto SqlResultset que simula un cursor actualizable de parte del servidor, a una versión beta anterior VS 2005. El equipo de VS 2005 retiró rápidamente el objeto SqlResultset tras concluir que propiciaba malos hábitos a la hora de programar, como por ejemplo mantener abierta una conexión durante las operaciones de edición. Un método ExecutePageReader, basado en el objeto SqlResultset, se eliminó al mismo tiempo y por las mismas razones.

La siguiente figura ilustra las relaciones entre los objetos ADO.NET Connection, Command, Parameter, DataReader, y XmlReader. Los parámetros son opcionales en ADODB y los comandos básicos de ADO.NET. Los tipos SqlClient se pueden substituir por los tipos OleDb u Odbc. Para poder utilizar el proveedor OleDb para devolver un objeto XmlDataReader desde un servidor SQL 2000 hay que instalar la versión SQLXML3.0 SP2 o posteriores; el proveedor Odbc no soporta XMLReaders. El programa de instalación de SQLServer 2005 instala la vesrión SQLXML4.0.

6

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 7

Pasar de ADO a ADO.NET

1.3 Creando objetos básicos de datos ADO.NET con SqlClient Las siguientes secciones muestran el código típico en VB 2005 para definir y abrir un objeto SqlConnection, especificar un objeto SqlCommand, e invocar los métodos de comando ExecuteReader y ExecuteXmlReader. Los procedimientos incluyen código para mostrar en pantalla la columna SqlDataReader y los valores del elemento XmlReader. Todos los ejemplos utilizan como fuente de datos, una base de datos de ejemplo (sample database) de un servidor local SQLServer 2000 o 2005 Northwind. Si está utilizando la instancia por defecto de SQLX en su máquina de prueba, cambie localhost por .\SQLEXPRESS en la cadena strConnconnection. Si está utilizando la instancia MSDE 2000 de Access como servidor local, cambie Northwind por NorthwindCS. Si está utilizando una instancia de servidor remoto SQL, substituya localhost por el nombre de la red del servidor remoto.

1.3.1 SqlDataReaders con juegos de resultados múltiples Uno de los usos más habituales de los objetos SqlDataReader es dar contenido a listas desplegables o cuadros de lista con datos extraidos de una consulta. Los juegos de resultados múltiples se pueden utilizar desde un solo formulario SQLbatch o un procedimiento almacenado para rellenar listas múltiples en el tratador de eventos FormName_Load. El siguiente procedimiento OpenDataReader abre una conexión a la base de datos de ejemplo de Northwind, especifica un objeto SqlCommand que devuelve dos juegos de resultados e invoca su método ExecuteReader para generar la instancia SqlDataReader. El argumento CommandBehavior.CloseConnection cierra la conexión cuando se 7

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 8

Bases de datos con Visual Basic cierra el DataReader. Todos los objetos básicos de datos ADO.NET siguen este modelo; sólo difieren el método ExecuteObject y el modo de iteración del DataReader. El método SqlDataReader.Read, que substituye a la instrucción muchas veces olvidada RecordSet.MoveNext, devuelve True mientras quedan filas por leer. De manera parecida, el método SqlDataReader.NextResult es True mientras queden juegos de resultados por procesar después de la iteración inicial. Sólo hay un juego de resultados abierto mientra se iteran los juegos de resultados múltiples, a diferencia de la característica Multiple Active Resultsets (MARS) del SQLServer 2005. Mas adelante, se describe cómo hacer posible la característica MARS.

Private Sub OpenDataReader() Dim cnnNwind As SqlConnection = New SqlConnection(strConn) Try cnnNwind.Open() Dim strSQL As String = "SELECT * FROM Shippers" strSQL += ";SELECT EmployeeID, FirstName, LastName FROM Employees" Dim cmdReader As SqlCommand = New SqlCommand(strSQL, cnnNwind) cmdReader.CommandType = CommandType.Text Dim sdrReader As SqlDataReader = _ cmdReader.ExecuteReader(CommandBehavior.CloseConnection) With sdrReader If .HasRows Then While .Read lstShippers.Items.Add(.Item(0).ToString + " - " + .Item(1).ToString) End While While .NextResult While .Read lstEmployees.Items.Add(.Item(0).ToString + " - " + _ .Item(1).ToString + " " + .Item(2).ToString) End While End While End If .Close() End With Catch exc As Exception MsgBox(exc.Message) End Try End Sub El uso de la propiedad HasRows is opcional, ya que la invocación inicial del método Read devuelve el valor False si la consulta no devuelve ninguna línea (fila). La propiedad SqlDataReader.Item(ColumnIndex) devuelve una variable de objeto que hay que convertir en una cadena para su concatenación. El código de tratamiento de error estructurado se ha suprimido para mejor legibilidad

8

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 9

Pasar de ADO a ADO.NET

1.3.2 XmlReaders con consulta FOR XML AUTO Añadir una cláusula FOR XML AUTO a una consulta SELECT del SQLServer o un procedimiento almacenado devuelve el juego de resultados en forma de corriente XML. El formato por defecto del documento XML se centra en los atributos; sólo hay que añadir el modificador Elements para devolver un documento de sintaxis de elementos. Aquí tenemos el documento XML devuelto por una consulta SELECT * FROM Shippers FOR XML AUTO, Elements: <Shippers> <ShipperID>1 Speedy Express (503) 555-9831 <Shippers> <ShipperID>2 United Package (503) 555-3199 <Shippers> <ShipperID>3 Federal Shipping (503) 555-9931

El nuevo método SqlCommand.ExecuteXmlReader de ADO.NET 2.0, carga un objeto System.Xml.XmlReader con la corriente, tal como se muestra en el siguiente listado de procedimiento OpenXmlReader. XmlReader es una clase abstracta con las implementaciones concretas XmlTextReader, XmlNodeReader, y XmlValidatingReader. El método ExecuteXmlReader de ADO.NET 2.0 devuelve una implementación concreta. Private Sub OpenXmlReader() Dim cnnNwind As SqlConnection = New SqlConnection(strConn) Try cnnNwind.Open() Dim strSQL As String = "SELECT * FROM Shippers FOR XML AUTO, Elements" Dim cmdXml As SqlCommand = New SqlCommand(strSQL, cnnNwind) cmdXml.CommandType = CommandType.Text Dim xrShippers As System.Xml.XmlReader = cmdXml.ExecuteXmlReader With xrShippers .Read() Do While .ReadState <> Xml.ReadState.EndOfFile txtXML.Text += .ReadOuterXml Loop txtXML.Text = Replace(txtXML.Text, "><", ">" + vbCrLf + "<")

9

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 10

Bases de datos con Visual Basic .Close() End With Catch exc As Exception MsgBox(exc.Message) Finally cnnNwind.Close() End Try End Sub Substituyendo xrShippers.MoveToContent seguido de xrShippers.ReadOuterXML (sin el bucle) devuelve sólo el primer grupo de elementos <Shippers>

Hay que ejecutar el método XmlReader.Read para ir al primer grupo de elementos, seguido de una invocación ReadOuterXml para cada grupo, lo que representa una fila para el juego de resultados. El método ExecuteXmlReader no soporta la enumeración CommandBehavior, por lo que el objeto SqlConnection se ha de cerrar expresamente. OleDbCommand no soporta el método ExecuteXmlReader; Microsoft quiere que se utilicen las clases SqlClient en todas las aplicaciones de acceso a datos del servidor SQL (SQLServer), incluido el código SQLCLR que se ejecuta en el proceso del SQLServer 2005. La siguiente figura muestra el formulario del proyecto BasicDataObjects tras ejecutarse desde el generador de eventos frmMain_Load, el cual ejecuta los procedimientos previos OpenDataReader y OpenXmlReader, y el procedimiento posterior LoadDataGridView. Las consultas o los procedimientos almacenados FOR XML AUTO suponen una mejora substancial en las aplicaciones de producción si se compara con los métodos tradicionales de acceso de datos, donde el servidor tiene que generar la corriente XML, por la red viajan muchos más bytes de datos y el cliente o componente tiene que transformar la corriente XML en un formato utilizable.

10

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 11

Pasar de ADO a ADO.NET

1.3.3 Rellenar un DataGridView con un DataReader Si la aplicación sólo tiene que mostrar en pantalla datos tabulados, una rejilla de sólo lectura poblada con código será lo que consuma menos recursos. El control DataGridView sustitye al control DataGrid de VS 2002 y VS 2003, y es muy fácil de rellenar desde el punto de vista de la programación. Un DataGridView de sólo lectura poblado con un DataReader tiene un comportamiento similar al control de rejilla VB6 estándar (no vinculado), con la diferencia de que los DataGridViews tienen columnas ordenables por defecto. El código que viene a continuación define las columnas dgvCusts de los controles DataGridView y después pobla cada fila con una instancia de un array objCells() Object que contiene valores de campo: Private Sub LoadDataGridView() Dim cnnNwind As SqlConnection = New SqlConnection(strConn) Try Dim strSql As String = "SELECT * FROM Customers" Dim cmdGrid As New SqlCommand(strSql, cnnNwind) cmdGrid.CommandType = CommandType.Text cnnNwind.Open() Dim sdrGrid As SqlDataReader = cmdGrid.ExecuteReader Dim intCol As Integer With sdrGrid If .HasRows Then dgvCusts.Rows.Clear() For intCol = 0 To .FieldCount - 1 dgvCusts.Columns.Add(.GetName(intCol), .GetName(intCol)) Next dgvCusts.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.ColumnHeader While .Read Dim objCells(intCol) As Object .GetValues(objCells) dgvCusts.Rows.Add(objCells) End While .Close() End If End With Catch exc As Exception MsgBox(exc.Message) Finally cnnNwind.Close() End Try End Sub Para distribuir el control DataGridView en valores de columna, clicar la cabecera de la columna. Clicando sucesivamente las columnas se ordenan en orden ascendente y descendente

11

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 12

Bases de datos con Visual Basic

1.3.4 Devolver una sola fila de datos Añadiendo un flag CommandBehavior.SingleRow al objeto SqlDataReader nos devuelve la primera fila de un juego de resultados especificado por una consulta SQL o un procedimiento almacenado. El siguiente código devuelve la primera fila de una tabla de clientes de Northwind, siempre que no se especifique ninguna cláusula WHERE. De lo contrario, el código devuelve la primera la primera fila especificada por los criterios WHERE. Si se añade un flag CommandBehavior.CloseConnection, la conexión se cierra automáticamente al cerrar el objeto SqlDataReader. Private Sub OpenExecuteRow() Dim cnnNwind As SqlConnection = New SqlConnection(strConn) Try cnnNwind.Open() Dim strSQL As String = "SELECT * FROM Customers" 'strSQL += " WHERE CustomerID = 'ALFKI'" Dim cmdRow As SqlCommand = New SqlCommand(strSQL, cnnNwind) cmdRow.CommandType = CommandType.Text 'Dim srRecord As SqlRecord = cmdRecord.ExecuteRow Dim sdrRow As SqlDataReader = cmdRow.ExecuteReader(CommandBehavior.SingleRow Or _ CommandBehavior.CloseConnection) With sdrRow If .HasRows Then .Read() Dim intFields As Integer = .FieldCount Dim strCustID As String = .GetString(0) Dim strCompany As String = .GetString(1) End If .Close() End With Catch exc As Exception MsgBox(exc.Message + exc.StackTrace) Finally cnnNwind.Close() End Try End Sub

1.3.5 Devolver un valor escalar El método SqlCommand.ExecuteScalar devuelve el valor de la primera columna de la primera fila de un juego de resultados. El uso más frecuente de ExecuteScalar es para devolver un único valor agregado SQL, como por ejemplo COUNT, MIN, o MAX. El siguiente listado de procedimiento OpenExecuteScalar devuelve el número de entradas de la tabla de clientes: Private Sub OpenExecuteScalar() Dim cnnNwind As SqlConnection = New SqlConnection(strConn) cnnNwind.Open()

12

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 13

Pasar de ADO a ADO.NET

Dim strSQL As String = "SELECT COUNT(*) FROM Customers" Dim cmdScalar As SqlCommand = New SqlCommand(strSQL, cnnNwind) cmdScalar.CommandType = CommandType.Text Dim intCount As Integer = CInt(cmdScalar.ExecuteScalar) cnnNwind.Close() End Sub

1.3.6 Ejecutar peticiones que no devuelven datos El método SqlCommand.ExecuteNonQuery se utiliza para ejecutar peticiones SQL o procedimientos almacenados que actualizan datos de tabla INSERT, UPDATE, y operaciones DELETE. Tal como muestra el siguiente código OpenExecuteNonQuery, ExecuteNonQuery rivaliza en simplicidad con ExecuteScalar: Private Sub RunExecuteNonQuery() Dim cnnNwind As SqlConnection = New SqlConnection(strConn) Dim intRecordsAffected As Integer Try cnnNwind.Open() Dim strSQL As String = "INSERT Customers (CustomerID, CompanyName) " + _ "VALUES ('BOGUS', 'Bogus Company')" Dim cmdUpdates As SqlCommand = New SqlCommand(strSQL, cnnNwind) cmdUpdates.CommandType = CommandType.Text intRecordsAffected = cmdUpdates.ExecuteNonQuery LoadDataGridView() Application.DoEvents() MsgBox("Click OK to continue with update.") strSQL = "UPDATE Customers SET CompanyName = 'Wrong Company' " + _ "WHERE CustomerID = 'BOGUS'" cmdUpdates.CommandText = strSQL intRecordsAffected += cmdUpdates.ExecuteNonQuery LoadDataGridView() Application.DoEvents() MsgBox("Click OK to continue with update.") strSQL = "DELETE FROM Customers WHERE CustomerID = 'BOGUS'" cmdUpdates.CommandText = strSQL intRecordsAffected += cmdUpdates.ExecuteNonQuery LoadDataGridView() Catch exc As Exception MsgBox(exc.Message + exc.StackTrace) Finally

13

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 14

Bases de datos con Visual Basic cnnNwind.Close() Dim strMsg As String If intRecordsAffected = 3 Then strMsg = "INSERT, UPDATE, and DELETE operations succeeded." Else strMsg = "INSERT, UPDATE, DELETE, or all failed. Check your Customers table." End If MsgBox(strMsg, , "RunExecuteNonQuery") End Try End Sub Ejecutar peticiones SQL de actualización contra bases de datos de producción no es una práctica recomendable, y la mayoría de las DBA no permitirán actualizaciones directas a tablas básicas de servidor. El objetivo del ejemplo anterior es simplemente ilustrar cómo funciona el método ExecuteNonQuery. En el mundo real son los procedimientos almacenados parametrizados los que actualizan normalmente las tablas.

1.4 Aplicar transacciones para actualizar las tablas múltiples Todas las actualizaciones para más de una tabla, dentro de un mismo procedimiento, deberían ejecutarse bajo el control de una transacción. El objeto SqlTransaction proporciona clientes capaces de actualizarse o, en casos excepcionales, de ir hacia atras en las actualizaciones de las tablas básicas del servidor SQL. La gestión de las transacciones en ADO.NET es parecida a las de los objetos ADODB.Connection, que tienen métodos BeginTrans, CommitTrans, y RollbackTrans. Los objetos SqlTransaction tienen los métodos correspondientes BeginTransaction, CommitTransaction, y RollbackTransaction. A diferencia de las conexiones ADODB, ADO.NET permite incluir selectivamente listas de comandos en una transacción activa. A continuación vemos los pasos necesarios para ejecutar las actualizaciones por transacción en ADO.NET: )

)

Definir una transacción local como un objeto SqlTransaction, OleDbTransaction, u OdbcTransaction. Invocar el método BeginTransaction con un argumento numérico opcional IsolationLevel. El valor por defecto de la propiedad IsolationLevel es ReadCommitted.

)

Incluir lista de comandos en la transacción mediante la propiedad Transaction.

)

Invocar el método ExecuteNonQuery para cada comando.

)

Invocar el método de transacción Commit.

)

Si sucede una excepción, invocar el método de transacción Rollback.

Las enumeraciones IsolationLevel de ADO.NET e IsolationLevelEnum de ADODB, tienen muchos miembros compartidos, tal como se muestra en la tabla siguiente.

14

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 15

Pasar de ADO a ADO.NET Miembro ADO.NET

Miembro ADODB

ADO.NET IsolationLevel Description

Chaos

adXactChaos

Impide que se sobreescriban los cambios dependientes de transacciones aisladas de mayor nivel

ReadCommitted

adXactReadCommitted dadXactCursorStability

Impide las lecturas sucias pero permite las lecturas no repetibles y los phantom data (por defecto)

ReadUncommitted

adXactReadUncommitted adXactBrowse

Permite lecturas sucias, filas no repetibles, y filas fantasma

RepeatableRead

adXactRepeatableRead

Impide lecturas no repetibles pero permite filas fantasma

Serializable

adXactSerializable adXactIsolated

Impide lecturas sucias, lecturas no repetibles y filas fantasma colocando un candado de rango en los datos que se están actualizando

Snapshot

None

Archiva una versión de los datos del SQLServer 2005 que los clientes pueden leer mientras otro cliente modifica esos mismos datos

Unspecified

adXactUnspecified

Indica que el proceedor está utilizando un nivel de aislamiento diferente y desconocido

Snapshot es un nuevo nivel de aislamiento en ADO.NET 2.0 únicamente para SQLServer 2005. El aislamiento Snapshot elimina los candados de lectura proporcionando a otros clientes una copia (snapshot) del archivo sin modificar hasta que se realiza la transacción. Hay que hacer posible el aislamiento Snapshot en el SQLServer Management Studio (SSMS) o bien proporcionando un comando T-SQL ALTER DATABASE DatabaseName SETALLOW_SNAPSHOT_ISOLATIONON para sacar partido de la mejora en la escalabilidad de la transacción que proporciona ese nuevo nivel de aislamiento. El siguiente listado RunInsertTransaction muestra como reutilizar un solo objeto SqlTransaction y SqlCommand para juegos de transacciones de actualización en las tablas de Clientes y Pedidos de Northwind. Ejecutando esta transacción se producen cambios irreversibles en la columna OrderID de la tabla de Pedidos, por lo que es recomendable hacer una copia de seguridad de la base de datos de Northwind antes de ejecutar este tipo de código. Nótese que hay que rehacer la lista del objeto database antes de ejecutar este código. Obsérvese que hay que listar de nuevo el objeto SqlCommand en la SqlTransaction después de haberse realizado una transacción previa. Public Sub RunInsertTransaction() Dim cnnNwind As SqlConnection = New SqlConnection(strConn) Dim trnCustOrder As SqlTransaction = Nothing Dim intRecordsAffected As Integer Dim strTitle As String = Nothing Try cnnNwind.Open()

15

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 16

Bases de datos con Visual Basic Try trnCustOrder = cnnNwind.BeginTransaction(IsolationLevel.RepeatableRead) strTitle = "INSERT " Dim strSQL As String = "INSERT Customers (CustomerID, CompanyName) " + _ "VALUES ('BOGUS', 'Bogus Company')" Dim cmdTrans As SqlCommand = New SqlCommand(strSQL, cnnNwind) cmdTrans.CommandType = CommandType.Text cmdTrans.Transaction = trnCustOrder intRecordsAffected = cmdTrans.ExecuteNonQuery strSQL = "INSERT Orders (CustomerID, EmployeeID, OrderDate, ShipVia) " + _ "VALUES ('BOGUS', 1, '" + Today.ToShortDateString + "', 1)" cmdTrans.CommandText = strSQL intRecordsAffected += cmdTrans.ExecuteNonQuery trnCustOrder.Commit() LoadDataGridView() Application.DoEvents() MsgBox("Click OK to continue with transaction.") strTitle = "DELETE " trnCustOrder = cnnNwind.BeginTransaction(IsolationLevel.RepeatableRead) strSQL = "DELETE FROM Orders WHERE CustomerID = 'BOGUS'" cmdTrans.CommandText = strSQL cmdTrans.Transaction = trnCustOrder intRecordsAffected += cmdTrans.ExecuteNonQuery strSQL = "DELETE FROM Customers WHERE CustomerID = 'BOGUS'" cmdTrans.CommandText = strSQL intRecordsAffected += cmdTrans.ExecuteNonQuery trnCustOrder.Commit() LoadDataGridView() Catch excTrans As SqlException MsgBox(excTrans.Message + excTrans.StackTrace, , _ strTitle + "Transaction Failed") Try trnCustOrder.Rollback() Catch excRollback As SqlException MsgBox(excTrans.Message + excTrans.StackTrace, , _ strTitle + "Rollback Failed") End Try

16

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 17

Pasar de ADO a ADO.NET End Try Catch exc As Exception MsgBox(exc.Message + exc.StackTrace) Finally cnnNwind.Close() Dim strMsg As String If intRecordsAffected = 4 Then strMsg = "INSERT and DELETE transactions succeeded." Else strMsg = "INSERT, DELETE, or both transactions failed. " + _ "Check your Customers and Orders tables." End If MsgBox(strMsg, , "RunInsertTransaction") End Try End Sub Este es otro ejemplo de operaciones de tipo cliente que muchas DBAs no permitirán. En aplicaciones comerciales, procedimientos almacenados con sentencias T-SQL BEGIN TRAN[SACTION], COMMIT TRAN[SACTION], y ROLLBACK TRAN[SACTION] tratan actualizaciones de tablas múltiples.

1.5 Utilizando clases de miembros OleDb, SqlXml, y Odbc La mayoría de proyectos de demostración centrados en datos VB 2005 conectan a un servidor de instancia SQL (SQLServer instance) con objetos SqlClient mientras los programadores se familiarizan con las clases System.Data de .NET. Por eso, los ejemplos anteriores utilizan el proveedor de datos SqlClient. La siguiente figura muestra el formulario del OleDbDataProject con cuadros de lista y de texto que muestran en pantalla los datos generados por cada uno de los tres proveedores. Al seleccionar el cuadro Use OdbcDataReader, el proveedor de datos OleDb queda substituido por el Odbc para rellenar el cuadro de lista Rowset 1 (Shippers). Se puede sacar mayor partido de los nuevos métodos de ADO.NET 2.0 DbProviderFactories.GetFactory(System.Data.Provider) y los métodos DbProviderFactory.CreateConnection y CreateCommand para generar una conexión a/desde todos los managed data provider disponibles. En el siguiente capítulo, muestra cómo escribir aplicaciones que utilizan sistemas de gestión de bases de datos múltiple-relacionales. Cada procedimiento de ejemplo tiene su propia cadena de conexión. Para señalar la instancia Microsoft Access, SQLServer, o SQLExpress hay que modificar cada una de las cadenas de conexión.

17

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 18

Bases de datos con Visual Basic

El proveedor de datos nativos SQLXML Managed Classes (Microsoft.Data.SqlXml) para el servidor SQLServer 2000 no es un miembr0o de .NET Framework 2.0. Es una componente de Microsoft SQLXML4.0, que VS 2005 y VB Express instalan como Microsoft.Data.SqlXml.dll.

1.5.1 Substituir OleDb por objetos SqlClient El proveedor de datos OleDb es la mejor apuesta para conectarse a los archivos o los servidores de la base de datos de Access (Jet 4.0) para los que no se tiene un proveedor nativo .NET. El proveedor OleDb permite, asimismo, crear aplicaciones que pueden trabajar con los servidores de bases de datos que elija el ususario. En la mayoría de los casos se puede sustituir ImportsSystem.Data.SqlServer por ImportsSystem.Data.OleDb, cambiar la correspondiente cadena OLE DB de conexión y remplazar el prefijo de los objetos de datos desde Sql u OleDb. En algunos casos será necesario modificar la sentencia SQL cuando nos encontremos con algún dialecto SQL específico final de una base de datos. Por ejemplo, el motor Jet de peticiones reconoce el punto y coma como un finalizador de sentencia SQL, pero no devolverá ningún juego de resultados adicional de ninguna otra sentencia SQL que siga al punto y coma. Por lo tanto, el código para Northwind.mdb en el siguiente listado OpenOleDbDataReader reutiliza OleDbCommand con una segunda sentencia SQL: Private Sub OpenOleDbDataReader() Dim strConn As String = "Provider=Microsoft.Jet.OLEDB.4.0;" + _ "Data Source=C:\SQL Server 2000 Sample Databases\Northwind.mdb;Persist Security Info=False" 'Dim strConn As String = "Provider=SQLOLEDB;" + _ ' "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=SSPI"

18

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 19

Pasar de ADO a ADO.NET Dim cnnNwind As OleDbConnection = New OleDbConnection(strConn) cnnNwind.Open() Dim strSQL As String = "SELECT * FROM Shippers" 'strSQL += ";SELECT EmployeeID, FirstName, LastName FROM Employees" Dim cmdReader As OleDbCommand = New OleDbCommand(strSQL, cnnNwind) cmdReader.CommandType = CommandType.Text Dim odbReader As OleDbDataReader = _ cmdReader.ExecuteReader(CommandBehavior.Default) lstShippers.Items.Clear() With odbReader If .HasRows Then While .Read lstShippers.Items.Add(.Item(0).ToString + " - " + .Item(1).ToString) End While .Close() End If End With lstEmployees.Items.Clear() cmdReader.CommandText = + _ "SELECT EmployeeID, FirstName, LastName FROM Employees" odbReader = cmdReader.ExecuteReader(CommandBehavior.CloseConnection) With odbReader If .HasRows Then While .Read lstEmployees.Items.Add(.Item(0).ToString + " - " + _ .Item(1).ToString + " " + .Item(2).ToString) End While End If .Close() End With End Sub Hay que cerrar el primer DataReader antes de cambiar la propiedad CommandText para reutilizar el objeto OleDbCommand

1.5.2 Cambiar SqlConnection y SqlCommand por SqlXmlCommand Devolver objetos XmlReader con el proveedor de datos OleDb significa añadir una referencia de proyecto a Microsoft.Data.SqlXml. Añadir una sentencia ImportsMicrosoft.Data.SqlXml al archivo de clase del formulario simplifica las referencias a las clases. Una característica interesante del objeto SqlXmlCommand es que no requiere ningún objeto SqlConnection, tal como muestra el siguiente listado para el procedimiento OpenSqlXmlReader:

19

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 20

Bases de datos con Visual Basic Private Sub OpenSqlXmlReader() Dim strConn As String = "Provider=SQLOLEDB; Server=.\SQLEXPRESS; " + _ "database=Northwind; Integrated Security=SSPI" 'Dim strConn As String = "Provider=SQLOLEDB;Data Source=localhost;" + _ ' "Initial Catalog=Northwind;Integrated Security=SSPI" Dim strSQL As String = "SELECT * FROM Shippers FOR XML AUTO, Elements" Dim cmdXml As SqlXmlCommand = New SqlXmlCommand(strConn) cmdXml.CommandText = strSQL Dim xrShippers As System.Xml.XmlReader = cmdXml.ExecuteXmlReader With xrShippers .Read() Do While .ReadState <> Xml.ReadState.EndOfFile txtXML.Text += .ReadOuterXml Loop 'Format the result txtXML.Text = Replace(txtXML.Text, "><", ">" + vbCrLf + "<") .Close() End With End Sub

1.5.3 Probando el proveedor de datos Odbc Si no debe trabajar con un servidor de base de datos heredada para el cual no haya ningún proveedor de datos OLE DB disponible, es muy poco probable que tenga que usar un proveedor de datos Odbc. El siguiente listado de procedimiento OpenOdbcDataReader se muestra sólo para hacer el libro más completo: Private Sub OpenOdbcDataReader() Dim strConn As String = "DRIVER={SQL Server};Server=.\SQLEXPRESS;" + _ "Trusted_connection=yes;database=Northwind;" Dim cnnNwind As OdbcConnection = New OdbcConnection(strConn) cnnNwind.Open() Dim strSQL As String = "SELECT * FROM Shippers" Dim cmdReader As OdbcCommand = New OdbcCommand(strSQL, cnnNwind) cmdReader.CommandType = CommandType.Text Dim sdrReader As OdbcDataReader = _ cmdReader.ExecuteReader(CommandBehavior.CloseConnection) If chkUseOdbc.Checked Then lstShippers.Items.Clear() End If With sdrReader If .HasRows Then While .Read Dim intShipperID As Integer = .GetInt32(0)

20

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 21

Pasar de ADO a ADO.NET Dim strCompany As String = .GetString(1) Dim strPhone As String = .GetString(2) If chkUseOdbc.Checked Then lstShippers.Items.Add(.Item(0).ToString + " - " + .Item(1).ToString) End If End While End If .Close() End With End Sub

1.6 Trabajando con datos DataReader y SqlResultSet tipificados El código anterior utiliza los métodos Reader.Item (ColumnIndex).ToString, Reader.GetString (ColumnIndex), y Reader.GetInt32 (ColumnIndex) para extraer valores de columna para tipos de datos originales .NET que define el namespace System. ADO.NET 2.0 proporciona las siguientes enumeraciones específicas de datos: )

)

)

System.Data.DbType es una enumeración genérica para definir los tipos de datos de los parámetros, campos y propiedades de Oledb y Odbc. System.Data.SqlDbType es una enumeración para utilizar únicamente con los objetos SqlParameter. VS 2005 añade automáticamente parámetros SqlParameters cuando se crean DataSets tipificados desde las tablas del SQLServer en las secciones siguientes. System.Data.SqlTypes es un namespace que contiene estructuras para todos los tipos de datos del SQLServer 2000 y 2005, excepto timestamp y las clases y enumeraciones relacionadas con él. Utilizando estructuras SqlTypes se mejora el acceso a los datos ya que se elimina la conversión a los tipos nativos .NET, y se asegura que los valores de columna no estén troncados. La ayuda online de VS 2005 proporciona documentación adecuada para las enumeraciones DbType y SqlDbType y las estructuras SqlTypes, por lo que este capítulo no incluye ninguna tabla de estas enumeraciones y tipos.

El siguiente listado OpenDataReaderSqlTypes muestra ejemplos de cómo se usan los típicos métodos GetSqlDataType(ColumnIndex): Private Sub OpenDataReaderSqlTypes() Dim cnnNwind As SqlConnection = New SqlConnection(strConn) Dim sdrReader As SqlDataReader = Nothing Try cnnNwind.Open() Dim strSQL As String = "SELECT Orders.*, " + _

21

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 22

Bases de datos con Visual Basic "ProductID, UnitPrice, Quantity, Discount " + _ "FROM Orders INNER JOIN [Order Details] ON " + _ "Orders.OrderID = [Order Details].OrderID WHERE CustomerID = 'ALFKI'" Dim cmdReader As SqlCommand = New SqlCommand(strSQL, cnnNwind) sdrReader = cmdReader.ExecuteReader(CommandBehavior.CloseConnection) With sdrReader If .HasRows Then While .Read Dim s_intOrderID As SqlInt32 = .GetSqlInt32(0) Dim s_strCustomerID As SqlString = .GetSqlString(1) Dim s_datOrderDate As SqlDateTime = .GetSqlDateTime(3) Dim s_monUnitPrice As SqlMoney = .GetSqlMoney(15) Dim s_sngDiscount As SqlSingle = .GetSqlSingle(17) End While End If End With Catch exc As Exception MsgBox(exc.Message + exc.StackTrace) Finally sdrReader.Close() End Try End Sub

Se pueden actualizar los valores de columna del objeto SqlResultSet con variables muy tipificadas invocando el método SqlResultSet.SetSqlDataType (ColumnIndex). En capítulos posteriores se verán más ejemplos de operaciones de recuperación y actualización de datos SQL server, muy tipificados, que utilizan estos métodos.

1.7 Objetos tipificados DataSet de ADO.NET El objeto DataSet es exclusivo de ADO.NET y los DataSets tipificados son el método preferido para recuperar y actualizar tablas relacionales, si bien los DataSets no se limitan al procesamiento de datos relacionales. Los objetos DataSet, definidos en un esquema XML e implementados con una gran cantidad de código VB 2005 autogenerado, se crean con los diseñadores de VS 2005. Los DataSet no tipificados son objetos en tiempo de ejecución que se crean con código. DataSet no tiene ningún objeto ADODB paralelo, pero ambas clases de DataSet se comportan de forma similar a los juegos de datos fuera de conexión en los siguientes aspectos: )

)

Abren una conexión, recuperan y cachean los datos a editar, después cierran la conexión. Llevan hasta los controles de los formularios, simples y complejos, de Windows, para su edición.

)

Permiten la edición de datos cacheados localmente una vez cerrada la conexión.

)

Se pueden guardar en archivos locales y reabrir para su edición.

22

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 23

Pasar de ADO a ADO.NET )

Permiten reabrir la conexión y aplicar actualizaciones a tablas básicas.

A continuación citamos las diferencias principales entre DataSets y Recordsets fuera de conexión: )

)

)

)

)

)

)

)

)

)

)

Un DataSet consiste en copias ocultas de uno o más Recordsets, llamados objetos DataTable, seleccionados de una o más tablas básicas individuales. Un Recordset es un juego único de datos que puede representar una vista de una, dos, o más tablas relacionadas. Al persistir un DataSet se serializan los datos de las DataTables en un documento XML Infoset jerarquizado, centrado en el elemento y lo guarda en el sistema de archivos locales. Los Recordsets sin conexión guardan datos localmente en un archivo XML no jerárquico, centrado en el atributo. Las DataTables normalmente, aunque no necesariamente, están relacionadas entre sí por relaciones de clave primaria. Las restricciones de clave primaria y clave externa, y las relaciones entre tablas, se tienen que definir manualmente, a menos que el DataSet se haya creado automáticamente con el ayudante Data Source Configuration Wizard de VS 2005. Las DataTables se pueden crear a partir de las tablas base de cualquier instancia accesible deservidor de base de datos. Las DataTables se pueden crear a partir de los documentos XML Infoset estructurados (tabular). TableAdapters rellenan y actualizan las DataTables a través de una conexión controlada. Los TableAdapters envuelven los objetos DataAdapter. El ayudante Data Source Configuration Wizard permite elegir una de las conexiones de datos existentes definidas en el Explorador de Servidores, o bien crear un nuevo objeto de conexión. El ayudante genera entonces peticiones SQL (SQL queries) o procedimientos almacenados para realizar las operaciones UPDATE, INSERT, y DELETE. Estas peticiones están basadas sen la petición SELECT o el procedimiento almacenado que se especifique para rellenar cada DataTable. DataSets cachea copias de datos de tabla originales y modificados en formato XML. Por lo tanto, DataSets con un gran número de filas consumen mucho más espacio en la RAM del cliente que Recordsets con el mismo número de filas. Se puede escribir código para crear conexiones de datos de tiempo de ejecución, DataAdapters, y DataSets básicos, pero es mucho más fácil aprovechar las ventajas de los procesos automatizados de VS 2005 para generar código y crear DataSets tipificados que ya vienen definidos en un esquema XML. Las actualizaciones de los DataSet se producen fila por fila si no se indica un valor mayor que 1 para la nueva propiedad DataAdapter.BatchSize, que es la que define el número máximo de filas actualizadas por batch.

La siguiente figura compara los objetos requeridos por los Recordsets ADODB actualizables y los DataSets tipificados de ADO.NET 1.x y 2.0. Los parámetros son opcionales en los comandos ADODB, pero no lo son en los TableAdapters actualizables, que tienen 23

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 24

Bases de datos con Visual Basic cuatro comandos estándar: SelectCommand, InsertCommand, UpdateCommand, y DeleteCommand. El uso de las nuevas componentes del ADO.NET 2.0 BindingNavigator es opcional. Más adelante, en este capítulo, describiremos cómo encaja la BindingSource en la arquitectura de acceso a los datos de ADO.NET 2.0. Las siguientes secciones muestran métodos alternativos para generar con VS 2005 y SQLServer 2000 o 2005 los objetos de ADO.NET que muestra la siguiente figura.

VS 2005 materializa TableAdapters, DataSets, BindingSources, y BindingNavigators como objetos nombrados en la bandeja de diseño del formulario. Los TableAdapters y DataSets también aparecen en el Cuadro de herramientas; la sección Datos contiene DataSet, BindingSource, y controles BindingNavigator. En los primeros días del largo periodo de gestación de VS 2005, estos objetos de diseño se llamaron colectivamente Data Components, BindingSource se llamó DataConnector, y BindingNavigator era DataNavigator. Este libro utiliza el término Data Components para referirse a los objetos de datos en tiempo de diseño, nombrados, que se encuentran en el Cuadro de herramientas.

24

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 25

Pasar de ADO a ADO.NET

1.7.1 Añadir un juego de datos tipificado desde un servidor SQL, fuente de datos ADO.NET utiliza el término origen de datos (data source) como sinónimo de DataSet tipificado con una conexión de base de datos predefinida y persistente. El proceso para crear una fuente de datos ADO.NET es parecido a utilizar el Data Environment Designer de VB6 para especificar un proveedor de datos OLE DB para una o más tablas. A diferencia de lo que ocurre con Data Environment Designer, los DataSets de tablas múltiples no tienen la estructura jerárquica que el OLE DB Shape provider crea para mostrar en pantalla el control Hierarchical FlexGrid de VB6. Los servicios Web e instancias de objeto también pueden actuar como fuentes de datos de ADO.NET, como ser verá en capítulos posteriores.

A continuación le indicamos cómo añadir una nueva fuente de datos SQLServer Northwind para un nuevo proyecto de formulario Windows y generar automáticamente un DataSet tipificado y sus componentes desde la tabla Costumers: 1. Seleccionar la opción Mostrar orígenes de datos del menú Datos para abrir el panel lateral Orígenes de datos, si es necesario, y pulse Agregar nuevo origen de datos… para iniciar el Asistente para la configuración de orígenes de datos. 2. En el cuadro Elegir un tipo de origen de datos, seleccione Base de datos (lo está por defecto) y pulse el botón Siguiente para acceder para acceder al cuadro Elija la conexión de datos, donde se mostrará en una lista desplegable las conexiones de datos existentes, si hay alguna. 3. Pulsando el botón Nueva conexión se accede al cuadro de diálogo Agregar conexión. Por defecto aparece como Origen de datos la opción Archivo de base de datos de Microsoft Access (OLE DB). Si accede a una base de datos de Access deberá buscarla a través del botón Examinar y seleccionarla en el cuadro de texto Nombre del archivo de la base de datos. Si, en cambio, trabaja con bases de datos SQL, en el cuadro Origen de datos deberá seleccionar el origen de los datos como Microsoft SQL Server (SqlClient). Este será nuestro caso. 4. Escriba localhost o \SQLEXPRESS en el cuadro de lista Nombre del servidor. Otra alternativa es seleccionar un SQLServer local, o de la red, o bien una instancia MSDE que tenga una base de datos Northwind o NorthwindCS. 5. Acepte la opción Utilizar autenticación de Windows, abra la lista Seleccionar o escribir nombre de base de datos y seleccione Northwind. Pulse el botón Probar conexión para verificar el objeto SqlConnection, tal como se muestra en la siguiente figura. 6. Pulse el botón Aceptar para cerrar el cuadro de diálogo y volver a Elija la conexión de datos en la que aparece ServerName.Northwind.dbo como el nombre de la nueva conexión, System.Data.SqlClient como proveedor, y Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=True como la Cadena de conexión. 7. Pulse el botón Siguiente para abrir el cuadro de diálogo Guardar cadena de connexion en el archive de config. de la aplicación. Seleccione la casilla de verificación Sí, guardar

25

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 26

Bases de datos con Visual Basic

la conexión como y aceptar la opción por defecto NorthwindConnectionString como nombre de la cadena de conexión. 8. Pulse Siguiente para abrir el cuadro Elija los objetos de base de datos, en el que se muestra un árbol con las tablas, vistas, procedimientos almacenados y funciones disponibles. Expandiendo la rama Tablas verá la tabla Customers. Seleccionela y pulse Finalizar. Como ve, hemos aceptado NorthwindDataSet como nombre para el juego de datos tal y como muestra la siguiente figura.

Al seleccionar una tabla se genera automáticamente SelectCommand, con el que se recuperan todas las filas de la tabla, y UpdateCommand, InsertCommand, y DeleteCommand para las actualizaciones de la tabla

26

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 27

Pasar de ADO a ADO.NET 9. Al pulsar el botón Finalizar se generará el juego de datos tipificado NorthwindDataSet y se mostrará en el panel Orígenes de datos. Expanda la rama Customers para mostrar las columnas de la tabla del mismo nombre tal como se muestra en la siguiente figura.

El nuevo objeto SqlConnection creado en los pasos 3 a 5 aparece en el nodo Conexiones de datos del Explorador de servidores como servidor\sqlexpress.Northwind.dbo. El nodo se puede renombrar en el Explorador de servidores con un nombre más sencillo, por ejemplo localhost.Northwind; el cambio no afecta a los objetos dependientes del proyecto. Añadir un DataSet tipificado genera un esquema XSD, NorthwindDataSet.xsd en este ejemplo, y añade 1.197 líneas de código VB 2005 al archivo de clase parcial NorthwindDataSet.Designer.vb, cuyo tamaño es de 73 KBytes. Las clases parciales son una característica nueva de VB 2005 y C# que permite expandir una clase, como la NorthwindDataSet, con archivos de clase adicionales. VB 2005 usa la sentencia Public Partial Class className para identificar archivos de clase parcial. Deberá tener seleccionado el botón Mostrar todos los archivos del panel Explorador de soluciones para ver NorthwindDataSet.Designer.vb y los dos archivos vacíos NorthwindDataSet.xsc y NorthwindDataSet.xss. Realice una doble pulsación en el nodo NorthwindDataSet.xsd situado en el Explorador de proyectos para mostrar la tabla de datos Customers y su adaptador de tabla Customers asociado, tal como muestra la siguiente figura, en la ventana principal. El código VB 2005 en DataSetName.Designer.vb proporciona IntelliSense para los objetos DataSet y los objetos, anteriormente vinculados, DataTable y DataSet. El código también permite acceso directo a las clases nombradas, métodos y eventos para DataSet y su adaptador de tabla del NorthwindDataSet.Designer.vb, código de las listas Classes y métodos de la ventana. 27

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 28

Bases de datos con Visual Basic Si ya ha trabajado con DataSets tipificados en VS 2003, se dará cuenta cuenta de que el esquema para ADO 2.0 DataSets tiene mucha más verborrea que la versión ADO 1.x, que consta sólo de 30 líneas que definen el Customers DataSet. ADO.NET 2.0 prefija el esquema de tiempo de diseño con 258 líneas de información <xs:annotation>, que proporcionan una definición completa del DataSet y su string de conexión, comandos y parámetros, así como los datos del mapping de columnas. La parte del esquema que define los elementos para los campos de tabla crece de 30 a 94 líneas porque las definiciones de los elementos contienen ahora valores para el atributo maxLength y utilizan atributos restrictionBase para especificar los tipos de dato XSD..

En la siguiente figura puede ver Internet Explorer mostrando las primeras líneas de las 352 que componen el esquema.

28

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 29

Pasar de ADO a ADO.NET Al usar los métodos DataSet.WriteXml y DataSet.WriteXmlSchema para persistir DataSets en archivos locales, se ve que el esquema Customers DataSet, que difiere enormemente de la versión en tiempo de diseño, ocupa 9,31 KBytes y el documento XML ocupa 37,3 KBytes. Más adelante en este libro, se incluye código para guardar el esquema del juego de datos Northwind Customers. El esquema guardado no se puede abrir en ventana principal del proyecto.

1.8 Añadir una DataGridView y BindingNavigator Controls Al abrir el Form1 y el panel Orígenes de datos cambia el aspecto de los nodos DataSource. Por defecto, el icono de la tabla de datos Customers representa un DataGridView. Arrastrando el nodo de la tabla Customers desde el panel Orígenes de datos hasta el Form1 por defecto del proyecto, se autogeneran cuatro componentes en la bandeja que hay bajo el diseñador y se añaden los controles DataGridView y DataNavigator a un formulario que ha crecido considerablemente, tal como muestra la siguiente figura.

Aquí están las descripciones de las cuatro componentes de la bandeja que muestra la figura anterior: )

NorthwindDataSet es la referencia del formulario a la fuente de datos para el formulario NorthwindDataSource.xsd. 29

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 30

Bases de datos con Visual Basic )

)

)

CustomersBindingSource es un objeto BindingSource basado en formulario, el cual unifica la unión y la navegación de datos de control y fiilas de datos para la tabla de datos Customers, al proporcionar acceso directo al objeto BindingManager. Para facilitar a los programadores de VB6 el cambio a ADO.NET 2.0, las BindingSources tienen propiedades y métodos que simulan a los objetos ADODB.Recordset. Ejemplo de ellos son las propiedades AllowEdit, AllowAddNew, y AllowRemove (delete) y los correspondientes métodos AddNew, CancelNew, EndNew, Edit, CancelEdit, y EndEdit. Los conocidos métodos MoveFirst, MoveLast, MoveNext, y MovePrevious se ocupan de la navegación por las filas. Hacer posible la navegación significa vincular un DataGridView o añadir otros controles para manipular la BindingSource. CustomersTableAdapter es el envoltorio del formulario para cualquier objeto SqlDataAdapter que llene la tabla de datos NorthwindDataSet´s Customers invocando el método CustomersTableAdapter.Fill. Los métodos Update, Insert, y Delete envían cambios en el juego de datos al servidor de la base de datos. La propiedad CustomersTableAdapter.Adapter permite acceder al SqlDataAdapter subyacente. CustomersBindingNavigator es un control habitual de ToolStrip que simula el botón VCR y otros de un ADODB.DataControl. Vincular el CustomersBindingNavigator con la CustomersBindingSource permite invocar con los botones los métodos Move..., AddNew, y Cancel.... Por defecto, los BindingNavigators suelen estar en la parte superior del formulario. Al ejecutar el formulario se puede arrastrar el BindingNavigator a una posición más cómoda, en la parte inferior del formulario, o también se le puede dar el valor Bottom a la propiedad Dock del DataNavigator en el diseñador de proyecto. DataComponents, DataConnectors, y DataNavigators son componentes y controles nuevos de ADO.NET 2.0 que substituyen los DataConnections y DataAdapters basados en formulario de ADO.NET 1.x. Las fuentes de datos de VS 2005 crean automáticamente relaciones entre los juegos de datos de diferentes tablas, que requieren una intervención manual previa. Los DataConnectors simplifican el código para navegar por las tablas de datos. El archivo DataSet.vb contiene clases, interfaces y tratadores de eventos para las componentes de datos.

El último paso en el proceso de autogeneración del formulario de datos 2005 es añadir el método CustomersComponent.Fill al evento Form1_Load; y código para salvar los cambios del DataSet no se añade automáticamente al evento bindingNavigatorSaveItem_Click, debido a la complejidad del código cuando el juego de datos contiene tablas múltiples. Salvar cambios múltiples en tablas madre y derivadas requiere secuencias para inserciones, actualizaciones y borrados, a fin de mantener la integridad referencial. Private Sub Form1_Load(ByVal sender As System.Object, ¨ ByVal e As System.EventArgs) Handles MyBase.Load 'TODO: This line of code loads data into the 'NorthwindDataSet.Customers' table. 'You can move, or remove it, as needed. Me.CustomersTableAdapter.Fill(Me.NorthwindDataSet.Customers) End Sub

30

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 31

Pasar de ADO a ADO.NET Private Sub bindingNavigatorSaveItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bindingNavigatorSaveItem.Click Me.CustomersBindingSource.EndEdit() Me.CustomersTableAdapter.Update(Me.NorthwindDataSet.Customers) End Sub

La siguiente figura muestra el formulario final después de reducir el tamaño, ampliar el control de DataGridView para llenar el espacio disponible y pulsar para crear, depurar y ejecutar el proyecto.

La CustomersDataGridView está vinculada a la tabla Customers y se puede editar por defecto. Los cambios que se hagan en la DataGridView no se validan en la tabla hasta que no se pulsa el botón Save Data. Para facilitar la edición, el ancho de columna se puede adaptar automáticamente al contenido definiendo para la propiedad AutoSizeColumnsMode de DataGridView el valor AllCells o DisplayedCells, que añade una barra de desplazamiento horizontal al control.

1.9 Persistir y reabrir el juego de datos El manejador de eventos del proyecto frmDataGridView_Load incluye el siguiente código para salvar el documento de datos XML NorthwindDataSet y el esquema solo. Se puede añadir código parecido después de la última invocación DataComponent.Fill o DataAdapter.Fill de cualquier proyecto para persistir su juego de datos. Private Sub Form1_Load(ByVal sender As System.Object, ¨ ByVal e As System.EventArgs) Handles MyBase.Load 'TODO: This line of code loads data into the 'NorthwindDataSet.Customers' table. 'You can move, or remove it, as needed. Me.CustomersTableAdapter.Fill(Me.NorthwindDataSet.Customers) Dim strPath As String = Application.StartupPath With Me.NorthwindDataSet .WriteXml(strPath + "CustsNoSchema.xml", XmlWriteMode.IgnoreSchema)

31

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 32

Bases de datos con Visual Basic .WriteXml(strPath + "CustsWithSchema.xml", XmlWriteMode.WriteSchema) .WriteXmlSchema(strPath + "CustsSchema.xsd") End With End Sub

Persistiendo el DataSet como documento XML, sin el esquema incrustado, permite dar soporte a los usuarios sin conexión, cargando de nuevo el DataSet del archivo. La sentencia siguiente se puede substituir por Me.CustomersTableAdapter.Fill(Me.NorthwindDataSet.Customers) cuando el usuario está desconectado: Me.NorthwindDataSet.ReadXml(strPath + CustsNoSchema.xml , XmlReadMode.Auto) El escenario en el mundo real para persistir y cargar de nuevo un juego de datos es mucho más complejo que lo que hemos visto aquí. En capítulos posteriores se describe cómo salvar y cargar de nuevo los cambios pendientes del DataSet que no se han pasado a las tablas base. El argumento XmlReadMode.Auto aparece por defecto, así que incluirlo es opcional.

1.10 Cambiar de un DataViewGrid a un Details Form La combinación por defecto de los controles DataViewGrid y DataNavigator acelera la creación de un formulario utilizable. De todos modos, un DataNavigator es mucho más útil para crear un formulario de detalles que muestre en pantalla los valores de columna en cuadros de texto u otros controles vinculados, como selectores de datos DateTime y cuadros de verificación para valores booleanos. La ventana Data Sources facilita el cambio de la DataGridView a un formulario de detalle. Borre el control DataGridView, muestre la ventana Orígenes de datos, abra la lista desplegable para la tabla de datos, y seleccione Detalles como se muestra en la siguiente figura.

Arrastre el icono DataTable hasta el formulario para añadir automáticamente una columna de etiquetas con controles asociados de vinculación de datos (cuadros de texto en este ejemplo) al formualrio. La siguiente figura, que es una versión modificada del proyecto GeneratedDataGridView, muestra las etiquetas y los cuadros de texto reordena-

32

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 33

Pasar de ADO a ADO.NET dos para reducir la altura del formulario.

1.11 Añadir un control de vínculo de datos relacionado Al panel Orígenes de datos se le puede añadir una tabla relacionada y después un control, como DataGridView, que se puede vincular al BindingAdapter relacionado. Para añadir un control relacionado OrdersDataGridView a una copia del proyecto GeneratedDetailView.sln, Debe realiar los siguientes pasos: 1. Copie y pege la carpeta GeneratedDetailView y renombre la nueva carpeta como OrdersDetailView. No renombre el proyecto. 2. Pulse para crear y compilar el proyecto. Corrija cualquier error de nombre que detecte el depurador. 3. Abra la ventana Orígenes de datos y pulse el botón del ayudante Configurar Dataset con el asistente para abrir la página Elija los objetos de la base de datos. 4. Expandir el árbol Tablas y seleccione la casilla de verificación de la tabla Orders. Pulse el botón Finalizar. De ese modo se añade en panel Orígenes de datos un nodo relacional Orders a la tabla Customers y un nodo individual Orders (ver siguiente figura). 5. Con DataGridView seleccionado en la lista desplegable, arrastre el nodo Orders relacionado por debajo de los cuadros de texto vinculados del formulario para autogenerar un control OrdersDataGridView. 6. Ajuste el tamaño y la posición de los controles y defina para la propiedad OrdersDataGridView.AutoSizeRowsMode el valor DisplayedCells. Opcionalmente se puede modificar la propiedad Text del formulario para reflejar el cambio en el diseño.

33

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 34

Bases de datos con Visual Basic

7. Pulse para crear y ejecutar el proyecto. El formulario aparecerá tal como muestra la siguiente figura.

Arrastrando el nodo relacionado de la tabla Orders hasta el formulario se añade un OrdersTableAdapter y OrdersBindingSource a la bandeja, y el control OrdersDataGridView al formulario. El valor de la propiedad OrdersDataGridView del control DataSource es OrdersBindingSource. 34

VisualBasic2005_01.qxp

02/08/2007

16:11

PÆgina 35

Pasar de ADO a ADO.NET La propiedad OrdersBindingSource tiene el valor CustomersBindingSource y el valor de la propiedad DataMember es FK_Orders_Customers, el cual es la relación de clave foránea en el campo CustomerID entre las tablas de Customers y Orders. Para verificar las propiedades de FK_Orders_Customers debe abrir el NorthwindDataSet.xsd en la ventana principal, pulsar con el botón secundario la línea de relación entre las tablas Customers y Orders, y seleccionar Editar relación para abrir el cuadro de diálogo Relación (ver figura siguiente).

Las relaciones que se definen añadiendo tablas relacionadas a la ventana Orígenes de datos no refuerzan la integridad referencial por defecto. Hay que cambiar el valor por defecto de la propiedad Sólo relación a uno de los otros valores para mantener la integridad referencial. También se puede especificar Cascade u otras opciones para las reglas actualización, eliminación, y aceptación o rechazo.

35

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 37

CAPÍTULO 2

Las novedades de ADO.NET 2.0 En este capítulo trataremos los nuevos objetos de ADO.NET 2.0 y los métodos, propiedades y eventos utilizados con ellos. De la misma forma que el capítulo anterior, este capítulo empieza con unas descripciones de los nuevos objetos en tiempo de ejecución, como DbProviderFactory y SqlBulkCopy, con los correspondientes ejemplos de código para crear y manejar los nuevos objetos. El capítulo continúa com más ejemplos avanzados de las componentes y controles de ADO.NET 2.0 para los formularios de Windows, que se pueden agragar con la ayuda de diseñadores: DataTables, BindingSources, BindingNavigators y DataGridViews. Todos los ejemplos de código SQLServer de este capítulo se pueden ejecutar con SQLServer 2000, SQLServer 2005 o SQLServer 2005 Express Edition (SQLX) y nencesitan los privilegios del administrador del sistema. Si trabajamos con SQLX, deberemos cambiar la cadena de conexión de cada proyecto de localhost a .\SQLEXPRESS.

2.1 Los objetos de formulario Este libro define un objeto en tiempo de ejecución como un tipo de objeto no visual, relacionado con los datos que se genera sin la ayuda de los múltiples asistentes. Los objetos en tiempo de ejecución de ADO.NET 2.0 se crean escribiendo código VB.NET 2005 sin la ayuda de los ayudantes de tiempo-diseño de VS 2005 ni código autogenerado. Microsoft ha dedicado una parte importante del esfuerzo invertido en el desarrollo de VS 2005 y ADO.NET 2.0 en simplificar con arrastrar y colocar la creación de formularios básicos Windows y Web de vinculación de datos. Otro aspecto que se ha cuidado ha sido dar soporte a las nuevas características del SQLServer 2005 con los objetos System.Data y System.Xml. Por eso, ADO.NET 2.0 sólo incluye algunos objetos y características nuevas y actualizadas que son compatibles con las fuentes de datos de SQLServer 2000. Más adelante, en este mismo libro, se tratarán las propiedades de ADO.NET 2.0 y VB.NET 2005 específicas para SQLServer 2005. A continuación indicamos los objetos en tiempo de ejecución y actualizados, métodos y características de lenguaje, más importantes para los proyectos de formulario Windows:

37

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 38

Bases de datos con Visual Basic )

)

)

)

)

)

)

El objeto DbProviderFactory permite escribir código común para proveedores de datos y servidores de bases de datos alternativos. El objeto SqlBulkCopy permite insertar con gran eficiencia datos de SQLServer de fuentes relacionales y XML. El método SqlConnection.RetrieveStatistics proporciona información detallada sobre la conexión abierta con el SQLServer. La ejecución asincrónica de SqlCommand permite entrelazar consultas o actualizaciones múltiples de larga ejecución. Los nuevos objetos actualizados DataTable soportan las características comunes de los DataSet, como son los métodos ReadXml y WriteXml, retornan valores de los servicios Web e interfaces remotas y streaming. A las tablas de datos se les puede asignar espacios-nombre y prefijos para los nombre de espacio. Los tipos Null permiten definir objetos fuertemente tipificados, con miembros a los que se puede asignar el valor DbNull.

En las secciones siguientes se explica cómo utilizar las características del precedente ADO.NET 2.0 con ejemplos de código derivado de los proyectos-ejemplo de formularios Windows.

2.1.1 Utilizar DbProviderFactories para crear proyectos con bases de datos agnósticas La nueva clase System.Data.Common.DbProviderFactories proporciona a los desarrolladores de bases de datos la oportunidad de enfrentarse a la creación de aplicaciones agnósticas frente a las fuentes de datos. Crear aplicaciones de entradas de datos no-triviales que puedan interactuar sin fisuras con todos los administradores de bases de datos relacionales, para los que existen proveedores de datos controlados, no es precisamente algo simple. Las diferencias menores en la sintaxis SQL, tipos de datos, dialectos de procedimientos almacenados, tratamiento de error, y otras características propias de una base de datos, requerirán sin duda un esfuerzo. Si actualmente utiliza el proveedor de datos controlados .NET Framework OleDb, o ADODB con proveedores OLE DB para asegurar la interoperabilidad de las bases de datos, seguramente encontrará que Microsoft y el tercero en cuestión, ADO.NET, ofrecen mejor rendimiento y, como resultado, mayor escalabilidad. Por otra parte, el nivel de ampliación y mejora que .NET garantiza a los proveedores de datos, hace difícil escribir código que sea totalmente transparente al proveedor. Los grupos de terceros de proveedores controlados .NET pueden reducir la interoperabilidad con costes de licencia añadidos. Por ejemplo, DataDirect Technologies ofrece proveedores de datos controlados para IBM DB2 y DB2 UDB; Oracle 8i, 9i, y 10g; SQLServer 7 y 2000; Sybase Adaptive Server 11.5 y 11.9; y Sybase Adaptive Server Enterprise 12.0 y 12.5. Todos los proveedores DataDirect buscan salidas para minimizar las diferencias de sintaxis SQL y comunicarse con servidores a través de los protocolos de los vendedores de bases de datos.

38

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 39

Las novedades de ADO.NET 2.0 Crear un objeto DataReader de la clase DbProviderFactories es un proceso en siete pasos: 1. Crear un objeto DbProviderFactory pasando el nombre completo de la clase del proveedor de datos, como System.Data.SqlClient, al argumento de una sentencia DimFactoryNameAsDbProviderFactory = DbProviderFactories.GetFactory(strProvider). 2. Crear un objeto IdbConnection invocando el método DimConnectionNameAsIDbConnection = FactoryName.CreateConnection(). 3. Definir el valor de la propiedad ConnectionName.Connection.String. 4. Crear un objeto IdbCommand invocando el método DimCommandNameAsIDbCommand = ConnectionName.CreateCommand(). 5. Definir para las propiedades CommandName.CommandType (opcional) y CommandName.CommandText los valores adecuados para el proveedor. 6. Llamar al método ConnectionName.Open(). 7. Crear un objeto IdataReader invocando el método DimReaderNameAsIDataReader = CommandName.ExecuteReader. El objeto IDataReader tiene los miembros que los DataReaders específicos del proveedor para ADO.NET 1.x y 2.0, más el nuevo método GetSchemaTable que se describe en el siguiente apartado. El proyecto de ejemplo DbFactoryTest.sln muestra datos en pantalla de una de las tres tablas Northwind creando y atravesando los objetos IDataReader de SqlClient, OleDb, u Odbc, que se especifiquen seleccionando la opción apropiada. El formulario incluye también un control DataGridView con el que se muestra en pantalla el esquema de tabla DataTable (del que trata el siguiente apartado) tal como muestra la siguiente figura.

El siguiente listado contiene el código para las declaraciones de variables y el botón de opción del manejador de eventos de OleDb DbProviderFactory:

39

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 40

Bases de datos con Visual Basic 'OleDb provider settings - Products table Private strOleDbProvider As String = "System.Data.OleDb" Private strOleDbConn As String = "Provider=SQLOLEDB;Data Source=.\SQLEXPRESS;" + _ "Initial Catalog=Northwind;Integrated Security=SSPI" Private strOleDbTable As String = "Products" Private Sub optOleDb_CheckedChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles optOleDb.CheckedChanged If optOleDb.Checked = True Then PopulateList(strOleDbProvider, strOleDbConn, strOleDbTable) Me.Text = "DbFactory Test Form - OleDb" End If End Sub El tratador de eventos optOleDB_CheckedChanged pasa los valores requeridos del parámetro OleDb al procedimiento PopulateList, ampliado con el código siguiente: Private Sub PopulateList(ByVal strProvider As String, _ ByVal strConn As String, ByVal strTable As String) Dim cnFactory As IDbConnection = Nothing Dim drData As IDataReader = Nothing Try Dim dpFactory As DbProviderFactory = _ DbProviderFactories.GetFactory(strProvider) cnFactory = dpFactory.CreateConnection() cnFactory.ConnectionString = strConn Dim cmFactory As IDbCommand = cnFactory.CreateCommand cmFactory.CommandType = CommandType.Text cmFactory.CommandText = "SELECT * FROM " + strTable cnFactory.Open() drData = cmFactory.ExecuteReader(CommandBehavior.KeyInfo) lstData.Items.Clear() Dim dtSchema As DataTable With drData While drData.Read lstData.Items.Add(.GetValue(0).ToString + _ " - " + .GetValue(1).ToString) End While dtSchema = drData.GetSchemaTable() With dgvSchema If dtSchema.Columns.Count > 1 Then .RowHeadersVisible = False .DataSource = dtSchema .AutoGenerateColumns = True Application.DoEvents() If .Columns.Count > 0 Then .Columns(0).Frozen = True .Columns("BaseSchemaName").Width = 110 If .Columns.Count = 24 Then .Columns(23).Width = 200 End If End If

40

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 41

Las novedades de ADO.NET 2.0 End If End With End With If dgvSchema.Columns.Count > 0 Then Dim intCtr As Integer Dim strDataCols As String = "" For intCtr = 0 To dgvSchema.Rows(0).Cells.Count - 1 strDataCols += dgvSchema.Columns(intCtr).Name + vbTab + _ dgvSchema.Rows(0).Cells(intCtr).Value.ToString + vbCrLf Next intCtr intCtr = 0 End If Catch exc As Exception MsgBox(exc.Message + exc.StackTrace) Finally If Not drData Is Nothing Then drData.Close() End If If Not cnFactory Is Nothing Then cnFactory.Close() End If End Try End Sub Hay que especificar CommandBehavior.KeyInfo como el argumento ExecuteReader para devolver las claves primarias correctas y las propiedades de campo relacionadas.

Si sus proyectos deben incluir independencia respecto al proveedor de datos y está dispuesto a escribir más para especificar las diferencias, sutiles o no, entre las diferentes mejoras de los proveedores de datos, pruebe con DbProviderFactories. Sin embargo, tenga en cuenta que el código independiente de proveedores tiene que usar tipos de datos originales .NET, antes que los tipos de datos específicos de cada proveedor para los diferentes add-in de SQLServer, Oracle, y otros servidores soportados por terceros. DbProviderFactories mejora la vinculación de la base de datos, lo que deja en clara desventaja a muchas propiedades del modelo de programación de ADO.NET. El SQL específico de un vendedor y la sintaxis de ejecución de los procedimientos almacenados hacen que escribir código transparente al vendedor con los proveedores de datos ADO.NET 2.0 sea difícil, si no imposible.

2.1.2 Restablecer los esquemas de las tablas base Los DataReaders de ADO.NET 1.x y 2.0 y los DataTableReaders de ADO.NET 2.0, tienen un método GetSchemaTable que devuelve los correspondientes esquemas del objeto en un objeto DataTable. Para dar información sobre el tipo de datos utilizado en los proyectos, que substituye el código para los controles de vinculación que muestran y actuali41

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 42

Bases de datos con Visual Basic zan las tablas base, se utilizan los valores de propiedad del esquema DataTable. Los esquemas DataTable dan valores ColumnLength para definir la propiedad MaxLength en cuadros de texto y valores IsReadOnly que se pueden aplicar a la propiedad ReadOnly de los controles de entrada de datos normal. Estos DataTable también devuelven información clave primaria como son los índices de columna y detalles de autoincrementación. El System.Data.ObjectSpaces.ObjectDataReader, que se incluía en las primeras versiones alpha y Community Technical Preview de VS 2005, daban miembros similares a los de otros DataReaders, incluido el método GetSchemaTable. En Mayo del 2004, Microsoft anunció que ObjectSpaces se lanzaría como componente dentro de las mejoras del sistema de archivos WinFS.

Para crear un esquema DataTable en un DataReader y poblar una DataGridView para mostrar las propiedades de columna se ha de utilizar código parecido al siguiente: Dim dtSchema As DataTable With drData While drData.Read lstData.Items.Add(.GetValue(0).ToString + _ " - " + .GetValue(1).ToString) End While dtSchema = drData.GetSchemaTable() With dgvSchema If dtSchema.Columns.Count > 1 Then .RowHeadersVisible = False .DataSource = dtSchema .AutoGenerateColumns = True Application.DoEvents() If .Columns.Count > 0 Then .Columns(0).Frozen = True .Columns("BaseSchemaName").Width = 110 If .Columns.Count = 24 Then .Columns(23).Width = 200 End If End If End If End With End With

El esquema DataTable contiene una fila por cada columna de tabla base y 27 campos de propiedades de columna SqlDataReader. OleDbDataReaders y OdbcDataReaders devuelven 18 propiedades; los DataTableReaders tienen 25 campos de propiedades. Como el objeto DataTableReader es nuevo en ADO.NET 2.0, en la tabla siguiente se comparan el índice de campos del esquema DataTable y los nombres de propiedades de las tres clases de DataReaders.

42

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 43

Las novedades de ADO.NET 2.0 Index

SqlDataReader

OleDb y Odbc DataReaders

DataTableReader

0

ColumnName

ColumnName

ColumnName

1

ColumnOrdinal

ColumnOrdinal

ColumnOrdinal

2

ColumnSize

ColumnSize

ColumnSize

3

NumericPrecision

NumericPrecision

NumericPrecision

4

NumericScale

NumericScale

NumericScale

5

IsUnique

DataType

DataType

6

IsKey

ProviderType

ProviderType

7

BaseServerName

IsLong

IsLong

8

BaseCatalogName

AllowDBNull

AllowDBNull

9

BaseColumnName

IsReadOnly

IsReadOnly

10

BaseSchemaName

IsRowVersion

IsRowVersion

11

BaseTableName

IsUnique

IsUnique

12

DataType

IsKey

IsKey

13

AllowDBNull

IsAutoIncrement

IsAutoIncrement

14

ProviderType

BaseSchemaName

BaseCatalogName

15

IsAliased

BaseCatalogName

BaseSchemaName

16

IsExpression

BaseTableName

BaseTableName

17

IsIdentity

BaseColumnName

BaseColumnName

18

IsAutoIncrement

AutoIncrementSeed

19

IsRowVersion

AutoIncrementStep

20

IsHidden

DefaultValue

21

IsLong

Expression

22

IsReadOnly

ColumnMapping

23

ProviderSpecificDataType

BaseTableNamespace

24

DataTypeName

BaseColumnNamespace

25

XmlSchema Collection Database

26

XmlSchema Collection OwningSchema

27

XmlSchema CollectionName

Las propiedades que se muestran en negrita son miembros de la nueva clase de ADO.NET 2.0 System.Data.Common .SchemaTableColumn y son necesarias. El resto son miembros opcionales de la clase SystemData.Common.SchemaOptionalTableColumn. Los campos XmlSchemaCollection aparecen sólo en las tablas del SQLServer 2005 y especifican el esquema, si existe, de los campos para los tipos de datos xml.

Los desarrolladores de bases de datos pueden traducir la mayor parte de las propiedades incluidas en la tabla-lista anterior. Por eso la tabla siguiente sólo ofrece la lista de las propiedades cuyo significado no es obvio o que devuelven valores inesperados. 43

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 44

Bases de datos con Visual Basic Nombre de la propiedad

Descripción

ColumnSize

Devuelve –1 si el dato no está disponible, de lo contrario, el tamaño de la columna en bytes.

DataType

El tipo de datos original de .NET que corresponde al tipo de dato de la columna, como en System.Int32 o System.String.

ProviderType

El valor íntegro de una enumeración de tipo de datos especifícos del proveedor.

IsLong

True indica un tipo de datos text o ntext de SQL, o un image,y un campo de objeto OLE o Jet Memo.

ProviderSpecificDataType

Uno de los tipos Sql, como SqlString o SqlInt32 (sólo SqlClient)

Expression

La expresión calculada para una columna de una DataTable (sólo DataTable)

ColumnMapping

Un valor String que especifica la columna de la tabla de destino o 1 si la columna no está mapeada (sólo DataTable)

BaseTableNamespace

El nombre de espacio XML asignado a la tabla, heredado del nombre de espacio del DataSet si está vacío (sólo DataTable)

BaseColumnNamespace

El nombre de espacio XML asignado a la tabla, heredado del nombre de espacio del DataSet si está vacío (sólo DataTable)

XmlSchema Collection Database

El nombre de la base de datos del servidor SQL Server 2005 que contiene el conjunto de esquemas para una columna del tipo xml (null si la columna xml no tiene esquema)

XmlSchema CollectionOwning Schema

Esquema relacional del SQL Server 2005 que contiene el conjunto de XmlSchema (null si la columna xml no tiene esquema)

XmlSchema CollectionName

Nombre del conjunto de esquemas para una columna del tipo xml (null si la columna xml no tiene esquema)

Más adelante en este capítulo, se describe cómo cargar y persistir DataTables desde bases de datos y archivos XML, y mostrar en pantalla la información del esquema de los objetos DataTable.

2.2 Comprobar las instancias de servidor SQL disponibles y los proveedores de datos ADO.NET 2.0 El método System.Data.Common.SqlDataSourceEnumerator.Instance.GetDataSources devuelve una DataTable que tiene una fila para cada instancia de servidor SQL 2000 y 2005 accesibles. Las columnas muestran las propiedades ServerName, InstanceName, IsClustered, y Version. Al invocar el método DbProviderFactories.GetFactoryClasses(), éste devuelve una tabla similar con una fila para cada proveedor Microsoft de datos controlados .NET instala-

44

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 45

Las novedades de ADO.NET 2.0 dos en el sistema, con columnas para las propiedades del proveedor Name, Description, InvariantName, y AssemblyQualifiedName y el número de SupportedClasses. Los proveedores de datos a terceros, como Oracle ODP.NET con Oracle 10g (Oracle.DataAccess.dll), no aparecen en la tabla. El archivo machine.config contiene un elemento para cada uno de los cuatro espacios de nombre de proveedores de datos ADO.NET 2.0, y una sección system.data que añade estos proveedores a DbProviderFactories. El método GetFactoryClasses lee el archivo machine.config para proporcionar la lista de proveedores instalados.

El siguiente código, del proyecto de ejemplo DataEnums.sln, puebla dos controles DataGridView con una instancia de SQLServer y un proveedor instalado de datos .NET de Microsoft: Private Sub frmDataEnums_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Dim dtServers As DataTable = SqlDataSourceEnumerator.Instance.GetDataSources With dgvServers .DataSource = dtServers .AutoGenerateColumns = True .RowHeadersVisible = False .BorderStyle = BorderStyle.None End With Dim dtProviders As DataTable = DbProviderFactories.GetFactoryClasses() With dgvProviders .DataSource = dtProviders .AutoGenerateColumns = True .RowHeadersVisible = False .RowTemplate.Height = 22 .BorderStyle = BorderStyle.None End With End Sub

Al ejecutar el proyecto DataEnums, éste enumera las instancias de SQLServer y los proveedores de datos instalados. La Figura siguiente muestra una instancia por defecto de un SQLServer 2000 (OAKLEAF-W2K3), y una instancia MSDE con nombre (OAKLEAFW2K3\SHAREPOINT), una instancia de SQLServer 2005 (OAKLEAF-MS18), y una instancia SQLExpress (SQLX) con nombre (OAKLEAF-MS18\SQLEXPRESS), así como proveedores de datos accesibles o instalados en el ordenador de desarrollo utilizado para escribir este libro.

45

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 46

Bases de datos con Visual Basic

2.2.1 Entradas Batch en tablas de servidor SQL con el objeto SqlBulkCopy La utilidad BCP del SQLServer y la sentencia BULK INSERT son los métodos tradicionales para añadir filas muy rápidamente a las tablas del SQL Server. ADO.NET 2.0 ofrece una opción alternativa: programar el nuevo objeto SqlBulkCopy. La fuente más habitual para las filas son los DataReader en tablas relacionales. Otra alternativa es insertar filas desde documentos tabulares XML creando un juego de datos en tiempo de ejecución runtime con una o más tablas de datos para copiar. Copiar documentos XML a las tablas del servidor SQL (un proceso llamado shredding) es mucho más sencillo con SqlBulkCopy que con la propiedad para cargar de SQLXML3.0. Cargar requiere un esquema XML anotado para mapear elementos o atributos y añadirlos a las columnas de las tablas base. SqlBulkCopy tiene una colección de ColumnMappings que permite definir la relación entre las columnas de la tabla de datos fuente y las de la tabla base destino.

Para insertar filas de un DataReader en una tabla base destino ya existente, hay que: 1. Crear una conexión y un comando para los datos fuente. Se puede usar cualquier proveedor .NET para conectarse a la fuente de datos y crear el DataReader. 2. Aplicar el método Command.ExecuteReader para crear el DataReader. 3. Crear un objeto nuevo SqlBulkCopy que tendrá como argumentos el string de conexión y la enumeración apropiada en SqlBulkCopyOptions. 4. Definir el valor de la propiedad SqlBulkCopy.DestinationTableName. 5. Añadir miembros ColumnMapping a la colección ColumnMappings si el esquema de la tabla destino difiere de la tabla o la petición fuente. 6. Definir otros valores opcionales para la propiedad SqlBulkCopy, como BatchSize y BulkCopyTimeout. 46

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 47

Las novedades de ADO.NET 2.0 7. Si la operación de copia implica un número muy alto de registros o ejecuciones con una conexión de red muy lenta, añadir un tratador para el evento SqlBulkCopy.SqlRowsCopied a fin de mostrar en pantalla el número o el porcentaje de registros copiados. 8. Invocar el método SqlBulkCopy.WriteToServer para ejecutar la operación de copia. 9. Aplicar el método SqlBulkCopy.Close() y, si ya ha terminado, cierre la conexión. En caso contrario, use de nuevo el objeto SqlBulkCopy para realizar cualquier otra operación. La tabla siguiente describe los miembros de la enumeración SqlBulkCopyOptions. Nombre del miembro Descripción

CheckConstraints

Aplica un chequeo restringido durante el proceso de copia.

Default

No utiliza opciones (por defecto) para la operación de copiar.

FireTriggers

Permite a detonadores INSERT dispararse durante el proceso de copia.

KeepIdentity

Utiliza valores de identificación de la tabla fuente en lugar de generar nuevos valores de identidad basados en los valores de integridad e incremento de la tabla destino.

KeepNulls

Conserva los valores null de la tabla fuente a pesar de los valores por defecto de las tablas destino.

TableLock

Aplica un candado a toda la tabla durante el proceso de copia, en lugar del candado por defeccto aplicado por filas.

UseInternalTransaction

Hace que cada batch de la copia bulk se ejecute dentro de una transacción.

KeepIdentity es el miembro más importante de la enumeración SqlBulkCopyOptions para tablas que usan una columna de identificación como clave primaria. Si no se especifica esta opción, las claves de la tabla destino podrían ser distintas de los valores en la tabla fuente. También es conveniente añadir la opción UseInternalTransaction para prevenir copias parciales si ocurriera alguna excepción durante el proceso.

El ejemplo más sencillo de una operación SqlBulkCopy crea copias de tablas en la misma base de datos. El siguiente código del proyecto BulkCopySameSchema.sln copia las tablas de productos Northwind (Northwind Products) como ProductsCopy: Private Sub btnCopyProds_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCopyProds.Click Dim sdrProds As SqlDataReader = Nothing Dim sbcProds As SqlBulkCopy = Nothing Try Dim lngTime As Long = Now.Ticks btnCopyProds.Enabled = False cnnNwind.Open() cmdProds.CommandText = "DELETE FROM ProductsCopy" Dim intRecs As Integer = cmdProds.ExecuteNonQuery

47

VisualBasic2005_02.qxp

02/08/2007

16:14

PÆgina 48

Bases de datos con Visual Basic cmdProds.CommandText = "SELECT * FROM Products" sdrProds = cmdProds.ExecuteReader() If chkKeepIdentity.Checked Then sbcProds = New SqlBulkCopy(strConn, _ SqlBulkCopyOptions.UseInternalTransaction Or _ SqlBulkCopyOptions.KeepIdentity) Else sbcProds = New SqlBulkCopy(strConn, _ SqlBulkCopyOptions.UseInternalTransaction) Dim blnUseCm As Boolean = True If blnUseCm Then sbcProds.ColumnMappings.Clear() Dim intCol As Integer For intCol = 1 To 9 sbcProds.ColumnMappings.Add(intCol, intCol) Next intCol End If End If AddHandler sbcProds.SqlRowsCopied, New SqlRowsCopiedEventHandler(AddressOf ProdRowAdded) With sbcProds .DestinationTableName = "ProductsCopy" .BatchSize = CInt(nudBatchSize.Value) .BulkCopyTimeout = 30 .NotifyAfter = 1 .WriteToServer(sdrProds) .Close() End With sdrProds.Close() lngTime = Now.Ticks - lngTime txtTime.Text = Format(lngTime / 10000000, "0.000") FillProdsList(True) Catch excCopy As Exception MsgBox(excCopy.Message + excCopy.StackTrace, , "Products Bulk Copy Exception") Finally If Not sbcProds Is Nothing Then sbcProds.Close() End If If Not sdrProds Is Nothing Then sdrProds.Close() End If If Not cnnNwind Is Nothing Then cnnNwind.Close() End If btnCopyProds.Enabled = True End Try End Sub

48

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 49

Las novedades de ADO.NET 2.0 La propiedad SqlBulkCopy.NotifyAfter determina el número de filas añadidas antes de dispararse el evento SqlRowsCopied. A continuación vemos el código para un tratador de eventos SqlRowsCopied que muestra el progreso del proceso de copia de las tablas de productos en un cuadro de texto: Sub ProdRowAdded(ByVal oSource As Object, ByVal oArgs As SqlRowsCopiedEventArgs) txtProdRows.Text = oArgs.RowsCopied.ToString Application.DoEvents() End Sub Mostrar el progreso de la copia reduce sustancialmente la velocidad de la copia. En las aplicaciones finales que deben proporcionar interacción al usuario, el valor de la propiedad NotifyAfter debe ser como mínimo el 10 por ciento del número total de registros añadidos.

La siguiente figura muestra el formulario del proyecto BulkCopySameSchema.sln después de copiar las dos tablas. Los scripts Transact-SQL recrean la tabla en el manejador de eventos frmBulkCopy_Load. Los cuadros de lista muestran la clave primera de la tabla fuente y los valores de segunda columna cuando se carga el formulario, y los valores de la tabla destino después de la copia. El deslizador Batch Size determina el número de filas por intervalo; 0 (el valor por defecto) intenta enviar todas las filas al servidor en un solo intervalo. Definiendo 1 para el tamaño del intervalo y copiando de nuevo las tablas se puede comparar el rendimiento de la copia frente a las operaciones fila por fila.

Cachear datos y código provoca una diferencia considerable entre el tiempo de ejecución de la copia bulk inicial y las siguientes. Por lo tanto, habría que comparar los tiempos de ejecución con batchs de diferentes tamaños después de una o dos pruebas con un tamaño de batch definido en 0.

Deseleccionando el cuadro de verificación Keep Source Identity, la opción KeepIdentity se elimina del constructor SqlBulkCopy de la tabla de productos. En este caso, los valores de clave primarios se incrementan en 77 por cada operación de copia. En el apartado

49

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 50

Bases de datos con Visual Basic siguiente se describe el tratador de eventos para el botón Show Connection Statistics (Mostrar estadísticas de conexión).

2.2.2 Obtener las estadísticas de conexión del servidor SQL El nuevo método SqlConnection.RetrieveStatistics averigua la instancia del servidor SQL con los datos de la conexión actual y devuelve un objeto IDictionary que contiene los 18 pares nombre/valor que muestra la siguiente figura.

Esta propiedad se ha de permitir explícitamente ejecutando una instrucción SqlConnection.EnableStatistics=True antes de invocar el método RetrieveStatistic. El método más sencillo para para tratar los valores nombre/valor es encrustar el objeto IDictionary en un tipo HashTable y, después reiterar la tabla Hash en un bucle ForEach...Next. El código siguiente del proyecto BulkCopySameSchema.sln muestra en pantalla las estadísticas en un cuadro de texto de un sencillo formulario frmConnStats: Private Sub btnShowStats_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnShowStats.Click Try htStats = CType(cnnNwind.RetrieveStatistics(), Hashtable) Dim txtStats As Control = frmConnStats.Controls.Item("txtStats") txtStats.Text = "" Dim oStat As Object Dim strStat As String For Each oStat In htStats.Keys strStat = oStat.ToString If InStr(strStat, "Time") > 0 Then txtStats.Text += strStat + " = " + _

50

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 51

Las novedades de ADO.NET 2.0 Microsoft.VisualBasic.Format(CLng(htStats(strStat)) /1000, _ "#,##0.000") + " secs" + vbCrLf Else txtStats.Text += strStat + " = " + htStats(strStat).ToString + vbCrLf End If Next frmConnStats.Show() frmConnStats.Controls.Item("btnClose").Focus() Catch excStats As Exception MsgBox(excStats.Message + excStats.StackTrace, , _ "Exception Displaying Connection Statistics") End Try End Sub

El código anterior y el formulario frmConnStats se pueden añadir a cualquier proyecto que utilice una SqlConnection. Invoque el método SqlConnection.ResetStatistics para inicializar los datos, excepto ConnectionTime. Recuperar las estadísticas de conexión requiere establecer de nuevo una conexión con el servidor, por lo tanto es mejor reservar el uso de esta función para diagnosticar problemas de conexión.

2.3 Ejecutar comandos SQL de forma asincrónica ADO.NET 2.0 añade los métodos BeginExecuteReader, BeginExecuteXmlReader, y BeginExecuteNonQuery (junto con los correspondientes métodos End) para las clases SqlCommand. Estos métodos permiten ejecutar código mientras se espera a que un comando complete su ejecución. Para ejecutar un comando SqlCommand hay que añadir Async=True a la cadena de comando que se pasó al constructor de la SqlConnection. En los apartados siguientes se describe, con el correspondiente código de ejemplo, para los tres modelos de ejecución de comandos SqlCommand asíncronos que soporta la interfaz IasyncResul. La siguiente figura ilustra las bases de datos, conexiones y comandos que se utilizan con los tres modelos. Obtendrá resultados más interesantes del proyecto de ejemplo AsyncDataOperations.sln si dispone de dos o tres instancias del servidor SQL Server 2000 o 2005 con la base de datos de ejemplo de Northwind para cada instancia (figura en la página siguiente). El proveedor de memoria compartida por defecto del SQLServer 2000 no soporta comando asícronos, por lo que hay que utilizar localhost, y no (local), como valor para el servidor o la fuente de datos de la cadena de conexión en cualquier instancia local del SQLServer 2000.

51

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 52

Bases de datos con Visual Basic

2.3.1 El modelo Polling El modelo Polling es el más sencillo de los tres. La siguiente figura ilustra el flujo del programa para tres conexiones asíncronas (figura en la página siguiente). El código siguiente abre un comando asíncrono en la base de datos Northwind, en una instancia del servidor local SQL y utiliza un bucle While que consulta constantemente para que se complete el método BeginExecuteReader: Private Sub PollingAsyncCommand() Try Dim strConn As String = "Data Source=localhost;" + _ "Initial Catalog=Northwind;Integrated Security=SSPI;Async=True" Dim cnnCusts As SqlConnection = New SqlConnection(strConn) cnnCusts = New SqlConnection(strCusts) Dim cmdCusts As SqlCommand = cnnCusts.CreateCommand With cmdCusts .CommandType = CommandType.Text .CommandTimeout = 60 .CommandText = "SELECT * FROM Customers" End With Dim asrCustsReader As IAsyncResult = _ cmdCusts.BeginExecuteReader(CommandBehavior.CloseConnection) While Not asrCustsReader.IsCompleted 'Do something while waiting

52

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 53

Las novedades de ADO.NET 2.0

End While Dim sdrCusts As SqlDataReader = cmdCusts.EndExecuteReader(asrCustsReader) 'Do something with the data sdrCusts.Close() sdrCusts.Dispose() Catch excAsync As Exception MsgBox(excAsync.Message + excAsync.StackTrace, , "Async Operation Exception") End Try End Sub

La ejecución asíncrona con polling es muy práctica para las operaciones sencillas dentro del bucle While, como mostrar una barra de progresión cuyo valor vienen definido por las pulsaciones e un contador. También se puede incluir código que permita al usuario cancelar un comando antes del tiempo indicado por su propiedad CommandTimeout. Al salir del bucle, la ejecución del mismo queda bloqueada hasta que se hayan completado todos los comandos o haya expirado su tiempo de ejecuión. El código se va ejecutando en el hilo del formulario, por lo que los comandos múltiples se eje53

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 54

Bases de datos con Visual Basic cutan secuencialmente en conexiones separadas. Si las operaciones múltiples DataReader.Read son complejas, se pueden ejecutar en un hilo dedicado al nuevo objeto BackgroundWorker. Esto permite invocar el siguiente método BeginExecuteReader inmediatamente después de que la propiead IAsyncResult.IsComplete cambie a True.

2.3.2 El módelo Callback El módelo asíncrono callback es más flexible que el polling porque utiliza un manejador de callback que ejecuta su propio hilo, extraído de la consulta. El modelo callback permite entrelazar comandos con bases de datos múltiples que se ejecutan en lo mismos servidores o en servidores distintos. En ese caso, hay que especificar el tratador callback y pasarle el comando como objeto al segundo parámetro del método sobrecargado BeginExecuteReader. Al pasar el comando se tiene acceso al método EndExecuteReader con la propiedad IAsyncResult.AsyncState en el tratador callback. La siguiente figura muestra el flujo del programa en el modo callback. Las líneas punteadas indican la ejecución directa de los métodos Read, sin tener que esperar a que estén disponibles dotos los juegos de filas.

54

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 55

Las novedades de ADO.NET 2.0 A continuación mostramos un ejemplo de código para un comando asíncrono sencillo SqlCommand que usa el método callback: Private Sub CallbackAsyncCommand() Try Dim strConn As String = "Data Source=localhost;" + _ "Initial Catalog=Northwind;Integrated Security=SSPI;Async=True" Dim cnnCusts As SqlConnection = New SqlConnection(strConn) cnnCusts = New SqlConnection(strCusts) Dim cmdCusts As SqlCommand = cnnCusts.CreateCommand With cmdCusts .CommandType = CommandType.Text .CommandTimeout = 60 .CommandText = "SELECT * FROM Customers" End With cnnCusts.Open() Dim objCmdCusts As Object = CType(cmdCusts, Object) Dim asrCustsReader As IAsyncResult = _ cmdCusts.BeginExecuteReader(New AsyncCallback(AddressOf CustomersHandler), _ objCmdCusts, CommandBehavior.CloseConnection) Catch excAsync As Exception MsgBox(excAsync.Message + excAsync.StackTrace, , "Async Operation Exception") End Try End Sub

Y aquí está el código del tratador callback para el procedimiento anterior: Private Sub CustomersHandler(ByVal iarResult As IAsyncResult) Try Dim sdrData As SqlDataReader = CType(iarResult.AsyncState, SqlCommand).EndExecuteReader(iarResult) With sdrData Dim intCtr As Integer While .Read For intCtr = 0 To .FieldCount - 1 objData = .GetValue(intCtr) Next intCtr End While .Close() .Dispose() End With Dim blnIsPool As Boolean = Thread.CurrentThread.IsThreadPoolThread CustomersDone(Thread.CurrentThread.ManagedThreadId, blnIsPool) Catch excHandler As Exception MsgBox(excHandler.Message + excHandler.StackTrace, , "Customers Handler Exception") End Try End Sub

55

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 56

Bases de datos con Visual Basic La mayor parte de los ejemplos de cliente de este libro conectan con finales back en la misma máquina que el cliente; eso significa que la ejecución sincrónica de los DataReaders se completa rápidamente o bien arroja inmediatamente una excepción. La ejecución asincrónica resulta especialmente eficaz en los proyectos con DataReaders múltiples que conectan individualemente a bases de datos remotas, especialmente si una o más conexiónes se ejecutan en un WAN.

El proyecto de ejemplo AsyncDataOperations.sln simula una aplicación de producción que conecta a bases de datos trabajadas en red múltiple estableciendo conexiones individuales SqlConnections con tablas Northwind de clientes, pedidos y detalles de pedidos (Customers, Orders, y Order Details). Si tiene acceso a tres instancias de servidor SQL puede modificar las cadenas de conexión cambiando los nombres del segundo y el tercer servidor (OAKLEAF-W2K3 y OAKLEAF-MS2K3) por RemoteServerName, y seleccionar el cuadro de texto Use Multiple Instances para mostrar la secuencia de invocaciones de los métodos Connection.Open, BeginExecuteReader, y EndExecuteReader. La siguiente figura muestra dos instancias del formulario AsyncDataOperations.

Una clase timer VB.NET, escrita por Alastair Dallas, proporciona la resolución requerida para obtener datos de sincronización con sentido. Los números entre paréntesis de las entradas del cuadro de lista son los valores System.Threading.Thread .CurrentThread.ManagedThreadId de las instancias del formulario y los tres manejadores de callback El sufijo P indica que los hilos del manejador son del pool de hilos. La sincronización de datos es para una segunda ejecución (cacheada).

El código de ejemplo ejecuta objetos Customer desde el host local y objeto Orders y Order Details desde los servidores de red. La tabla Orders Details tiene unas 500.000 filas, por lo que leer toda la tabla lleva unos 2 segundos. La velocidad de ejecución en una LAN de bajo tráfico es normalmente suficiente para devolver los datos en la secuencia BeginExecuteReadercalling, como muestra la figura anterior (izquierda). Todas las operaciones de restablecimiento de datos se ejecutan en un solo hilo (13). Para simular una conexión WAN con la tabla Orders, el código en OrdersHandler provoca un retraso de unos segundos mediante múltiples operaciones en cada fila DataReader en un bucle ani56

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 57

Las novedades de ADO.NET 2.0 dado. En este caso, DataReader de Orders completa la ejecución antes que el DataReader de Customers, el cual termina la ejecución antes que el DataReader de Order Details, tal como muestra la figura anterior (derecha). En este caso, la restauración de Order Details se ejecuta en un hilo (14P), y Customers y Orders en otro distinto (13P). El uso del modelo callback en las aplicaciones de formulario Windows es un tema controvertido. Miembros del equipo de datos de VS 2005 de Microsoft recomiendan no utilizar este modelo con los proyectos de formulario de Windows. Los objetos de ADO.NET no son seguros en los hilos, y los problemas con hilos son difíciles de depurar.

2.3.3 El modelo WaitAll Una alternativa al método callback es utilizar un array WaitHandle y asignarlo a un elemento en cada llamada de método BeginExecuteReader. Un WaitHandle.WaitAll(whArray) detiene la ejecución del código hasta que todos los DataReaders están listos para sus llamadas EndExecuteReader. Este comportamiento hace al modelo WaitAll especialmente adecuado para clientes que procesan juegos de filas relacionados, ya que no se necesita el bucle de sincronización que se mostró anteriormente. La siguiente figura muestra el diagrama de flujo en el modelo WaitAll.

57

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 58

Bases de datos con Visual Basic La manera más sencilla de ver los resultados del método WaitAll en un entorno de formulario Windows es crear una versión multi-hilo o multi-threaded apartment (MTA) de un procedimiento habitual Sub Main. Por defecto, los procedimientos de VB.NET utilizan el modelo de hilo único (single-threaded apartment, STA) requerido para los formularios basados en Win32. Llamando WaitAll con múltiples WaitHandles, arroja una excepción dentro de los procedimiento STA, por lo que hay que añadir el prefijo <MTAThreadAttribute()> a la sentencia SharedSubMain. El siguiente listado es una adaptación del código del modelo callback para implementar el array multi-elemento WaitHandle: <MTAThreadAttribute()> _ Shared Sub Main() Dim blnIsMultiServer As Boolean Try cnnCusts = New SqlConnection(strCusts) Dim cmdCusts As SqlCommand = cnnCusts.CreateCommand With cmdCusts .CommandType = CommandType.Text .CommandTimeout = 10 .CommandText = "SELECT * FROM Customers" End With If blnIsMultiServer Then cnnOrders = New SqlConnection(strOrders) Else cnnOrders = New SqlConnection(strCusts) End If Dim cmdOrders As SqlCommand = cnnOrders.CreateCommand With cmdOrders .CommandType = CommandType.Text .CommandTimeout = 10 .CommandText = "SELECT * FROM Orders" End With If blnIsMultiServer Then cnnDetails = New SqlConnection(strDetails) Else cnnDetails = New SqlConnection(strCusts) End If Dim cmdDetails As SqlCommand = cnnDetails.CreateCommand With cmdDetails .CommandType = CommandType.Text .CommandTimeout = 10 .CommandText = "SELECT * FROM [Order Details]" End With Dim timHiRes As New clsTimer timHiRes.Start()

58

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 59

Las novedades de ADO.NET 2.0 Dim awhHandle(2) As WaitHandle cnnCusts.Open() astrListItems(0) = Format(timHiRes.ElapsedTime, "0.000") + " - 1 Opened Customers connection" Dim asrCustomersReader As IAsyncResult asrCustomersReader = cmdCusts.BeginExecuteReader(CommandBehavior.CloseConnection) awhHandle(0) = asrCustomersReader.AsyncWaitHandle astrListItems(1) = Format(timHiRes.ElapsedTime, "0.000") + " - 2 BeginExecuteReader: Customers" cnnOrders.Open() astrListItems(2) = Format(timHiRes.ElapsedTime, "0.000") + " - 3 Opened Orders connection" Dim asrOrdersReader As IAsyncResult asrOrdersReader = cmdOrders.BeginExecuteReader(CommandBehavior.CloseConnection) awhHandle(1) = asrOrdersReader.AsyncWaitHandle astrListItems(3) = Format(timHiRes.ElapsedTime, "0.000") + " - 4 BeginExecuteReader: Orders" cnnDetails.Open() astrListItems(4) = Format(timHiRes.ElapsedTime, "0.000") + " - 5 Opened Details connection" Dim asrDetailsReader As IAsyncResult asrDetailsReader = cmdDetails.BeginExecuteReader(CommandBehavior.CloseConnection) awhHandle(2) = asrDetailsReader.AsyncWaitHandle astrListItems(5) = Format(timHiRes.ElapsedTime, "0.000") + " - 6 BeginExecuteReader: Order Details" WaitHandle.WaitAll(awhHandle) Dim sdrCustomers As SqlDataReader = cmdCusts.EndExecuteReader(asrCustomersReader) sdrCustomers.Close() sdrCustomers.Dispose() Dim sdrOrders As SqlDataReader = cmdOrders.EndExecuteReader(asrOrdersReader) sdrOrders.Close() sdrOrders.Dispose() Dim sdrDetails As SqlDataReader = cmdDetails.EndExecuteReader(asrDetailsReader) sdrDetails.Close()

59

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 60

Bases de datos con Visual Basic sdrDetails.Dispose() astrListItems(6) = Format(timHiRes.ElapsedTime, "0.000") + " - 7 EndExecuteReader: All readers" frmAsync.ShowDialog() Catch excAsync As Exception MsgBox(excAsync.Message + excAsync.StackTrace, , "Async Operation Exception") End Try End Sub

El primer paso es crear un array WaitHandle con el mismo número de elementos que comandos asíncronos. Al igual que con el modelo callback, se abren las conexiones, se ejecutan las instrucciones SqlCommand.BeginExecuteReader, y se añaden los correspondientes objetos SqlDataReader.AsyncWaitHandle al array WaitHandle sin importar el orden. La ejecución se detiene al llegar a la instrucción WaitHandle.WaitAll(awhHandle) hasta que se completan todos los DataReaders. Al retomarse la ejecución, los juegos de filas se procesan en el orden deseado (en este caso padre, hijo, nieto). El código Shared Sub Main del proyecto de ejemplo AsyncDataOperations.sln se puede ejecutar abriendo la ventana de propiedades del proyecto, seleccionando la página de Aplicación, marcando el cuadro de verificación Habilitar marco de trabajo de la aplicación y pulsando + <S> para guardar los cambios. La siguiente figura muestra el formulario con el valor True para blnIsMultiServerflag.

2.3.4 Crear tablas de datos independientes Las tablas de datos de ADO.NET 1.x son miembros, normalmente, de los objetos DataSet. ADO.NET 2.0 permite crear tablas de datos ligeras, independientes, que comparten muchos métodos DataSet, como ReadXml, ReadXmlSchema, WriteXml, y WriteXmlSchema. Las tablas de datos también soportan interfaces DataReader con el método Load(DataReader) y el objeto DataTableReader. También se puede asignar un prefijo nombre de espacio a la tabla de datos. Los apartados anteriores de este capítulo introducí60

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 61

Las novedades de ADO.NET 2.0 an las tablas de datos y los controles DataGridView poblados por los métodos GetSchemaTable, GetDataSources, y GetFactoryClasses. El proyecto StandaloneDataTables.sln ilustra las siguientes características de las tablas de datos: )

)

) )

)

Crear una DataTable con un SqlDataReader, ejecutar un DataTableReader, y vincular la DataTable a una DataGridView editable. Persistir el contenido de una DataTable en archivos XML sólo para datos y esquema, en formato DataSet, y con ediciones de DataTable en formato “diffGram”. Definir los valores de Namespace y, opcionalmente, de la propiedad Prefix. Utilizar el método ReadXml para cargar una tabla de datos desde el archivo guardado DataSet.xml Mostrar en pantalla el esquema DataTable con el método DataTable.GetSchemaTable

La siguiente figura muestra el formulario del proyecto para la la tabla de datos independiente StandaloneDataTables.sln después de una mínima edición de la columna ContactName de la primera fila. Los botones Show... abren documentos XML guardados en Internet Explorer. La parrilla inferior muestra el esquema DataTable del SqlDataReader o bien, después de pulsar el botón Reload from XMLFiles, el esquema de la tabla de datos primaria.

El procedimiento siguiente carga una base de datos del archivo Northwind Customers, añade un nombre de espacio y un prefijo adicionales, designa la columna clave primaria (si falta), crea un esquema DataTable, itera la tabla de datos primaria con un DataTableReader, y activa el procedimiento LoadDataGridView para mostrar en pantalla 61

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 62

Bases de datos con Visual Basic los contenidos y el esquema de la tabla: Private Sub LoadFromDatabase(ByVal blnWithNamespace As Boolean) Dim strConn As String = "Server=.\SQLEXPRESS;Integrated Security=True;Database=Northwind" Dim cnnNwind As SqlConnection = New SqlConnection(strConn) Try Dim cmdCusts As SqlCommand = cnnNwind.CreateCommand With cmdCusts .CommandType = CommandType.Text .CommandText = "SELECT * FROM Customers" End With cnnNwind.Open() Dim drCusts As SqlDataReader = cmdCusts.ExecuteReader(CommandBehavior.KeyInfo) dtCusts = New DataTable dtSchema = drCusts.GetSchemaTable With dtCusts .TableName = "Customers" If blnWithNamespace Then '.Prefix = "custs" .Namespace = "http://www.oakleaf.ws/schemas/northwind/custo mers" End If .Load(drCusts) .AcceptChanges() If .PrimaryKey.Length = 0 Then Dim acolKeys(1) As DataColumn acolKeys(0) = .Columns(0) .PrimaryKey = acolKeys End If If Not .DataSet Is Nothing Then Dim strName As String = .DataSet.DataSetName MsgBox(strName) End If End With drCusts.Close() Dim dtrCusts As New DataTableReader(dtCusts) intRows = 0 While dtrCusts.Read intRows += 1 End While dtrCusts.Close()

62

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 63

Las novedades de ADO.NET 2.0 LoadDataGridViews() Catch excDT As Exception MsgBox(excDT.Message + excDT.StackTrace, , "DataTable Load Exception") Finally cnnNwind.Close() End Try End Sub Eliminar el argumento CommandBehavior.KeyInfo del método ExecuteReader method para añadir la clave primaria con código. Las instrucciones del test prueban que las tablas de datos no generan juegos de datos de manera oculta.

Las tablas de datos que se cargan desde los DataReaders son actualizables, y se pueden persistir como archivos de documentos XML en formatos DataSet sólo-datos, sóloesquema o diffGram. El procedimiento SaveXmlFiles genera documentos XML de datos y esquema y mantiene el contenido de la tabla de datos en formato DataSet. El procedimiento guarda como archivo diffGram todas las ediciones que se hagan en DataGridView. Private Sub SaveXmlFiles(ByVal blnShowMessage As Boolean) DeleteXmlFiles() With dtCusts .WriteXml(strPath + "Data.xml", System.Data.XmlWriteMode.IgnoreSchema) .WriteXml(strPath + "DataSet.xml", System.Data.XmlWriteMode.WriteSchema) .WriteXmlSchema(strPath + "Schema.xsd") End With btnShowData.Enabled = True btnShowDataSet.Enabled = True btnShowSchema.Enabled = True Dim dtChanges As New DataTable dtChanges = dtCusts.GetChanges Dim strMsg As String If dtChanges Is Nothing Then strMsg = "Data and schema for " + intRows.ToString + " rows written to '" _ + strPath + "' folder." btnShowDiffGram.Enabled = False Else dtChanges.WriteXml(strPath + "Diffgram.xml", System.Data.XmlWriteMode.DiffGram) strMsg = "Data for " + intRows.ToString + " rows, schema, and chan ges diffgram written to '" + _ strPath + "' folder and changes accepted." dtCusts.AcceptChanges() btnShowDiffGram.Enabled = True

63

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 64

Bases de datos con Visual Basic End If If blnShowMessage Then MsgBox(strMsg, , "XML Files Saved") End If btnReadXML.Enabled = True End Sub

El manejador de eventos btnReadXML_Click carga la tabla de datos desde el archivo guardado DataSet.xml, aplica las ediciones previas guardadas como archivo diffGram y muestra en pantalla el esquema DataTable. Si se añade un nombre de espacio a la tabla de datos cuando se importan valores de la tabla base, se provocará un fallo en la validación del esquema al guardar el archivo de datos XML. Private Sub btnReadXML_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnReadXML.Click btnShowDiffGram.Enabled = False Try dtCusts = New DataTable With dtCusts .ReadXml(strPath + "DataSet.xml") If File.Exists(strPath + "Diffgram.xml") Then .ReadXml(strPath + "Diffgram.xml") End If .AcceptChanges() End With Dim dtrCusts As New DataTableReader(dtCusts) dtSchema = dtrCusts.GetSchemaTable intRows = 0 While dtrCusts.Read intRows += 1 End While dtrCusts.Close() LoadDataGridViews() Catch excXML As Exception MsgBox(excXML.Message + excXML.StackTrace, , "DataTable ReadXml Exception") End Try End Sub Las tablas de datos DataTablestienen colecciones de ChildRelations y ParentRelations que permiten añadir código para definir las relaciones entre los diferentes objetos de las tablas de datos múltiples. En la mayoría de los casos, sin embargo, crear un juego de datos tipificado es lo mejor cuando se trabaja con proyectos que tienen más de una tabla de datos relacional (o relacionada).

64

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 65

Las novedades de ADO.NET 2.0

2.4 Utilizar tipos Nullable que soporten valores DBNull .NET Framework 2.0 añade tipos genéricos a VB.NET 2005 cuando añade el parámetro (OfType) a las declaraciones de variable. Las variables de tipo nullable son una extensión de tipos genéricos que permiten valores Integer, Int16, Decimal, Date, DateTime, y similares y soportan valores nulos. Asignando Nothing a un valor, éste devuelve el valor por defecto para el tipo (0 para los tipos numéricos y 01/01/0001 12:00:00 AM para las fechas). Para hacer posibles los valores nulos hay que remplazar los identificadores del tipo de valores con Nullable (OfType). Las referencias del tipo String ya soportan de por sí valores nulos, por lo que añadir Nullable (OfString) no resulta apropiado. La aplicación más útil de las variables de tipo nullable la tenemos en las rúbricas de método, donde los tipos de valores nullable hacen innecesaria la sobrecarga. Por ejemplo, si se inserta una fila nueva en la tabla de pedidos Northwind (Northwind Orders) desde un juego de datos tipificado, normalmente se necesitarán las dos rúbricas de método Insert, como mostramos a continuación, y las dos funciones de sobrecarga correspondientes: Function Insert(ByVal CustomerID As String, ByVal EmployeeID As Integer, _ ByVal OrderDate As Date, _ ByVal RequiredDate As Date, _ ByVal ShippedDate As Date, _ ByVal ShipVia As Integer, ByVal Freight As Decimal, ByVal ShipName As String, _ ByVal ShipAddress As String, ByVal ShipCity As String, _ ByVal ShipRegion As String, ByVal ShipPostalCode As String, _ ByVal ShipCountry As String) As Integer Function Insert(ByVal CustomerID As Object, ByVal EmployeeID As Object, _ ByVal OrderDate As Object, ByVal RequiredDate As Object, _ ByVal ShippedDate As Object, _ ByVal ShipVia As Object, _ ByVal Freight As Object, _ ByVal ShipName As Object, _ ByVal ShipAddress As Object, _ ByVal ShipCity As Object, _ ByVal ShipRegion As Object, _ ByVal ShipPostalCode As Object, _ ByVal ShipCountry As Object) As Integer

65

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 66

Bases de datos con Visual Basic La primera rúbrica de método es válida si están presentes todos los valores. Si alguno de los tipos de valores dados a la función es nulo, se necesita la segunda rúbrica, no tipificada. En ese caso el código podría proporcionar un valor String en lugar de Integer o Decimal, un error que el compilador no detectaría. Añadiendo Nullable(OfType) a los tipos de valores, tal como mostramos aquí, permite manejar valores nulos con una sola función: Function Insert(ByVal CustomerID As String, _ ByVal EmployeeID As Nullable(Of Integer), _ ByVal OrderDate As Nullable(Of Date), _ ByVal RequiredDate As Nullable(Of Date), _ ByVal ShippedDate As Nullable(Of Date), _ ByVal ShipVia As Nullable(Of Integer), _ ByVal Freight As Nullable(Of Decimal), _ ByVal ShipName As String, _ ByVal ShipAddress As String, _ ByVal ShipCity As String, _ ByVal ShipRegion As String, _ ByVal ShipPostalCode As String, _ ByVal ShipCountry As String) As Integer

Si queremos definir valores para los parámetros INSERT o UPDATE asociados con tipos nullable, deberemos comprobar que hay un valor con la propiedad HasValue y, si el valor de HasValue es True, dárselo a la propiedad Value, tal como se muestra en el siguiente fragmento de comando INSERT (al que se han tenido que añadir parámetros): ... Me.InsertCommandParameters(0).Value = CustomerID If EmployeeID.HasValue Then Me.InsertCommandParameters(1).Value = EmployeeID.Value Else Me.InsertCommandParameters(1).Value = DBNull.Convert End If If OrderDate.HasValue Then Me.InsertCommandParameters(2).Value = OrderDate.Value Else Me.InsertCommandParameters(2).Value = DBNull.Convert End If If RequiredDate.HasValue Then Me.InsertCommandParameters(3).Value = RequiredDate.Value Else Me.InsertCommandParameters(3).Value = DBNull.Convert End If If ShippedDate.HasValue Then Me.InsertCommandParameters(4).Value = ShippedDate.Value Else Me.InsertCommandParameters(4).Value = DBNull.Convert End If

66

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 67

Las novedades de ADO.NET 2.0 .Nullable(OfType) también se puede aplicar a los miembros de clase Public o Private. A continuación vemos un ejemplo de un sencillo objeto de negocios con propiedades Public que hacen un mapa de los campos de la tabla Orders. Las reglas de negocios y las restricciones de llave foránea determinan qué campos son de tipo nullable (RequiredDate, ShippedDate, Freight, ShipRegion, y ShipPostalCode en este ejemplo). ShipRegion y ShipPostalCode son tipos de referencia, nullable por definición. Public Class Orders Public OrderID As Integer Public CustomerID As String Public EmployeeID As Integer Public OrderDate As Date Public RequiredDate As Nullable(Of Date) Public ShippedDate As Nullable(Of Date) Public ShipVia As Integer Public Freight As Nullable(Of Decimal) ... Public ShipCountry As String End Class

A continuación, una versión abreviada de la clase precedente, que utiliza miembros privados con accesssors Get y Set: Public Class Orders Private m_OrderID As Integer Public Property OrderID() As Integer Get Return m_OrderID End Get Set(ByVal value As Integer) m_OrderID = value End Set End Property ... Private m_RequiredDate As Nullable(Of Date) Public Property RequiredDate() As Nullable(Of Date) Get Return m_RequiredDate End Get Set(ByVal value As Nullable(Of Date)) m_RequiredDate = value End Set End Property Private m_ShippedDate As Nullable(Of Date)

67

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 68

Bases de datos con Visual Basic Public Property ShippedDate() As Nullable(Of Date) Get Return m_ShippedDate End Get Set(ByVal value As Nullable(Of Date)) m_ShippedDate = value End Set End Property ... Private m_Freight As Nullable(Of Decimal) Public Property Freight() As Nullable(Of Decimal) Get Return m_Freight End Get Set(ByVal value As Nullable(Of Decimal)) m_Freight = value End Set End Property ... Private m_ShipCountry As String Public Property ShipCountry() As String Get Return m_ShipCountry End Get Set(ByVal value As String) m_ShipCountry = value End Set End Property End Class

Especificar miembros de clase nullable y utilizar las propiedades HasValue y Value es equivalente a utilizar los tests IfReferenceTypeIsNothingThen...o IfValueType=NothingThen... para los valores de propiedad asignados. Los dos tests del proyecto de ejemplo NullableTypes.sln con objetos poblados desde un SqlDataReader de la tabla de pedidos Orders.

2.5 Utilizar objetos persistentes de formulario Windows de ADO.NET 2.0 Este libro define los objetos persistent como elementos que son visibles (están en la superficie) en los formularios Windows o en la bandeja de diseño de los formularios y cuyos valores se pueden definir en el modo diseño. Los objetos persistent se añaden desde la categoría Datos del Cuadro de herramientas o con herramientas de generación de 68

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 69

Las novedades de ADO.NET 2.0 código (diseñadores) que se invocan arrastrando los nodos de tabla o de campo desde el nuevo panel Orígenes de datos. El panel Orígenes de datos de VS 2005 substituye al Explorador de servidores como punto inicial para añadir los juegos y las tablas de datos a los proyectos. VS 2005 substituye los controles de datos del Cuadro de herramientas de versiones anteriores de Windows – excepto DataSet- con los siguientes objetos y encapsuladores (wrappers) nuevos: )

)

)

)

TableAdapters sustituyen a los adaptadores de conexión y de datos especificos del proveedor, como son SqlConnection y SqlDataAdapter. Los adaptadores de datos y de conexión específicos del proveedor ya no aparecen en la categoría Datos del Cuadro de herramientas. BindingSources son encapsuladores para las fuentes de datos del proyecto, que normalmente, aunque no necesariamente, son tablas de datos (DataTables) miembros de un DataSet tipificado. BindingSources permite, mediante código, navegar por los datos y las listas y editarlos. BindingSources sirve asimismo como fuente de vinculación de la DataGridView con otros controles vinculados de edición. BindingNavigators son controles ToolStrip para fines específicos, que se asocian a una BindingSource para hacer posible, al estilo de un cuadro de herramientas, la navegación por listas o el grabado de datos, y otras operaciones relacionadas como son añadir nuevas entradas, borrarlas y guardar datos editados. Los controles DataGridView sustituyen al control DataGrid. Los DataGridViews se pueden vincular a los DataConnectors, DataTables, y ArrayLists. A diferencia de los DataGrid, los DataGridViews no pueden mostrar en pantalla datos jerarquizados.

Los adaptadores de datos y conexión específicos del proveedor ya no aparecen en el formulario de la bandeja del diseñador. Los miembros privados de la clase PartialPublicClassTableNameTableAdapter del juego de datos definen el tiempo de ejecución de SqlConnections, SqlDataAdapters, y SqlTransactions para los proyectos basados en el SQLServer. Las clases parciales para el código generado por el diseñador, guardadas en los archivos ClassName.Designer.vb, permiten añadir código a las clases DataSet que no han sido sobreescritas por el diseñador al reconfigurar nosotros los DataSets. En las secciones siguientes se introducen los nuevos controles y actualizados de ADO.NET 2.0, así como los formularios de edición y de muestra en pantalla de datos parametrizados autogenerados, y la nueva propiedad de actualización de batchs para las tablas de datos.

2.5.1 Comparando los diseñadores de datos de ADO.NET 1.x y 2.0 Como ya se mencionó en el capítulo anterior, uno de los objetivos básicos del equipo de desarrollo de VS 2005 es suavizar la curva de aprendizaje de los desarrolladores –especialmente los de VB que emigran de VS 6.0 a VS 2005. Añadir el nombre-espacio My y sus clases a los proyectos VB.NET es un ejemplo de cómo se ha simplificado el

69

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 70

Bases de datos con Visual Basic acceso a las propiedades del ordenador local y sus recursos, a expensas de una mayor complejidad en la estructura de archivos del código fuente. Afortunadamente, los nuevos asistentes de VS 2005 y sus diseñadores para crear formualrios básicos de entrada de datos, la generación inicial de los juegos de datos, sin hacer el código más complicado. Las dos secciones siguientes comparan el proceso de generación de un formulario de edición y de entrada de datos, basado en parrillas, con los asistentes y diseñadores de ADO.NET 1.x y ADO.NET 2.0.

2.5.2 ADO.NET 1.x A continuación veremos el método convencional de ADO.NET 1.x para crear un juego de datos tipificado, con una tabla única especificada en una sentencia SQL, y para mostrar registros en un control DataGrid: 1. Añada un SqlDataAdapter del Cuadro de herramientas a la bandeja del formulario. Eso abrirá el asistente para la configuración del adaptador de datos (Data Adapter Configuration Wizard). 2. Seleccione una conexión al SQLServer ya existente, o cree una nueva, especifique las sentencias SQL y genere las sentencias SQLSELECT, INSERT, UPDATE, y DELETE. El diseñador añadirá a la bandeja los objetos SqlConnection1 y SqlDataAdapter1. 3. Seleccione Data/Generate Dataset para crear un juego de datos tipificado con la tabla de datos especificada en la consulta SELECT. El diseñador añade DataSetName1 a la bandeja. 4. Añada un control DataGrid al formulario, defina DataSetName1 como valor de su propiedad DataSource, y déle a la propiedad DataMember el nombre de la tabla especificado en la consulta SELECT. 5. Añada un botón Fill y la instrucción SqlDataAdapter1.Fill(DataSetName1) al manejador de eventos btnFill_Click. 6. Añada un botón Update de actualización y una instrucción SqlDataAdapter1.Update(DataSetName1) al manejador de eventos btnUpdate_Click.

2.5.3 ADO.NET 2.0 Los diseñadores de VS 2005 y ADO.NET 2.0 simpllifican la creación de los formularios de una sola tabla al cambiar la sencuencia para la generación de juegos de datos y otros componentes relacionados. El proceso a seguir con ADO.NET 2.0: 1. Si es necesario, abra la ventana Orígenes de datos seleccinando Datos/Mostrar orígenes de datos, y pulse el enlace Agregar nuevo origen de datos para iniciar el Asistente para la configuración de orígenes de datos. 2. Seleccione Base de datos como el tipo de fuente y seleccione una de las conexiones existentes a la base de datos o cree una nueva en el cuadro de diálogo Agregar conexión. Opcionalmente, guarde la cadena de conexión en el archivo de configuración de la aplicación. Siguiendo los pasos del asistente se añade un árbol TableName 70

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 71

Las novedades de ADO.NET 2.0 bajo el nombre DataSetName ya existente en la parte superior de la ventana Orígenes de datos. 3. Arrastre al formulario el árbol TableName. El diseñador añade los elementos DatabaseNameDataSet, TableNameBindingSource, TableNameTableAdapter, y TableNameBindingNavigator a la bandeja, y los controles TableNameDataGridView y TableNameBindingNavigator al formulario. El paso 3 también añade una instrucción TableNameTableAdapter.Fill(DatabaseNameDataSet) al manejador de eventos Form1_Load y un manejador de eventos bindingNavigatorSaveItem_Click al formulario. A diferencia del proceso ADO.NET 1.x, usted no tiene la oportunidad de crear o seleccionar procedimientos almacenados para llenar o actualizar las tablas de datos creadas por el asistente. No obstante, puede reconfigurar los DataTableAdapters para crear nuevos procedimientos, o utilizar los que hay guardados, abriendo el archivo DataSetName.xsd en el Explorador de soluciones, pulsando con el botón secundario la cabecera del TableNameTableAdapter y seleccionando la opción Configurar DataSet con el asistente para iniciar el Asistente para la configuración de orígenes de datos. Pulse el botón Anterior para mostrar el cuadro de diálogo Elija la conexión de datos, y pulse el botón Nueva conexión y siga los pasos del asistente hasta el final. Los diseñadores dse ADO.NET 2.0 hacen mucho más fácil la creación de formularios de edición de datos maestro con subformularios DataGridView de nivel único o múltiple, que VS 2005 llama formularios Master Detail. En el capítulo anterior describimos el proceso para crear un formulario Northwind de Clientes-Pedidos. Más adelante, le mostramos cómo añadir un subformulario vinculado de segundo nivel. Y también es mucho más fácil crear formularios de entrada de datos parametrizados, tal como descubrirá más adelante en este mismo capítulo.

2.6 Añadir los controles ADO.NET que faltan Si desea utilizar los componentes de ADO.NET 1.x para crear objetos DataAdapter específicos del proveedor, tendrá que añadir los correspondientes controles Connection y DataAdapter al Cuadro de herramientas. Si tiene otros proveedores específicos de datos, como Oracle ODP.NET para Oracle 10g o anteriores, tendrá que añadir ODP.NET OracleConnection y OracleDataAdapter al Cuadro de herramientas. También puede añadir el control DataGrid de ADO.NET 1x para formularios Windows y crear con él nuevos proyectos similares en entorno y manejo a los de VS 2005. Para añadir controles no estándar al Cuadro de herramientas, pulse con el botón secundario del ratón en la sección Datos y seleccione Elegir elementos para abrir el cuadro de diálogo Elegir elementos del cuadro de herramientas. Escriba las primeras letras del componente o el control en el cuadro de texto Filtro para simplificar la selección. La siguiente figura muestra el cuadro de diálogo con cuatro proveedores Oracle.DataAccess.Client. Marque las casillas de verificación de las entradas que quiera instalar y pulse Aceptar para añadirlas al Cuadro de herramientas y cerrar el cuadro de diálogo.

71

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 72

Bases de datos con Visual Basic

Los controles no estándar añadidos al Cuadro de herramientas se pueden eliminar de nuevo pulsando con el botón secundario el Cuadro de herramientas y seleccionando Restablecer Cuadro de herramientas.

2.7 Actualizar proyectos de 1.x con componentes de ADO.NET 2.0 Al abrir un proyecto VS 2002 o VS 2003 en VS 2005 se abre el Visual Studio Upgrade Wizard, que convierte el proyecto al formato VS 2005 y, opcionalmente, guarda una copia de seguridad del proyecto original en el archivo que se especifique. Con los proyectos simples los únicos cambios que se perciben son referencias actualizadas a las versiones .NET Framework 2.0 y las fuentes de datos para el formulario, que aparecen automáticamente en la ventana Orígenes de datos. Actualizar el proyecto no añade archivos de código para mejorar o ampliar el nombre de espacio My, ni supone adiciones específicas de ADO.NET 2.0 al código DataSet. Añadir la fuente de datos como un nodo de la ventana Orígenes de datos permite reemplazar rápidamente un DataGrid con un DataGridView y añadir, automáticamente, los controles BindingSource y BindingNavigator para la edición y la navegación de datos. Borre el control DataGrid y arrastre el nodo TableName al formulario para añadir los dos controles de ADO.NET 2.0. No verá el componente TableNameDataAdapter en la bandeja, ni una ventana de diseñador de esquema porque DataSourceName.xsd no está actualizado al nuevo formato de esquema DataSet.

2.7.1 Añadir subformularios multinivel VS 2005 y VB Express automatizan la generación de formularios multinivel para la entrada y la edición de datos para tablas relacionadas. Al añadir tablas múltiples relacionales a la ventana Orígenes de datos, el diseñador de esquemas de VS 2005 determina atuomáticamente las restricciones de clave foránea y establece relaciones entre las 72

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 73

Las novedades de ADO.NET 2.0 tablas basadas en restricciones preestablecidas de tabla base. El diseñador de esquemas nombre las relaciones FK_ManySideTable_OneSideTable, como muestra la siguiente figura para las tablas Northwind Customers, Orders y Order Details.

Con VS 2002 y VS 2003 debía establecer manualmente todas las relaciones en el cuadro de diálogo Relation. Por defecto, VS 2005 no muestra en cascada los cambios de valores clave en las operaciones de borrado y actualización, pero este comportamiento se puede modificar definiendo otras propiedades de relación en el cuadro de diálogo Relación. Primero, añada un DataGridView o, preferentemente, cuadros de texto vinculados y un DataNavigator para la fuente de datos maestra del formulario. Segundo, añada el primer nivel de detalle arrastrando el nodo de tabla relacionado con la ventana Orígenes de datos (Orders en este ejemplo) al formulario para mostrar en un DataGridView los registros relacionados. A continuación, arrastre nodos de tabla de niveles más profundos; en este caso Order Details, para mostrar niveles adicionales de registros relacionados. Finalmente, compruebe que el diseñador ha añadido estas tres instrucciones DataTableTableAdapter.Fill al manejador de eventos FormName_Load: 73

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 74

Bases de datos con Visual Basic Private Sub frmMasterDetails_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load 'TODO: This line of code loads data into the 'NorthwindDataSet.Order_Details' table. You can move, or remove it, as needed. Me.Order_DetailsTableAdapter.Fill(Me.NorthwindDataSet.Order_Details) 'TODO: This line of code loads data into the 'NorthwindDataSet.Orders' table. You can move, or remove it, as needed. Me.OrdersTableAdapter.Fill(Me.NorthwindDataSet.Orders) 'TODO: This line of code loads data into the 'NorthwindDataSet.Customers' table. You can move, or remove it, as needed. Me.CustomersTableAdapter.Fill(Me.NorthwindDataSet.Customers) End Sub

A continuación, pulse para construir y ejecutar el proyecto, que aparecerá tal como se muestra en la siguiente figura.

2.8 Diseñar y mostrar informes con el control ReportViewer Las versiones VB y VS anteriores a VS .NET se basaban en los complementos Crystal Reports para diseñar, mostrar y publicar informes en forma de tabla, gráficos, o ambos, desde una fuente de datos específica. Otros fabricantes de software independientes (ISV, for Independent Software Vendors) ofrecen redactores de informes y diseñadores de gráficos para VS 2002 y versiones posteriores. Para eliminar la dependencia de terceras partes, Microsoft introdujo los servidores SQL de Servicios de informes (Reporting Services) como un complemento sin carga para SQLServer 2000. Todas las ediciones del 74

VisualBasic2005_02.qxp

02/08/2007

16:15

PÆgina 75

Las novedades de ADO.NET 2.0 SQLServer 2005, excepto SQLX, integran los Reporting Services, que incluyen un Report Server y un Report Builder en la configuración del programa. Estas ediciones usan el Report Service Project del proyecto Business Intelligence de VS IDE, o el Report Server Project Wizard o plantillas Report Model Project para diseñar y desarrollar los informes basados en servidor (también llamados remotos), independientes de los proyectos de formulario de .NET Windows o la Web. El control ReportViewer para los formularios Windows tiene capacidad para una barra de herramientas, parecido a un control BindingNavigator, y un área de visualiación del informe para mostrar los informes convencionales (tablas) o los crosstab (matriciales), o los mapas vinculados a las fuentes de datos ADO.NET 2.0. Los mapas son muy parecidos a los Excel PivotCharts o a los creados con el control Office Web Components (OWC). La barra de herramientas tiene botones Page Setup, Page Layout, y Print para imprimir, y un botón Export que permite guardar los informes en la hoja de cálculo de Excel o en el formato Adobe PDF. Los informes creados con el control ReportViewer consumen muchos menos recursos del cliente que sus correspondientes versiones con Crystal Reports. ReportViewer permite diseñar informes con un diseñador cliente (local) derivado del ReportBuilder. El diseñador local de VS 2005 o VBX sirve para crear archivos de informe desde el cliente local en la carpeta del proyecto. La ayuda online le guiará a través del proceso de creación de un informe sencillo a partir de las tablas AdventureWorks. La siguiente figura muestra la aplicación ReportViewerDemo mostrando el mapa por categoría de producto de un área de pedidos recibidos en los diferentes trimestres de 1997.

75

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 77

Capítulo 3

Concretando proyectos reales Los negocios actuales requieren el acceso en tiempo real a una amplia variedad de datos públicos y privados. Prácticamente todas las aplicaciones orientadas a negocios conectan a una o más fuentes de datos en red. Las tablas de bases de datos relacionales son las fuentes más comunes, pero los sistemas de correo electrónico, hojas de cálculo, archivos de procesamiento de datos y, cada vez más, los documentos XML, también sirven como fuente de datos. ADO.NET requiere fuentes de datos en forma de tabla, por lo que este capítulo se centra en las mejores prácticas para procesar los datos contenidos en tablas relacionales, en juegos XML estructurados y mensajes SOAP. Microsoft Access y Visual Basic ofrecen conectividad de datos en el propio escritorio a millones de usuarios de Windows. Ambas plataformas permiten un acceso rápido y relativamente sencillo a las bases de datos locales y en la red. Access permite a los usuarios de Office crear “front ends” de bases de datos y configurar bases de datos Jet multiusuario para proyectos. Visual Basic ofrece a los desarrolladores profesionales y amateurs una amplia y variada gama de funciones para crear aplicaciones cliente para servidores de bases de datos corporativas. Gran parte de esta temprana actividad de desarrollo vino impulsada por la incapacidad, o la falta de voluntad, de los departamentos TI para proporcionar aplicaciones aprobadas oficialmente según un tiempo establecido (o cuando fuera). Los proyectos no aprobados y ad-hoc, a menudo, quedaban fuera del radar de la dirección TI y no se descubrían hasta que se hacía evidente la contaminación de los datos, o las conexiones mal gestionadas repercutían en la eficacia de las bases de datos. Las auditorías al cumplirse el año 2000 y las subsiguientes actualizaciones de bases de datos descubrieron “front ends” con bases de datos defectuosas e irregulares. Los “back ends” de servidores SQL con cuentas y contraseñas vacías o fáciles de adivinar, estaban aseguradas. Los departamentos TI de la mayoría de organizaciones se habían hecho con todo el control de las conexiones internas a bases de datos gubernamentales o corporaciones centralizadas. La administración centralizada de las bases de datos y el establecimiento de las mejores prácticas para el desarrollo de bases de datos “front-end” se conviertieron en norma. Las posibilidades infinitas de conexión de Internet y la persistencia de los ataques externos se tradujeron en un énfasis aumentado por establecer las mejores prácticas para garantizar la seguridad y la integridad de los datos. De todos modos, muchas empresas pequeñas y medianas siguen funcionando con prácticas informales en el desarrollo de sus aplicaciones con bases de datos. 77

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 78

Bases de datos con Visual Basic El fenómeno SQLSnake/Spida, en Mayo de 2002, probó que muchas de las bases de datos de los servidores SQL accesibles por Internet tenían contraseñas vacías o fáciles de adivinar. En enero de 2003, el virus Slammer/Sapphire demostraba que miles de instancias MSDE 2000 no regularizadas, así como los servidores SQL sin patches, controlados por los departamentos TI, eran accesibles desde el puerto TCP 1433.

El U.S. General Accounting Office define las mejores prácticas como procesos y sistemas identificados en organizaciones públicas y privadas de excelente rendimiento, ampliamente reconocidas por mejorar el rendimiento y la eficacia de las organizaciones en áreas específicas. Identificar y aplicar con éxito las mejores prácticas puede reducir los gastos del negocio y mejorar la eficacia de la organización. Independientemente del tamaño de la empresa o de los clientes consultores, adoptar y reforzar un conjunto de prácticas óptimas en las áreas de desarrollo de las aplicaciones produce a corto y a largo plazo un incrémento en los réditos de inversión. Incluso si sus deberes en cuanto a desarrollo no están guiados por un conjunto oficial de "mejores prácticas", tómese el tiempo necesario para familiarizarse con las recomendaciones de Microsoft en cuando a mejoras y arquitectura actuales de los proyectos .NET. Este capítulo muestra las pautas para el desarrolo de las aplicaciones .NET, en secuencia descendiente desde la arquitectura general hasta las recomendaciones específicas para incrementar la escalabilidad, interoperabilidad, rendimiento y seguridad, y la reutilización de código en todos los proyectos .NET centrados en datos.

3.1 Establecer la arquitectura Mantenerse al día en la evolución de los distintos marcos y arquitecturas en que se desarrollan las aplicaciones de Microsoft puede ser una tarea a jornada completa. Los proyectos convencionales de cliente-servidor dieron paso los diseños de tres niveles, basados en COM, y después a los de arquitecturas de n-niveles con componentes controladas por el servidor de transacciones de Microsoft (Microsoft Transaction Server). Las arquitecturas Windows Distributed interNet Architecture (WinDNA) y Universal Data Access (UDA), que Microsoft introdujo en 1997, formalizaron el diseño de las aplicaciones Web de tres niveles. En 1999, Windows DNA2000 añadió servicios XML y Web al acceso de datos. Bill Gates anunció en Julio de 2002 la joven plataforma .NET, que incorporaba todos los sistemas de servidor de Microsoft: Visual Studio .NET, y el malafortunado proyecto .NET My Services. La arquitectura más vigente de Microsoft es la Microsoft Enterprise Application Platform que combina Windows 2003 Server, Visual Studio .NET 2003/5, y modelos y prácticas de guía arquitectónica. Los P&P están desarrollados por el equipo Platform Architectural Guidance (PAG) de Microsoft, que ha establecido una conferencia cumbre de tres días bajo el título de International Patterns and Practices Summit y ofrece información semanal en la Web sobre temas relacionados con el desarrollo de aplicaciones y la arquitectura .NET. A continuación se describen brevemente los cuatro elementos P&P: )

78

Arquitecturas referenciales: identifican las decisiones en cuanto al diseño y hacen recomendaciones generales para mejorar las soluciones con componentes interconexas. La guía de mejoras en los Data Services "Windows Server System Reference

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 79

Concretando proyectos reales Architecture” (WSSRA) y Application Architecture for .NET: Designing Applications and Services son los documentos más útiles sobre arquitectura, de referencia para los desarrolladores de ADO.NET. )

)

)

Modelos: son modelos de operaciones habituales realizadas por aplicaciones que se presentan como pares de problema/solución. Un ejemplo típico, tomado del libro de 196 páginas, Data Patterns, es el modelo Master/Slave Snapshot Replication para copiar la información que cambia lentamente, como las listas de productos y clientes, desde las tablas de las bases de datos hasta los portátiles de usuarios a menudo desconectados. Bloques de aplicación: son componentes VB y C# que proporcionan un marco para elementos específicos de aplicaciones o componentes. Un ejemplo es el bloque de aplicación de Data Access para .NET. Hay varias guías de diseño con documentación sobre los bloques de aplicación. Guías de diseño: proporcionan recomendaciones detalladas sobre arquitectura y mejoras para tipos específicos de aplicaciones, componentes y servicios. La .NET Data Access Architecture Guide (2003) y Designing Data Tier Components and Accessing Data Through Tiers (2002) son los dos miembros más importantes de este grupo para los programas de ADO.NET.

Los P&P originales no se han visto afectados significativamente por las actualizaciones de VS 2005 y ADO.NET 2.0 ni por la migración al SQLServer 2005. Los principios del diseño son consistentes para todas las versiones .NET. Los apartados siguientes proporcionan información más detallada sobre los miembros de la lista anterior, enfatizando los elementos de mayor interés para los desarrolladores de bases de datos.

3.2 Las arquitecturas referenciales Las arquitecturas referenciales proporcionan a los arquitectos y desarrolladores de .NET una guía a nivel de sistema en situaciones típicas como son los sistemas distribuidos de aplicaciones para venta al detalle por la Web y de banca. Las arquitecturas referenciales intentan ejemplificar las estructuras TI típicas y las operaciones de empresas grandes y medianas. Los siguientes apartados describen las arquitecturas referenciales primarias para los objetos controlados por datos y los multi-nivel. La página Web de Microsoft sobre arquitecturas referenciales es http://msdn.microsoft.com/architecture/. Esta página contiene vínculos al Microsoft Architects JOURNAL (archivos PDF), otras páginas relaciones y Weblogs.

3.2.1 Windows Server System Reference Architecture La arquitectura referencial WSSRA es la actualización del servidor Windows Server 2003 de Microsoft Systems Architecture 1.5 para Windows 2000 Server. WSSRA proporciona recomendaciones sobre el hardware y la configuración de los sistemas operativos a nivel de empresa. La guía de implementación de los Data Services toma como base el SQL Server 2000, pero las recomendaciones pueden aplicarse también al SQLServer 2005. 79

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 80

Bases de datos con Visual Basic

3.2.2

Designando aplicaciones y servicios

Application Architecture for .NET: Designing Applications and Services (2002) es un libro de 169 páginas que describe la arquitectura recomendada para los sistemas distribuidos, construidos con múltiples niveles. Los capítulos individuales tratan las mejoras y metas referenciales, el diseño de componentes y su interacción, los temas sobre seguridad, gestión y desarrollo. Esta publicación proporciona los fundamentos para todas las demás arquiteturas referenciales para .NET. Todos los desarrolladores de aplicaciones .NET no triviales deberían leer este libro. La implementación de referencia es una aplicación relativamente sencilla para ventas al detalle por la Web que incluye los siguientes tipos de componentes: User interface

Agente de servicio

User process components

Interfaz de servicio

Business workflows

Securidad

Business components and entities

Administración

Data access logic

Comunicación

Más adelante en este capítulo se describen los componentes lógicos de acceso a datos (en inglés: data access logic components o DALCs). Los componentes interfaz de servicio y agente de servicio conectan a los servicios Web XML. Microsoft publicó a principios de 2003 el PAG Enterprise Template: Application Architecture for .NET 2002 and 2003. PAG es el acrónimo de Prescriptive Architecture Guidance. El instalador añade una arquitectura de aplicación para el nodo del Ayudante .NET a la carpeta de plantillas de VS 2003. Las plantillas de subnodo crean borradores de proyectos para 11 de los tipos de componentes descritos en el libro. La mayor parte de los borradores contienen referencias a los espacios-nombre de .NET que se requieren para el proyecto de los componentes, pero no incluyen código fuente.

3.2.3 Arquitecrura referencial para el desarrollo empresarial La Microsoft Enterprise Development Reference Architecture (ERDA), versión 1.0 (originalmente codificada como Shadowfax) es un marco para las aplicaciones orientadas al servicio de desarrollo con SQLServer, servicios Web ASP.NET, Microsoft Message Queue (MSMQ), y otros sistemas de “back-end”. La implementación de referencia, llamada GlobalBank, es el punto de partida para un portal de banca online que permite a los clientes acceder a su información bancaria personal. Más adelante en este capítulo, describe el nuevo Integration Patterns, que utiliza Global Bank como implementación de referencia.

3.3 Encontrar modelos para proyectos Un modelo de software suele definir una solución común para las labores recurrentes específicas en TI, como restablecer o actualizar los datos de las tablas de bases de datos con aplicaciones que muchas veces carecen de conectividad con las bases de datos en red durante un extenso periodo, la clásica situación del cliente desconectado. La regla

80

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 81

Concretando proyectos reales general es que un modelo determinado debe ser aplicable al menos a tres instancias de la tarea. La página "What Is a Pattern" del sitio Web AntiPatterns (http://www.antipatterns.com/whatisapattern/) describe la primera instancia como un evento, la segunda como una coincidencia y la tercera como un posible modelo. Otras instancias adicionales prestan mayor credibilidad al modelo. Un modelo cada vez más utilizado en las organizaciones o comunidades de software tiene muchas posibilidades de convertirse en una plantilla. Una definición común de plantilla modelo es la de documentación estructurada para un modelo que se puede añadir a un catálogo de plantillas o de modelos. Los apartados siguientes describen modelos que se pueden aplicar a todas las aplicaciones .NET en general y las aplicaciones “data-driven” en particular.

3.3.1 Enterprise Solution Patterns Using Microsoft .NET Enterprise Solution Patterns Using Microsoft .NET (2003, versión 2.0) es la madre de todas las plantillas .NET. Este libro de 367 páginas recoge 32 plantillas divididas en cinco grupos: Web Presentation, Data Access, Performance and Reliability, Services y Deployment. "Data Transfer Object", "Implementing Data Transfer Object in .NET with a Data Set" e "Implementing Data Transfer Object in .NET with a Typed Data Set", temas tratados más adelante en este libro, son los miembros del grupo "Data Access”. Se puede leer, o descargar una versión en PDF del libro en http://msdn.microsoft.com/library/en-us/dnpatterns/html/Esp.asp.

El libro define el objeto de transferencia de datos, Data Transfer Object (DTO), como un simple contenedor para un conjunto de datos agregados que hay que transferir a través de un proceso o más allá de los límites de la red, y después dedica unas cuantas páginas considerando los aspectos “hunky versus chatty" en las llamadas remotas de datos. Se hace mucha referencia a "Data Transfer Object in .NET with Serialized Objects", aunque este tópico no aparece en la segunda edición. Su fuente se puede identificar en el Apéndice A, "Pattlets", como Microsoft P&P, pero una búsqueda más a fondo no lleva más allá de las entradas relacionadas con los temas relacionados con los juegos de datos (DataSet). Las dos implementaciones del libro proporcionan código de ejemplo C# para testar las unidades con el espacio-nombre NUnit.Framework. La sección posterior "Automate Test-Driven Development" da más detalles sobre cómo testar las plantillas con NUnit. Hay un salto considerable en describir los DTOs como "simples contenedores" y recomendar a continuación su implementación con los juegos de datos no tipificados de ADO.NET o, más aún, con los tipificados de ADO.NET 2.0. Las plantillas de implementación reconocen la fiabilidad de la no interoperabilidad de los juegos de datos, pero el libro no trata el tema del XML añadido por los juegos de datos tipificados, cuando vienen gestionados por .NET accediendo a XML o en formato binario, o serializados a mensajes de servicios Web. A diferencia de Application Architecture for .NET: Designing Apllications and Services, aquí puede saltarse tranquilamente los tópicos sobre los datos de esta colección de modelos.

81

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 82

Bases de datos con Visual Basic

3.3.2 Data Patterns Data Patterns (2003) es un libro de 196 páginas que identifica un grupo, o cluster, de movimiento de datos. Un cluster es un grupo de modelos relacionados entre sí, con un modelo raíz para todo el grupo Moving Copy of Data es el cluster de la primera edición de Data Patterns. Los clusters de modelos tiene niveles variables de abstracción (arquitectura, diseño e implementación) sólo la implementación depende de la plataforma y es específica para cada vendedor de base de datos. Base de datos, aplicación, desarrollo y puntos de vista estructurales son aspectos que representan a los miembros de un departamento TI típico: DBAs, desarrolladores, administradores de red y arquitectos de sistema. El cluster Moving Copy of Data engloba operaciones de extraer-transformar-cargar, en inglés extract-transform-load (ETL), y varios tipos de réplica basada en el servidor, como maestro-maestro y maestro-esclavo, con diseños transaccionales e instantáneos. Las implementaciones, por supuesto, usan el servidor de Microsoft SQL 2000 o posteriores. Cada modelo tiene una plantilla con los tópicos Contexto, Problema, Fuerzas y Soluciones.

3.3.3 Modelos de sistemas distribuidos La publicación oficial Distributed Systems Patterns (Version 1.1.0) esboza en líneas generales un cluster de modelos para la colaboración de objetos a través de procesos y redes. El cluster incluye modelos para invocar objetos remotos con modelos Singleton y Broker para .NET remoting, y el objeto Data Transfer Object (DTO), que crea una copia local de la instancia de un objeto remoto. Este es uno de los pocos libros sobre modelos y publicaciones oficiales que no trata de la arquitectura orientada a servicios ni de servicios Web.

3.3.4 Modelos de integración Integration Patterns (2004) es un catálogo de modelos EAI para la integración de aplicaciones de empresa, en inglés: enterprise application integration (EAI) patterns. El Diccionario de Comercio Electrónico (Electronic Commerce Dictionary, en http://www.tedhaynes.com/haynes1/atol.html) define los EAI como: Vincular y compartir muchos datos y aplicaciones de empresa, inclusive extensiones a socios, a través del uso de módulos aplicación-a-aplicación, o plataformas de servidor “multi-tier”. Una de las motivaciones de EAI es la necesidad de implementar rápidamente los proyectos basados en la Web, la necesidad de vincularse a datos heredados y la necesidad de crear vínculos a diferentes sistemas adquiridos a través de mezcladores y adquisiciones corporados. Integration Patterns marca los pasos que el grupo ficticio TI de desarrollo de Global Bank sigue para desarrollar un portal Web de autoservicio para pagos del usuario. El portal conecta a diferencias fuentes de datos e implementa las operaciones de pago con los diez modelos EAI:

82

Entity Aggregation

Function Integration

Process Integration

Service-Oriented Integration

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 83

Concretando proyectos reales Implementing Process Integration with BizTalk Server 2004

Implementing Service-Oriented Integration with ASP.NET

Portal Integration

Implementing Service-Oriented Integration with BizTalk Server 2004

Data Integration

Presentation Integration

El capítulo "Data Integration" de Integration Patterns trata de los tres métodos para restablecer y actualizar datos: )

)

)

Base de datos compartida: da acceso directo desde muchas bases de datos a otra base determinada; este método minimiza los datos latentes. Mantener copias de datos: proporciona a cada aplicación su propia base de datos, la cual copia los datos a y desde una base de datos maestra. El tipo de copia, o réplica, y su sincronización determinan la latencia y sincronización de los datos. Transferencia de archivo: implica mover archivos lógicos entre el almacén de datos y las aplicaciones independientes. Enviar juegos de datos normalizados en archivos XML para su almacenamiento permanente en el cliente es un ejemplo del método por transferencia de archivo.

Como la mayor parte de las P&P, Integration Patterns también enfatiza el uso de los servicios Web y la difusión en los proyectos EAI. Más adelante en este capítulo, se examinam las ventajas y los inconvenientes de utilizar servicios Web para acceder a los datos.

3.3.5 Utilizar librerías de bloques de aplicaciones Los bloques de aplicaciones (application blocks) son bibliotecas de clases con componentes reutilizables a nivel de subsistema para implementar los servicios de aplicaciones comunes, como son el acceso a datos, encriptamiento e incluso logging. Cada bloque de aplicación viene con un rápido ejemplo inicial, documentación y código fuente. El código fuente permite modificar y ampliar los bloques para adaptarlos al propio entorno de desarrollo y requisitos del sistema. Microsoft lanzó los bloques originales para VS 2002 (entonces llamado Visual Studio .NET) y .NET 1.0 en 2002 como versión 1.0. Los bloques se actualizaron como versión 2.0 con VS 2003 y .NET 1.1 en 2004. Muchas de las más originales bibliotecas de bloques de aplicación se reescribieron entonces como elementos de los patterns & practices de Enterprise Library, que Microsoft lanzó en Enero de 2005. Enterprise Library incorpora partes de la Avenade Connected Architecture para .NET (ACA.NET). Avenade, Inc., es una empresa independiente consultora de software formada por Accenture y Microsoft en el año 2000. A continuación mencionamos los bloques disponibles en las versiones .NET 1.x en el momento de escribir este libro: Aggregation Application Block

Data Access Application Block*

Asynchronous Invocation Application Block

Exception Handling Application Block*

Authorization and Profile Application Block

Logging and Instrumentation Application Block*

Caching Application Block*

Security Application Block*

83

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 84

Bases de datos con Visual Basic Configuration Application Block*

Smart Client Offline Application Block

Cryptography Application Block*

Updater Application Block User Interface Process Application Block - V2

Los bloques de aplicación de la tabla anterior marcados con un asterisco (*) están incluidos en la descarga de bloques de aplicación de Enterprise Library de Enero de 2005.

Los bloques de aplicación de Enterprise Library requieren que compile el código fuente de .NET 1.1 con archivos de comandos o VS 2003 para crear Microsoft.Practices.EnterpriseLibrary.BlockName.dll. Después hay que añadir referencias en el proyecto VS 2005 a los ensamblajes apropiados. QuickStart clients implica escribir soluciones desde proyectos múltiples y con muchos archivos. Muchos de los ensamblajes de bloque dependen de otros ensamblajes raíz, como Microsoft.Practices.EnterpriseLibrary.Common.dll y Microsoft.Practices.EnterpriseLibrary.Configuration.dll. Versiones anteriores de los bloques de aplicación incluían bibliotecas VB y C#; Enterprise Library sólo tiene bibliotecas C#. De todos modos, los QuickStart clients incluyen código fuente VB y C#. Los dos apartados siguientes describen el bloque Data Access Application Block (DAAB) y su QuickStart test client, un ejemplo de proyecto de formulario Windows que usa el bloque Data Application para restablecer y actualizar datos del SQLServer 2000 o 2005. El ayudante VS 2005 Upgrade Wizard no es de gran ayuda con VS 2005 y el código fuente de la Enterprise Library de Enero del 2005, ya que hace fallar la actualización automática. A cambio, el proyecto de ejemplo DataAccessQuickStart.sln VB 2005 incluye los componentes actualizados manualmente, necesarios para crear objetos DAB y ejecutar sus métodos en VS 2005.

3.4 El bloque de aplicación Data Access (Data Access Application Block) El objetivo original del DAAB era minimizar el número de líneas de código que el usuario necesita para crear y manipular las componentes runtime de acceso de datos del servidor SQL de ADO.NET 1.x. La versión de Enterprise Library permite integrar otros bloques de aplicación que proporcionan la condiguración estándar, instrumentación y seguridad para las operaciones de restablecimiento y actualización de datos. El DAAB actualizado manipula DataSets, DataReaders, XmlReaders, y valores escalares de las tablas de base de datos SQL Server, Oracle y DB2. El DAAB de Enterprise Library es totalmente incompatible con las versiones anteriores. Lo único que tienen en común las dos versiones es que ambas utilizan la clase SqlCommandBuilder para autogenerar objetos SqlCommand. Para instalar la base de datos de ejemplo, los procedimientos almacenados y para probar el proyecto actualizado DataAccessQuickStart.sln, hay que seguir estos pasos: 1. Primero debemos bajar el DAAB desde la dirección http://www.microsoft.com/downloads/details.aspx?FamilyId=F63D1F0A-9877-4A7B-88EC-0426B48DF275&displaylang=en

84

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 85

Concretando proyectos reales Al terminar la instalación se crea un submenú en Inicio\Programas\Microsoft Application Blocks for .NET dentro de este hay un submenú Data Access dentro del cual encontraremos Documentación referente a DAAB y accesos para cargar el proyecto ya sea en VB o en C#. Ya instalado el DAAB, abrimos el proyecto hecho en VB y seleccionamos la opción Generar Solución del menú Generar el la biblioteca ya que el archivo de instalación no lo tiene o bien no lo crea al instalar el DAAB. 2. Si trabaja con SQL Server 2000 2005, abra el script DataAccessQuickStart.sql en el programa SQL Server Management Studio (SSMS), y ejecútelo para crear en localhost la base de datos de ejemplo EntLibQuickStarts del SQL Server con las tablas Customers, Products, Credits, y Debits, ocho procedimientos almacenados y dos triggers en la tabla de Products. Si trabaja con SQLServer Express, ejecute el script DataAccessQuickStart.sql con SqlCmd.exe. En este caso, debe cambiar el atributo value del parámetro server del archivo dataConfiguration.config por .\SQLEXPRESS o \localhost\SQLEXPRESS en lugar de localhost, tal como ilustra el siguiente listado. 3. Abra DataAccessQuickStart.sln en VS 2005 o VBX, y pulse para crear y ejecutar el proyecto. 4. Si obtiene mensajes de error porque faltan espacios-nombre, borre las referencias a las tres directivas Microsoft.Practices... y créelos de nuevo desde las copias DLL en la carpeta ...\DataAccessQuickStart\Assemblies folder. 5. Pruebe la conectividad de la base de datos y el código ejemplo actualizado clicando en cada uno de los siete botones, lo cual invocará el método aplicable al bloque de aplicación de datos.

3.4.1 El archivo de configuración de datos El archivo dataConfiguration.config contiene los valores de configuración de la cadena de conexión específica a la base de datos. El ensamblaje del bloque de aplicación Configuration deserializa el archivo de configuración. A continuación vemos el archivo dataConfiguration.config de la base de datos de ejemplo del EntLibQuickStarts SQLServer de DataAccessQuickStart, con los elementos databaseType y connectionString: <xmlSerializerSection type= “Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data”> <enterpriseLibrary.databaseSettings xmlns:xsd=”http://www.w3.org/2001/XMLSchema” xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” defaultInstance=”DataAccessQuickStart” xmlns=”http://www.microsoft.com/practices/enterpriselibrary/08-312004/data”>

85

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 86

Bases de datos con Visual Basic <parameters> <parameter name=”server” value=”localhost” isSensitive=”false” /> <parameter name=”database” value=”EntLibQuickStarts” isSensitive=”false” /> <parameter name=”Integrated Security” value=”True” isSensitive=”false” />


En teoría, sólo hace falta un cambio en el archivo dataConfiguration.config para cambiar a cualquiera de los tres de tipos de base de datos soportados. Especificar el databaseType determina la conexión, comando y la clase del operador.

3.4.2

Código de restablecimiento de datos

Después de definir una conexión a una base de datos con el archivo de configuración y una instrucción DimdbAsDatabase=DatabaseFactory.CreateDatabase(), se puede restablecer o actualizar datos con sobrecargas del método db.DBCommandWrapper y una de las instrucciones siguientes: db.ExecuteReader(dbCommandWrapper) db.ExecuteXmlReader(dbCommandWrapper) db.ExecuteScalar(dbCommandWrapper) db.ExecuteDataSet(dbCommandWrapper) db.UpdateDataSet(dbCommandWrapper) db.ExecuteNonQuery(dbCommandWrapper)

A modo de ejemplo, el extracto siguiente devuelve un objeto no tipificado DataSet que puede servir como valor de la propiedad DataGridView.DataSource: Dim dbSQL as Database = DatabaseFactory.CreateDatabase() Dim strSQL as String = SELECT * FROM Products WHERE CategoryID = 2

86

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 87

Concretando proyectos reales Dim cwSQL as DBCommandWrapper = dbSQL.GetSqlStringCommandWrapper(strSQL) Dim dsProducts As DataSet = dbSQL.ExecuteDataSet(cwSQL)

Las sobrecargas DBCommandWrapper.AddInParameteter() y DBCommandWrapper.AddOutParameteter() manejan búsquedas parametrizadas y procedimientos almacenados. El siguiente esquema de código presupone que el procedimiento almacenado GetProductDetails contiene un parámetro de entrada @ProductID, y los parámetros de salida @ProductName y @UnitPrice: Dim dbSQL as Database = DatabaseFactory.CreateDatabase() Dim cwSP as DBCommandWrapper = _ dbSQL.GetStoredProcCommandWrapper( GetProductDetails ) cwSP.AddInParameter( @ProductID , DbType.Int32, 2) cwSP.AddOutParameter( @ProductName , DbType.String, 50) cwSP.AddOutParameter( @UnitPrice , DbType.Currency, 8) dbSQL.ExecuteNonQuery(cwSP) Dim strReturn As String = cwSP.GetParameterValue( @ProductID ).ToString + cwSP.GetParameterValue( @ProductName ).ToString + , + _ Format(cwSP.GetParameterValue( @UnitPrice ), $#,##0.00 )

,

+ _

El tercer argumento del método AddInParameter es el valor proporcionado al parámetro del procedimiento almacenado. El tercer argumento del método AddOutParameter es la longitud de los datos.

Restablecer los metadatos de parámetro para el método GetStoredProceCommandWrapper(strProcName) normalmente requiere ir hasta el servidor cada vez que se ejecuta un procedimiento almacenado parametrizado. El bloque de aplicación de datos elimina los restablecimientos repetitivos ocultando metadata de parámetro en una hashtable. El método restablece los parámetros de un procedimiento específico sólo si no están presentes en la memoria cache. Los parámetros ocultos tienen soporte automático.

3.4.3

Código de actualización de datos

El método Database.UpdateDataSet (dsDataSet, strTableName, cwInsert, cwUpdate, cwDelete, intUpdateBehavior) realiza múltiples operaciones de actualización en la tabla de datos especificada en el argumento strTableName. La enumeración UpdateBehavior determina la respuesta del método ante un error de actualización: Standard (0, por defecto) detiene la ejecución, Continue (1) actualiza las filas restantes, y Transactional (2) retrocede todas las actualizaciones. La base de datos de ejemplo del DAAB incorpora los procedimientos almacenados AddProduct, UpdateProduct, y DeleteProduct. El siguiente extracto, proveniente del proyecto de ejemplo DataAccessQuickStart, crea un nuevo juego de datos no tipificado, añade y puebla una tabla de datos Products, añade una fila nueva a la tabla base Products, actualiza una fila existente y anuncia que borrará la fila añadida: Dim dbSQL As Database = DatabaseFactory.CreateDatabase() Create an untyped DataSet; add and populate the Products table Dim dsProducts As DataSet = New DataSet

87

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 88

Bases de datos con Visual Basic Dim cwSelect As DBCommandWrapper = dbSQL.GetSqlStringCommandWrapper( SELECT * FROM Products ) dbSQL.LoadDataSet(cwSelect, dsProducts, Products ) Dim dtProducts As DataTable = dsProducts.Tables( Products ) Add a new row to the Products table Dim objRow(3) As Object objRow(0) = DBNull.Value objRow(1) = Added Row Product Name objRow(2) = 11 objRow(3) = 12.5 dtProducts.Rows.Add(objRow) Dim cwInsert As DBCommandWrapper = dbSQL.GetStoredProcCommandWrapper( AddProduct ) cwInsert.AddInParameter( @ProductName , DbType.String, ProductName , DataRowVersion.Current) cwInsert.AddInParameter( @CategoryID , DbType.Int32, CategoryID , DataRowVersion.Current) cwInsert.AddInParameter( @UnitPrice , DbType.Currency, UnitPrice , DataRowVersion.Current) Dim cwDelete As DBCommandWrapper = dbSQL.GetStoredProcCommandWrapper( DeleteProduct ) cwDelete.AddInParameter( @ProductID , DbType.Int32, ProductID , DataRowVersion.Current) dtProducts.Rows(0).Item(1) = Modified Row Product Name Dim cwUpdate As DBCommandWrapper = dbSQL.GetStoredProcCommandWrapper( UpdateProduct ) cwUpdate.AddInParameter( @ProductID , DbType.Int32, ProductID , DataRowVersion.Current) cwUpdate.AddInParameter( @ProductName , DbType.String, ProductName , DataRowVersion.Current) cwUpdate.AddInParameter( @LastUpdate , DbType.DateTime, LastUpdate , DataRowVersion.Current) Dim intRowsUpdated = dbSQL.UpdateDataSet(dsProducts, Products , cwInsert, _ cwUpdate, cwDelete, UpdateBehavior.Transactional)

Invocando el método dbSQL.UpdateDataSet() se ejecuta el comando cwDeletecommand pero no se borra la fila de la tabla Products que añade el comando cwInsert. El valor de DataRowVersion.Current para la fila añadida es DbNull.Value, por lo que el comando no borra ninguna fila de la tabla base. Puede comprobar que no se ha borrado la fila añadida, "New product", ejecutando el proyecto DataAccessQuickStart.sln, pulsando el botón Update a Database Using a DataSet y abriendo la tabla EntLibQuickStarts.Products en VS 2005 Standard Edition o superiores, SSMS, o XM. Para resolver el problema anterior se puede añadir el siguiente código para devolver la tabla Products a su estado original (exceptuando el valor current identity seed) después de los cambios realizados por el extracto anterior o el DataAccessQuickStartClient: Dim dbSQL As Database = DatabaseFactory.CreateDatabase() Dim strDeleteSQL As String = DELETE FROM Products WHERE ProductName =

88

Added Row

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 89

Concretando proyectos reales Product Name Dim intCtr As Integer = dbSQL.ExecuteNonQuery(CommandType.Text, strDeleteSQL) strDeleteSQL = DELETE FROM Products WHERE ProductName = New product intCtr += dbSQL.ExecuteNonQuery(CommandType.Text, strDeleteSQL) Dim strUpdateSQL As String = UPDATE Products SET ProductName = Chai WHERE ProductID = 1 intCtr += dbSQL.ExecuteNonQuery(CommandType.Text, strUpdateSQL)

Los desarrolladores de la Enterprise Library invirtieron gran esfuerzo en el desarrollo por tests de los bloques de aplicación en C# y en añadir casos de prueba Nunit, pero apenas cambiaron la primera implementación del DataAccessStartClient de la Entreprise Library.

3.5 El cliente DataAccessQuickStart El cliente DataAccessQuickStart incluye la clase VB salesData que simula un sencillo DALC específico de base de datos para el SQL Server y la base de datos de ejemplo EntLibQuickStarts. Manejadores de eventos de siete botones invocan métodos salesData como GetCustomerList(), GetProductsInCategory(intCategory), y UpdateProducts(). La siguiente figura muestra el formulario QuickStartForm después de pulsar el botón superior (Retrieve multiple rows using a DataReader), que invoca el método SalesData.GetCustomerList().

A continuación vemos código comentado para el método SalesData.GetCustomerList(): Public Function GetCustomerList() As String Dim db As Database = DatabaseFactory.CreateDatabase() Dim sqlCommand As String = SELECT CustomerID, Name, Address, City,

89

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 90

Bases de datos con Visual Basic Country, PostalCode FROM Customers Dim dbCommandWrapper As DBCommandWrapper = _ db.GetSqlStringCommandWrapper(sqlCommand) Dim dataReader As IDataReader = db.ExecuteReader(dbCommandWrapper) Dim readerData As StringBuilder = New StringBuilder While dataReader.Read() readerData.Append(dataReader(Name)) readerData.Append(Environment.NewLine) End While dataReader.Close() Return readerData.ToString() End Function

A continuación vemos código convencional ADO.NET 2.0 para acceder a los objetos SqlClient directamente desde el archivo de clase QuickStartForm.vb con la cadena de conexión guardada en el archivo app.config: Private Sub compareUsingReaderButton() strConn = My.Settings.QuickStartConnection Dim cnQS As New SqlClient.SqlConnection(strConn) Dim strSQL As String = SELECT CustomerID, Name, Address, City, Country, PostalCode FROM Customers Dim cmQS As New SqlClient.SqlCommand(strSQL, cnQS) cnQS.Open() Dim sdrData As SqlClient.SqlDataReader = cmQS.ExecuteReader Dim sbData As New System.Text.StringBuilder With sdrData While .Read sbData.Append(sdrData(1).ToString + vbCrLf) End While .Close() End With cnQS.Close() Me.DisplayResults( Alternative Data Reader , sbData.ToString) End Sub

Comparando los dos ejemplos de código anteriores, que tienen aproximadamente el mismo número de líneas activas, vemos que no hay una reducción apreciable en la cantidad de código necesaria para implementar un DataReader con DAAB. Actualizar datos con DAAB requiere menos código escrito manualmente, pero no implementa juegos de datos no tipificados, ni grupos secuenciales de actualizaciones, entradas y eliminación de datos de las tablas base referentes a la actualización. La empresa de estudios de mercado Gartner citaba Avenade como uno de sus cuatro "Cool Vendors in IT Services and Outsourcing 2005" debido a las nuevas funciones orientadas a la programación (en inglés aspect-oriented programming, AOP) que ACA.NET 4.0 añade a los bloques de aplicación de la Enterprise Library basadas en ACA.NET. Uno de los principios de AOP es "separar preocupaciones". Las preocupaciones en torno a una 90

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 91

Concretando proyectos reales aplicación pueden ser su rendimiento, precisión, auditabilidad, seguridad, estructuras de datos y flujos de datos. Si los programadores de la aplicación pueden confiar en encontrar métodos estándar seguros para gestionar las estructuras y los flujos de datos, no tendrán que preocuparse de escribir código para implementar estos métodos. La cuestión es si el nivel adicional de abstracción en la gestión de datos que proporcionan los, más allá del inherente al juego de herramientas de ADO.NET 2.0 y VS 2005 justifica la curva de aprendizaje de DAAB, sus limitaciones potenciales, o ambas cosas.

3.6 Seguir las guías de diseño Las guías de diseño son una formación predadora dentro del grupo de P&P y no comparten una estructura o un estilo de escritura común con los modelos y arquitecturas referenciales. Las guías no incluyen los nuevos objetos de ADO.NET 2.0 ni las propiedades descritas en los dos capítulos anteriores, pero la mayor parte de sus recomendaciones se pueden aplicar a los proyectos con .NET 2.0.

3.6.1 La guía .NET Data Access Architecture Guide La guía .NET Data Access Architecture Guide (2001, actualizada en 2003), de 86 páginas, es un documento dirigido a desarrolladores novicios en ADO.NET. La guía actualizada hace recomendaciones específicas sobre los siguientes tópicos de ADO.NET 1.1: Managing Database Connections

Performing Database Updates with DataSets

Error Handling

Using Strongly Typed DataSet Objects

Performance

Working with Null Data Fields

Connecting Through Firewalls

Transactions

Handling BLOBs

Data Paging

La guía enfatiza el uso de DataSets, lo cual no es sorprendente si se considera que los DataSets son una de las propiedades básicas distintivas de ADO.NET y que Microsoft hizo una gran inversión para automatizar la creación de DataSet tipificados en todas las versiones de VS.

3.6.2 Mejorando el rendimiento y la escalabilidad de la aplicación .NET Improving .NET Application Performance and Scalability (2004), con sus 1.124 páginas, es la más larga de todas las publicaciones sobre P&P, y va destinada específicamente a los desarrolladores de la aplicación .NET 1.1, incorpora y actualiza consejos sobre rendimiento sacados de libros anteriores sobre modelos de diseño y las mejores prácticas. La mayoría de los capítulos incluyen una lista de chequeo donde se resumen en detalle las recomendaciones sobre rendimiento. Los cuatro capítulos más interesantes para los desarrolladores de aplicaciones con bases de datos son los siguientes: )

Capítulo 12, “Improving ADO.NET Performance”.

)

Capítulo 14, “Improving SQL Server Performance”.

91

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 92

Bases de datos con Visual Basic )

Capítulo 10, “Improving Web Service Performance”.

)

Capítulo 11, “Improving Remoting Performance”.

El capítulo 12, publicado en Mayo del 2004, incluye algunas referencias sobre las mejoras de rendimiento que significan los objetos de las versiones pre-beta de ADO.NET 2.0.

3.6.3 Diseñar componentes Data Tier y pasar datos por tier Designing Data Tier Components and Passing Data Through Tiers (2002) es una edición sobre papel, de 65 páginas, que incluye consejos para añadir nuevas capas a la presentación convencional de tres tiers, reglas de negocios y datos. La publicación define las entidades de negocio (en inglés business entities – BEs), componentes lógicas de acceso a datos (DALCs, data access logic components), y componentes de proceso de negocios (BPCs, business process components) y trata las relaciones de BEs, DALCs y BPCs con la presentación tier y el soporte físico de almacenamiento de datos. Es la publicación en papel más citada de todas las relacionadas con .NET. A continuación describimos brevemente las tres capas definidas por la publicación: )

)

)

92

Las BEs representan a los típicos elementos de una operación de negocios –como cliente, pedido, factura, producto o proveedor– como objetos de negocios. Las BEs normalmente se mapean en forma de tablas relacionales, en cuyo caso la BE puede contener datos de tablas relacionadas. Por ejemplo, las BE Pedido y Factura contienen miembros de ítem línea porque los pedidos y las facturas no son válidos sin, al menos, un ítem de línea. Si una tabla Clientes tiene otras tablas relacionadas en las que se guardan contacto, dirección de factura o de envío, la BE Cliente incluirá esos mismos miembros. Los comerciantes y distribuidores pueden incluir datos del proveedor relacionados con un producto BE. Las DALCs proporcionan BEs abstrayendo operaciones de creación, restablecimiento, actualización y borrado (CRUD: create, retrieve, update y delete) del soporte de datos. Las DALC son clases sin estado que esconden datos sobre detalles de implementación, como metadata de esquemas y propiedades de procedimiento almacenado, de objetos que invocan sus métodos. Son igualmente responsables de gestionar la consistencia de los datos y manejar los conflictos de concurrencia al ejecutar las sentencias SQL más que los procedimientos almacenados para actualizaciones. Una DALC bien diseñada debería ser capaz de proporcionar una BE a formularios Windows o Web, servicios Web o periféricos manuales. Las BPCs implementan las reglas de negocio y añaden datos y gestión de manejo de datos cuando las operaciones implican más de una BE. Las BPCs son responsables de implementar un sinfín de relaciones, como las que hay entre las BEs Cliente y Prodcuto. Si la BEs se mapea en tabla en bases de datos múltiples o depende de los servicios Web, para las actualizaciones son necesarias transacciones distribuidas. Las BPCs pueden incorporar gestión de workflow para transacciones de larga ejecución, que pueden requerir transacciones compensativas para invertir los cambios realizados anterioremente en los almacenes de datos.

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 93

Concretando proyectos reales Como en otras muchas guías de diseño e implementación .NET, los ejemplos de código fuente están escritos sólo en C#, lo que refuerza la creencia de que VB.NET está considerado entre los desarrolladores de Microsoft como un "ciudadano de segunda clase". Por otra parte, esta publicación garantiza tiempos iguales para implementar BEs con objetos custom data y DataSets. De todos modos, la implementación del objeto custom data de la clase OrderEntity especifica un miembro OrderDetails del tipo DataSet, que supera la interoperabilidad entre plataformas. A continuación, un ejemplo sencillo de un objeto BE Order tipificado y jerárquico: Public Class Order Public OrderID As Int32 Public CustomerID As String Public EmployeeID As Int32 Public OrderDate As Date Public RequiredDate As Date Public ShippedDate As Date Public ShipVia As Int32 Public Freight As Decimal Public ShipName As String Public ShipAddress As String Public ShipCity As String Public ShipRegion As String Public ShipPostalCode As String Public ShipCountry As String Public OrderDetails(24) As OrderDetail End Class Public Class OrderDetail Public OrderID As Int32 Public ProductID As Int32 Public UnitPrice As Decimal Public Quantity As Int16 Public Discount As Decimal End Class

El diseño de la clase Order enfatiza la versatilidad e interoperabilidad, por lo que expone campos públicos y representa ítems de línea en un sencillo array de ítems OrderDetail con una longitud inicial máxima, más que un objeto ArrayList o un objeto genérico List(OfOrderDetail). (Una sentencia RedimPreserve elimina los elementos OrderDetail vacíos después de poblar el array.) Este diseño asegura la independencia de plataforma y lenguaje, y permite a los métodos Web de VS 2002 y 2003 serializar BEs Pedido en mensajes SOAP. Los servicios WEB de .NET 2.0 también manejan objetos con manejadores Get y Set para propiedades privadas de campo. A continuación, un ejemplo de BE Order serializada: 1617968 QUICK

93

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 94

Bases de datos con Visual Basic <EmployeeID>9 1996-08-15T00:00:00.0000000-07:00 1996-09-02T00:00:00.0000000-07:00 <ShippedDate>1996-09-02T00:00:00.0000000-07:00 <ShipVia>3 76.07 <ShipName>QUICK-Stop <ShipAddress>Taucherstra e 10 <ShipCity>Cunewalde <ShipRegion /> <ShipPostalCode>01307 <ShipCountry>Germany 1617968 5 21.35 13 0.18 1617968 17 39 11 0.12


Aquí tenemos un sencillo esquema XML para la BE serializada con atributos para soportar las restricciones propias de la integridad referencial, un máximo de 25 ítemas de línea por pedido, y valores opcionales (nillable) dateTime, decimal, y string: <xs:schema attributeFormDefault= unqualified elementFormDefault= qualified xmlns:xs= http://www.w3.org/2001/XMLSchema > <xs:element name= Order > <xs:complexType> <xs:sequence> <xs:element name= OrderID type= xs:int /> <xs:element name= CustomerID minOccurs= 1 > <xs:simpleType> <xs:restriction base= xs:string > <xs:length value= 5 fixed = true />

94

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 95

Concretando proyectos reales <xs:element name= EmployeeID type= xs:int /> <xs:element name= OrderDate type= xs:dateTime /> <xs:element name= RequiredDate type= xs:dateTime nillable= true /> <xs:element name= ShippedDate type= xs:dateTime nillable= true /> <xs:element name= ShipVia type= xs:int /> <xs:element name= Freight type= xs:decimal nillable= true /> <xs:element name= ShipName type= xs:string minOccurs= 1 /> <xs:element name= ShipAddress type= xs:string minOccurs= 1 /> <xs:element name= ShipCity type= xs:string minOccurs= 1 /> <xs:element name= ShipRegion type= xs:string nillable= true /> <xs:element name= ShipPostalCode type= xs:string nillable= true /> <xs:element name= ShipCountry type= xs:string minOccurs= 1 /> <xs:element name= OrderDetails > <xs:complexType> <xs:sequence> <xs:element minOccurs = 1 maxOccurs= 25 name= OrderDetail > <xs:complexType> <xs:sequence> <xs:element name= OrderID type= xs:int /> <xs:element name= ProductID type= xs:int /> <xs:element name= UnitPrice type= xs:decimal /> <xs:element name= Quantity type= xs:short /> <xs:element name= Discount type= xs:decimal />

El método Web GetOrderSP devuelve el pedido especificado en un parámetro intOrderID. El método UpdateOrInsertOrderSP actualiza un pedido individual; un valor 0 en intOrderIDvalue inserta un nuevo pedido. El servicio Web es compatible con Java, Perl, y otros toolkits para los clientes de los servicios Web. A diferencia de otros servicios Web que proporcionan y actualizan juegos de datos, el esquema XML para los objetos custom BE no contiene detalles sobre implementación. VS 2005 convierte a los servicios Web que publican custom BEs en la fuente de datos equivalente a los juegos de datos tipificados. Cuando se añade una referencia Web a un servicio Web que publica un objeto BE serializado, fuertemente tipificado, la ventana de fuentes de datos muestra iconos de campo que son casi idénticos al par correspondiente de datos relacionados. A modo de ejemplo, la representación de la fuente de datos para la BE Orders es casi idéntica a las fuentes de datos para las tablas Northwind

95

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 96

Bases de datos con Visual Basic Orders y Order Details, sólo cambian las secuencias de campos. Como con un par de tablas de datos relacionadas, aquí también se puede arrastrar el nodo Orders hasta un formulario Windows y generar una OrderBindingSource, una OrderDetailsBindingSource, y un conjunto de cuadro de texto y controles DataGridView vinculados a los detallesmaestro. El proyecto NWOrdersWSClient.sln ilustra la simplicidad de una aplicación de edición con formulario Windows para un pedido BE de ventas. Pulsando el botón Connect to Web Service del formulario BoundClient.vb, se llena el cuadro combinado con una lista de los diez últimos pedidos de venta. Pulsando el botón Get Selected Order se restablece el pedido de una determinada venta y sus items de línea con sólo cuatro líneas de código. Se puede editar los datos de la cabecera del pedido y los ítems de línea y después actualizar la base de datos clicando el botón Update Order, ejecutando una sola línea de código. Los ítems de línea son un array sencillo, por lo que no se puede añadir o borrar ítems de línea en un DataGridView sin código adicional.

"Designing Data Tier Components and Passing Data Through Tiers" es una de las guías más útiles sobre arquitectura de aplicaciones para los proyectos .NET centralizados en datos, ya que proporciona ejemplos detallados de implementación en diferentes situaciones y con diferentes usos de las BE. Puede prescindir de los ejemplos que substituyen DataSets con arrays o colecciones de objetos hijo si su BE tiene que interoperar con aplicaciones en sistemas diferentes de Windows. 96

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 97

Concretando proyectos reales

3.7 Aplicar las directrices de diseño para la biblioteca de clas (Apply Class Library Design Guidelines) Directrices de diseño para desarrolladores de la biblioteca de clases – Design Guidelines for Class Library Developers es un documento de referencia que proporciona una guía detallada y prescriptiva para programar clases .NET. Para leer este miembro de .NET Framework General Reference, busque el sitio MSDN para "Design Guidelines" (con las comillas). Las directrices de la biblioteca de clases consisten en 14 tópicos principales que derivan a numerosos subtópicos. Dos de los tópicos más importantes son "Naming Guidelines" y "Class Member Usage Guidelines".

3.7.1 Naming Guidelines La mayoría de desarrolladores de VB6 y VBA aplican el estilo de notación húngaro (camelCase), prefijos tipificados para las variaables, nombres de formulario, controles, clases y miembros de clases. Esta práctica para prefijos de tres letras en nombres de objeto tuvo su origen en la "Visual Basic Programmers Guide" de Microsoft en la era de VB3. Los servicios de consultoría de Microsoft extendieron la práctica recomendada de los prefijos de dos y tres letras para los objetos Jet de bases de datos. La mayoría de los ejemplos de este libro y algunos ejemplos de código VB de Microsoft usan prefijos similares para los nombres de instancias de tipo y variable de VB.NET. El tópico "Naming Guidelines" contiene subtópicos para dar nombre a clases y sus miembros, pero no nombres de instancias. PascalCase es de rigor para los nombres de clases y miembros en .NET, excepto los parámetros, que utilizan el camelCase, como en typeName. La BE Orders anterior seguía las prácticas .NET sobre nombres de PascalCase, pero no las recomendaciones de no usar campos de instancia Public. Las directrices también recomendaban no usar guiones bajos en los nombres, pero el uso de _ o m_ como prefijo para campos de instancia Private o Protected es una práctica común.

97

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 98

Bases de datos con Visual Basic Finalmente, utilizar el tipo de clase como prefijo (C como en CTypeName) ha quedado fuera de uso, pero siempre se debe utilizar el prefijo I para identificar las interfaces. Algunos desarrolladores tienen tendencia a utilizar camelCase al nombrar miembros y clases, por lo que las instancias serializadas son conformes de-facto a la convención camelCase para elementos y atributos XML. Todos los ejemplos de la recomendación W3C Extensible Markup Language (XML) 1.0 (tercera edición) usan nombres del tipo camelCase para elementos y atributos. En W3C XML Schema Part 0: Primer recommendation en http://www.w3.org/TR/xmlschema-0/ se utiliza el modelo camelCase en las etioquetas de elemento y en Infosets XML de ejemplo, como po-xml, que tiene una estructura similar a la de la BE Pedido. También verá ejemplos de nombres tipificados camelCase en algunos métodos de servicio Web NWOrdersWS; estos nombres se crearon para cumplir las convenciones de nombres XML de InfoPath 2003. Los nombres de las etiquetas XHTML requieren minúsculas, pero los Infosets XML no. Lo mejor es seguir las "Naming Guidelines" para las clases públicas y sus miembros, y elegir según criterio propio camelCase o PascalCase para los nombres de elementos y atributos XML.

3.7.2 Class Member Usage Guidelines El tópico "Class Member Usage Guidelines" tiene subtópicos para todos los miembros de clase. El subtópico "Field Usage Guidelines " recomienda no exponer los campos de instancia Public o Protected a los desarrolladores porque convertir un campo público en una propiedad no mantiene la compatibilidad binaria. En su lugar, es mejor utilizar los métodos de acceso Get y Set. La guía también recomienda usar constantes para campos que no cambian de valor, ya que el compilador guarda las constantes directamente en el código que llama al objeto. Este tópico recomienda camelCase para distinguir los nombres de campo privados de los nombres de propiedad públicos, cosa que funciona con C# pero no con VB, insensible a los casos. El subtópico "Property Usage Guidelines" proporciona útiles consejos para determinar cuándo utilizar un método o una propiedad y, si se decide por la propiedad, como evitar los picos con las propiedades indexadas, como son las propiedades que definen y devuelven arrays. Utilice una sola propiedad indexada por clase y hágala la propiedad indexada por defecto. Estas recomendaciones valen también para el miembro OrderDetails de la BE Orders, si cambia el miembro de un campo público a otro privado y añade la propiedad pública OrderDetails.

3.8 Prepararse para la arquitectura orientada al servicio La arquitectura orientada al servicio, en inglés Service-oriented architecture (SOA), es actualmente la referencia en a IT. Una búsqueda en Google por "service-oriented architecture" nos devuelve cerca de °25! millones de entradas con esas palabras. Los analistas de negocios, marketing y relaciones públicas son los máximos contribuyentes en la alimentación de la máquina SOA, que empezaron a ensamblar en los últimos estados de la combustión del punto-com. El auge de la SOA coincidió con el decaímiento de los servicios Web (XML) basados en SOAP.

98

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 99

Concretando proyectos reales No es de estrañar que Microsoft subiera al tren de la SOA. Casi la mitad de las sesiones de la Tech*Ed 2004 sobre arquitectura incluían "service" y "oriented" en sus títulos (9 de 19). El sitio MSDN devuelve más de 200 entradas con artículos en torno a la SOA, publicaciones estatales, transmisiones Web (Web casts), y episodios de TV MSDN. El sitio www.microsoft.com ofrece como cuatro veces más referencias a la SOA. Uno de los mayores incentivos de Microsoft en animar a los desarrolladores a comprar SOA es promover la venta de licencias VS 2005 y que se adopte el sistema .NET Framework 2.0. VS 2002 y 2003 simplifican considerablemente el proceso de escribir y publicar servicios Web ASP.NET básicos; el jurado todavía tiene sus dudas sobre si VS 2005 simplifica o complica el código y los tests de los servicios Web.

3.8.1 El camino a la Arquitectura orientada al servicio (SOA) En el último medio siglo, la arquitectura de las aplicaciones de procesamiento de datos se ha desarrollado en las tres fases siguientes: )

)

)

Arquitectura monolítica: encapsulaba la interfaz del usuario, la lógica de los negocios y las operaciones de almacenamiento de datos en una sola componente. Las primeras aplicaciones monolíticas consistían en terminales alfanuméricas conectadas a bases de datos mainframe y gestores de transacciones. El PC permitió a usuarios y desarrolladores beneficiarse de las ventajas del software de gestión de base de datos para escritorio, con programas como dBASE, Fox Pro, y Access, para crear aplicaciones monolíticas con almacenamiento de datos en los archivos locales o de red. La lógica orientada a los negocios incorporada en una aplicación no se puede utilizar con otras aplicaciones. Arquitectura cliente-servidor: cambió la gestión y el almacenamiento de datos del escritorio a una aplicación de red, pero retuvo la UI, la lógica de los negocios, y los elementos de acceso a datos en un solo programa, como Visual Basic o algún otro ejecutable, o un archivo .adp de Access. La arquitectura cliente-servidor permitía centralizar la gestión de datos y pasar muchas de las aplicaciones CRUD (procesamiento de búsquedas) del PC cliente al servidor con las bases de datos. Cada cliente mantenía una conexión exclusiva con el servidor de la base de datos, lo cual limitaba la escalabilidad de la aplicación. La lógica de negocios en el cliente y el código de acceso a los datos no se podían compartir con otras aplicaciones. Arquitectura n-tier: encapsula la lógica de negocios y el acceso a datos en componentes por capas individuales. Las UI del cliente acceden a la componente de lógica de negocios, la cual conecta uno o más componentes de acceso de datos. Las capas de acceso de datos sin estado comparten conexiones a la base de datos con componentes múltiples de lógica de negocios, la cual comparten con el cliente las responsabilidades de la gestión de estado. DCOM, CORBA, y otras tecnologías de componente permiten que los componentes de lógica de negocios y de acceso de datos residan en múltiples servidores, haciendo así a las aplicaciones más escalables, robustas y fáciles de mantener.

La arquitectura cliente-servidor permite a los clientes operar con bases de datos back end que proporcionan controladores .NET, ODBC, JDBC u OLE DB y se ejecutan bajo

99

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 100

Bases de datos con Visual Basic Windows, UNIX, Linux y sistemas operativos mainframe. Es mucho más difícil alcanzar interoperabilidad entre componentes n-tier distribuidas, escritas en otros lenguajes de programación o que se ejecutan en sistemas operativos múltiples, o ambas cosas a la vez. Superar los problemas de interoperabilidad de los componentes distribuidos ha hecho que proliferen los productos de software combinado y los servicios de consultoría, llamados "enterprise application integration (EAI)". El mercado de EAI se mantuvo relativamente fuerte durante la caída del punto-com y resurgió de nuevo más rápido que ningún otro segmento del mercado IT cuando se recuperó la economía. Añadir una capa exclusiva EAI entre componentes de otro modo incompatibles hace aumentar la fragilidad (brittlenesse en inglés) de la aplicación. Se dice que una aplicación es frágil cuando el menor cambio en un solo componente desemboca en un fallo catastrófico del sistema. Este fenómeno es parecido al de las fisuras que se producen en un avión puesto al límite y que finalmente pueden provocar que el avión se estrelle; sólo que en el caso de los sistemas n-tier todo sucede con mucha más rapidez. Otro problema en la arquitectura n-tier son los componentes estrechamente vinculados que se comunican por llamadas de procedimiento remoto, en inglés remote procedure calls (RPC), implementadas en DCOM, CORBA, Java RMI, o J2EE Enterprise Beans. Los componentes tradicionales middle-tier utilizan RPC sincrónicos, los cuales requieren una respuesta inmediata a cada petición; si no se obtiene la respuesta a tiempo de alguno de los componentes, todo el proceso queda bloqueado. Los RPC asincrónicos y los sistemas de messaging –como Microsoft Message Queue Server (MSMQ) o IBM Qseries– mitigan este problema, aunque no dan necesariamente una solución válida a todos los niveles. Ninguno de los anteriores métodos con RPC puede comunicarse traspasando los cortafuegos de red actuales, los cuales restringen el tráfico normalmente a los puertos TCP 80 y 443. Esta limitación hace que el acceso por Internet a componentes lógicos de negocios específicos resulte difícil, por no decir imposible.

3.8.2 Implementar SOA con servicios Web La arquitectura orientada al servicio resuelve la mayor parte de los problemas de interoperabilidad de la arquitectura n-tier descritos en el apartado anterior. A continuación, los requisitos básicos para SOA: ) )

)

) )

100

Interfaces con puntos de acceso de base estándar a componentes de lógica de negocios. Encapsulamiento de los componentes de lógica de negocios y sus funciones para ocultar los detalles de implementación a quienes acceden desde fuera. Loose-coupling a través de métodos de acceso sin estado, asincrónicos o semi-sincrónicos, implementados con mensajes basados en texto (normalmente en Unicode). Formatos de mensaje y descripciones de interfaz basados en estándares. Protocolos estándar para la comunicación con interfaces y funciones, incluyendo la capacidad de transmitir mensajes a través de cortafuegos, si los hubiere.

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 101

Concretando proyectos reales )

Propiedades basadas en estándares del tratamiento de error y la seguridad.

)

Interoperabilidad estándar entre el proveedor de la interfaz y sus usuarios.

Implementar la SOA no implica necesariamente el uso de servicios Web basados en SOAP, pero sólo los servicios Web estándar SOAP, con alcance en toda la industria –Web Services Description Language (WSDL), y WS-Security– combinados con otros estándares W3C, IETF y OASIS, cumplen actualmente con todos los requisitos mencionados anterioremente para SOA. Los documentos e infosets XML 1.0 son la base de los servicios Web. Los documentos WSDL definen las interfaces (puerto y operaciones) de los servicios Web y los puntos de acceso (direcciones) e incluyen un esquema XML para los documentos de mensajes de request y response SOAP. El esquema permite a los programadores de clientes de servicios Web utilizar una copia local de un documento WSDL para incorporar propiedades IDE de tiempo de diseño, como por ejemplo IntelliSense. El esquema y los mensajes no incluyen detalles sobre la implementación del servicio. Los protocolos de transporte más comunes para los servicios Web basados en SOAP son HTTP y HTTPS, pero TCP, email (SMTP, POP3 y otros) y FTP son alternativas potenciales. Independientemente del tipo de transporte, los servicios Web son sin estado y autónomos. El estado lo ha de mantener el cliente del servicio Web o alguno de los estándares derivados para las transacciones que implementan el servicio Web (WSCoordination, WS-AtomicTransaction) o procesos de negocios (WS-BusinessActivity, Business Process Execution Language for Web Services [BPEL4WS], WS-Choreography). Los temas relacionados con la seguridad de los servicios Web son el primer impedimento para la adopción generalizada de SOA. HTTPS encripta mensajes SOAP entre dos puntos de acceso (a menudo llamados puntos final o end points), y HTTPS con certificados de cliente puede autentificar a los usuarios particulares que llaman al servicio. Las implementaciones de seguridad más sofisticadas requieren firmas digitales y encriptación personalizada de mensajes, proporcionada por la especificación WSSecurity. Implementar la WS-Security con servicios Web ASP.NET 2.0 implica instalar Web Services Extensions (WSE) 2.0 SP3, o versiones posteriores, en el servidor del servicio Web y las máquinas cliente.

3.8.3 Garantizar total interoperabilidad del servicio Web La mejor práctica para la arquitectura SOA exige que los servicios operen independientemente del sistema y de los lenguajes de programación. Como ejemplo, un cliente de servicio Web programado con Java y ejecutado con FreeBSD o Linux debe tener interoperabilidad con otros servicios Web VB.NET o C# ASP.NET proporcionados por un servidor de Windows 2000 o 2003. VS 2005 ha intentado garantizar que los servicios Web ASP.NET que usted crea sigan la normativa básica establecida en Basic Profile (BP) 1.0 de la Web Services Interoperability (WS-I) Organization. BP 1.0 alcanzó estatus de "Final Specification" en Abril de 2004, más de dos años después de la fundación de WS-I y 53 miembros más de la comunidad de servicios Web. BP 1.0 prohibe explícitamente el uso de la codificación SOAP, sección 5, la cual excluye los formatos de mensaje rpc/encoded y documento/encoded por temas de interoperabilidad. BP 1.0 soporta ambos formatos: 101

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 102

Bases de datos con Visual Basic document/literal (doc/lit) y rpc/literal, pero los servicios rpc/literal son muy infrecuentes. El formato estándar para los mensajes SOAP de ASP.NET es doc/lit. La primera directriz en el apartado 1.3 de los "Guiding Principles" de BP 1.1 dice: "No existe garantía de interoperabilidad. Es imposible garantizar completamente la interoperabilidad de un servicio concreto. De todos modos, el Perfil encara los problemas más comunes que la práctica de la implementación ha sacado a la luz del día hasta ahora". Si esta cláusula no estuviera ahí, los desarrolladores más inocentes podría suponer que los servicios Web ASP.NET 2.0 que proporcionan y actualizan objetos DataSet serializados, y dicen ser conformes a los requisitos de BP 1.1, interoperan con clientes escritos en Java, Perl, Python, o cualquier otro lenguaje (inclusive VB6 o VBA) que contiene un toolkit SOAP 1.1 o 1.2 conforme a BP 1.0. Los toolkits de servicios Web hacen mapas de mensajes SOAP a objetos haciendo referencia al esquema incluido en el documento WSDL para los servicios Web doc/lit. Microsoft desestimó el SOAP Toolkit 3.0 a favor de .NET Framework a principios de 2004 y el soporte estándar quedó garantizado sólo hasta Abril del 2005 (el soporte extendido se mantiene hasta Abril del 2008). El formato de mensaje original en Toolkit es rpc/encoded, que no cumple el BP 1.0, y escribir servicios document (doc/lit) con el API (muy bajo nivel) de Toolkit es una agonía, por decirlo suavemente. Otra razón para retirar Toolkit es que Windows Server 2003 no soporta los componentes del servidor de Toolkit ni ISAPI Listener.

Los servicios Web ASP.NET 2.0 que proporcionan o actualizan DataSets genéricos no interoperan con las versiones actuales de toolkits, excepto las de Microsoft. Los casusantes son la referencia a s:schema y el elemento wildcard <s:any/> en los nodos de método en la Web. Esta combinación es un flag que le dice al procesador WSDL de .NET que el esquema esta incrustado en el mensaje SOAP de respuesta. Aquí vemos un fragmento de un documento WSDL típico para un juego de datos tipificado o no: <s:element name= GetAllCustomersResponse > <s:complexType> <s:sequence> <s:element minOccurs= 0 maxOccurs= 1 <s:complexType> <s:sequence> <s:element ref= s:schema <s:any />

name= GetAllCustomersResult >

/>

Los DataSets genéricos son presumiblemente dinámicos, por lo que exponen su esquema durante el tiempo de ejecución incrustándolo en el mensaje SOAP en lugar del documento WSDL. De todos modos, los DataSets tipificados estáticos producen nodos de esquema WSDL idénticos a los de los DataSets no tipificados. Eso significa que los 102

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 103

Concretando proyectos reales toolkits que no son de Microsoft tienen que usar un API de bajo nivel para procesar el mensaje SOAP de respuesta como si fuera una XMLNodeList, lo cual implica un proyecto de programación nada trivial. Escribir código Java para conseguir un diffgram con actualizaciones de juegos de datos en un mensaje SOAP de request sería una tarea hercúlea que probablemente tendría el éxito de Sísifo. Incrustar esquemas XML en mensajes SOAP no va contra las especificaciones de SOAP 1.1 o 1.2, pero es una práctica muy poco convencional (y muy controvertida). Los esquemas de DataSets incorporan numerosos espacios-nombre (de propiedad) específicos de Microsoft, como xmlns:msdata= "urn:schemas-microsoft-com:xml-msdata" y xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1". Los mensajes también se decoran con xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1". Los DataSets añaden atributos de propiedad a los esquemas y mensajes msdata:IsDataSet="true", msdata:PrimaryKey="true", diffgr:id="Customers1" , y msdata:rowOrder=" 0" son dos ejemplos. Los esquemas de DataSets tipificados de ADO.NET 2.0 contienen mucha más información detallada que las versiones ADO.NET 1.x, casi idénticas a los esquemas no tipificados de DataSets. Como ejemplo, ADO.NET 2.0 añade ueve atributos msprop:PropertyName a cada tag <xs:element...>. Estos elementos añadidos muestran detalles operacionales del servicio Web a los clientes del servicio, lo cual contraviene el dictado SOA de que los servicios han de ocultar los detalles de implementación a quienes acceden a ellos. Los apartados siguientes son un adelanto de los servicios Web que se verán más adelante en este mismo libro y testan a los clientes para demostrar temas de interoperabilidad con los servicios Web que procesan amobs tipos de DataSets.

3.8.4 Instalar y publicar el servicio Web DataSetWS El proyecto de ejemplo DataSetWS.sln, un servicio Web ASP.NET 2.0, expone cuatro de los métodos Web que operan con el DataSet no tipificado dsNwind DataSet: GetAllCustomers, GetOrdersByCustomerID, UpdateCustomersDataSet, y UpdateOrdersDataSet. Para instalar, testar y publicar el servicio Web en su instancia local de IIS, siga los pasos siguientes: 1. Cargue el archivo DataSetWS.vb y cambie el valor del string de conexión strConn para adecuarlo a las configuraciones de seguridad del SQLServer. 2. Pulse para iniciar el servicio DataSetWS, pulse el vínculo GetAllCustomers y pulse Generar para devolver un mensaje de respuesta que contenga un diffgram con todos los datos de los Clientes. 3. Vuelva a la página principal de ayuda del servicio Web, pulse el vínculo GetOrdersByCustomerID, escriba RATTC en el cuadro de texto Customer ID Parameter Value y pulse Generar para volver al diffgram con los datos de los Orders de “Rattlesnake Canyon Grocery”. 4. Cierre la página DataSetWS.asmx o pulse <Mayús>+ para finalizar la instancia del servicio Web. 5. Seleccione Generar/Publicar para abrir el cuadro de diálogo Publicar Web, escriba http://localhost/DataSetWS en el cuadro de texto y pulse Aceptar para crear el direc-

103

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 104

Bases de datos con Visual Basic torio virtual IIS y añadir los archivos precompilados a la carpeta \Inetpub\DataSetWS. 6. Para comprobar el desarrollo de IIS, abra Internet Explorer y navegue hasta http://localhost/datasetws/datasetws.asmx, allí invoque los métodos GetAllCustomers y GetOrdersByCustomerIDWeb. El incentivo principal para usar el atajo DataSet en la parte del servidor es que así el código necesario para añadir métodos Web es mínimo. Como ejemplo, veamos el código para implementar el método Web GetAllCustomers: <WebMethod(Description:=strGetCustomers)> _ Public Function GetAllCustomers() As DataSet Dim dsNwind As New DataSet Dim daCusts As SqlDataAdapter = Nothing Try daCusts = New SqlDataAdapter("SELECT * FROM Customers", strConn) daCusts.Fill(dsNwind) dsNwind.DataSetName = "Northwind" dsNwind.Namespace = "http://oakleaf.ws/webservices/datasetws/northwind" With dsNwind.Tables(0) 'Assign the table name .TableName = "Customers" 'Assigning a table namespace breaks the published Web service '.Namespace = "http://oakleaf.ws/webservices/datasetws/north wind/customers" 'Specify the primary key .PrimaryKey = New DataColumn() {.Columns(0)} 'Require a CompanyName value (table constraint) .Columns(1).AllowDBNull = False End With Return dsNwind Catch excSys As Exception Dim excSoap As New SoapException(excSys.Message, _ SoapException.ClientFaultCode, Context.Request.Url.AbsoluteUri) Throw excSoap Finally dsNwind.Dispose() daCusts.Dispose() End Try End Function

El código para actualizar un DataSet no tipificado es igual de sencillo. El objeto de ADO.NET 2.0 SqlCommandBuilder genera automáticamente las sentencias SQL y, si está especificada, la colección Parameters necesaria para una concurrencia optimista en las actualizaciones y borrados cuando se especifica cbCusts.ConflictOption=ConflictOption.CompareAllSearchableValues, tal como aparece en negrita en el código siguiente:

104

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 105

Concretando proyectos reales <WebMethod(Description:=strUpdateOrdersDataSet)> _ Public Function UpdateOrdersDataSet(ByVal dsNwind As DataSet) As Boolean Dim cnNwind As New SqlConnection(strConn) Dim daOrders As SqlDataAdapter = Nothing Dim cbOrders As SqlCommandBuilder = Nothing Try 'daOrders = New SqlDataAdapter("SELECT * FROM Orders", cnNwind) 'To accommodate timestamp column daOrders = New SqlDataAdapter(strOrdersSelect, cnNwind) cbOrders = New SqlCommandBuilder(daOrders) cbOrders.ConflictOption = ConflictOption.CompareAllSearchableValues daOrders.Update(dsNwind, "Orders") Return True Catch excSys As Exception Dim excSoap As New SoapException(excSys.Message, _ SoapException.ClientFaultCode, Context.Request.Url.AbsoluteUri) Throw excSoap Return False Finally cbOrders.Dispose() daOrders.Dispose() dsNwind.Dispose() End Try End Function

Compare el código anterior con el que se necesitaba para actualizar las tablas Orders y Orders detail en el ejemplo anterior de NWOrdersWS. Creando una instancia CommandBuilder en tiempo de ejecución para generar objetos DeleteCommand, InsertCommand, y UpdateCommand, hace bajar la efectividad del código y, por lo tanto, no es la mejor práctica.

3.9 Use FxCop para validar el código del proyecto FxCop es una herramienta de Microsoft para analizar código que comprueba que los ensamblajes de código gestionado sean conformes a las directrices .NET Framework Design Guidelines y los estándares de código. Aproximadamente la mitad de los 200 tests comprueban la conformidad con las Design Guidelines. FxCop se originó en las versiones de línea de comando de Windows, pionero en esta tarea, como miembro de la colección de herramientas GotDotNet y .NET Framework 2.0 incluye clases FxCop. VS 2005 Team System (VSTS) integra el análisis opcional con FxCop para todos los proyectos. Para activar FxCop se ha de abrir la ventana Propiedades del <nombre proyecto>, seleccionar la ficha Compilar y deseleccionar el cuadro de verificación Deshabilitar todas las advertencias. Se pueden desactivar las reglas de nueve categorías y expandir los nodos de categoría para leer las descripciones de las reglas y desactivarlas individualmente. Ejecutar un proyecto relativamente sencillo –DataAccessQuickStart.sln para este ejemplo con CodeAnalysis de VSTS activado– da 1 error y 38 advertencias; FxCop v.1.312 da 49 errores y mensajes. 105

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 106

Bases de datos con Visual Basic Para mostrar las advertencias de Code Analysis en los proyectos de ejemplo, escríbalos de nuevo. Si la ventana Errors no es visible, seleccione en el menú Ver, la opción Lista de errores.

Un sencillo proyecto con DataSet genera 90 errores y advertencias en FxCop 1.312 con Office 2003 instalado en la máquina que ejecuta VS 2005. FxCop utiliza el dicionario estándar y los diccionarios personalizados, si los hay. Prácticamente todos los mensajes provienen del código autogenerado VB 2005. Es evidente que los desarrolladores de Microsoft que escribieron el generador de código para los DataSet no se atuvieron a las reglas FxCop sobre clases autogeneradas. Los actuales juegos de reglas de Code Analysis y FxCop superan con creces la capacidad de la mayoría de proyectos formulario de Windows. Se puede personalizar el juego de reglas aplicado a un proyecto específicoy FxCop o VSTS lo perpetuarán cuando se cierre el proyecto. No obstante, establecer un juego de reglas personalizado que se pueda aplicar a todos los proyectos representa un esfuerzo considerable. El cuadro de diálogo Opciones no permite especificar un juego de reglas FxCop por defecto para todos los proyectos.

3.10 Automatizar Test-Driven Development Test-driven development (TDD) es una metodología de programación basada en la verificación del código por pequeños segmentos (unidades) y en la escritura de unidades de test automatizado (unit tests) antes de escribir el código de la aplicación. Los unit tests han de ser conformes a las especificaciones de la aplicación y hay que escribir el código para determinar si las unidades son conformes o no a los requerimientos especificados. Después se añaden instrucciones especiales al código para definir cada unidad de test. Mientras se desarrolla la aplicación, las unidades de test definidas pasan a ser miembros de un test completo que se ejecutará al construir el proyecto. Una de las principales ventajas del test por unidades es que el proceso detecta los bugs de regresión que tantas veces aparecen en proyectos largos realizados por varios equipos de desarrollo. VSTS es un conjunto de herramientas add-on para VS 2005 con cinco ediciones dedicadas respectivamente a los arquitectos, adminsitradores de proyecto, desarrolladores individuales, desarrolladores de grupo y verificadores (testers). Las ediciones VSTS for Developers y VSTS for Testers incluyen la posibilidad de generar y ejecutar unit tests automatizadas y totalmente integradas. Instalando VSTS for Developers o Testers se añaden plantillas Test Project y Empty Test Project al cuadro de diálogo Add New Project.

3.11 Ejecutar Best Practices Analyzer para SQL Server 2000 La herramienta Best Practices Analyzer (BPA) para el SQL Server 2000 de Microsoft es una aplicación .NET 1.1 que testa las instancias SQL Server y MSDE 2000 para mejores prácticas en seguridad y administración. Esta aplicación está dirigida básicamente a los administradores de IT y DBAs, aunque los desarrolladores de bases de datos pueden y deberían ejecutar análisis periódicos en sus servidores de desarrollo y testeo. Una vez configurado el BPA, los tests se pueden sincronizar para su ejecución nocturna o en otros periodos de baja actividad para la base de datos

106

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 107

Concretando proyectos reales El análisis incluye las categorías de Backup y Recovery, Configuration Options, Database Administration, Database Design, Deprecation, Full-Text, General Administration, SQL Server 2005 Readiness, y T-SQL. Curiosamente, BPA no incluye ninguna categoría de test de seguridad que verifique la existencia y la potencia de de contraseñas sa para instancias que implementan el modo mixto de autentificación. Al instalar BPA se crea una base de datos sqlbpa de reposición en el servidor BPA que se indique. Después hay que especificar las instancias de SQL Server, MSDE o SQL Express para testar y configurar los grupos Best Practices Groups (BPGs) para cada instancia, y especificar los grupos a ejecutar, tal como muestra la siguiente figura.

Pulsando el vínculo Scan SQL Server Instances aparece una lista de los servidores para los que se han especificado grupos BPGs y seleccionado para la ejecución. Pulsando el vínculo Next se inicia el escaneo del servidor, cuya duración depende de la carga y rendimiento del servidor o la red, y del número de objetos en el servidor. Una vez completado el análisis, los resultados se pueden filtrar con non-compliance para destacar las partes que necesiten corrección, tal como muestra la figura de la páginasiguiente.

3.12

Applicar Best Practices específicas a los proyectos de ADO.NET 2.0

Si se ha tomado el tiempo de leer las publicaciones P&P de Microsoft que pertencen a ADO.NET, probablemente estará familiarizado con casi todas –si no todas– las mejores prácticas recomendadas para ADO.NET 2.0 que veremos a continuación. 3.12.1 Use cadenas de conexión idénticas para las conexiones de bases de datos Pool Todos los proveedores de datos ADO.NET, Microsoft y otros, soportan el pooling de conexión a la base de datos. El primer cliente en conectarse a la base de datos añade automáticamente una conexión al pool, si el pool no se ha creado todavía. Todos los 107

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 108

Bases de datos con Visual Basic

demás clientes con valores idénticos en ConnectionString comparten conexiones en pool. Un cambio mínimo en el valor de un ConnectionString para un servidor y una base de datos específicos, como cambiar IntegratedSecurity=SSPI por IntegratedSecurity=True o añadir/eliminar un espacio, genera un nuevo pool de conexión. Crear una conexión nueva en el Explorador de Servidores genera una cadena de conexión estándar para la autentificación de Windows o SQL Server. De todos modos, no se tiene acceso directo al texto de la cadena de conexión en este punto. Asegurar una armonía perfecta entre las cadenas de conexión que se añaden al código y los generados por Explorador de Servidores implica el mantenimiento de copias de referencia. Guarde la versión del Explorador de Servidores en un archivo ConnectionStrings.txt la primera vez que lo use en un proyecto. Puede copiar la cadena de conexión en el archivo de texto del cuadro de texto expandiendo el botón Cadena de conexión en el primer paso del Asistente para la configuración de orígenes de datos, Elija la conexión de datos. 3.12.2 Definir el tamaño del pool de conexión El tamaño mínimo por defecto del pool es 0 y el tamaño máximo por defecto es 100. Se puede maximizar el rendimiento de clientes ampliamente visitados incrementando el tamaño mínimo por defecto del pool hasta 10 o más, e incrementando el tamaño máximo hasta el número esperado de clientes conectados simultáneamente. Por ejemplo, la siguiente cadena de conexión establece un tamaño mínimo de pool de 10 y un máximo de 200: Dim strConn As String = Server=OAKLEAF-MS16;Database=Northwind; Integrated Security=True;Min Pool Size=10;Max Pool Size=200

108

+ _

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 109

Concretando proyectos reales Crear el pool de diez conexiones supone una disminución del rendimiento para el primer cliente que abre una conexión, pero mejora el rendimiento en los otros nueve clientes que se conectan simultáneamente. Definir un valor MinPoolSize para los servicios Web es una práctica común, ya que la primera llamada a un servicio Web ASP.NET no oculto implica un retraso de instanciación mucho más largo que el tiempo requerido para crear las diez conexiones. 3.12.3 Guardar cadenas de conexión en archivos de configuración Es una práctica común incluir un ConnectionString o un atributo key de nombre similar a los archivos App.config o Web.config. Cuando se selecciona el cuadro de verificación Sí, guardar la conexión como… en el cuadro de diálogo Guardar cadena de conexión… del Asistente para la configuración de orígenes de datos, el diseñador de juegos de datos guarda la cadena de conexión en el archivo App.config, como se indica aquí en negrita:

El diseñador de DataSet añade la entrada a la lista de de la página de propiedades MyProject.MySettings, y código en el procedimiento InitConnection del archivo DataSetName.Designer.vb para restablecer el valor ClientConnectionString de App.config. De ese modo ya no será necesario alterar el código fuente ni reescribir los proyectos cuando cambien los nombres del servidor o de la base de datos. 3.12.4 Encriptar cadenas de conexión que contienen nombres de usuario y contraseñas Parece bastante razonable someter a la cadena string de conexión a su autentificación por parte de Windows; el archivo Web.config para páginas ASP.NET o servicios Web no es accesible a los usuarios de Internet o intranets. Para ellos, el archivo.config sólo revela los nombres del servidor y la base de datos. Cualquier cadena de conexión que contenga un userID o valores debería estar encriptado, independientemente de que se encuentre en el código fuente del proyecto o en los archivos Web.config o App.config. ASP.NET 2.0 proporciona dos nuevos proveedores de código de encriptación –DataProtectionConfigurationProvider y RSAProtectedConfigurationProvider– pensados específicamente para simplificar la protección de secciones específicas de los archivos Web.config. De todos modos, una vez protegida la sección con encriptamiento, habrá que desencriptar y reencriptar cualquier cambio que se realice en la cadena de conexión de la aplicación. 3.12.5 Ejecutar el SQL Server Profiler para inspeccionar las consultas SQL y RPC El SQL Server Profiler es su amigo. Puede utilizar Profiler para inspeccionar las sentencias batch SQL enviadas para su ejecución directa o las llamadas execsp_executesqlRPC con sentencias SQL parametrizadas. Profiler también le puede mostrar el tiempo que el servidor SQL necesita para ejecutar las consultas y los procedimientos almacenados. 109

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 110

Bases de datos con Visual Basic Profiler genera trazas basadas en un juego de plantillas estándar diseñadas para tareas específicas. Se puede modificar la plantilla estándar o bien diseñar plantillas personalizadas para crear trazas con la información más importante para analizar el rendimiento del sistema o realizar otro tipo de análisis. En la siguiente figura vemos al Profiler mostrando en la plantilla de trazas T-SQL_Duration eventos capturados por DataSetWSClient al ejecutar los métodos Web DataSetWS. Las trazas de Profiler son igualmente útiles para comparar el rendimiento de las actualizaciones con batches con el de las actualizaciones convencionales que requieren acceder al servidor en cada cambio introducido en un juego de datos, pero ese es el tema de otro apartado, más adelante en este capítulo.

3.12.6 Evitar añadir instancias CommandBuilder en tiempo de ejecución Microsoft recomienda no instanciar objetos CommandBuilder en tiempo de ejecución, y muchos gurus y formadores de ADO.NET están de acuerdo con ello. Tal y como se mencionó anteriormente en este capítulo, los objetos CommandBuilder generan instancias DeleteCommand, InsertCommand, y UpdateCommand desde la sentencia SQL de SelectCommand. Regenerar esos comandos en tiempo de ejecución provoca una caída del rendimiento. Las mejores prácticas requiren escribir código para definir una colección SqlParameter estática en tiempo de diseño para las habituales operaciones CRUD. Otra alternativa es cachear los parámetros con la técnica usada por DAAB. El objeto actualizado SqlCommandBuilder de VS 2005 permite paliar en parte la disminución de rendimiento en tiempo de ejecución especificando el miembro CompareRowVersion u OverwriteChanges de la enumeración ConflictOption. Más adelante se describe cómo conseguir esta mejora en el rendimiento. 3.12.7 Sustituir las consultas SQL Batch por procedimientos almacenados Esta mejor práctica sólo dice lo evidente, pero verá que la mayoría de ejemplos de código DataReader y DataSet, incluyendo algunos de este libro, ejecutan sentencias SQL en 110

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 111

Concretando proyectos reales operaciones CRUD en lugar de procedimientos almacenados. Arrastrando tablas del SQL Server desde la ventana Orígenes de datos a un formulario de Windows genera sentencias SQL para las cuatro operaciones. Sustituir procedimientos almacenados ya existentes o nuevos por sentencias SQL requiere reconfigurar los DataSet en el diseñador. Las sentencias SQL ejecutadas por aplicaciones que conectan a back ends de bases de datos que soportan procedimientos almacenados o sus equivalentes se pueden justificar en aplicaciones prototipo y en ejemplos de código sencillos o situaciones en las que no sería práctico trabajar con un gran número de procedimientos almacenados. Por lo demás, las mejores prácticas dictan que todos los front ends de las bases de datos devuelvan vistas o ejecuten procedimientos almacenados y no accedan directamente a las tablas base. Las diferencias de rendimiento entre los procedimientos alm. parametrizados y las sentencias batch SQL ejecutadas con llamadas RPC exec sp_executesql realmente tan fuertes. Los tests de rendimiento a gran alcance con SQL Server 2000 indican que las consultas SQL batch parametrizadas, transactuadas, actualizan juegos típicos de datos relacionados (Northwind Orders y Order Details) en torno a un 13 por ciento más rápido que ejecutando procedimientos múltiples almacenados dentro de una transacción específica. No obstante, las operaciones SELECT e INSERT son más rápidas con procedimientos almacenados. 3.12.8 Definir valores por defecto en los parámetros que no son necesarios Si gestiona sus propias colecciones SqlParameter, puede minimizar el tamaño de las sentencias exec para los procedimientos almacenados definiendo valores por defecto en los parámetros de campos que no requieren ningún valor en casos específicos. Por ejemplo, en la BE de Orders, los campos RequiredDate, ShippedDate, Freight, Region y PostalCode pueden tener valor nulo. Si asigna NULL como valor por defecto a los parámetros de estos campos, puede omitir los miembros correspondientes de la colección de parámetros mencionados cuando actualice o inserte nuevos datos. Esta práctica conlleva la ventaja añadida de no insertar Enero 1, 0001 como valor nulo de System.Xml (0001-01-01T00:00:00.0000000-07:00 como Pacific Standard Time) de las fechas serializadas en los documentos XML. 3.12.9 Utilizar sp_executesql y parámetros con nombre para reutilizar los Cached Query Plans Si tiene que usar sentencias SQL parametrizadas para actualizar tablas base, aproveche las ventajas de sp_executesql para impedir que se regenere un nuevo plan query cada vez que se ejecuta una sentencia SQL. Este consejo es válido si ejecuta su propio código cliente de actualización en lugar de usar las instancias autogeneradas DeleteCommand, InsertCommand, y UpdateCommand del Data Adapter. Tests parecidos a los del apartado substituir consultas batch SQL por procedimientos almacenados muestran que utilizando sp_executesql con un parámetro con nombre y un valor aleatorio para devolver un objeto Order aumenta el rendimiento en un 37 por ciento frente a ejecutar la misma sentencia con el valor OrderID como parámetro sin

111

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 112

Bases de datos con Visual Basic nombrar. El descenso en el rendimiento tiene luegar porque SQL Server regenera la consulta SELECT en cada ejecución con un valor OrderID diferente. 3.12.10 Añadir columnas timestamp para el control de concurrencia Los SqlDataAdapters usan por defecto optimismic concurrency control en las actualizaciones y borrado de datos de las tablas blase. El optimistic concurrency control implica comparar los valores originales de cada campo en el momento de la actualización o el borrado con los que tenían cuando se llenó el DataSet. A continuación vemos la sentencia batch SQL de 3.878 caracteres (7.756 bytes) que actualiza una fila de la tabla Northwind Orders con valores basados en el optimistic concurrency control: exec sp_executesql N UPDATE [dbo].[Orders] SET [CustomerID] = @CustomerID, [EmployeeID] = @EmployeeID, [OrderDate] = @OrderDate, [RequiredDate] = @RequiredDate, [ShippedDate] = @ShippedDate, [ShipVia] = @ShipVia, [Freight] = @Freight, [ShipName] = @ShipName, [ShipAddress] = @ShipAddress, [ShipCity] = @ShipCity, [ShipRegion] = @ShipRegion, [ShipPostalCode] = @ShipPostalCode, [ShipCountry] = @ShipCountry WHERE (([OrderID] = @Original_OrderID) AND ((@IsNull_CustomerID = 1 AND [CustomerID] IS NULL) OR ([CustomerID] = @Original_CustomerID)) AND ((@IsNull_EmployeeID = 1 AND [EmployeeID] IS NULL) OR ([EmployeeID] = @Original_EmployeeID)) AND ((@IsNull_OrderDate = 1 AND [OrderDate] IS NULL) OR ([OrderDate] = @Original_OrderDate)) AND ((@IsNull_RequiredDate = 1 AND [RequiredDate] IS NULL) OR ([RequiredDate] = @Original_RequiredDate)) AND ((@IsNull_ShippedDate = 1 AND [ShippedDate] IS NULL) OR ([ShippedDate] = @Original_ShippedDate)) AND ((@IsNull_ShipVia = 1 AND [ShipVia] IS NULL) OR ([ShipVia] = @Original_ShipVia)) AND ((@IsNull_Freight = 1 AND [Freight] IS NULL) OR ([Freight] = @Original_Freight)) AND ((@IsNull_ShipName = 1 AND [ShipName] IS NULL) OR ([ShipName] = @Original_ShipName)) AND ((@IsNull_ShipAddress = 1 AND [ShipAddress] IS NULL) OR ([ShipAddress] = @Original_ShipAddress)) AND ((@IsNull_ShipCity = 1 AND [ShipCity] IS NULL) OR ([ShipCity] = @Original_ShipCity)) AND ((@IsNull_ShipRegion = 1 AND [ShipRegion] IS NULL) OR ([ShipRegion] = @Original_ShipRegion)) AND ((@IsNull_ShipPostalCode = 1 AND [ShipPostalCode] IS NULL) OR ([ShipPostalCode] = @Original_ShipPostalCode)) AND ((@IsNull_ShipCountry = 1 AND [ShipCountry] IS NULL) OR ([ShipCountry] = @Original_ShipCountry))) , N @CustomerID nchar(5),@EmployeeID int,@OrderDate datetime, @RequiredDate datetime,@ShippedDate datetime,@ShipVia int,@Freight money, @ShipName nvarchar(26),@ShipAddress nvarchar(15),@ShipCity nvarchar(11), @ShipRegion nvarchar(2),@ShipPostalCode nvarchar(5),@ShipCountry nvarchar(3), @Original_OrderID int,@IsNull_CustomerID int, @Original_CustomerID nchar(5),@IsNull_EmployeeID int, @Original_EmployeeID int,@IsNull_OrderDate int, @Original_OrderDate datetime,@IsNull_RequiredDate int, @Original_RequiredDate datetime,@IsNull_ShippedDate int, @Original_ShippedDate datetime,@IsNull_ShipVia int,

112

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 113

Concretando proyectos reales @Original_ShipVia int,@IsNull_Freight int,@Original_Freight money, @IsNull_ShipName int,@Original_ShipName nvarchar(26), @IsNull_ShipAddress int,@Original_ShipAddress nvarchar(15), @IsNull_ShipCity int,@Original_ShipCity nvarchar(11), @IsNull_ShipRegion int,@Original_ShipRegion nvarchar(2), @IsNull_ShipPostalCode int,@Original_ShipPostalCode nvarchar(5), @IsNull_ShipCountry int,@Original_ShipCountry nvarchar(3) , @CustomerID = N RATTC , @EmployeeID = 1, @OrderDate = May 6 1998 12:00:00:000AM , @RequiredDate = Jun 3 1998 12:00:00:000AM , @ShippedDate = NULL, @ShipVia = 2, @Freight = $8.5300, @ShipName = N Rattlesnake Canyon Grocery , @ShipAddress = N 2817 Milton Dr. , @ShipCity = N Albuquerque , @ShipRegion = N NM , @ShipPostalCode = N 87110 , @ShipCountry = N USA , @Original_OrderID = 11077, @IsNull_CustomerID = 0, @Original_CustomerID = N RATTC , @IsNull_EmployeeID = 0, @Original_EmployeeID = 1, @IsNull_OrderDate = 0, @Original_OrderDate = May 7 1998 12:00:00:000AM , @IsNull_RequiredDate = 0, @Original_RequiredDate = Jun 3 1998 12:00:00:000AM , @IsNull_ShippedDate = 1, @Original_ShippedDate = NULL, @IsNull_ShipVia = 0, @Original_ShipVia = 2, @IsNull_Freight = 0, @Original_Freight = $8.5300, @IsNull_ShipName = 0, @Original_ShipName = N Rattlesnake Canyon Grocery , @IsNull_ShipAddress = 0, @Original_ShipAddress = N 2817 Milton Dr. , @IsNull_ShipCity = 0, @Original_ShipCity = N Albuquerque , @IsNull_ShipRegion = 0, @Original_ShipRegion = N NM , @IsNull_ShipPostalCode = 0, @Original_ShipPostalCode = N 87110 , @IsNull_ShipCountry = 0, @Original_ShipCountry = N USA

El objeto SqlCommandBuilder actualizado de ADO.NET 2.0 tiene una propiedad ConflictOption que proporciona los tres miembros siguientes de enumeración para especificar cómo se comportan los DataSet actualizados frente a los cambios producidos en la tabla base después de poblar el DataSet: )

)

)

ConflictOption.CompareAllSearchableValues (por defecto) genera sentencias batch SQL parametrizadas o comandos EXECUTE para los procedimientos almacenados que requieren optimistic concurrency control basado en valores. ConflictOption.CompareRowVersion genera sentencias batch SQL parametrizadas más cortas o comandos EXECUTE para procedimientos almacenados contra tablas con una columna del tipo de datos timestamp (también llamada rowversion) proporcionada específicamente para el optimistic concurrency control. ConflictOption.OverwriteChanges genera sentencias batch SQL parametrizadas todavía más cortas o bien comandos EXECUTE para procedimientos almacenados que no refuerzan el optimistic concurrency control. Los datos se borran y actualizan independientemente de que otro usuario haya cambiado valores de columna en las filas.

Para seleccionar ConflictOption.CompareRowVersion la tabla debe incluir una columna con datos del tipo timestamp del servidor SQL. Un valor timestamp corresponde a un array de .NET del tipo Byte con un valor Length de 8. Cambiar el valor de una fila hace que se actualice el valor timestamp, que es siempre único dentro la tabla. Comparar el valor original de timstamp con el que tiene durante la actualización es la manera más 113

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 114

Bases de datos con Visual Basic rápida y segura de impedir que se sobreescriban los datos que cambiaron después de poblar un DataSet o cachear datos originales con código personalizado. A continuación vemos una sentencia típica de actualización, de 262 caracteres, para un control óptimo de concurrencia basado en timestamp: exec sp_executesql N UPDATE [OrdersTS] SET [OrderDate] = @p1 WHERE (([OrderID] = @p2) AND ([timestamp] = @p3)) , N @p1 datetime,@p2 int,@p3 timestamp , @p1 = May 6 2005 12:00:00:000AM , @p2 = 11077, @p3 = 0x0000000000004CB3

Faltan los parámetros Original_ColumnName y IsNull_ColumnName y sus valores; sólo se incluye el valor cambiado (en este ejemplo, OrderDate). Por lo tanto, sustituir la optimistic concurrency control basada en valores por la optimistic concurrency control basada en timestamp reduce considerablemente el tráfico en la red y disminuye el consumo del procesador de búsqueda de la base de datos. La siguiente figura muestra el formulario del proyecto de ejemplo TimeStampTest.sln con un tamaño de batch de 5 filas, valor especificado después de inducir a propósito un error de concurrencia. Nótese los indicadores de error en las cabeceras de las cinco primeras filas del control DataGridView.

Una alternativa a la columna timestamp es añadir y poblar una columna datatime de nombre datetime UltimaModificacion, o algo similar, a todas las tablas. Los valores UltimaModificacion se pueden serializar al tipo XML de datos de lectura dateTime. Los campos del tipo datetime tienen una incertidumbre inherente de 3,33 milisegundos. El método Now de VB, basado en tiempo de sistema, dice tener una resolución de 10 milisegundos. Los tests muestran que la resolución se acerca a 16 milisegundos con la mayoría de temporizadores de sistema, por lo que debe contarse con una incertidumbre potencial de hasta 20 milisegundos. Si se añade una columna UltimaModificacion hay que proporcionar también triggers insert y update para mantener el valor y escribir el código personalizado para tests de concurrencia. 3.12.11 Verificar registros en test de concurrencia La concurrencia de datos con DataSets puede traer resultados inesperados. Si el usuario A añade un registro a una tabla relacionada, por ejemplo Order Details, y el usuario

114

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 115

Concretando proyectos reales B altera o borra un registro de la misma tabla, los tests convencionales de concurrencia optimista fallarán para el usuario B, tanto con el método timestamp o con el de valores originales. El mismo problema ocurre si un usuario A borra un record que el usuario B no intenta actualizar. Este problema, que sucede si el usuario B no actualiza su DataSet inmediatamente antes de actualizarlo, puede tener consecuencias graves en bases de datos que guardan información crítica, como pueden ser recetas médicas para pacientes reales. Si un médico A introduce un tratamiento nuevo para un paciente, las consecuencias de que el médico B substituya o actualice un fármaco con una dosis diferente puede ser una amenza vital. Incluir en el DataSet una cuenta de los records relacionados es una solución parcial, pero no resuelve el problema que se plantea si el usuario A añade un record nuevo relacionado y borra otro que el usuario B no comprueba. Una solución verdadera implica comparar la fila de la cuenta y los valores originales de columna o timestamp en cada record relacionado, con los de la tabla base. Puede utilizar el nuevo método de ADO.NET 2.0, RowState.SetModified para marcar todas las filas relacionadas de la DataTable como modificadas e incluirlas en la comparación. 3.12.12 Evitar SqlExceptions con las validaciones del cliente Los DataSets manejan la unicidad de la clave primaria y las restricciones de la clave foránea de las tablas de datos, pero muchas operaciones de entrada de datos requieren el testeo de las restricciones de la clave foránea con tablas que no están incluidas en el juego de datos. La razón más frecuente para no incluir las tablas de fuentes relacionadas es el consumo excesivo de recursos del DataSet y la carga en el servidor de la base de datos al poblar las tablas. Se puede minimizar el consumo de recursos y validar los valores de llave foránea haciendo lo siguiente: )

)

)

Obtener los valores MIN y MAX de las columnas de clave primaria int identity con un SqlDataReader que devuelve un solo juego de filas por cada tabla, y guarde los valores en variables PrivateInteger. La aplicación arrojará excepciones por parte del servidor para los items borrados, pero será muy raro que falten valores realmente. Obtenga los valores individuales de las columnas de clave primaria char, nchar, varchar, y guárdelos en un objeto ArrayList. Alternativamente, cree un DataSet no tipificado y añada columnas basadas en caracteres a las tablas de datos y defina la clave primaria para poder utilizar el método Find con un índice. El método Select no utiliza índices.

Use el control ErrorProvider para indicar los valores numéricos fuera de rango y los juegos de caracteres no hallados. Sustitya el nuevo control MaskedTextBox por uno convencional TextBox para eliminar las entradas de testeo con juegos de caracteres no válidos. Para los datos de referencia que cambian con poca frecuencia, como rosters de empleados, listas de clientes y productos, y las empresas de transporte, se puede crear un juego de datos no tipificado que contenga datos básicos de consulta, por ejemplo ID y nombre del empleado en tablas de datos. Se pueden utilizar datos de consulta para poblar cuadros de lista desplegables que simplifican la selección de la clave foránea. Si

115

VisualBasic2005_03.qxp

02/08/2007

16:18

PÆgina 116

Bases de datos con Visual Basic elige este método, añada una tabla de datos para mantener una fila de valores mínimos y máximos para la clave primaria si no quiere guardarlos como fila individual en su propia DataTable. Puede poblar todas las tablas de datos con un solo acceso al servidor, ejecutando un solo procedimiento almacenado compuesto o, si tiene que ser así, una consulta SQL. Si la aplicación debe soportar usuarios que trabajan frecuentemente desconectados, puede guardar copias completas o de consulta de las tablas particionadas en DatsSets y perpetuarlas en archivos XML para su uso sin conexión.

116

VisualBasic2005_04.qxp

02/08/2007

16:20

PÆgina 117

Capítulo 4

Programar TableAdapters, BindingSources y DataGridViews Los capítulos anteriores introducían las nuevas componentes de VS 2005: BindingSources, BindingNavigators, y TableAdapters y el control DataGridView. Este capítulo muestra cómo sacar el mayor partido de estos componentes de tiempo de diseño en una configuración típica de cliente/servidor. Los ejemplos de este capítulo toman como punto de partida algunas de las mejores soluciones descritas en el capítulo 3. El formulario de Windows UI contiene las capas data access logic component (DALC) y business process component (BCP). Las entidades de negocio, o business entities (BEs) son DataTables de un DataSet tipificado. Esta arquitectura representa el modelo clásico de cliente/servidor, de dos-tier, no el modelo n-tier, la estructura basada en servicios Web de la estrategia de Microsoft "connected solutions". Según un informe aparecido a mediados de 2006, muchas de las organizaciones de TI están migrando de las aplicaciones cliente basadas en Web a las aplicaciones de Windowds "Smart Client", que incluyen Microsoft Office 2003 y Business Solutions, y formularios Windows de proyectos en VS 2005. Esta moda cogerá todavía más auge cuado Windows Vista esté realmente introducido en el mercado.

Los formularios de entrada de datos que se crean en este capítulo empiezan con un formualrio de orden de entrada generado por un diseñador, que usted puede convertir al formato más típico de ventana tabular. La primera ficha muestra los datos del cliente y una parrilla de Orders, el segundo muestra cuadros de texto para datos de Orders y una parrilla para objetos de línea. Los pasos finales añaden columnas DataGridViewComboBox para seleccionar claves foráneas numéricas. Completar los ejemplos de este capítulo le calificará como programador de componentes de datos a nivel de aprendiz y controles DataGridView.

4.1 Diseñar un formulario básico Customer-OrdersOrder Details Los procesos de negocios más comunes y esenciales son tratar los Orders de los Customers, emitir facturas y asegurar que las facturas se pagan. Estas actividades requieren formularios que muestren datos específicos de un determinado cliente, historial del pedido/factura e items de línea. Esta estructura en tres niveles es otro tópico de los ser-

117

VisualBasic2005_04.qxp

02/08/2007

16:20

PÆgina 118

Bases de datos con Visual Basic vicios profesionales. Los abogados, tal vez utilicen una estructura cliente/caso/actividad y los médicos pueden utilizar un modelo paciente/visita/tratamiento o algo similar. El escenario más simple en que se puede presentar un esquema de base de datos del tipo Customers/Orders/items en línea, o similar, es una vista de detalle (controles vinculados TextBox) para un cliente específico y los Orders más recientes y sus ítems de línea en controles vinculados DataGridView. La nueva ventana DataSources de VS 2005, los componentes de datos y el control DataGridView permiten crear un UI de tres niveles arrastrando la tabla superior y sus derivadas desde el panel Orígenes de datos hasta el formulario.

4.1.1 Reducir el tamaño del DataSet Las herramientas de VS 2005 permiten crear un formulario básico de entrada de datos sin escribir una sola línea de código. Cuando se arrastra una tabla desde el panel Orígenes de datos, el diseñador añade una sentencia Me.TableNameTableAdapter.Fill(Me.DataSetName.TableName) al manejador de eventos FormName_Load. El precio que hay que pagar a cambio es la generación de inmensos DataSets cuando las tablas base contiene un gran número de registros. Abrir el formulario crea una carga demasiado pesada en el servidor de la base de datos y en la red, y los usuarios tendrán que esperar bastante hasta que aparezca el formulario. La base de datos NorthwindCS, que es la fuente de datos utilizada en capítulos anteriores, contiene cerca de 173.000 registros Orders y 470.000 registros de Order Details para los registros de los 91 Customers originales. Cargar todos esos datos crea un DataSet de 250 MBytes en la memoria y un retraso de 30 segundos al abrir el formulario de test. Las instrucciones autogeneradas TableAdapter.Fill ayudan a los aprendices a crear un formulario sencillo de entrada de datos con las bses de datos Northwind o Pubs. No use nunca el código Fill por defecto en una aplicación de producción.

Los métodos FillBy de las consultas parametrizadas hacen más fácil mostrar un registro de un cliente específico y los registros relacionados de sus Orders e items de línea. Las consultas FillBy se añaden con un cuadro de diálogo Search Criteria Builder que genera un cuadro de texto ToolStrip para definir los valores de los parámetros, un botón para ejecutar los métodos FillBy de cada DataTable, y manejador de eventos para los botones. Aquí vemos la llamada típica de un método FillBy con el nombre por defecto de la consulta cambiado a FillOrders: Me.OrdersTableAdapter.FillOrders(Me.NorthwindDataSet.Orders, _ CustomerIDToolStripTextBox.Text)

Restablecer los ítems de línea de un pedido específico requiere una consulta sub-select como valor de CommandText para el método FillBy si la tabla de ítems de línea no contiene un valor de clave foránea para el cliente. A continuación vemos una consulta subselect para la tabla Order Details: SELECT OrderID, ProductID, UnitPrice, Quantity, Discount FROM dbo.[Order Details] WHERE OrderID IN (SELECT OrderID FROM Orders WHERE CustomerID = @CustomerID)

118

VisualBasic2005_04.qxp

02/08/2007

16:20

PÆgina 119

Programar TableAdapters, BindingSources y DataGridViews Los cuatro apartados siguientes explican cómo diseñar y modificar un código autogenerado para un formulario básico de entrada y edición de Orders para la base de datos de ejemplo Northwind. Las instrucciones son más detalladas que las de los capítulos anteriores porque el proceso no es precisamente intuitivo y es mucho más complejo que crear un formulario Access o InfoPath de entrada de datos. Por otra parte, los formularios vinculados a datos generados por VS 2005 ofrecen entrada y edición de datos sin conexión, mayor flexibilidad de programación y mejor tratamiento de errores. Muchos de los proyectos de ejemplo de este libro usan la base de datos Northwind porque su diseño (definición de esquema) es más sencillo que el de la base de datos de ejemplo AdventureWorks del SQL Server 2005. Crear un equivalente de la versión AdventureWorks del formulario de entrada de datos Customer-Orders-Order Details requiere al menos 12 tablas relacionadas: Sales.Customer, Sales.CustomerAddress, Sales.Individual, Sales.Store,Person.Contact, Person.Address, Person.StateProvince, Person.CountryRegion, Sales.SalesOrderHeader, Sales.SalesOrderDetail, Sales.SpecialOffer y Sales.SpecialOfferProduct. La mayoría de los temas tratados en los archivos de ayuda de VS 2005 usan la tabla Northwind o similares mientras que la mayoría de los ejemplos de SQLServer 2005 Books Online usan AdventureWorks.

4.1.2 Crear el origen de datos y añadir los controles El primer paso en el proceso de diseño es añadir una vista de detalle para los datos del cliente, una DataGridView madre (maestra) para mostrar los registros de Orders, y una DataGridView (detalles) para los registros de Order Details. Vea la siguiente figura, más adelante, para el diseño del formulario. Para generar un formulario que carga y muestra todos los records de Customers, Orders y Order Details, haga lo siguiente: 1. Cree un nuevo proyecto llamado OrdersByCustomer, y seleccióne la opción Agregar nuevo origen de datos en el menú Datos para iniciar el Asistente para la configuración de orígenes de datos. Seleccione Base de datos, y pulse el botón Siguiente para seleccionar la conexión de datos. 2. Si ya había creado una conexión a la base de datos Northwind, selecciónela en la lista desplegable. En caso contrario, pulse el botón Nueva conexión para abrir el cuadro de diálogo Agregar conexión y seleccione como origen de datos Microsoft SQL Server (SqlClient) y en el cuadro Nombre del servidor seleccione .\SQLEXPRESS. 3. Seleccione Northwind en área Establecer conexión con una base de datos, pulse el botón Probar conexión para verificar la correcta conexión y pulse el botón Aceptar cerrar el cuadro de diálogo. Pulse el botón Siguiente, guarde la cadena de conexión con el nombre por defecto –NorthwindConnectionString– y pulse Siguiente para abrir el cuadro de diálogo Elija los objetos de la base de datos. 4. Expanda los nodos de tabla y seleccione las tablas que quiere incluir en el proyecto –en este ejemplo: Customers, Orders y Order Details. 5. Acepte el nombre por defecto del DataSet (NorthwindDataSet), y prosiga con el Asistente para la configuración de orígenes de datos y sus tablas en el panel Orígenes de datos.

119

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 120

Bases de datos con Visual Basic 6. En el panel Orígenes de datos, que se muestra seleccionando la opción Mostrar orígenes de datos del menú Datos, seleccione la tabla Customers, cambie el modo a Detalles pulsando sobre la flecha en el nombre de la tabla, y arrastre el icono de la tabla al Form1. El diseñador añade al formulario etiquetas, cuadros de texto vinculados a datos, y un control BindingNavigator; y a la bandeja, los iconos NorthwindDataSet, CustomersBindingSource, CustomersTableAdapter, y CustomersBindingNavigator. Distribuya los cuadros de texto vinculados en dos columnas para conservar el área del formulario. 7. Expanda el icono de la tabla Customers y arrastre el icono de la tabla vinculada Orders (bajo el icono de campo Fax) hasta el formulario para añadir un control OrdersDataGridView al Form1, y OrdersBindingSource y OrdersTableAdapter a la bandeja. 8. Expanda el icono de la tabla Orders y arrastre el icono de la tabla vinculada Order Details (bajo el icono de campo ShipCountry) hasta el formulario para añadir un control Order_DetailsDataGridView a Form1, y Order_DetailsBindingSource y Order_DetailsTableAdapter a la bandeja. 9. Pulse para construir y ejecutar el proyecto, que aparecerá tal como se muestra en la figura siguiente. Muestre en pantalla algunos registros de Customers para verificar que los dos DataGridViews muestran registros vinculados, y cierre finalmente el formulario pra volver al modo diseño.

En este punto, el código autogenerado del método Fill en el manejador de eventos Form1_Load carga todos los records de las tres tablas base en el NorthwindDataSet.

4.1.3 Añadir métodos FillBy para cada tabla de datos Hay que añadir un método FillBy para poblar cada tabla con una consulta SELECT que incluya un parámetro @CustomerID. Renombrar por defecto la consulta FillBy para 120

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 121

Programar TableAdapters, BindingSources y DataGridViews darle un nombre de método más descriptivo hará el código más legible. Siga estos pasos para añadir y validar las consultas Fillby renombradas de cada tabla: 1. Pulse con el botón secundario del ratón en el icono CustomersTableAdapter y seleccione la opción Agregar consulta para abrir el cuadro de diálogo Generador de criterios de búsqueda. 2. Acepte la fuente por defecto NorthwindDataSet.Customers como la tabla fuente y cambie el nombre de la nueva consulta de FillByCustomerID. En el cuadro de diálogo Texto de la consulta, escriba el criterio WHERE CustomerID=@CustomerID después de un FROM dbo.Customers, tal como se muestra en la figura siguiente. Pulse el botón Aceptar para añadir un ToolStrip con un cuadro de texto para entrar el valor del parámetro CustomerID y un botón FillByCustomerID para ejecutar la consulta.

3. Pulse con el botón secundario del ratón el cuadro de texto CustomerID del ToolStrip, arriba en el formulario, y seleccione Convertir en/ComboBox. Abra la ventana Propiedades del cuadro, cambie el nombre de la propiedad por cboCustomerID, cambie DropDownStyle por DropDownList, añada algunos valores de ejemplo CustomerID a la colección Items, y cambie el valor de Widht a 75. 4. Seleccione el botón FillByCustomerID y cambie el valor de su propiedad Text por GetOrders y el valor ToolTipText por Select a CustomerID. Borre del formulario los elementos CustomerIDLabel y CustomerIDTextBox. 5. Seleccione la barra de separación y el botón Guardar datos del CustomersBindingNavigator, pulse + , seleccione el ToolStrip de arriba y pulse + para añadir los objetos. A continuación seleccione el CustomersBindingNavigator y elimínelo. 6. Construya y ejecute el proyecto, seleccione THEBI en el cuadro combinado y pulse el botón Get Orders para mostrar el registro de datos THEBI, y vuelva al modo diseño. 121

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 122

Bases de datos con Visual Basic 7. Pulse con el botón secundario el OrdersTableAdapter, y seleccione Agregar Consulta. 8. Selecione NorthwindDataSet.Orders como tabla fuente y cambie el nombre de la consulta por FillByCustomerID. En el cuadro Texto de la consulta, añada WHERE CustomerID=@CustomerID ORDER BY OrderID DESC después de FROM dbo.Orders. Pulse el botón Aceptar para añadir otro ToolStrip con un cuadro de texto para entrar el valor del parámetro CustomerID. 9. Pulse con el botón secundario Order_DetailsTableAdapter, y seleccione Agregar Consulta. 10. Seleccione NorthwindDataSet.Order_Details como la tabla fuente y cambie el nombre de la nueva consulta nombrándola FillByCustomerID. En el cuadro Texto de la consulta, añada el criterio WHERE OrderID IN (SELECT OrderID FROM Orders WHERE Customer ID=@CustomerID) después de FROM [dbo.OrderDetails]. Pulse el botón Aceptar para añadir un tercer ToolStrip con un cuadro de texto para escribir el valor del palámetro CustomerID. 11. Construya y ejecute el proyecto, escriba THEBI en los dos cuadros de texto vacíos ToolStrip, y Pulse los tres botones para probar los controles. 12. Seleccione una fila diferente de Orders en la parrilla Orders para verificar que la vinculación con la parrilla Order Details funciona correctamente. El formulario debería aparecer tal como muestra la siguiente figura.

4.1.4 Modificar el código autogenerado para llenar los controles Los pasos anteriores añadían código autogenerado en el manejador de eventos Form1_Load y los manejadores de evento Click de los tres botones ToolStrip. Ahora ya hay código para cargar las DataTables, pero todavía hay que llevar las instrucciones para llenar 122

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 123

Programar TableAdapters, BindingSources y DataGridViews las tablas Orders and Order_Details DataTables hasta el manejador de eventos GetCustomerOrdersStripButton_Click. Veamos el procedimiento para eliminar los métodos Fill por defecto y llevar las dos instrucciones nuevas FillByCustomerID a su posición correcta: 1. Abra Form1.vb y borre el manejador de eventos Form1_Load, el cual contiene las instrucciones autogeneradas para llenar el juego de datos con todo el contenido de las tablas base. 2. Copie la instrucción Me.OrdersTableAdapter.FillByCustomerID... desde el manejador de eventos FillByCustomerIDToolStripButton1_Click, y sitúela bajo la instrucción Me.CustomersTableAdapter.FillByCustomerID del manejador de eventos FillByCustomerIDToolStripButton_Clic. 3. Repita el paso 2 para la instrucción Me.Order_DetailsTableAdapter.FillByCustomerID.... 4. Cambie CustomerIDToolStripTextBox y CustomerIDToolStripTextBox1 por cboCustomerID, de modo que el cuadro combinado porporcione el valor del parámetro @CustomerID para las tres instrucciones del manejador de eventos FillByCustomerIDToolStripButton_Click. A continuación vemos el código final del manejador de eventos FillByCustomerIDToolStripButton_Click: Private Sub FillByCustomerIDToolStripButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles FillByCustomerIDToolStripButton.Click Try Me.CustomersTableAdapter.FillBy(Me.NorthwindDataSet.Customers, cboCustomerID.Text) Me.OrdersTableAdapter.FillByCustomerID(Me.NorthwindDataSet.Orders, cboCustomerID.Text) Me.Order_DetailsTableAdapter.FillBy(Me.NorthwindDataSet.Order_Details, cboCustomerID.Text) Catch ex As System.Exception System.Windows.Forms.MessageBox.Show(ex.Message) End Try End Sub

4.1.5 Llenar el cuadro combinado con valores CustomerID En este punto, la colección Items del cuadro combinado cboCustomerID Items sólo contiene algunos valores de ejemplo para test. El método más rápido y sencillo para llenar listas semi-estadísticas es utilizando el objeto SqlDataReader. Por lo tanto, añada ImportsSystem.Data e ImportsSystem.Data.SqlClient a Form1.vb. Realice una doble pulsación en el formulario para regenerar el manejador de eventos Form1_Load y añada el código siguiente para poblar la lista del cuadro combinado cboCustomerID: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

123

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 124

Bases de datos con Visual Basic Dim cnNwind As New SqlClient.SqlConnection(My.Settings.NorthwindConnectionString) Dim strSQL As String = "SELECT CustomerID FROM dbo.Customers" Dim cmNwind As New SqlClient.SqlCommand(strSQL, cnNwind) Try cnNwind.Open() Dim sdrCustID As SqlClient.SqlDataReader = cmNwind.ExecuteReader With sdrCustID If .HasRows Then cboCustomerID.Items.Clear() While .Read cboCustomerID.Items.Add(sdrCustID(0).ToString) End While cboCustomerID.Text = cboCustomerID.Items(0).ToString End If .Close() End With Catch exc As Exception MsgBox("Error loading CustomerID combo box.") Finally cnNwind.Close() End Try End Sub

4.1.6 Limpiar la UI y el código Estos pasos finales verifican los cambios anteriores y eliminan los ToolStrips que no son necesarios: 1. Construya y ejecute el proyecto, que se abrirá con todos los controles vacíos a excepción del cuadro combinado. Pulse el botón Get Orders para verificar que el código que añadió y modificó puebla esos controles. 2. Cierre el formulario y borre los manejadores de evento FillByCustomerIDToolStripButton1_Click y FillByCustomerIDToolStripButton1_Click. 3. Seleccione y borre los dos ToolStrip añadidos FillByCustomerID. 4. Construya y ejecute el proyecto de nuevo y clique el botón Get Orders para verificar su operabilidad. La siguiente figura muestra el formulario después de todas estas modificaciones. Este formulario relativamente simple de entrada de datos genera un esquema largo y complejo de juegos de datos y archivos de código generados por el diseñador. El archivo NorthwindDataSet.xsd es una carga considerable con sus 129 KBytes y NorthwindDataSet.Designer.vb contiene cerca de 3.300 instrucciones. Las anotaciones del esquema para las consultas FillBy se pueden localizar abriendo el esquema en IE y buscando el @CustomerID.

124

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 125

Programar TableAdapters, BindingSources y DataGridViews

4.2 Añadir y dar formato a DataGridView Para que añadir y editar nuevos Orders y, especialmente, registros Order Details y controles DataGridView, sea más fácil para los operadores de entrada de datos, hay que desactivar el word-wrap, ajustar el ancho de columna, y dar formato a los valores de moneda y porcentajes. Los ejemplos de los capítulos anteriores usan código para hacer todos esos cambios de formato. El cuadro de diálogo Editar columnas de DataGridView simplifica las tareas relacionadas con la gestión de columnas. El cuadro de diálogo Editar columnas permite especificar el ancho de columna, reordenar las columnas y añadir columnas calculadas sin vínculos. El cuadro de diálogo Generador de CellStyle permite definir valores para las propiedades de columna Format y WrapMode.

4.2.1 Dar formato a las columnas OrdersDataGridView Pulse con el botón secundario un DataGridView y seleccione Editar columnas. Se abrirá el cuadro de diálogo del mismo nombre, el cual muestra la lista Columnas seleccionadas, con unas cuantas columnas vinculadas, y el área Propieddes de columnas enlazadas. El método más rápido y efectivo para definir anchos de columna es especificando el valor de la propiedad AutoSizeMode. Dar tamaño automático a las columnas es más rápido con ColumnHeader, ya que no se requiere ningún examen previo del ancho máximo de fila. Por lo tanto, debería especificar AllCells o DisplayedCells únicamente donde el ancho de fila excede o es probable que exceda el ancho de la cabecera de columna. Para OrdersDataGridView, el valor más apropiado en AutoSizeMode es ColumnHeader, para todas las columnas excepto OrderDate y Freight, las cuales requieren AllCells. La siguiente figura muestra los ajustes de la propiedad para la columna OrderID con Frozen especificado como True para mostrar la columna cuando se hace un scroll horizontal.

125

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 126

Bases de datos con Visual Basic

La columna Freight require formato para la moneda. Para dar formato a los valores de columa, seleccione la columna y la propiedad DefaultCellStyle, y abra el cudro de diálogo Generador de CellStyle. Seleccione la propiedad Format, abra el Cuadro de diálogo de formato de cadenas, y seleccione Moneda con dos decimales y una celda vacía para el valor DbNull, tal como muestra la siguiente figura. El Generador de CellStyle y el cuadro de diálogo Cuadro de diálogo de formato de cadenas tienen otros muchos ajustes de propiedades que no discutiremos aquí. El efecto de la mayoría de los ajustes se puede deducir fácilmente de sus nombres. Es aconsejable que asisgne el valor NotSortable o Programmatic a la propiedad SortMode de todas las columnas –excepto, quizá, OrderID– ya que los usuarios podrían cambiar accidelmente el orden de las columnas y no saber cómo volver al orden original. Lo mismo sucede con el valor de la propiedad Resizable; definalo como False a menos que tenga una buena razón para hacer lo contrario. Definiendo la propiedad SortMode con el valor NotSortable eliminará el padding derecho de las cabeceras de columna necesario para acomodar las flechas de direccionamiento. Tal vez tenga que definir la propiedad Width en pixeles, para lo cual tendrá que darle el valor None a la propiedad AutoSizeMode.

126

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 127

Programar TableAdapters, BindingSources y DataGridViews

4.2.2 Añadir y dar formato a una columna calculada en Order_DetailsDataGridView Los registros de Order Details tienen valores numéricos, por lo que HeadersOnly es un valor apropiado para el valor AutoSizeMode de todas las columnas, a menos que se desactive el ordenamiento por columna. Una práctica común en Quantity es dar información previa sobre el producto en los formularios de Orders de venta y facturas; por lo tanto, sitúe la columna Quantity detrás de OrderID con el botón de flecha hacia arriba del cuadro de diálogo Editar columnas. Dé formato a la columna UnitPrice con el string de formato C2. Dé formato ahora a la columna Discount con un valor porcentual de un solo lugar decimal (P1).

4.2.3 Añadir la columna Extended amount Una columna no vinculada Extended amount para mostrar los valores de Quantity y UnitPrice less Discount, es una adquisición muy valiosa para la parrilla de Order Details. Para añadir una columna no vinculada, seleccione la columna Discount en el cuadro de diálogo Editar columnas y pulse el botón Agregar para abrir el cuadro de diálogo Agregar columna. Seleccione la opción Columna sin enlazar y escriba Extended como valor en los cuadros de texto Nombre y Texto de encabezado, y seleccione el cuadro de verficación Sólo lectura tal como muestra la siguiente figura. Pulse el botón Agregar y pulse el botón Cerrar para crear la nueva columna y defina los valores de las propieades SortMode y AutoSizeMode o Width. Finalmente, dé formato a la columna con el string de formato de moneda C2.

4.2.4 Calcular y mostrar el valor Extended El valor calculado se añade a la columna Extended definiendo, en la ventana de propiedades, el valor True para la propiedad VirtualMode del Order_DetailsDataGridView y manejando a continuación el evento DataGridView_CellValueNeeded. El modo virtual es necesario cuando un control vinculado DataGridView incluye columnas no vinculadas.

127

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 128

Bases de datos con Visual Basic También se necesita el modo virtual y el evento CellValueNeeded para paginar filas adicionales en un DataGridView de filas limitadas, vinculada a una tabla de datos muy grande. El argumento DataGridViewCellValueEventArgs del evento CellValueNeeded devuelve valores de propiedad ColumnIndex y RowIndex que especifican la celda actual cuyo valor es necesario, y una propiedad Value para definir ese valor. La fórmula para la propiedad Value es Quantity * UnitPrice * (1 - Discount); estos valores se obtienen de las celdas 1, 3, y 4 de la fila actual. Si alguna de esas celdas es del tipo DBNull, si se le asigna una variable numérica se obtendrá una excepción. Por lo tanto, hay que comprobar el tipo DBNull antes de asignar valores. A continuación vemos el código para el manejador de eventos Order_DetailsDataGridView_CellValueNeeded: Private Sub Order_DetailsDataGridView_CellValueNeeded(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellValueEventArgs) _ Handles Order_DetailsDataGridView.CellValueNeeded 'Calculate and display the unbound Extended column values With Order_DetailsDataGridView 'Test for correct column and DBNull values, which throw exceptions If e.ColumnIndex = 5 And _ Not (TypeOf (.Rows(e.RowIndex).Cells(1).Value) Is DBNull _ OrElse TypeOf (.Rows(e.RowIndex).Cells(3).Value) Is DBNull _ OrElse TypeOf (.Rows(e.RowIndex).Cells(4).Value) Is DBNull) Then 'Variables are declared for readability Dim intQuan As Integer Dim decPrice As Decimal Dim decDisc As Decimal intQuan = CInt(.Rows(e.RowIndex).Cells(1).Value) decPrice = CDec(.Rows(e.RowIndex).Cells(3).Value) decDisc = CDec(.Rows(e.RowIndex).Cells(4).Value) e.Value = intQuan * decPrice * (1 - decDisc) End If End With End Sub Puede sustituir el nombre de columna por el valor numérico Cells(ColumnIndex), pero si lo hace tendrá una ligera baja en el rendimiento.

La figura de la página siguiente muestra el formulario de entrada Orders con DataGridViews formateados y la columna Extended poblada. Este ejemplo es específico de Order_DetailsDataTable, pero el proceso es básicamente el mismo para cualquier columna calculada. Los valores de celda no son los únicos que se pueden utilizar para calcular los valores de columnas no vinculadas.

128

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 129

Programar TableAdapters, BindingSources y DataGridViews

4.3 Proporcionar valores por defecto a los nuevo records En capítulos anteriores vimos ejemplos de código apra definir valores por defecto cuando se añadía un nuevo record a un DataGridView. El DataGridView tiene un evento DefaultValuesNeeded muy parecido al evento CellValuesNeeded, pero DefaultValuesNeeded se dispara cuando el usuario añade una fila nueva en el modo virtual o no virtual. Escribiendo un manejador de eventos DefaultValuesNeeded puede simplificar la entrada de datos para un nuevo pedido y minimizar los errores potenciales causados por valores que faltan al añadir nuevos records en Order Details. El argumento DataGridViewRowEventArgs tiene una propiedad Row que deuelve la nueva instancia DataGridViewRow. 4.3.1 Añadir valores Default Orders Record Los valores por defecto resultan apropiados para todas las columnas de Orders excepto OrderID y ShippedDate, pero los usuarios deben entrar al menos un valor. Por lo tanto, EmployeeID conserva el valor por defecto DBNull. El valor Freight no se conoce hasta, o casi, la fecha de envío, pero para Freight no está permitido el valor DBNull. A continuación vemos el código para poblar una nueva fila Orders: Private Sub OrdersDataGridView_DefaultValuesNeeded(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewRowEventArgs) _ Handles OrdersDataGridView.DefaultValuesNeeded With e.Row .Cells(1).Value = Me.cboCustomerID.Text .Cells(2).Value = 2

129

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 130

Bases de datos con Visual Basic .Cells(3).Value = Today.ToShortDateString .Cells(4).Value = Today.AddDays(14).ToShortDateString .Cells(6).Value = 2 .Cells(7).Value = 0 .Cells(8).Value = Me.CompanyNameTextBox.Text .Cells(9).Value = Me.AddressTextBox.Text .Cells(10).Value = Me.CityTextBox.Text .Cells(11).Value = Me.RegionTextBox.Text .Cells(12).Value = Me.PostalCodeTextBox.Text .Cells(13).Value = Me.CountryTextBox.Text Dim intCtr As Integer For intCtr = 0 To 13 .Cells(intCtr).Selected = False Next .Cells(2).Selected = True End With End Sub

El usuario debe cambiar al menos un valor, que suele ser EmployeeID, para disparar el evento UserAddedRows y añadir una nueva fila vacía al OrdersDataGridView. Por lo tanto, el código define EmployeeID como celda seleccionada.

4.3.2 Añadir valores por defecto en los registros de Order Details Dar valores por defecto a las columnas Order Details resulta problemático, ya que ProductID es un miembro de la clave primaria compuesta de la tabla. Así, los valores por defecto de ProductID, definidos, por ejemplo, el valor Rows.Count, podrían entrar en conflicto con otras selecciones previas. Hasta ahora, el valor ProductID se define sin comprobar valores anteriores. A continuación, vemos el código para el manejador de eventos Order_DetailsDataGridView_DefaultValuesNeeded: Private Sub Order_DetailsDataGridView_DefaultValuesNeeded(ByVal sender _ As Object, ByVal e As System.Windows.Forms.DataGridViewRowEventArgs) _ Handles Order_DetailsDataGridView.DefaultValuesNeeded With e.Row .Cells(1).Value = 1 .Cells(2).Value = 17 .Cells(3).Value = 0 .Cells(4).Value = 0 End With End Sub

La siguiente figura muestra el formulario de entrada de Orders con las filas añadidas Orders y Order Details con los valores por defecto.

130

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 131

Programar TableAdapters, BindingSources y DataGridViews

4.4 Manejar el evento DataErrors Cuando un DataGridView arroja una excepción, el mensaje de error por defecto contiene más información de la que la mayoría de usuarios desean conocer sobre el problema. Añadir un manejador de eventos delegado DataGridView.DataErrors permite substituir el mensaje por defecto "The following exception occurred in the DataGridView", seguido del string StackTrace, por otro mensaje más apropiado. El mensaje que devuelve e.Exception.Message es "Exception has been thrown by the target of an invocation". Por lo tanto, hay que proporcionar un mensaje propio; añadir los números de fila y columna puede ayudar a los usuarios a encontrar sus trangresiones. Veamos un ejemplo de un manejador de eventos DataErrors sencillo: Private Sub OrdersDataGridView_DataError(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewDataErrorEventArgs) _ Handles OrdersDataGridView.DataError Dim strMsg As String = "Invalid data in column " + e.ColumnIndex.ToString + _ " of row " + e.RowIndex.ToString + " of the Orders grid. " + _ "Press Esc to cancel the edit or enter an appropriate value." MsgBox(strMsg, MsgBoxStyle.Exclamation, "Data Entry Error") End Sub

En capítulos más avanzados veremos ejemplos de manejadores de error más sofisticados que no muestran al usuario la información más específica sobre el error.

131

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 132

Bases de datos con Visual Basic

4.5 Entrada de datos Streamline Heads-Down Las entradas de datos Heads-down implican que el usuario de una aplicación pasa la mayor parte de su tiempo restableciendo y actualizando datos. Las aplicaciones headsdown típicas son de teléfono o entrada de pedido, customer service, procesar datos sobre el seguro, preguntas de help-desk, y asistencia técnica sobre el software. Los requisitos básicos de esos proyectos son alta velocidad en el restablecimiento de datos y una entrada de datos eficiente. Por lo tanto, los formularios de Windows, más que los formularios Web, son los UI más comunes en la entrada de datos heads-down. A continuación vemos algunas "mejores prácticas" para el diseño de formularios de entrada de datos heads-down: )

)

)

)

)

)

)

132

Proporcionar claves de aceleración ( + ) para todos los botones y aquéllas etiquetas adyacentes a los cuadros de texto y combinados más utilizados en la aplicación. Mover una mano del teclado al ratón y de nuevo al teclado reduce la productividad en la entrada de datos y hace que el operador se fatigue. Seleccione claves de aceleración para los que no haya que contorsionar los dedos, por ejemplo + . Si se queda sin claves alfabéticas relacionadas, puede especificar otras combinaciones, como + <Mayús> + con un manejador de eventos KeyDown. Evite los gráficos demasiado coloridos, los logotipos de compañías y otros elementos irrelevantes en las tareas de entrada de datos. Diseñe para una resolución de 800 x 600 píxeles, así maximizara la legibilidad del formulario. Los operadores de entrada de datos suelen ser los últimos en recibir las actualizaciones del hardware del PC y del sistema operativo. No permita el modo Edit como el modo por defecto para la entrada de datos. Un cuadro de texto vinculado y controles DataGridView deberán abrirse en modo sólo lectura, de modo que para la edición de datos fuera necesaria una acción explícita del operador. Esta práctica impide que se editen los datos por descuido. Esconder o desactivar los controles que no son apropiados para la tarea actual o el papel del usuario. El espacio-nombre My.User proporciona el método IsInRole para determinar la autorización del usuario actual para, por ejemplo, editar datos basándose en la pertenencia al grupo de seguridad del dominio. Sustituir cuadros combinados por cuadros de texto para definir los valores de clave primaria. Poblar las listas combinadas desde tablas de consulta rápida que pueden ser DataTables independientes o miembros de un DataSet no tipificado. Minimizar el tamaño de las tablas de datos y la carga del servidor especificando sólo las columnas necesarias para poblar la lista. En los ejemplos de este capítulo, los valores de CustomerID, EmployeeID, ShipperID, y ProductID se deberían definir a través de listas combinadas. Añadir ayudas rápidas para proporcionar instrucciones para los botones y cuadros de texto importantes y otros controles. Los operadores nuevos utilizarán con gusto el ratón para repasar el propósito de esos misteriosos controles. Las ayudas

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 133

Programar TableAdapters, BindingSources y DataGridViews rápidas serán suficiente hasta que complete los archivos de ayuda online o impresos para el proyecto. )

)

)

)

)

No añada menús a menos que los necesite para imprimir o guardar archivos locales. La mayoría de las aplicaciones heads-down de entrada de datos sólo tienen una finalidad. Sería aconsejable que sustituyera los controles del cuerpo del formulario principal por controles ToolStrip. Los ToolStrips tienen un reperotorio limitado de control y deben residir en uno de los cuatro contendores. Lo mejor es situar cuadros de texto, listas combinadas y botones cerca de los demás controles asociados. Use el control MaskedTextBox para los cuadros de texto que requieran un formato específico de datos, como los números de teléfono y de la seguridad social, y claves primarias alfabéticas o alfanuméricas. Los controles DataGridView y ToolStrip no soportan los controles MaskedTextBox. Elija el valor de propiedad DataGridView.EditMode que cumpla mejor con las preferencias del operador. Es aconsejable que sustituya el modo por defecto EditOnKeystrokeOrF2 por EditOnEnter. Si selecciona EditOnEnter, será más fácil remplazar la selección de columnas por defecto que definió en el manejador de eventos DefaultValuesNeeded. No fuerce a los usuarios a ver o editar datos complejos, en multi-columna, en una fila de DataGridView. Algunos operadores de entrada de datos están acostumbrados a hacer scroll horizontal mientras editan, pero usted debería permitir la opción de editar la fila con los cuadros de texto. Las limitaciones del área del formulario pueden hacer necesario un formulario tabulado para dejar espacio a los cuadros de texto.

133

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 134

Bases de datos con Visual Basic )

Use un formulario individual tabulado en lugar de varios formularios para proporcionar métodos de edición alternativos. Los formularios tabulares con tabs que siguen la secuencia del ritmo de trabajo son preferibles a los formularios MDI en la mayoría de las aplicaciones de entrada de datos. Otra ventaja de los formularios tabulados es que navegar por sus páginas tabulares es parecido a moverse entre páginas Web.

La figura de la página anterior muestra una versión modificada del formulario de entrada, el cual implementa muchas de las mejores prácticas mencionandas anteriormente e incluye el código descrito en los apartados precedentes anteriores. Aquí tenemos algunas propiedades añadidas a OrderByCustomerV2: ) )

) )

) )

)

)

Todos los botones ToolStrip y los dos DataGridViews tienen métodos abreviados. El formulario se abre con controles y cuadros de texto vacíos, de sólo lectura, y DataGridViews desactivados de sólo lectura. Pulsando el botón Get Orders se puede desplazar por los DataGridViews. Debe pulsar los Edit Customer Data para poder editar los cuadros de texto, excepto CustomerID, y mostrar el OrdersToolStrip en la parte inferior del formulario. Pulsando Edit Orders se activan las dos vistas DataGrid. Pulsando New Customer los cuadros de texto se borran de contenido y se pueden editar, y se activa el cuadro de texto CompanyName. Escribiendo un CompanyName y puslando el tabulador se genera un valor CustomerID de cinco caracteres y se activa el OrdersDataGridView. Añadiendo un registro nuevo a OrdersDataGridView se activa el Order_DetailsDataGridView.

4.6 Migrar el UI a un formulario tabular Los formularios tabulares soportan aplicaciones de workflow, como añadir y editar datos del cliente, Orders, ítems de línea, devolución de Orders (pedidos), y facturas. Una de las principales ventajas de los formularios tabulares es que permiten especificar la visibilidad de la página según el papel que vaya a desempeñar el usuario. No se puede convertir un formulario convencional de entrada de datos en una versión tabulada del mismo que proporcione páginas múltiples para las diversas tareas de entrada de datos; es mucho más efectivo diseñar desde el principio un formulario tabular. Mover controles desde el formulario hasta una página tabular requiere los pasos siguientes: 1. Ajustar el tamaño del formulario para acomodar las fichas. 2. Recortar los controles del cuerpo de formulario al portapapeles. 3. Añadir un control Tab de dos o más páginas. 4. Seleccionar la primera ficha y pegarle los controles.

134

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 135

Programar TableAdapters, BindingSources y DataGridViews 5. Si su formulario contiene controles DataNavigator o ToolStrip controls, bórrelos y sustitúyalos por botones y, donde sea necesario, cuadros de texto en la ficha correspondiente, que suele ser la primera. Este paso implica cambios en el botón asociado de código del manejador de eventos. 6. Cambie la propiedad BackColor de todas las etiquetas a Transparent para que coincidan con la propiedad fija BackColor de las fichas.

4.6.1 Comprobar el proyecto OrdersByCustomersV3 La versión inicial del proyecto OrdersByCustomersV3 incluye las siguientes modificaciones del proyecto OrdersByCustomersV2: ) )

)

)

)

)

)

)

)

Controles convencionales Button y TextBox sustituyen a los controles ToolStrip. Botones individuales Save y Cancel permiten añadir Customers y Orders, editarlos y borrar items en línea y Orders. Las tablas base que se han de actualizar no están implementadas en la versión inicial. Items de ejemplo pueblan la lista desplegable cboCustomerID y cuadros de texto cuando se añade un cliente nuevo. Cuando se abre el formulario y cuando se añade un nuevo cliente, en pantalla se ve un cuadro de texto de ayuda vacío. El cuadro de texto se puede poblar desde un string constante o un archivo de texto que se incluya como fuente del proyecto. La lógica de negocios impide que se editen o se borren Orders que ya se han enviado definiendo True para la propiedad ReadOnly de las filas con valores ShippedDate. Pulsando el botón Add New Order se sitúa el control OrdersDataGridView en la fila del registro nuevo y se añade un nuevo registro a OrdersBindingSource y Order_DetailsBindingSource. Una casilla de verificación Edit permiten editar Orders y Order Details en una segunda ficha que reduce la altura del formulario y proporciona cuadros de texto para editar el pedido seleccionado. La lógica de negocios impide que se añada más de un pedido por cliente sin guardar antes el nuevo pedido y sus ítems de línea. Pulsando el botón Edit Orders se selecciona el último pedido de la parrilla. La flecha hacia abajo selecciona Orders anteriores. Si se ha comprobado Edit en la ficha, pulsando cuando haya seleccionado un pedido no enviado, se abre la segunda ficha para edición de datos y en la que se pueden entrar de datos sin el ratón.

La figura de la página siguiente muestra el formulario en formato de una sola página.

135

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 136

Bases de datos con Visual Basic

4.6.2 Fijar los valores por defecto que faltan al añadir filas con código Añadir filas a BindingSource y su DataGridView vinculado a través de código no hace que se dispare el evento DefaultValuesNeeded. Estos eventos están guiados por UI. Por lo tanto, hay que modificar el manejador de evento OrdersDataGridView_DefaultValuesNeeded llevando su código hasta otro procedimiento, SetDefaultValues en este ejemplo, y llamando el procedimiento tal como se muestra a continuación: Private Sub OrdersDataGridView_DefaultValuesNeeded(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewRowEventArgs) _ Handles OrdersDataGridView.DefaultValuesNeeded SetDefaultOrderValues(e.Row) End Sub

De la misma manera hay que modificar también el manejador de eventos Order_DetailsDataGridView_DefaultValuesNeeded. Una vez realizadas esas modificaciones ya se pueden añadir en el código de programación las filas con valores por defecto, como vemos seguidamente: Private Sub btnNewOrder_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnNewOrder.Click EnableOrdersGrid(True, False) With OrdersBindingSource .AddNew() .MoveLast() End With With OrdersDataGridView .Focus()

136

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 137

Programar TableAdapters, BindingSources y DataGridViews Dim rowAdded As DataGridViewRow = .Rows(.Rows.Count - 2) SetDefaultOrderValues(rowAdded) End With blnIsNewOrderRow = True btnCancelOrderEdits.Enabled = True btnSaveOrders.Enabled = False EnableOrder_DetailsGrid(True, False) With FK_Order_Details_OrdersBindingSource .AddNew() .MoveLast() End With With Order_DetailsDataGridView Dim rowAdded As DataGridViewRow = .Rows(.Rows.Count - 2) SetDefaultDetailsValues(rowAdded) End With btnNewOrder.Enabled = False If blnUseTabs Then If tabOrders.TabPages.Count = 1 Then tabOrders.TabPages.Add(pagEditOrder) End If tabOrders.SelectedTab = pagEditOrder blnIsNewOrderRow = False EmployeeIDTextBox.Focus() End If pagEditOrder.Text = Edit New Order End Sub

4.6.3 Editar un record DataGridView seleccionado en la segunda ficha Los controles Tab tienen dos fichas por defecto. La ventana Orígenes de datos de VS 2005 y el control BindingSources hacen más fácil añadir cuadros de texto de datos vinculados y un control clonado Order_DetailsDataGridView a la segunda ficha. BindingSource sincroniza automáticamente los dos DataGridViews vinculados. Añada los cuadros de texto definiendo el nodo de la ventana Orígenes de datos en Detalles para las fuentes de datos, en este ejemplo Orders. Para ello debe arrastrar los controles hasta la página y reajustar el formato. Los campos Date y DateTime aparecen como controles DateTimePicker. Defina el valor True para la propiedad ReadOnly de los cuadros de texto de clave primaria y clave foránea. Copie el DataGridView de la primera ficha arrastrando su nodo hasta la página. Debe añadir campos no vinculados y campos de formato numérico, tal como se requiere para cortar y pegar controles DataGridViews desde un formulario a una página tabulada. Un solo manejador de evento CellValueNeeded es suficiente para todas las instancias DataGridView clonadas. El siguiente código impide que el usuario edite Orders que tienen valores ShippedDate no nulos al pulsar :

137

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 138

Bases de datos con Visual Basic Private Sub tabOrders_KeyDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyEventArgs) Handles tabOrders.KeyDown If blnUseTabs And e.KeyCode = Keys.F2 And OrdersDataGridView.Enabled Then Try With OrdersDataGridView If .SelectedCells(0).ColumnIndex = 0 Then If .Rows(.SelectedCells(0).RowIndex).Cells(ShippedDate).Value _ Is DBNull.Value Then If tabOrders.TabPages.Count = 1 Then tabOrders.TabPages.Add(pagEditOrder) End If tabOrders.SelectedTab = pagEditOrder End If End If End With Catch excSys As Exception End Try End If End Sub

La siguiente figura muestra la ficha Edit Selected Order con el último pedido para CustomerID RATTC abierto para su edición.

138

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 139

Programar TableAdapters, BindingSources y DataGridViews

4.7 Crear y vincular listas de consulta Lookup para valores de clave primaria Los operadores de entrada de datos no tienen porqué recordar los campos de nombre asociados con los valores de clave primaria o foránea. Por lo tanto, las columnas y cuadros de texto de DataGridView vinculadas a valores de clave primaria o foránea necesitan cuadros combinados poblados por listas lookup y vinculados al campo de clave. Para crear la fuente de datos de una lista lookup en un cuadro combinado existen los siguientes métodos: )

)

)

Si el juego de datos tipificado del proyecto contiene la tabla de datos para poblar una lista lookup en un DataGridView, en la columna Edit, defina el valor de la propiedad DataSource de la tabla de datos, el valor de ValueMember de la clave primaria, y defina DisplayMember en el campo para poblar la colección Items. Si el juego de datos tipificado del proyecto contiene la tabla de datos para poblar una lista lookup en un cuadro combinado del formulario, en la ventana Propiedades, expanda el nodo (DataBinding), defina el valor (Advanced) de la propiedad para la tabla de datos, defina SelectedValue para el valor de la clave primaria y defina SelectedItem para el campo para poblar las colecciones Items. De lo contrario, cree un juego de datos no tipificado, añada DataAdapters para las tablas lookup y defina los valores de DataSource, ValueMember, y DisplayMember con código. Si el cuadro combinado está en un formulario, no en una columna DataGridView, deberá invocar el método DataBindings.Add con un nuevo objeto NewBinding para vincular el cuadro combinado con el campo apropiado de la tabla de datos del DataSet tipificado.

La ventaja de añadir una tabla lookup al juego de datos tipificado es que permite definir los valores de propiedad requeridos en tiempo de diseño y mantener automáticamente la integridad referencial. La parte negativa de este método es que no permite personalizar los valores DisplayMember(Items) del cuadro combinado. En cualquiera de los dos casos, las tablas lookup se pueden guardar para reutilizarlas como archivo DataSet XML. Cargando las tablas lookup desde un archivo local se reduce al menos en uno el número de accesos al servidor cuando los usuarios abren una nueva sesión durante el proyecto. Creando y cargando un juego de datos lookup no tipificado se pueden llenar todas las tablas lookup en un solo acceso. Los apartados siguientes muestran cómo crear un juego de datos no tipificado que incluya tablas de datos lookup creadas a partir de las tablas Northwind Customers, Employees, Shippers, y Products, y después poblar con cuadros combinados vinculados y no vinculados.

4.7.1 Crear un juego de datos lookup no tipificado y sus tablas de datos Los juegos de datos (no tipificados) en tiempo de ejecución y sus tablas son objetos mucho más ligeros que las tablas añadidas a los juegos de datos tipificados. Como se mencionó en el apartado anterior, todas las tablas no tipificadas se pueden llenar con un solo acceso al servidor. Para minimizar la carga del servidor habría que guardar el juego de datos lookup en un archivo local XML, así en las sesiones siguientes se podrán cargar las tablas desde ese archivo. 139

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 140

Bases de datos con Visual Basic Añada el siguiente procedimiento para crear las cuatro tablas de datos que pueblan múltiples cuadros combinados en la versión inicial de las dos páginas tabuladas del formulario y guarda el juego de datos como archivo LookupsDataSet.xml: Private Sub LoadLookupLists() Me.Cursor = Cursors.WaitCursor Customers() Dim strSQL As String = SELECT CustomerID, CustomerID, CompanyName AS IDName FROM dbo.Customers; Employees() strSQL += SELECT EmployeeID, LastName,FirstName AS EmployeeName FROM dbo.Employees; Shippers() strSQL += SELECT ShipperID, CompanyName FROM dbo.Shippers; Products() strSQL += SELECT ProductID, ProductName, UnitPrice, QuantityPerUnit FROM dbo.Products; Dim strConn As String = My.Settings.NorthwindConnection.ToString Dim daLookups As New SqlDataAdapter(strSQL, strConn) Try daLookups.Fill(dsLookups) With dsLookups .Tables(0).TableName = CustsLookup .Tables(1).TableName = EmplsLookup .Tables(2).TableName = ShipsLookup .Tables(3).TableName = ProdsLookup End With Dim strFile As String = Application.StartupPath + \LookupsDataSet.xml dsLookups.WriteXml(strFile, XmlWriteMode.WriteSchema) Catch excFill As Exception MsgBox(excFill.Message + excFill.StackTrace, , Error Filling Lookup Tables ) Finally If daLookups.SelectCommand.Connection.State = ConnectionState.Open Then daLookups.SelectCommand.Connection.Close() End If End Try End Sub

Borre el código siguiente, que añade datos de ejemplo a la lista, desde el final del manejador de eventos OrderForm_Load: If blnUseSampleData Then With cboCustomerID .Items.Add( QUEDE - Que Del cia ) .Items.Add( QUEEN - Queen Cozinha ) ... .Items.Add( SPECD - Sp cialit s du monde ) .SelectedIndex = 4

140

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 141

Programar TableAdapters, BindingSources y DataGridViews End With End If

Sustituya el código borrado por el siguiente, que llama los procedimientos LoadLookupLists cuando es necesario y proporciona un valor por defecto para CustomerID para su verificación a conveniencia: Dim strFile As String = Application.StartupPath + \LookupsDataSet.xml If File.Exists(strFile) Then Load dsLookups from the file dsLookups.ReadXml(strFile) Else LoadLookupLists() End If LoadAndBindComboBoxes() Following is optional Set the combo box to RATTC which has an unshipped order With dsLookups.Tables(0) Dim intRow As Integer For intRow = 0 To .Rows.Count - 1 If Mid(.Rows(intRow).Item(0).ToString, 1, 5) = RATTC Then cboCustomerID.SelectedIndex = intRow Exit For End If Next intRow End With

4.7.2 Rellenar el cuadro combinado cboCustomerID Añada el código siguiente para poblar el cuadro combinado con valores CustomerID e ítems CustomerID/CustomerName de la tabla de datos CustsLookup: Private Sub LoadAndBindComboBoxes() With cboCustomerID .DataSource = dsLookups.Tables(CustsLookup) .DisplayMember = CustIDName .ValueMember = CustomerID End With ... End Sub

4.7.3 Sustituir los cuadros de texto de DataGridView por cuadros combinados Los cuadros de texto EmployeeID y ShipVia del OrdersDataGridView son candidatos lógicos para ser substituidos por cuadros combinados. Para remplazar una columna cuadro de texto con una columna cuadro combinado, abra el cuadro de diálogo Editar columnas de DataGridView, seleccione la columna apropiada, defina el valor 141

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 142

Bases de datos con Visual Basic DataGridViewComboBoxColumn para la propiedad ColumnType, cambie el valor de HeaderName si es necesario, y defina el valor de Width para ajustarlo a la lista de ítems. En este ejemplo, cambie la cabecera de columna EmployeeID por Employee, y defina una anchura de 120 píxeles. Cambie la anchura de la columna ShipVia a 110 pixeles. La figura siguiente muestra dos cambios en la columna EmployeeID; Width queda fuera de la vista.

4.7.4 Añadir código para poblar los cuadros combinados Employees y ShipVia Antes de ejecutar el programa, debe añadir código para poblar los dos cuadros combinados nuevos, si no lo hace, se producirán muchos DataErrors durante la navegación por el DataGridView. Para encontrar los nombres de los dos cuadros combinados, DataGridViewComboBoxColumn seguido de un número entero arbitrario, busque ‘DataGridViewComboBox (incluida la comilla simple) en OrdersForm.Designer.vb para encontrar los grupos de definición de los dos cuadros combinados, destacados en negrita en las siguientes secuencias de código: ‘ DataGridViewComboBoxColumn2 ‘ Me.DataGridViewComboBoxColumn2.DataPropertyName = EmployeeID Me.DataGridViewComboBoxColumn2.DefaultCellStyle = DataGridViewCellStyle1 Me.DataGridViewComboBoxColumn2.HeaderText = Employee Me.DataGridViewComboBoxColumn2.MaxDropDownItems = 8 Me.DataGridViewComboBoxColumn2.Name = EmployeeID Me.DataGridViewComboBoxColumn2.Resizable = _ System.Windows.Forms.DataGridViewTriState.[True] Me.DataGridViewComboBoxColumn2.SortMode = _ System.Windows.Forms.DataGridViewColumnSortMode.Automatic Me.DataGridViewComboBoxColumn2.ValueType = GetType(Integer) Me.DataGridViewComboBoxColumn2.Width = 120

142

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 143

Programar TableAdapters, BindingSources y DataGridViews

‘ DataGridViewComboBoxColumn3 Me.DataGridViewComboBoxColumn3.DataPropertyName = ShipVia Me.DataGridViewComboBoxColumn3.DefaultCellStyle = DataGridViewCellStyle1 Me.DataGridViewComboBoxColumn3.HeaderText = ShipVia Me.DataGridViewComboBoxColumn3.MaxDropDownItems = 8 Me.DataGridViewComboBoxColumn3.Name = ShipVia Me.DataGridViewComboBoxColumn3.Resizable = _ System.Windows.Forms.DataGridViewTriState.[True] Me.DataGridViewComboBoxColumn3.SortMode = _ System.Windows.Forms.DataGridViewColumnSortMode.Automatic Me.DataGridViewComboBoxColumn3.ValueType = GetType(Integer) Me.DataGridViewComboBoxColumn3.Width = 110

Usando los nombres que ha descubierto, cuyos sufijos numéricos probablemente diferirán de los del código anterior, añada el código siguiente al procedimiento LoadAndBindComboBoxes: Private Sub LoadAndBindComboBoxes() ... With DataGridViewComboBoxColumn2 .DataSource = dsLookups.Tables(EmplsLookup) .DisplayMember = EmployeeName .ValueMember = EmployeeID End With ... With DataGridViewComboBoxColumn3 .DataSource = dsLookups.Tables(ShipsLookup) .DisplayMember = CompanyName .ValueMember = ShipperID End With End Sub

4.7.5 Remplazar los valores nulos por defecto en las filas nuevas Los cuadros combinados no pueden procesar valores nulos sin mostrar un error, por lo que deberá asignar un valor por defecto válido en EmployeeID en el manejador de evento SetDefaultOrderValues de la versión inicial. El valor lógico sería ‘0’ para EmployeeID con Unassigned como valor de LastName, pero eso requeriría modificar la tabla de Empleados (Employees). Una alternativa es especificar una consulta UNION para poblar el cuadro combinado con el ítem añadido. Si elige este método, cambie la sentencia SELECT para la tabla EmplsLookup por: SELECT 0, Unassigned UNION SELECT EmployeeID, LastName + FirstName AS EmployeeName FROM dbo.Employees;.

,

+

La alternativa más sencilla es vincular por defecto todos los Orders al vicepresidente de ventas, tal como se muestra a continuación en negrita:

143

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 144

Bases de datos con Visual Basic Private Sub SetDefaultOrderValues(ByVal rowAdded As DataGridViewRow) With rowAdded .Cells(1).Value = Me.CustomerIDTextBox.Text .Cells(2).Value = 2 .Cells(3).Value = Today.ToShortDateString .Cells(4).Value = Today.AddDays(14).ToShortDateString ... End With End Sub

Cuando construya y ejecute el formulario, los Orders DataGridView con un nuevo pedido añadido aparecerá tal como se muestra en la siguiente figura.

4.7.6 Asociar cuadros combinados con cuadros de texto La Ficha Edit Selected Orders necesita cuadros combinados lookup similares, pero conservar los cuadros de texto originales EmployeeID y ShipVia verifica que los valores de la columna vinculada varían al seleccionar diferentes valores de los cuadros combinados. Para este ejemplo, añada cuadros combinados llamados cboEmployeeID y cboShipVia a la ficha Edit Selected Orders y cambie el valor de su propiedad DropDownStyle por DropDownList. Añada el código siguiente para poblar y vincular los cuadros combinados de los campos EmployeeID y ShipVia de OrdersDataTable: Private Sub LoadAndBindComboBoxes() ... With cboEmployeeID .DataSource = dsLookups.Tables(EmplsLookup)

144

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 145

Programar TableAdapters, BindingSources y DataGridViews .DisplayMember = EmployeeName .ValueMember = EmployeeID .DataBindings.Clear() Any of these bindings work; BindingSource is the preferred data source .DataBindings.Add(SelectedValue, NorthwindDataSet.Orders, EmployeeID) .DataBindings.Add(New Binding(SelectedValue, NorthwindDataSet, _ Orders.EmployeeID)) .DataBindings.Add(New Binding(SelectedValue, OrdersBindingSource, _ EmployeeID, True)) End With ... With cboShipVia .DataSource = dsLookups.Tables(ShipsLookup) .DisplayMember = CompanyName .ValueMember = ShipperID .DataBindings.Clear() .DataBindings.Add(New Binding(SelectedValue, OrdersBindingSource, _ ShipVia, True)) End With ... End Sub

Una peculiaridad al sincronizar cuadros combinados y cuadros de texto vinculados al mismo campo es que se hace imposible la actualización bidireccionalidad de los cuadros de texto. Para actualizar los cuadros de texto con los cambios de los cuadros combinados, añada los siguientes manejadores de eventos: Private Sub cboEmployeeID_SelectionChangeCommitted(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles cboEmployeeID.SelectionChangeCommitted EmployeeIDTextBox.Text = cboEmployeeID.SelectedValue.ToString End Sub Private Sub cboShipVia_SelectionChangeCommitted(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles cboShipVia.SelectionChangeCommitted ShipViaTextBox.Text = cboShipVia.SelectedValue.ToString End Sub Debe manejar el evento SelectionChangeCommitted, no el evento Click, el cual ocurre antes de que el cambio seleccionado sea válido.

Para actualizar la selección del cuadro combinado con los cambios de los cuadros de texto, añada al código inicial la modificación destacada en negrita, y un manejador para el evento TextChanged de ShipViaTextBox: Private Sub EmployeeIDTextBox_TextChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles EmployeeIDTextBox.TextChanged With EmployeeIDTextBox If Val(.Text) > 0 And CInt(Val(.Text)) <= cboEmployeeID.Items.Count Then

145

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 146

Bases de datos con Visual Basic btnCancelPage1Changes.Enabled = True btnSavePage1Changes.Enabled = True cboEmployeeID.SelectedIndex = CInt(Val(.Text)) - 1 End If End With End Sub Private Sub ShipViaTextBox_TextChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles ShipViaTextBox.TextChanged With ShipViaTextBox If Val(.Text) > 0 And CInt(Val(.Text)) <= cboShipVia.Items.Count Then cboShipVia.SelectedIndex = CInt(Val(.Text)) - 1 btnCancelPage1Changes.Enabled = True btnSavePage1Changes.Enabled = True End If End With End Sub

La siguiente figura muestra la página Edit Selected Order con los dos cuadros combinados añadidos.

4.8 Añadir un cuadro combinado que defina valores adicionales Cambiar el valor de un cuadro de texto vinculado o de un cuadro combinado a menudo tiene efectos secundarios que se deben tratar con código. A modo de ejemplo, el valor de la columna UnitPrice de Order_DetailsDataGridView se tiene que actualizar cambiando la columna ProductID. La versión inicial OrdersByCustomerV3 requiere que

146

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 147

Programar TableAdapters, BindingSources y DataGridViews el operador de entrada de datos haga referencia a una lista de correlación para los valores ProductID, ProductName, y UnitPrice. Por lo tanto, la columna ProductID necesita un cuadro combinado para mostrar los valores de ProductName, y seleccionar un item debe proporcionar el valor correcto de UnitPrice. La tabla de datos ProdsLookup incluye los datos UnitPrice, así como una columna QuantityPerUnit. Mostrar QuantityPerUnit en una columna no vinculada es opcional.

4.8.1 Crear y vincular un DataView ordenado por ProductName Para remplazar el cuadro de texto de la columna ProductID por un cuadro combinado en los dos DataGridViews hay que seguir el mismo proceso que para las columnas EmployeeID y ShipVia de la parrila Orders. Los ítems de lista EmployeeID y ShipVia aparecen en el pedido de la columna vinculada a la tabla de datos. Eso no representa ningún problema para los cuadros combinados que contienen pocos ítems de lista, pero el cuadro combinado ProductID debería estar ordenado alfabéticamente por ProductName. Ordenar los ítems requiere crear un DataView ordenado según la tabla de datos. Primero, añada las siguientes variables de formulario a la clase OrdersForm.vb: Private dvProdsLookup As DataView Private blnHasLoaded As Boolean

Añada el código siguiente al procedimiento LoadAndBindComboBoxes para crear un DataView dvProdsLookup ordenado por ProductName, y poblar las listas del cuadro con datos de dvProdsLookup: Private Sub LoadAndBindComboBoxes() ... ' ProductID combo boxes ' Create a dvProdsLookup = New DataView(.Tables(3)) dvProdsLookup.Sort = ProductName With DataGridViewComboBoxColumn4 .DataSource = dvProdsLookup .DisplayMember = ProductName .ValueMember = ProductID End With With DataGridViewComboBoxColumn5 .DataSource = dvProdsLookup .DisplayMember = ProductName .ValueMember = ProductID End With blnHasLoaded = True End Sub

147

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 148

Bases de datos con Visual Basic

4.8.2 Comprobar que no haya duplicados y actualizar la columna UnitPrice La tabla Order Details tiene una clave primaria compuesta, OrderID y ProductID, para impedir que se dupliquen los items de línea existentes. Para impedir accesos al servidor que devuelvan mensajes de error de violación de clave, debería comprobar los valores nuevos o modificados de ProductID para verificar las duplicaciones e informar al operador del error. Las entradas duplicadas de ProductID arrojarán una excepción DataError cuando el operador complete la edición y vaya a la fila siugiente. De todos modos, es es una práctica más que buena capturar el error inmediatamente después de que ocurra.

Si el nuevo valor de ProductID es aceptable, hay que escanear la tabla ProdsLookup para encontrar la fila correspondiente y actualizar el precio por unidad con el procedimiento siguiente, aplicable a las dos parrillas Order Details. Hay que pasar dgvDetails por referencia para obtener un puntero de la instancia activa DataGridView. Private Sub GetUnitPrice(ByVal intRow As Integer, ByVal intCol As Integer, _ ByRef dgvDetails As DataGridView) Try If intCol = 2 Then Dim intProdID As Integer = CInt(dgvDetails.Rows(intRow).Cells(2).Value) Dim decPrice As Decimal Dim intRowCtr As Integer Dim rowProd As DataRow Dim strName As String = Nothing Dim intDups As Integer With dgvDetails For intRowCtr = 0 To .Rows.Count - 1 If CInt(.Rows(intRow).Cells(2).Value) = intProdID Then intDups += 1 If intDups > 1 Then Exit For End If End If Next intRowCtr End With If intDups > 1 Then Dim strMsg As String = "ProductID " + intProdID.ToString + _ " has been added previously to this order. " + vbCrLf + vbCrLf + _ " Please select a different product or press Esc to cancel the edit." MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) Return End If

148

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 149

Programar TableAdapters, BindingSources y DataGridViews With dsLookups.Tables(3) For intRowCtr = 0 To .Rows.Count - 1 rowProd = .Rows(intRowCtr) If CInt(rowProd.Item(0)) = intProdID Then decPrice = CDec(rowProd.Item(2)) With Order_DetailsDataGridView1 .Rows(intRow).Cells(3).Value = decPrice Exit For End With End If Next intRowCtr End With End If Catch exc As Exception MsgBox(exc.Message + exc.StackTrace, , exc.Source) End Try End Sub

Una alternativa a tener presente es crear un DataView con el valor de la propiedad Filter definido como ProductID=intProdID. Las expresiones Filter utilizan la sintaxis de consulta SQL WHERE (sin WHERE), por lo que los argumentos literales del string deben ir entre comillas simples. Hay que dar los valores apropiados de intRow e intCol y un puntero DataGridView al procedimiento de los manejadores de evento CellValueChanged añadidos. Private Sub Order_DetailsDataGridView_CellValueChanged(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles Order_DetailsDataGridView.CellValueChanged 'Get the UnitPrice value If blnHasLoaded Then GetUnitPrice(e.RowIndex, e.ColumnIndex, Order_DetailsDataGridView) End If End Sub Private Sub Order_DetailsDataGridView1_CellValueChanged(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles Order_DetailsDataGridView1.CellValueChanged If blnHasLoaded Then GetUnitPrice(e.RowIndex, e.ColumnIndex, Order_DetailsDataGridView1) If Not (e.ColumnIndex = 0 Or e.ColumnIndex = 5) Then 'Update the items subtotal for Quantity, ProductID, 'UnitPrice, and Discount changes GetOrderSubtotal() btnCancelPage1Changes.Enabled = True btnSavePage1Changes.Enabled = True End If End If End Sub

149

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 150

Bases de datos con Visual Basic A continuación vemos el código para el procedimiento GetOrderSubtotal, que actualiza el cuadro de texto txtSubtotal: Private Sub GetOrderSubtotal() With Order_DetailsDataGridView1 Dim decSubtotal As Decimal Dim intCtr As Integer For intCtr = 0 To .Rows.Count - 1 decSubtotal += CDec(.Rows(intCtr).Cells(5).Value) Next txtSubtotal.Text = Format(decSubtotal, "$#,##0.00") End With End Sub

La siguiente figura muestra la ficha Edit Selected Order con la columna ProductID DataGridView convertida de cuadro de texto a cuadro combinado, varios items de línea añadidos a un pedido nuevo, y el valor Items Subtotal actualizado.

4.9 Añadir filas a las tablas lookup para entradas de nuevos Customers El proyecto inicial OrdersByCustomersV3 añade el item CustomerID computado al cuadro combinado cboCustomerID cuando se completa la entrada CompanyName para un nuevo cliente. No es posible añadir items a cuadros combinados cuya DataSource es un tabla de datos, por lo que hay que añadir una fila nueva a la tabla de datos CustsLookup. La manera más sencilla de conseguir tablas de datos en tiempo de ejecución es añadir una fuente vinculada (BindingSource) al formulario y utilizar sus métodos para añadir

150

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 151

Programar TableAdapters, BindingSources y DataGridViews una fila para el nuevo valor CustomerID. Las tareas de edición se manejan con los métodos AddNew, EndEdit, CancelNew, CancelEdit de la BindingSource.

4.9.1 Añadir y vincular una BindingSource CustomerID Añadir un componente BindingConnector1 del cuadro de herramientas y renombrarlo como bsCustsLookup. A continuación, añadir el código de vinculación siguiente después de la sentencia blnHasLoaded=True del procedimiento LoadAndBindComboBoxes: bsCustsLookup.DataSource = dsLookups bsCustsLookup.DataMember = “CustsLookup” ‘Test the BindingSource (optional) Dim intRows As Integer = bsCustsLookup.Count En el evento ContactNameTextBox_GotFocusevent, elimine el código siguiente que añade un cuadro combinado que provoca una “runtime exception”. .Items.Add(strCustID + “ - “ + CompanyNameTextBox.Text) 'List is sorted, so need to find the new entry '(Lists can’t be sorted when they use a DataSource) For intCtr = 0 To .Items.Count - 1 If Mid(.Items(intCtr).ToString, 1, 5) = strCustID Then .SelectedIndex = intCtr Exit For End If Next

Sustituya el código borrado por el siguiente para añadir un nuevo registro al final de la tabla de datos y definir sus valores: Dim objNewRow As Object = bsCustsLookup.AddNew() Dim drvNewRow As DataRowView = CType(objNewRow, DataRowView) With drvNewRow .Item(0) = strCustID .Item(1) = strCustID + “ - “ + CompanyNameTextBox.Text .EndEdit() End With .SelectedIndex = .Items.Count - 1

Aplicar drvNewRow.EndEdit implica eliminar la fila añadida –en lugar de llamar a CancelEdit– en el manejador de eventos btnCancelCustEdit_Click. Añada la siguiente línea destacada en negrita: Private Sub btnCancelCustEdit_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnCancelCustEdit.Click Dim intCtr As Integer = CustomersBindingSource.Count If blnIsNewCustomer Then 'Remove the added (last) record dcCustsLookup.RemoveAt(dcCustsLookup.Count - 1) ClearCustomerTextBoxes() CustomersBindingSource.CancelEdit()

151

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 152

Bases de datos con Visual Basic blnIsNewCustomer = False Else CustomersBindingSource.CancelEdit() End If ... End Sub

4.9.2 Comprobar la existencia de duplicados con un DataRowView Cambiar la fuente de datos del cuadro combinado por una tabla de datos requiere modificaciones en el test para comprobar duplicados del CustomerID. La expresión .Items(intCtr).ToString del siguiente bloque de código devuelve System.Windows.Forms.ComboBox,Items.Count=94, no el string esperado CustomerID - CustomerName: For intCtr = 0 To .Items.Count - 1 If Mid(.Items(intCtr).ToString, 1, 5) = strCustID Then CompanyNameTextBox.Focus() Dim strMsg As String = "CustomerID ‘" + strCustID + _ "‘ duplicates existing entry ‘" + .Items(intCtr).ToString + "." + strHelp MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) blnIsDup = True Exit For End If Next intCtr

Hay que convertir el ítem del cuadro de texto en un objeto DataRowView y comprobar el valor DataRowView.Row.Item(0), para lo cual se han de añadir al bloque anterior los siguientes cambios resaltados en negrita: For intCtr = 0 To .Items.Count - 1 Dim drvCustID As DataRowView = CType(.Items(intCtr), DataRowView) With drvCustID.Row If .Item(0).ToString = strCustID Then CompanyNameTextBox.Focus() Dim strMsg As String = "CustomerID ‘" + strCustID + _ "‘ duplicates existing entry ‘" + .Item(1).ToString + "." + strHelp MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) blnIsDup = True Exit For End If End With Next intCtr

152

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 153

Programar TableAdapters, BindingSources y DataGridViews

4.10 Aplicar reglas de negocio a las ediciones Como ya se mencionó al principio del capítulo, las reglas de negocios quedan validadas en el sentido de activadas, con valor por la aplicación del cliente en los ejemplos de este capítulo. Las reglas reforzadas en la presentación por tiers contravienen las mejores prácticas, ya que un cambio en las reglas implica desplegar una nueva versión de la aplicación para todos los usuarios del PC. Si refuerza reglas de negocios con triggers en el SQL Server o procedimientos almacenados, cada error en la entrada de datos requiere una nueva ejecución del servidor. Hay dos reglas de negocios, sin embargo, que no es probable que cambien: prohibir Orders sin ítems de línea y valores UnitPrice de valor 0,00. Añada el código siguiente al principio del manejador de eventos btnSaveOrders_Click para reforzar las dos reglas: Dim strMsg As String If Order_DetailsBindingSource.Count < 1 Then strMsg = "An new order must have at least one line item. " + _ "Please add a line item or click Cancel All Changes." MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) Return End If 'Test for $0.00 as UnitPrice Dim intRow As Integer strMsg = "A UnitPrice of $0.00 isn’t permitted. Please edit line " With Order_DetailsDataGridView1 For intRow = 0 To .Rows.Count - 2 If CDec(.Rows(intRow).Cells(3).Value) = 0D Then strMsg += (intRow + 1).ToString + "." MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) Return End If Next End With

La mayoría de operadores de entrada de datos no están autorizados a dar descuentos arbitrarios a los Customers, pero tampoco se espera de ellos que sepan de memoria las tablas de descuentos. En este ejemplo se da una cantidad única de descuento aplicable a todos los productos y Customers, y cambiar un descuento para los ítems existentes con valor Discount distinto de 0,00% está prohibido. Para establecer una cantidad fija como descuento progoramada, añada el código siguiente al principio del procedimiento GetUnitPrice: If intCol = 1 Then 'Calculate fixed discounts for default 0.0% With dgvDetails If CInt(.Rows(intRow).Cells(4).Value) = 0D Then Dim intQuan As Integer = CInt(.Rows(intRow).Cells(intCol).Value) Dim decDisc As Decimal Select Case intQuan

153

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 154

Bases de datos con Visual Basic Case Is >= 100 decDisc = 0.25D Case Is >= 50 decDisc = 0.15D Case Is >= 25 decDisc = 0.1D Case Is >= 10 decDisc = 0.075D Case Is >= 5 decDisc = 0.05D End Select .Rows(intRow).Cells(4).Value = decDisc End If End With End If

Quantity y ProductID son ahora los únicos valores de campo de Order Details que no están autogenerados por el usuario, por lo tanto debería definir como True el valor de la propiedad ReadOnly de las columnas UnitPrice y Discount.

4.11 Guardar los cambios en las tablas base Hasta ahora, los botones Save... de las dos fichas sólo actualizan los juegos de datos tipificados. Antes de realizar cambios en las tablas base Northwind, debería decidir una estrategia de actualización. Se pueden acumular cambios en los juegos de datos y añadir un botón para enviar todos los cambios al servidor en un batch, o enviar cambios incrementales a medida que el usuario realiza procesos de edición. Ambas alternativas conllevan el mismo número de accesos al servidor, a menos que se activen actualizaciones batch DataAdapter. Activar actualizaciones batch implica añadir sentencias Me.m_adapter.UpdateBatchSize=n al archivo DataSetName.designer.vb y no proporciona una mejora substancial del rendimiento en un entorno LAN. La mejor política para Customers bien conectados (LAN) es guardar los cambios en las tablas base cuando el operador clica cualquier botón Save... Este método minimiza la probabilidad de que haya conflictos de concurrencia y reduce la pérdida de datos en el caso de que haya un fallo en la aplicación cliente, un fallo en el hardware o en el suministro de electricidad.

4.11.1 Mantener la integridad referencial Mantener la integridad referencial implica ejecutar sentencias DELETE, UPDATE, e INSERTSQL o procedimientos almacenados para las tablas relacionadas en un orden específico. Para los procesos de borrado se require una secuencia ascendente en la jerarquía de la relación, y las actualizaciones y adiciones deben ocurrer en orden descendente, a menos que se especifiquen actualizaciones y borrados en casacada para las tablas por debajo de la tabla superior. Las relaciones Northwind FK_Order_Details_Orders y FK_Orders_Customers no tienen especificados borrados o actualizaciones en cascada.

154

VisualBasic2005_04.qxp

02/08/2007

16:21

PÆgina 155

Programar TableAdapters, BindingSources y DataGridViews Para cambiar las reglas de actualización y borrado en la ventana del diseñador del DataSet, sólo tiene que pulsar con el botón secundario del ratón la línea de relación entre las tablas padre e hijo, y seleccionar Editar relación para abrir el cuadro de diálogo Relación. El diseñador de DataSet crea una relación por defecto entre sus tablas de datos. Se puede especificar Sólo relación, Sólo relación Foreign Key y Tanto relación como restricción Foreign Key. Si especifica una restricción de clave foránea tendrá las opciones que vemos a continuación para los valores de las propiedades ForeignKeyConstraint.UpdateRule, DeleteRule, y AcceptChangesRule: )

)

)

)

Cascade (por defecto) borra los registros de la tabla hijo cuando se borran los de la tabla padre y actualiza el valor de la clave foránea de los registros hijo con el nuevo valor de clave primaria en la tabla padre. None no modifica los registros hijo cuando se borra la tabla padre o se modifica el valor de la clave primaria, lo cual arroja excepciones automáticamente. El resultado son registros hijo huérfanos, a menos que los cambios en los registros hijo se manejen con código en el bloque Catch. SetNull define una clave primaria en la tabla hijo con valor DBNull y deja huérfanos los registros. SetDefault define el valor de la clave foránea de la tabla hijo con el valor por defecto de la columna, el cual depende del tipo de datos de la columna.

Los ejemplos de este capítulo no permiten borrar registros Customers ni alterar el valor de CustomerID, por lo tanto especificar Tanto relación como restricción Foreign Key y aceptar el valor None por defecto para la clave foránea FK_Orders_Customers es válido para los tres valores Reglas. La siguiente figura muestra el cuadro de diálogo Relación con el cambio aplicado.

4.11.2 Crear y comprobar la función UpdateBaseTables Independientemente de que se especifiquen actualizaciones o borrados en cascada, o ambos, para los DataSets o las tablas base, la regla general es aplicar las actualizaciones de las tablas base según la siguiente secuencia: 1. Borrar los registros de la tabla hijo. 2. Insertar, modificar y borrar los registros de la tabla padre. 3. Insertar y modificar los registros de la tabla hijo. Actuar conforme a estas reglas implica que el código de actualización de la tabla base cree un nuevo ChangeTypeDataTable para cada tipo de actualización de cada tabla base, y ejecute TableNameTableAdapter.Update (ChangeTypeDataTable) para todas las tablas de datos con cambios. Se puede generar cada tabla copiando las filas de datos actualizadas identificadas por su valor de enumeración DataRowState: Added, Modified, o Deleted.

155

VisualBasic2005_04.qxp

02/08/2007

16:22

PÆgina 156

Bases de datos con Visual Basic

4.11.3 Entender la generación de cambios en tablas y las instrucciones para la actualización de las tablas base Los DataAdapters de ADO.NET 1.x requieren la expresión siguiente para generar un ChangeTypeDataTable y actualizar la table base correspondiente: Dim ChangeTypeDataTable As DataSet.DataTable= _ DataSet.DataTable.GetChanges(DataRowState.Type) If Not ChangeTypeDataTable Is Nothing Then OrdersDataAdapter.Update(ChangeTypeDataTable) End If

Los TableAdapters requieren convertir ChangeTypeDataTable en el tipo DataSet.DataTable. Para poblar un ChangeTypeDataTable en ADO.NET 2.0 y actualizar la tabla base se puede seguir la siguiente instrucción genérica: Dim ChangeTypeDataTable As DataSet.DataTable= _ CType(DataSet.DataTable.GetChanges(DataRowState.Type), DataSet.DataTable) If Not ChangeTypeDataTable Is Nothing Then OrdersTableAdapter.Update(ChangeTypeDataTable) End If

Proyectar el tipo es un precio muy bajo para la versatilidad que se gana con los TableAdapters añadidos. Veamos el código para conseguir filas modificadas en la OrdersDataTable y actualizar la tabla base Orders:

156

VisualBasic2005_04.qxp

02/08/2007

16:22

PÆgina 157

Programar TableAdapters, BindingSources y DataGridViews Dim ModOrders As NorthwindDataSet.OrdersDataTable = _ CType(NorthwindDataSet.Orders.GetChanges(DataRowState.Modified), _ NorthwindDataSet.OrdersDataTable) If Not ModOrders Is Nothing Then OrdersTableAdapter.Update(ModOrders) End If

Para actualizar las tres tablas Northwind habría que aplicar el código anterior en ocho variaciones, tal como ilustra la siguiente figura. Para incluir la posibilidad, altamente peligrosa, de borrar un record Costumer, se necesitarían nueve versiones. Utilice operaciones de copiar, pegar, editar y substituir para minimizar las entradas por teclado.

157

VisualBasic2005_04.qxp

02/08/2007

16:22

PÆgina 158

Bases de datos con Visual Basic

4.11.4 Añadir la función UpdateBaseTables Es una buena práctica comprobar el código de actualización antes de introducir cambios en las tablas base. Una manera de comprobar el procedimiento de actualización es guardar en un archivo XML de formato diffgram los cambios propuestos y comprobar después ese archivo con las operaciones típicas de actualización. Otra práctica recomendada es hacer saber a los usuarios si tienen cambios pendientes –preferentemente cuántos cambios– antes de cerrar la aplicación. El código siguiente para la función UpdateBaseTables cumple todos esos objetivos: Private Function UpdateBaseTables(ByVal blnTest As Boolean) As Boolean If NorthwindDataSet.HasChanges Then Dim NewCustomers As NorthwindDataSet.CustomersDataTable = _ CType(NorthwindDataSet.Customers.GetChanges(DataRowState.Added), _ NorthwindDataSet.CustomersDataTable) Dim ModCustomers As NorthwindDataSet.CustomersDataTable = _ CType(NorthwindDataSet.Customers.GetChanges(DataRowState.Modified), _ NorthwindDataSet.CustomersDataTable) Dim DelOrders As NorthwindDataSet.OrdersDataTable = _ CType(NorthwindDataSet.Orders.GetChanges(DataRowState.Deleted), _ NorthwindDataSet.OrdersDataTable) Dim NewOrders As NorthwindDataSet.OrdersDataTable = _ CType(NorthwindDataSet.Orders.GetChanges(DataRowState.Added), _ NorthwindDataSet.OrdersDataTable) Dim ModOrders As NorthwindDataSet.OrdersDataTable = _ CType(NorthwindDataSet.Orders.GetChanges(DataRowState.Modified), _ NorthwindDataSet.OrdersDataTable) Dim DelDetails As NorthwindDataSet.Order_DetailsDataTable = _ CType(NorthwindDataSet.Order_Details.GetChanges(DataRowState.Deleted), _ NorthwindDataSet.Order_DetailsDataTable) Dim NewDetails As NorthwindDataSet.Order_DetailsDataTable = _ CType(NorthwindDataSet.Order_Details.GetChanges(DataRowState.Added), _ NorthwindDataSet.Order_DetailsDataTable) Dim ModDetails As NorthwindDataSet.Order_DetailsDataTable = _ CType(NorthwindDataSet.Order_Details.GetChanges(DataRowState.Modified), _ NorthwindDataSet.Order_DetailsDataTable) Dim dsChanges As DataSet = Nothing Dim intChanges As Integer If blnTest Then dsChanges = New DataSet dsChanges.DataSetName = "dsChanges" End If Try '1. Delete Order Details records If Not DelDetails Is Nothing Then If blnTest Then DelDetails.TableName = "DelDetails" dsChanges.Tables.Add(DelDetails)

158

VisualBasic2005_04.qxp

02/08/2007

16:22

PÆgina 159

Programar TableAdapters, BindingSources y DataGridViews Else Order_DetailsTableAdapter.Update(DelDetails) End If intChanges += DelDetails.Count End If '2. Delete Orders records If Not DelOrders Is Nothing Then DelOrders.TableName = "DelOrders" If blnTest Then dsChanges.Tables.Add(DelOrders) intChanges += DelOrders.Count Else OrdersTableAdapter.Update(DelOrders) End If intChanges += 1 End If '3. Insert New Customers records If Not NewCustomers Is Nothing Then If blnTest Then NewCustomers.TableName = "NewCustomers" dsChanges.Tables.Add(NewCustomers) Else CustomersTableAdapter.Update(NewCustomers) End If intChanges += NewCustomers.Count End If '4. Update Modified Customers records If Not ModCustomers Is Nothing Then If blnTest Then ModCustomers.TableName = "ModCustomers" dsChanges.Tables.Add(ModCustomers) Else CustomersTableAdapter.Update(ModCustomers) End If intChanges += ModCustomers.Count End If '5. Insert New Orders records If Not NewOrders Is Nothing Then If blnTest Then dsChanges.Tables.Add(NewOrders) NewOrders.TableName = "NewOrders" Else OrdersTableAdapter.Update(NewOrders) End If intChanges += NewOrders.Count End If '6. Update Modified Orders records

159

VisualBasic2005_04.qxp

02/08/2007

16:22

PÆgina 160

Bases de datos con Visual Basic If Not ModOrders Is Nothing Then If blnTest Then dsChanges.Tables.Add(ModOrders) ModOrders.TableName = "ModOrders" Else OrdersTableAdapter.Update(ModOrders) End If intChanges += ModOrders.Count End If '7. Insert New Order Details records If Not NewDetails Is Nothing Then If blnTest Then dsChanges.Tables.Add(NewDetails) NewDetails.TableName = "NewDetails" Else Order_DetailsTableAdapter.Update(NewDetails) End If intChanges += NewDetails.Count End If '8. Update Modified Order Details records If Not ModDetails Is Nothing Then If blnTest Then dsChanges.Tables.Add(ModDetails) ModDetails.TableName = "ModDetails" Else Order_DetailsTableAdapter.Update(ModDetails) End If intChanges += ModDetails.Count End If If blnTest Then Dim strFile As String = Application.StartupPath + _ "\DataSetUpdategram.xml" If intChanges > 0 Then dsChanges.WriteXml(strFile, XmlWriteMode.DiffGram) Dim strMsg As String = "You have update(s) pending to " + _ intChanges.ToString + " records(s)." + vbCrLf + vbCrLf + _ "Are you sure you want to quit without " + _ " saving these updates to the Northwind database?" If MsgBox(strMsg, MsgBoxStyle.Question Or MsgBoxStyle.YesNo, _ "Pending Updates Not Saved") = MsgBoxResult.Yes Then Return False Else Return True End If Else If File.Exists(strFile) Then

160

VisualBasic2005_04.qxp

02/08/2007

16:22

PÆgina 161

Programar TableAdapters, BindingSources y DataGridViews File.Delete(strFile) End If End If End If Return True Catch exc As Exception MsgBox(exc.Message + exc.StackTrace, MsgBoxStyle.Exclamation, _ "Database Updates Failed") Return False Finally If Not dsChanges Is Nothing Then dsChanges.Dispose() End If If Not NewCustomers Is Nothing Then NewCustomers.Dispose() End If If Not ModCustomers Is Nothing Then ModCustomers.Dispose() End If If Not DelOrders Is Nothing Then DelOrders.Dispose() End If If Not NewOrders Is Nothing Then NewOrders.Dispose() End If If Not ModOrders Is Nothing Then ModOrders.Dispose() End If If Not DelDetails Is Nothing Then DelDetails.Dispose() End If If Not NewDetails Is Nothing Then NewDetails.Dispose() End If If Not ModDetails Is Nothing Then ModDetails.Dispose() End If End Try Else If Not blnTest Then MsgBox("There are no data updates to save.", MsgBoxStyle.Information, _ "Save Requested Without Updates") End If Return False End If End Function

161

VisualBasic2005_04.qxp

02/08/2007

16:22

PÆgina 162

Bases de datos con Visual Basic

4.11.5 Operaciones previas de actualización La manera más simple de generar archivos iniciales de prueba DataSetUpdategram.xml es añadiendo un botón provisional para Test Updates en la ficha Customer Orders y añadir el siguiente manejador de evento Click: Private Sub btnTestUpdates_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles btnTestUpdates.Click 'Temporary button for testing Dim blnQuit As Boolean = UpdateBaseTables(True) End Sub

Haga algunos cambios en los records de Customers, Orders, y Order Details, pulse el botónTest Updates, y examine el archivo DataSetUpdategram.xml con Internet Explorer. Compruebe que los cambios que ha hecho han quedado reflejados en el grupo .

4.11.6 Invocar la función UpdateBaseTables Una vez finalizado el test de la función UpdateBaseTables con NorthwindDataSet, elimine temporalmente el botón de test e invoque la función añadiendo las líneas que destacamos en negrita en el código siguiente después de llamar al método EndEdit del manejador de evento btnSaveCustData_Click. Así: CustomersBindingSource.EndEdit() If UpdateBaseTables(False) Then NorthwindDataSet.Customers.AcceptChanges() Else Return End If

Invoque la función del manejador de evento btnSaveOrders_Click para actualizar las tablas Orders y OrderDetails con el siguiente código añadido: Order_DetailsBindingSource.EndEdit() OrdersBindingSource.EndEdit() If UpdateBaseTables(False) Then NorthwindDataSet.Orders.AcceptChanges() NorthwindDataSet.Order_Details.AcceptChanges() Else Return End If

Ahora añada una sentencia ImportsSystem.IO a OrdersForm.vb y, a continuación, el siguiente código por el final del manejador de evento ContactNameTextBox_GotFocus para repoblar cboCustomerID con nuevos valores CustomerID cuando se reinicie el proyecto: .SelectedIndex = .Items.Count - 1 Dim strFile As String = Application.StartupPath + _

162

VisualBasic2005_04.qxp

02/08/2007

16:22

PÆgina 163

Programar TableAdapters, BindingSources y DataGridViews \LookupsDataSet.xml If File.Exists(strFile) Then File.Delete(strFile) End If

4.11.7 Comprobar los valores CustomerID del servidor para evitar duplicados Añadir un cliente nuevo y una carpeta inicial será imposible si otro usuario ha añadido ya un cliente con el mismo CustomerID después del último refresco del DataSet dsLookups. A menos que se guarde el diagrama de la actualización y se añada código para reintentarlo con un CustomerID diferente, se habrá perdido toda la entrada. Para prevenir la pérdida de datos, debería comprobar que el nuevo CustomerID no exista ya en la tabla Customer del servidor. Añada la siguiente función CheckServerForCustID en la que se utiliza el método SqlCommand.ExecuteScalar para un chequeo rápido de duplicados en el servidor: Private Function CheckServerForCustID(ByVal strCustID As String) As Boolean Dim cnNwind As SqlConnection = Nothing Try Dim strConn As String = My.Settings.NorthwindConnection.ToString cnNwind = New SqlConnection(strConn) Dim strSQL As String = "SELECT COUNT(CustomerID) FROM Customers " + _ "WHERE CustomerID = ‘" + strCustID + "‘" Dim cmCustID As New SqlCommand(strSQL, cnNwind) cnNwind.Open() Dim intCount As Integer = CInt(cmCustID.ExecuteScalar) cnNwind.Close() If intCount > 0 Then 'Duplicate found Return True Else Return False End If Catch exc As Exception MsgBox(exc.Message + exc.StackTrace, MsgBoxStyle.Exclamation, _ "Test Duplicates Error") Return False Finally If Not cnNwind Is Nothing Then If Not cnNwind.State = ConnectionState.Closed Then cnNwind.Close() End If cnNwind.Dispose() End If End Try End Function

163

VisualBasic2005_04.qxp

02/08/2007

16:22

PÆgina 164

Bases de datos con Visual Basic Para ejecutar la función CheckServerForCustID, añada las líneas destacadas en negrita a continuación del procedimiento ContactNameTextBox_GotFocus antes del test ExitSub: If Not blnIsDup Then 'Function is in OrderFormV3.vb blnIsDup = CheckServerForCustID(strCustID) If blnIsDup Then CompanyNameTextBox.Focus() Dim strMsg As String = "CustomerID ‘" + strCustID + _ "‘ duplicates existing entry in Customers table." + strHelp MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) End If End If If blnIsDup Then Exit Sub End If

En este momento, ya ha desarrollado todo un frente bastante completo para afrontar la entrada de datos, pero todavía no está preparado para desplegarlo sin encapsular antes el proceso de entrada de Orders en una transacción y generar mensajes de error que permitan a los usuarios superar los problemas siempre que sea posible. En los ejemplos de los capítulos siguientes añadirá funciones para la gestión de transacciones y otras carcterísticas a éste y otros proyectos similares de gestión de datos, después de lo cual habrá adquirido un nuevo estatus como programador casi independiente de componentes de datos y DataGridView.

164

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 165

Capítulo 5

Añadir código para validar datos y gestionar la concurrencia Validar las entradas de datos en cuadros de texto, sencillos controles vinculados de formularios Windows o DataGridViews, es una tarea relativamente simple. Para tener controles de formulario vinculados hay que definir un objeto ErrorProvider que especifique la posición y otros atributos del icono de error, el cual, por defecto, es un signo de exclamación blanco dentro de un círculo rojo. Los DataGridViews tienen un detector de errores integrado que hace todavía más simple la validación de los valores de celda. La validación de datos es suficiente para los frentes de bases de datos de un solo usuario, aunque tal vez sea necesaria una consulta para el servidor dentro del manejador de evento de la validación para impedir que fallen las actualizaciones de datos, por ejemplo, cuando se añade una fila nueva a una tabla con una clave primaria basada en valores, como la tabla Customers de Northwind. Si la clave primaria propuesta ya existe, se obtendrá una SqlException y habrá que realizar de nuevo la actualización. Los dos primeros apartados de este capítulo hablan de las técnicas de validación de entrada de datos con cuadros de texto vinculados y controles DataGridViews. Las aplicaciones para un frente multi-usuario –mucho más comunes que la variedad de un solo usuario– requieren una gestión explícita de la concurrencia. Las operaciones UPDATE o DELETE en tablas base del servidor realizadas con DataTableAdapters ejecutan por defecto pruebas de concurrencia basadas en valores por defecto. Si un valor de la tabla base en el servidor no concuerda con los valores originales de un DataRow, el método DataTableAdapter.Update falla y usted recibe una DBConcurrencyException. Resolver los errores de concurrencia con un proceso que sea razonablemente sencillo para los usuarios no es una tarea simple, tal como descubrirá enseguida. La mayor parte de este capítulo está dedicada a la gestión de la concurrencia. Este capítulo explica dos técnicas de gestión de concurrencia que no se encuentran en la ayuda online de Visual Studio 2005 ni en las publicaciones sobre "mejores prácticas" con ADO.NET. Las dos técnicas son: comparar el número de registros hijo del servidor con los de la tabla de datos del cliente, y restablecer un pedido que otro usuario ha borrado en el servidor.

Este capítulo incluye código VB 2005 para la validación de datos y la gestión de concurrencia con un proyecto de formulario Windows de ejemplo: OrdersByCustomerTx.sln. La siguiente figura muestra el formulario principal del proyecto OrdersByCustomerTx.sln, basado en el formulario del capítulo anterior OrdersByCustomerV2, la función UpdateBaseTables del proyecto (final) OrdersByCustomerV3.sln, y los correspondientes manejadores de evento para las operaciones de actualización. Si bien los errores de concurrencia se pueden simular ejecutando dos instancias de OrdersByCustomerTx.sln, es mucho 165

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 166

Bases de datos con Visual Basic más aconsejable desarrollar y comprobar las estrategias de gestión de concurrencia simulando conflictos en una sola instancia de proyecto. Por lo tanto, el formulario principal tiene tres botones que provocan errores de concurrencia cuando se escriben actualizaciones directamente en el servidor.

Todos los proyectos de ejemplo de los capítulos anteriores presuponen que usted o el usuario del ordenador cliente tienen una conexión de red permanente al servidor de la base de datos. Este capítulo muestra cómo diseñar aplicaciones que soporten usuarios desconectados que actualizan los juegos de datos del cliente offline y después actualizan las tablas del servidor al conectarse de nuevo a la red. Seleccionando el cuadro de verificación Emulate Disconnected User en el proyecto de ejemplo, se simula el estado offline. Si hace las actualizaciones offline y después deselecciona el cuadro de verificación, el proceso de actualización se inicia automáticamente. Las técnicas de gestión de concurrencia son similares para los usuarios conectados y usuarios que se reconectan, pero hay que añadir una cantidad substancial de código para crear y manejar los juegos de datos locales del usuario desconectado. La cadena de conexión por defecto de App.config requiere la base de datos de ejemplo de Northwind para poder instalarlo en una instancia local (localhost) de SQL Server 2000, MSDE 2000, o SQL Server 2005. Si utiliza SQL Express, cambie localhost por .\SQLEXPRESS.

El proyecto de ejemplo añade más de 2.500 líneas de código Visual Basic a su predecesor. La mayor parte del código añadido implementa la gestión de concurrencia. Desarrollar y comprobar las técnicas de gestión de concurrencia a nivel de producción con ADO.NET 2.0 requiere una base de datos jerarquizada en un mínimo de tres niveles, varios tipos de datos de campo y datos de ejemplo representativos, incluyendo

166

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 167

Añadir código para validar datos y gestionar la concurrencia valores DBNull. Sencillas tablas maestras de detalles con algunas columnas y filas no serán suficientes para explicar los numerosos aspectos que describe este capítulo sobre la implementación de la gestión de concurrencia y su diseño.

5.1 Validar las entradas de datos La mayor parte de los controles de los formularios Windows “disparan” un evento Validating cuando el usuario edita un valor de control y un evento Validated después de que el valor esté editado. Los eventos de validación para operaciones con teclado como o <Mayús> + ocurren dentro de la siguiente secuencia de eventos: Enter, GotFocus, Leave, Validating, Validated, y LostFocus. Las operaciones con el ratón y el método Focus generan una secuencia ligeramente distinta: Enter, GotFocus, LostFocus, Leave, Validating, y Validated. Para validar el valor de un control vinculado sencillo o uno no vinculado, añada un manejador de evento ControlName_Validating con expresiones para comprobar el valor editado y generar un icono y una herramienta de error con un objeto ErrorProvider. Los iconos y herramientas de error son mucho menos “invasivos” que los cuadros de mensaje que los usuarios tienen que confirmar pulsando el botón Aceptar.

5.1.1 Validar cuadros de texto A continuación vemos un ejemplo sencillo de un manejador de evento TextBox_Validating para asegurar que el cuadro de texto CompanyName contiene al menos cinco caracteres: Private Sub CompanyNameTextBox_Validating(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) Handles CompanyNameTextBox.Validating If CompanyNameTextBox.Text.Length < 5 Then Dim strError As String = "CompanyName requires at least five characters" e.Cancel = True epCompanyName.SetError(CompanyNameTextBox, strError) Else epCompanyName.SetError(CompanyNameTextBox, String.Empty) End If End Sub

El proyecto de ejemplo de este capítulo, OrdersByCustomerTx.sln, incluye los ejemplos de validación del cuadro de texto y DataGridView. Construya y ejecute el proyecto, pulse Add New Costumer, y pulse para generar un CustomerID. Después vacíe el cuadro de texto CompanyName para mostrar el icono de error. Escriba al menos cinco caracteres en el cuadro de texto y pulse el botón Cancel Edit para finalizar el añadido de datos a Customers. Definiendo e.Cancel=True se impide que el usuario pueda salir del control sin corregir el error. La expresión epCompanyName.SetError(CompanyNameTextBox,strError) requiere 167

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 168

Bases de datos con Visual Basic que se defina un objeto ErrorProvider en el manejador de evento FormName_Load o el constructor del formulario con código como el que sigue: Private epCompanyName As ErrorProvider() ... epCompanyName = New ErrorProvider() With epCompanyName .SetIconAlignment(CompanyNameTextBox, ErrorIconAlignment.MiddleRight) .SetIconPadding(CompanyNameTextBox, 2) .BlinkRate = 500 'half-second .BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.AlwaysBlink End With

Si existe un botón Cancel o similar para salir de la entrada sin corregir el error de validación, deberá añadir una instrucción ControlName.SetError(CompanyNameTextBox,String.Empty) para eliminar el icono y que el usuario pueda obtener de nuevo el control del foco. Un control con un objeto activo ErrorProvider impide igualmente que el usuario cierre el formulario, a menos que se añada un manejador de evento FormName_Closing y se defina e.Cancel=False.

5.1.2 Validar controles DataGridViews Los DataGridViews tienen un detector de errores integrado, por lo que no es necesario añadir ningún objeto ErrorProvider para este tipo de control. Además de los eventos comunes Validating y Validated, válidos para todo el contenido del control, los DataGridViews también disparan eventos CellValidating, CellValidated, RowValidating, y RowValidated. Los eventos Cell... se disparan cuando el usuario intenta abandonar, o abandona, la celda actual, y los eventos Row... tienen lugar cuando el usuario intenta salir, o sale, de la fila actual. El evento CellValidating es el más útil de los seis eventos de validación. Las propiedades e.ColumnIndex y e.RowIndex devuelven las coordenadas de la celda con el error. Añadiendo un mensaje de error a la propiedad DataGridView.Row.ErrorText se muestra un icono en la correspondiente RowHeader y se añade un cuadro de ayuda rápida a la fila. El icono y el cuadro de ayuda rápida se pueden eliminar definiendo la propiedad DataGridView.Row.ErrorText en un string vacío del evento DataGridView_CellValidating o DataGridView_CellEndEdit. Los valores por defecto añadidos a la columna EmployeeID de nuevas filas de Orders y ProductID para nuevas filas de Order Details, causan una restricción de clave foránea SqlException si el usuario intenta guardar los cambios sin cambiar antes 0 por un valor aceptable. Este problema se resuelve definiendo valores de clave foránea en una lista desplegable; para que sea más sencillo, el proyecto ejemplo de este capítulo requiere que el usuario entre valores numéricos. El siguiente código de ejemplo para manejar los eventos DataGridView_CellValidating y DataGridView_CellValidating o DataGridView_CellEndEdit, es típico por sus sencillas expresiones de validación. Private Sub OrdersDataGridView_CellValidating(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellValidatingEventArgs) _ Handles OrdersDataGridView.CellValidating

168

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 169

Añadir código para validar datos y gestionar la concurrencia Try 'Validate EmployeeID column value With OrdersDataGridView If .Rows.Count > 1 Then If e.ColumnIndex = 2 Then If Not e.FormattedValue.ToString = "(null)" Then If CInt(e.FormattedValue) < 1 Or CInt(e.FormattedValue) > 9 Then Dim strError As String = "EmployeeID value must be a number " + _ "between 1 and 9" .Rows(e.RowIndex).ErrorText = strError 'Prevent saving the order SaveOrdersToolStripButton.Enabled = False e.Cancel = True Else End If End If End If End If End With Catch exc As Exception MsgBox(exc.Message, MsgBoxStyle.Information, "Invalid EmployeeID Entry") End Try End Sub Private Sub OrdersDataGridView_CellEndEdit(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles OrdersDataGridView.CellEndEdit With OrdersDataGridView If e.ColumnIndex = 2 Then .Rows(e.RowIndex).ErrorText = End If End With End Sub Private Sub Order_DetailsDataGridView_CellValidating(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellValidatingEventArgs) _ Handles Order_DetailsDataGridView.CellValidating Try With Order_DetailsDataGridView Dim strError As String = Nothing If e.ColumnIndex = 2 Then If CInt(e.FormattedValue) < 1 Or CInt(e.FormattedValue) > 77 Then strError = "ProductID value must be a number between 1 and 77"

169

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 170

Bases de datos con Visual Basic .Rows(e.RowIndex).ErrorText = strError e.Cancel = True SaveOrdersToolStripButton.Enabled = False End If End If End With Catch exc As Exception MsgBox(exc.Message, MsgBoxStyle.Information, "Invalid ProductID Entry") End Try End Sub

Ejecute el proyecto de ejemplo y pulse el botón Add New Order ToolStrip para mostrar los iconos y los mensajes de ayuda rápida. Escriba con el teclado valores aptos para EmployeeID y ProductID para eliminar los iconos de error. Pulse el botón Cancel Orders Edit para finalizar la nueva entrada de Order.

5.1.3 Capturar las violaciones de restricción de clave primera durante la entrada La tabla Order Details tiene una clave primaria compuesta: OrderID y ProductID, para que cualquier valor duplicado de ProductID arroje una excepción de restricción de clave primaria en la tabla local Order Details. Por lo tanto, el código de validación anterior para la columna ProductID se debería comprobar para que no tuviera ningún duplicado. Un método sencillo y ligero para detectar valores duplicados es crear una instancia HashTable y poblar sus pares clave/valor con el valor de ProductID formateado del DataGridView y el número de fila. Si se añade una clave ProductID duplicada, la HashTable arroja una excepción que se deberá procesar con el código que aparece en negrita en el siguiente listado: Private Sub Order_DetailsDataGridView_CellValidating(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellValidatingEventArgs) _ Handles Order_DetailsDataGridView.CellValidating Try With Order_DetailsDataGridView Dim strError As String = Nothing If e.ColumnIndex = 2 Then If CInt(e.FormattedValue) < 1 Or CInt(e.FormattedValue) > 77 Then strError = "ProductID value must be a number between 1 and 77" .Rows(e.RowIndex).ErrorText = strError e.Cancel = True SaveOrdersToolStripButton.Enabled = False Else 'Create a hashtable of ProductID values 'Adding a duplicate key value throws an exception Dim htDupes As Hashtable = New Hashtable

170

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 171

Añadir código para validar datos y gestionar la concurrencia Dim intRow As Integer Try 'Remove previous error text .Rows(intRow).ErrorText = "" For intRow = 0 To .Rows.Count - 2 'Use the EditedFormattedValue property for proposed value Dim objID As Object = .Rows(intRow).Cells(2).EditedFormattedValue htDupes.Add(.Rows(intRow).Cells(2). EditedFormattedValue, intRow) Next intRow Catch exc As Exception If intRow = e.RowIndex Then strError = "ProductID duplicates entry in ano ther row" SaveOrdersToolStripButton.Enabled = False Else strError = "ProductID duplicates entry in row " + _ intRow.ToString SaveOrdersToolStripButton.Enabled = False End If .Rows(e.RowIndex).ErrorText = strError e.Cancel = True End Try End If End If End With Catch exc As Exception MsgBox(exc.Message, MsgBoxStyle.Information, "Invalid ProductID Entry") End Try End Sub

Construya y ejecute el proyecto de ejemplo y pulse el botón New Order. Escriba un valor apto en EmployeeID para eliminar el icono de error del OrderDataGridView. Cambie el valor por defecto de ProductID de la fila por 1, o cualquier otro valor apto, y añada entonces un nuevo registro de Order Detail con el mismo valor para mostrar el icono y cuadro de ayuda rápida de error de la clave primaria.

5.1.4 Validar valores por defecto Al añadir valores por defecto que contienen, deliberadamente, valores erróneos, como una lista desplegable con un ítem por defecto No Value Selected; una buena práctica es poner un flag en la nueva fila con un icono de error para indicar que hace falta editar algo. El manejador de evento NewOrderToolStripButton_Click simplifica la tarea de añadir un pedido nuevo insertando y seleccionado una nueva fila en ambas parrillas. El

171

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 172

Bases de datos con Visual Basic código siguiente muestra en negrita las instrucciones para añadir los iconos y cuadros de ayuda rápida: Private Sub NewOrderToolStripButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) _ Handles NewOrderToolStripButton.Click Try OrdersDataGridView.EndEdit(DataGridViewDataErrorContexts.Commit) Order_DetailsDataGridView.EndEdit(DataGridViewDataErrorContexts.Commit) EditOrdersToolStripButton.PerformClick() OrdersBindingSource.AddNew() OrdersBindingSource.MoveLast() Dim dgvRow As DataGridViewRow = Nothing With OrdersDataGridView dgvRow = .Rows(.Rows.Count - 2) .CurrentCell = .Rows(.Rows.Count - 2).Cells(2) Dim strError As String = "EmployeeID value must be a number " + _ "between 1 and 9" dgvRow.ErrorText = strError End With AddDefaultOrderValues(dgvRow) Order_DetailsBindingSource.AddNew() Order_DetailsBindingSource.MoveLast() With Order_DetailsDataGridView dgvRow = .Rows(0) .CurrentCell = .Rows(0).Cells(2) Dim strError As String = "ProductID value must be a number " + _ "between 1 and 77" dgvRow.ErrorText = strError End With AddDefaultDetailsValues(dgvRow) OrdersDataGridView.Focus() blnIsNewOrder = True SaveOrdersToolStripButton.Visible = True CancelOrdersEditToolStripButton.Visible = True Catch exc As Exception MsgBox(exc.Message + exc.StackTrace, , "New Order Exception") End Try End Sub

172

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 173

Añadir código para validar datos y gestionar la concurrencia Añadir una fila nueva con código no hace que se dispare el evento DataGridView_DefaultValuesNeeded, por lo que debería añadir las instrucciones en negrita a todos aquellos manejadores de evento para comprobar las filas nuevas que el usuario añade manualmente. Otra alternativa es añadir esas instrucciones a los procedimientos AddDefaultOrderValues y AddDefaultDetailsValues, que son los que proporcionan los valores por defecto. La siguiente figura muestra el formulario OrdersByCustomerTx con iconos de error para el cuadro de texto CompanyName y los dos controles DataGridViews. (La parte Customers del formulario es una capa superior; no se puede añadir ningún cliente nuevo mientras se editan las filas de Orders u Order Details.)

5.2 Gestionar las transgresiones de concurrencia El control de concordancia impide que el usuario sobrescriba las modificaciones realizadas por otros usuarios en los datos de una misma tabla base. Como el número de usuarios que acceden simultáneamente a una aplicación de edición de datos ADO.NET 2.0 va en aumento, el tipo de control de concurrencia que utilice la aplicación en la edición de datos juega un papel cada vez más importante para determinar la disponibilidad de los datos. La disponibilidad de los datos es el factor determinante en la escalabilidad de una aplicación que gestiona gran cantidad de datos. A continuación se explican los dos métodos más comunes para el control de la concurrencia: )

El control pesimista de concurrencia cierra con candado cualquier fila que está siendo modificada por un usuario. El candado impide que otros usuarios puedan leer o modificar las filas hasta que el primer usuario pase las modificaciones a la base de datos. Es una práctica común de concurrencia pesimista poner candados a

173

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 174

Bases de datos con Visual Basic todos los records hijo cuando un usuario actualiza un registro padre. En una caso así, un usuario que suspenda la actualización puede impedir el acceso a todos los demás usuarios a un número potencialmente muy alto de registros. )

El control optimista de concurrencia pone candado a las filas sólo mientras se actualizan, proceso que requiere entre 5 y 50 milisegundos. La aplicación comprueba si otros usuarios han actualizado las filas antes de pasar los cambios en los datos. Si otro usuario actualiza una fila después de que el usuario actual la lea, se produce una violación de concurrencia. A menos que la aplicación final contenga lógica de negocios para controlar qué actualización tiene preferencia, la última actualización se validará. El método "gana el último usuario", con el que se sobrescriben los cambios realizados anteriormente en la fila con los valores del usuario actual, no es un método de control de concurrencia. Con este método, el front end no implementa el control de concurrencia. El control de concurrencia es esencial para prácticamente todas las bases de datos front-end multi-usuario.

La arquitectura de componentes sin conexión requiere un control de concurrencia optimista en los entornos multiusuario. Los usuarios reciben una muestra de datos para mostrar en pantalla y editarlos. Las tablas de datos almacenan datos muestra como valores Original; los valores Current almacenan datos editados y no modificados. Conforme crece el número de datos que se editan (volatilidad) en la base de datos y la edad (latencia) de la muestra, la probabilidad de que se produzcan transgresiones de concurrencia también aumenta. Las transgresiones potenciales de concurrencia se pueden minimizar refrescando muestra inmediatamente antes de actualizar los registros con, por ejemplo, un botón Edit Records que elimine también las restricciones de sólo lectura (read-only). De todos modos, este método aumenta la carga en el servidor de la base de datos y en la red, por lo que la escalabilidad se reduce y no tiene demasiado sentido para los usuarios de portátiles, frecuentemente desconectados. A menos que se implemente un test de timeout, el usuario puede refrescar la muestra, ir a almorzar o tomarse un descanso y comenzar después la edición con una muestra antigua.

5.2.1 Control de concurrencia y cambios de transacción en ADO.NET 2.0 Los TableAdapters y DataSets tipificados de ADO.NET 2.0 requieren un nuevo método para escribir código de control optimista de concurrencia. Los juegos de datos sin conexión de ADODB proporcionan un sofisticado mecanismo para manejar las transgresiones de concurrencia al aplicar el método Recordset.UpdateBatch. Las transgresiones de concurrencia añaden miembros a la colección Errors, y aplicando Recordset.Filter=adFilterConflictingRecords se obtiene el conjunto de registros que contienen errores de concurrencia. Los juegos de datos sin conexión permiten invocar el método Recordset.Resync y deshacer todas las actualizaciones transgresoras. ADO.NET 1.x y 2.0 no tienen integrado nada parecido al método Resync, por lo que habrá que añadir código para restablecer los datos de la tabla base actual si se quiere dar a los usuarios la posibilidad de elegir entre sobrescribir los datos del servidor o deshacer sus actualizaciones pendientes.

174

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 175

Añadir código para validar datos y gestionar la concurrencia Los DataAdapters de ADO.NET 1.x y los TableAdapters de ADO.NET 2.0 detectan los errores de concurrencia en las operaciones UPDATE y DELETE incluyendo los valores originales de las operaciones DataTable.Fill en la cláusula WHERE. Actualizar el valor ProductID de un registro de un Order Details de entre 2 o 3, genera la siguiente sentencia SQL UPDATE: exec sp_executesql N UPDATE [dbo].[Order Details] SET [OrderID] = @OrderID,[ProductID] = @ProductID, [UnitPrice] = @UnitPrice, [Quantity] = @Quantity, [Discount] = @Discount WHERE (([OrderID] = @Original_OrderID) AND ([ProductID] = @Original_ProductID) AND ([UnitPrice] = @Original_UnitPrice) AND ([Quantity] = @Original_Quantity) AND ([Discount] = @Original_Discount)) , N @OrderID int,@ProductID int,@UnitPrice money,@Quantity smallint,@Discount real, @Original_OrderID int,@Original_ProductID int,@Original_UnitPrice money, @Original_Quantity smallint,@Original_Discount real , @OrderID = 11094, @ProductID = 3, @UnitPrice = $12.0000, @Quantity = 10, @Discount = 7.500000298023224e-002, @Original_OrderID = 11094, @Original_ProductID = 2, @Original_UnitPrice = $12.0000, @Original_Quantity = 10, @Original_Discount = 7.500000298023224e-002

La sentencia SQL anterior la capturó el Profiler del SQLServer 2005. Los valores decimales en la mantisa de Discount values son el resultado de utilizar el valor real (single-precision floating point) en lugar del valor decimal (4,2) o doble, como tipo de dato. Este error de redondeo tiene su origen en la base de datos de ejemplo Microsoft Access 1.0 Northwind.mdb, que fue la que adoptó el equipo del servidor SQL sin cambiar el tipo de dato. Cambiar de las conexiones, adaptadores de datos y manejadores de concurrencia de ADO.NET 1.x a los nuevos adaptadores de tabla y las fuentes vinculadas de ADO.NET 2.0, complica la gestión de la trasgresión de concurrencia. El juego de datos tipificado absorbe la gestión de la conexión a la base de datos y no expone propiedades importantes del DataAdatper.

5.2.2 Propiedades ocultas de conexión y transacción La mayoría de DBAs no permiten que las aplicaciones front-end accedan directamente a las tablas o actualicen tablas relacionadas sin envolver dentro de una transacción las operaciones de actualización múltiple. Sustituir procedimientos almacenados por sentencias SQL con el Asistente para la configuración de orígenes de datos, cumple el primero de los requisitos pero no el segundo. Con objetos SqlConnection y SqlDataAdapter es fácil implementar transacciones de parte del cliente, pero no con los componentes de datos de ADO.NET 2.0. Los juegos de datos tipificados de VS 2005 Beta 1 exponían las propiedades TableAdapter.Transaction y TransactionConnection como FriendPropertyTransaction() As System.Data.SqlClient.SqlTransaction y FriendReadOnlyPropertyConnection() As

175

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 176

Bases de datos con Visual Basic System.Data.SqlClient.SqlConnection. Posteriormente, estas propiedades se hicieron privadas, por lo que ya no se puede implementar transacciones directamente con los juegos de datos tipificados de ADO.NET 2.0. Los TableAdapters abren y cierran la conexión para cada comando, por lo que es imposible crear un objeto SqlTransaction utilizable. Las transacciones se concentran en una sola conexión que debe permanecer abierta hasta que se invoca el método Commit o Rollback. Tal como se verá más adelante en este libro, escribir código PartialClass DataSetName para permitir sólo un SqlConnection y crear su objeto SqlTransaction no es sencillo. Algunas DBAs pueden no aceptar las transacciones ADO.NET por parte del cliente como un sustituto de las transacciones T-SQL que envuelven los procedimientos almacenados. Antes de dedicar su tiempo a implementar una clase parcial, verifique que las DBAs que administran las bases de datos en cuestión son aptas para las transacciones por parte del cliente.

5.2.3 La propiedad ContinueUpdateOnError Los DataAdapters de ADO.NET 1.x y los TableAdapters de ADO.NET 2.0 proporcionan una propiedad ContinueUpdateOnError para la que se puede definir el valor True e impedir así las DBConcurrencyExceptions cuando se producen transgresiones de concurrencia después de ejecutar comandos DataAdapter.Update. Eliminar esas excepciones permite que múltiples actualizaciones –algunas de las cuales contienen transgresiones de concurrencia– se sigan desarrollando sin la intervención del usuario. Cuando se comete una trasgresión, el DataAdapter define la propiedad DataRow.RowError de su tabla de datos fuente con el valor "Concurrency violation: the UpdateCommand affected 0 of 1 records" o algo similar. Las filas que contienen errores aparecen un signo rojo de exclamación cuando se muestran en DataGridViews. Para resolver los errores hay que escribir un número substancial de líneas de código de lógica de negocios. La propiedad ContinueUpdateOnError es útil principalmente en las actualizaciones sencillas de una sola base de datos. Actualizar tablas relacionadas implica crear tres tablas de datos temporales por cada DataSet.DataTable, como se describía en el capítulo 4, y como se verá más adelante en este capítulo. Definir el valor True para la propiedad ContinueUpdateOnError en más de una tabla relacionada no es una buena práctica en la programación de bases de datos, ya que resolver las transgresiones de concurrencia en las tablas relacionadas es muy difícil.

5.2.4 Estrategias de control de concurrencia Antes de empezar a escribir código de control de concurrencia, usted y su cliente consultor, o el propietario de las aplicaciones, deberían acordar qué especificación van a usar para el control de concurrencia. Los siguientes puntos cubren los elementos de especificación más importantes a la hora de manejar las transgresiones de concurrencia: )

176

¿Habría que permitir que las actualizaciones se realizaran con más de una entidad de datos, como un registro de cliente o una orden de venta (pedido), sin guardar en el servidor los datos de cada entidad individual? En los entornos de conexión LAN de conexión permanente, es una buena práctica guardar los cambios en una

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 177

Añadir código para validar datos y gestionar la concurrencia entidad de datos antes de crear o editar otra. De todos modos, los usuarios de portátil frecuentemente desconectados, no necesitan actualizar múltiples entidades antes de reconectarse a la red y guardar sus cambios. Las actualizaciones de múltiples entidades implican procesar la actualización fila por fila para permitir el control de concurrencia, tal como se describe en el apartado más adelante en este capítulo. Procesar archivos de actualización temporal como batch complica los tests de concurrencia en los registros hijo. Para añadir nuevas entidades múltiples no es necesario el control de concurrencia. )

)

)

)

)

¿Se permitirá a todos los usuarios decidir si sobrescriben o no los cambios de otros usuarios? Si sobrescribir cambios realizados por usuarios específicos se reserva para determinados tipos de usuario, todas las tablas deberían incluir una columna para identificar al último usuario que añadió o modificó datos en una fila. ¿Qué información se ha de dar al usuario para que pueda decidir con criterio si sobrescribir datos o no? En la mayoría de los casos, el usuario necesita ver los cambios realizados por otros en la fila; obtener esos datos implica acceder al servidor. Mostrar valores originales, además de las modificaciones en curso de los usuarios, es práctico pero no realmente esencial. ¿Son suficientes los mensajes con la información anterior para resolver los conflictos o se necesita una UI más compleja, como un DataGridView u otro formulario? Los mensajes suelen ser suficientes, pero tal vez sería conveniente un cuadro desplegable, o una página tabular, para tratar las transgresiones en estructuras complejas de datos. ¿Debería una sola trasgresión de concurrencia impedir cualquier cambio de actualización o simplemente deshacer los realizados hasta ahora? Para deshacer los realizados hasta ahora se necesita una transacción por parte del usuario, lo cual no es tarea fácil con los componentes de datos, como se verá más adelante en este libro. Y todavía es más complicado asignar una transacción específica a una entidad concreta de datos cuando se está permitiendo actualizar múltiples entidades de datos en una sola operación. ¿Necesitan los usuarios poder regenerar un nuevo pedido si otro usuario ha borrado todos los datos del pedido del servidor? El proceso de recrear el pedido es relativamente sencillo, pero habría que comprobar que el pedido se ha borrado realmente antes de abordar una operación de actualización o modificar los registros hijos. El código para comparar el número de registros hijo detectará el pedido borrado, pero ese código no tiene la capacidad de regenerar el pedido.

Las especificaciones que se acuerden serán un factor determinante en el número de horas que se dediquen a escribir y depurar el código para la gestión de concurrencia, tal como verá en los apartados siguientes y al explorar el proyecto.

5.2.5 Los "vínculos perdidos" en la gestión de la concurrencia Al trabajar con tablas relacionadas, hay muchos usuarios que pueden insertar o borrar filas hijo de tablas padre. Las filas hijo insertadas recientemente no son visibles para los

177

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 178

Bases de datos con Visual Basic usuarios que trabajan con snapshots "pasados". Las filas hijo borradas tampoco se detectan a menos que el usuario que realiza la actualización los modifique y aplique después el método ChildTableAdapter.Update. Antes de detectar que se han borrado datos en la tabla hijo, el usuario que está actualizando puede haber modificado la fila padre, o haber añadido o borrado filas hijo durante la actualización. Alterar registros en la tabla base antes de detectar los registros hijo añadidos o borrados puede ser peligroso. Por ejemplo, un médico con un portátil o un PC de bolsillo desconectado de la red podría alterar el tratamiento de un paciente, los medicamentos o las dosis, sin saber que otro empleado de sanidad ha añadido o borrado ya un medicamento o dosis. Cuando el médico se reconecta a la red y actualiza la base de datos, los datos añadidos o borrados que no se habían visto pueden ser una amenaza vital para la salud del paciente o, incluso, su vida. Existen muy pocos artículos o código de ejemplo sobre control de concurrencia, incluidos los de la ayuda online de VS 2005, que incluyan tests para detectar los fallos de concurrencia en los records hijo. Esta omisión resulta sorprendente si se considera el impacto potencial que pueden tener las modificaciones no detectadas de otro usuario.

5.2.6 Detectar los fallos de concurrencia en los registros hijo Para detectar los datos añadidos o borrados en los registros hijo por otros usuarios, antes de ejecutar ninguna actualización en filas hijo o maestro, es necesario comparar el número de registros hijo del servidor con los de la tabla de datos local del usuario que está actualizando. Los comandos UPDATE autogenerados por el TableAdaptar no incluyen este tipo de test, por lo que habrá que añadir código para detectar la diferencia numérica de filas hijo. La siguiente función de ejemplo obtiene el contador del Order Details actual del servidor, ajusta la cuenta local de filas que hay que añadir o borrar en operaciones posteriores de actualización, si es necesario, y devuelve True si los valores contados concuerdan: Private ByVal Dim Dim Dim Dim Try

Function TestNumberOfDetails(ByVal intOrderID As Integer, _ intAdded As Integer) As Boolean strConn As String = My.Settings.NorthwindConnection cnNwind As New SqlConnection(strConn) cmCurrent As New SqlCommand("", cnNwind) intCurrent As Integer cnNwind.Open() Dim strSQL As String = "SELECT COUNT(*) FROM [Order Details] WHERE " + _ "OrderID = " + intOrderID.ToString With cmCurrent .CommandType = CommandType.Text .CommandText = strSQL intCurrent = CInt(.ExecuteScalar) End With cnNwind.Close() If intCurrent = 0 Then

178

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 179

Añadir código para validar datos y gestionar la concurrencia Return True End If Dim dvDetails As New DataView Dim intCount As Integer With dvDetails .Table = NorthwindDataSet.Order_Details .Sort = "OrderID" Dim drvDetails As DataRowView() drvDetails = .FindRows(intOrderID) intCount = drvDetails.Length .Dispose() End With If intCurrent = intCount - intAdded Then Return True Else Return False End If Catch exc As Exception MsgBox(exc.Message, MsgBoxStyle.Exclamation, _ "Can’t Retrieve Current Server Data") Return False Finally If Not cnNwind.State = ConnectionState.Closed Then cnNwind.Close() End If cmCurrent.Dispose() cnNwind.Dispose() End Try End Function

La entrada de la ayuda en línea " Sorting and Filtering Data Using a DataView" sugiere que crear un DataView de la tabla y aplicar el método DataView.FindRows para devolver un array de objetos DataRowView es mucho más rápido que devolver un juego de DataRows de un objeto filtrado DataView. El valor de la propiedad DataView.Sort se ha de definir de acuerdo con el nombre de la columna apropiada para aplicar el método DataView.FindRows. La eficiencia de ambos métodos será bastante parecida si se trabaja con un número pequeño de filas. Aplique el test anterior antes de invocar los métodos TableAdapter.Update para las tablas padre e hijo –Orders y Order Details en este ejemplo. El proyecto de ejemplo utiliza una consulta SQL, pero se puede sustituir fácilmente por un procedimiento almacenado.

5.2.7 Detectar otros conflictos potenciales de concurrencia Otra práctica recomendable es testar todas las filas hijo por posibles errores de concurrencia, y no sólo las filas modificadas por el usuario en un DataGridView. Para ello se ha de aplicar el método SetModified a todas las filas cuando el usuario cambia un valor en el DataGridView. A modo de ejemplo, el siguiente manejador de evento DataGrid179

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 180

Bases de datos con Visual Basic View_CellValueChanged define todas las filas Order Details de la tabla actual Orders como Modified cuando el usuario cambia un solo valor de celda: Private Sub Order_DetailsDataGridView_CellValueChanged(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles Order_DetailsDataGridView.CellValueChanged If Order_DetailsDataGridView.Enabled Then SaveOrdersToolStripButton.Visible = True CancelOrdersEditToolStripButton.Visible = True Dim blnMarkAllRows As Boolean = True 'For testing Dim intRow As Integer If blnMarkAllRows Then Try Dim objCurrent As Object = _ FK_Order_Details_OrdersBindingSource.Current If Not objCurrent Is Nothing Then Dim drvCurrent As DataRowView = CType(objCurrent, DataRowView) Dim strOrderID As String = drvCurrent.Item(0).ToString With NorthwindDataSet.Order_Details Dim drDetails As DataRow() = .Select("OrderID = " + strOrderID) If drDetails.Length > 0 Then For intRow = 0 To drDetails.Length - 1 If drDetails(intRow).RowState = DataRowState.Unchanged Then Try drDetails(intRow).SetModified() Catch exc As Exception End Try End If Next End If End With End If Catch exc As Exception MsgBox(exc.Message + exc.StackTrace) End Try End If End If End Sub

El método DataTable.Select devuelve un array de objetos DataRow que se duplica al aplicar el nuevo método DataRow.SetModified. Antes hay que comprobar cada fila con el método DataRow.DataRowState, ya que el método SetModified sólo se puede aplicar a las filas no modificadas.

180

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 181

Añadir código para validar datos y gestionar la concurrencia

5.2.8 Permitir a los usuarios re-ccrear los pedidos borrados Las entradas individuales de datos pueden borrar, inadvertida o maliciosamente, algún pedido activo existente. Si el DataSet de un usuario que está actualizando datos incluye una copia del pedido borrado, se puede añadir código para poder reinstaurar el pedido borrado con las actualizaciones realizadas. La función IsOrderModifiedOrDeleted, que veremos a continuación, devuelve un miembro de la enumeración OrderServerStatus. Esta versión no comprueba los registros Order Details modificados en el servidor porque los errores de concurrencia ya detectan esas modificaciones. En una versión de producción, esta función podría incluir el código de gestión de concurrencia descrito más adelante en este capítulo.

El juego inicial de tests impide que se comprueben los registros de Orders y Order Detail que el usuario haya añadido al DataSet local antes de guardar las actualizaciones en el servidor. En ese caso, DataRowState es Added. Dése cuenta de que para comprobar los registros de Order Details debe utilizar el nuevo valor temporal de OrderID asignado por la tabla de datos (intOrigID), no el valor de OrderID del nuevo registro de Orders (intOrderID). Esto es así porque la secuencia de actualización inserta los registros de Order antes que los de Order Details. Private Function IsOrderModifiedOrDeleted(ByVal intOrderID As Integer, _ ByVal intProductID As Integer, ByVal intOrigID As Integer) As OrderServerStatus Dim eStatus As OrderServerStatus Dim drAdded As DataRow If intProductID = 0 Then drAdded = NorthwindDataSet.Orders.FindByOrderID(intOrderID) If Not drAdded Is Nothing Then If drAdded.RowState = DataRowState.Added Then Return OrderServerStatus.NewRow End If End If Else If intOrigID <> intOrderID Then drAdded = _ NorthwindDataSet.Order_Details.FindByOrderIDProductID(intOrigID, _ intProductID) If drAdded Is Nothing Then Return OrderServerStatus.Unmodified Else If drAdded.RowState = DataRowState.Added Then Return OrderServerStatus.NewRow End If End If End If End If Dim dtModified As DataTable = NorthwindDataSet.Orders.Clone

181

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 182

Bases de datos con Visual Basic Dim strSQL As String = "SELECT * FROM Orders " + _ "WHERE OrderID = " + intOrderID.ToString Dim strMsg As String = Nothing Dim strConn As String = My.Settings.NorthwindConnection Dim cnNwind As New SqlConnection(strConn) Dim cmCurrent As New SqlCommand(strSQL, cnNwind) Try cnNwind.Open() Dim sdrCurrent As SqlDataReader = cmCurrent.ExecuteReader With sdrCurrent If .HasRows Then dtModified.Load(sdrCurrent, LoadOption.OverwriteRow) Else eStatus = OrderServerStatus.DeletedOnServer End If .Close() .Dispose() End With cnNwind.Close() Catch exc As Exception MsgBox(exc.Message, MsgBoxStyle.Exclamation, _ "Can’t Retrieve Current Server Data") eStatus = OrderServerStatus.ServerInaccessible Finally If Not cnNwind.State = ConnectionState.Closed Then cnNwind.Close() End If cnNwind.Dispose() cmCurrent.Dispose() End Try 'Test the local DataSet Try Dim drCurrent As DataRow = NorthwindDataSet.Orders.FindByOrderID(intOrderID) If drCurrent Is Nothing Then eStatus = OrderServerStatus.DeletedLocally ElseIf eStatus <> OrderServerStatus.DeletedOnServer Then If intProductID > 0 Then drAdded = _NorthwindDataSet.Order_Details. .FindByOrderIDProductID(intOrderID, _ intProductID) If drAdded Is Nothing Then eStatus = OrderServerStatus.DeletedLocally Else If drAdded.RowState = DataRowState.Added Then eStatus = OrderServerStatus.NewRow Else eStatus = OrderServerStatus.Unmodified

182

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 183

Añadir código para validar datos y gestionar la concurrencia End If End If End If End If If eStatus = OrderServerStatus.DeletedOnServer Then strMsg = “Another user has deleted order “ + intOrderID.ToString + _ “ from the server.” + vbCrLf + vbCrLf + “Click Yes if you agree “ _ “that the order should be deleted.” + vbCrLf + vbCrLf + _ “Click No to create a new order with your current order data “ + _ “and notify the customer of the OrderID change.” If MsgBox(strMsg, MsgBoxStyle.Exclamation Or MsgBoxStyle.YesNo, _ "Order " + intOrderID.ToString + " Deleted from Database") = _ MsgBoxResult.Yes Then Dim drRows As DataRow() Dim drRow As DataRow drRows = NorthwindDataSet.Order_Details.Select("OrderID = " + _ intOrderID.ToString) If drRows.Length > 0 Then For Each drRow In drRows If drRow.RowState = DataRowState.Added Then drRow.AcceptChanges() End If drRow.Delete() drRow.AcceptChanges() Next End If drRows = NorthwindDataSet.Orders.Select("OrderID = " + _ intOrderID.ToString) If drRows.Length > 0 Then drRows(0).Delete() drRows(0).AcceptChanges() End If eStatus = OrderServerStatus.DeletedLocally Else Dim drRows As DataRow() drRows = NorthwindDataSet.Orders.Select("OrderID = " + _ intOrderID.ToString) If drRows.Length > 0 Then drRows(0).AcceptChanges() drRows(0).SetAdded() End If Dim drRow As DataRow drRows = NorthwindDataSet.Order_Details.Select("OrderID = " + _ intOrderID.ToString) If drRows.Length > 0 Then For Each drRow In drRows drRow.AcceptChanges()

183

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 184

Bases de datos con Visual Basic If drRow.RowState = DataRowState.Deleted Then Stop Else drRow.SetAdded() End If Next End If eStatus = OrderServerStatus.AddedLocally End If End If Catch exc As Exception MsgBox(exc.Message + exc.StackTrace) End Try Return eStatus End Function Private Enum OrderServerStatus As Integer Unmodified = 0 NewRow = 1 DeletedLocally = 2 DeletedOnServer = 3 ModifiedOnServer = 4 ServerInaccessible = 5 AddedLocally = 6 DeletedFromAddedOrder = 7 End Enum

Pulsando el botón Sí en el mensaje borra las filas locales del pedido y sus registros hijo al definir DataRowState como Deleted y aplicar el método AcceptChanges. Pulsar el botón No genera un nuevo pedido al definir DataRowState como Added y aplicar el método AcceptChanges.

5.3 Anticipar las transgresiones de restricción de clave primaria basada en valores Usar una columna (GUID) int identity o unique identifier como clave primaria de la tabla base, elimina las transgresiones potenciales de la clave primaria. Si la clave primaria de la tabla es un campo de caracteres, como la columna CustomerID de 5 caracteres en la tabla Customers, al añadir un nuevo cliente con un valor CustomerID ya existente se obtendrá una SqlException por trasgresión de clave primaria. En un caso así, el usuario debe elegir entre cancelar los datos añadidos o editar el valor de clave primaria e intentar de nuevo la actualización. Para que esta elección sea posible hay que proporcionar el código necesario para que el usuario obtenga del servidor los datos en conflicto; por ejemplo, el código que vemos resaltado en negrita en el siguiente listado: El código del manejador de eventos SqlException de la función UpdateBaseTables llama la función siguiente ResolveDuplicateCustomerID si el mensaje de excepción contiene PK_Customers.

184

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 185

Añadir código para validar datos y gestionar la concurrencia Private Function ResolveDuplicateCustomerID(ByVal strCustomerID As String) As Boolean Dim strSQL As String = "SELECT * FROM Customers " + _ "WHERE CustomerID = ‘" + strCustomerID + "‘" Dim strConn As String = My.Settings.NorthwindConnection Dim cnNwind As New SqlConnection(strConn) Dim cmCurrent As New SqlCommand(strSQL, cnNwind) Dim objCurrent(10) As Object Try cnNwind.Open() Dim sdrCurrent As SqlDataReader = cmCurrent.ExecuteReader With sdrCurrent .Read() .GetValues(objCurrent) .Close() .Dispose() End With cnNwind.Close() Catch exc As Exception MsgBox(exc.Message, MsgBoxStyle.Exclamation, _ "Can’t Retrieve Current Server Data") Finally If Not cnNwind.State = ConnectionState.Closed Then cnNwind.Close() End If cnNwind.Dispose() cmCurrent.Dispose() End Try Dim intOrders As Integer With NorthwindDataSet.Orders Dim drRows As DataRow() drRows = .Select("CustomerID = ‘" + strCustomerID + "‘") intOrders = drRows.Length End With Dim strDetails As String = Nothing Dim intCol As Integer Dim strColName As String = Nothing With NorthwindDataSet.Customers For intCol = 0 To objCurrent.Length - 1 If intCol = 0 Then frmCustomer.Text = "Current Customers Record for CustomerID ‘" + _ objCurrent(0).ToString + "‘" End If strColName = .Columns(intCol).ColumnName Dim strPad As New String(" "c, 13 - Len(strColName)) strDetails += strColName + ":" + strPad + _ objCurrent(intCol).ToString + vbCrLf

185

VisualBasic2005_05.qxp

02/08/2007

18:26

PÆgina 186

Bases de datos con Visual Basic Next intCol End With frmCustomer.txtDetails.Text = strDetails Dim strMsg As String = "CustomerID ‘" + strCustomerID + _ "‘ exists on the server. Review the customer information " + _ "below to determine if it duplicates your new customer entry. " + _ "If so, click Cancel New Customer. Otherwise click Edit New " + _ "Customer, modify the CustomerID value, and click Save again." If intOrders > 0 Then strMsg += vbCrLf + vbCrLf + "You have " + intOrders.ToString + _ " order(s) pending for ‘" + strCustomerID + "‘. New orders will " + _ "be preserved in either case." blnSaveNewOrders = True Else strMsg += vbCrLf + vbCrLf + "There are no " + _ "orders pending for ‘" + strCustomerID + "‘." End If frmCustomer.lblMessage.Text = strMsg If frmCustomer.ShowDialog = Windows.Forms.DialogResult.Cancel Then frmCustomer.Dispose() CustomerIDToolStripComboBox.Items.Remove(strCustomerID) With OrdersBindingSource With NorthwindDataSet.Customers Dim rowDup As DataRow rowDup = .FindByCustomerID(strCustomerID) If Not rowDup Is Nothing Then rowDup.AcceptChanges() End If End With With CustomerIDToolStripComboBox .Text = .Items(0).ToString End With GetCustomerOrdersToolStripButton.PerformClick() SaveCurrentDiffGram() Return False End With Else frmCustomer.Dispose() NewCustomerControlState(True) EnableOrdersGrid(True, False) EnableOrder_DetailsGrid(True, False) blnSyncCustomerID = True blnAutoContinue = False LockTextBoxes(False, False) Return False End If End Function

186

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 187

Añadir código para validar datos y gestionar la concurrencia El método SqlDataReader.GetValues devuelve un array Object de valores de campo. La siguiente figura muestra el cuadro de diálogo después de haber poblado el cuadro de texto con los datos existentes en el servidor, con el código subrayado en negrita en la función ResolveDuplicateCustomerID.

5.4 Manejar elegantemente los errores de concurrencia El método más sencillo para tratar las DBConcurrencyExceptions y los números no concordantes de registros hijo es mostrar un sencillo mensaje de advertencia. El usuario o el código deberán actualizar el DataSet con los valores de la tabla base de la fila discordante y sus filas hijo para eliminar las transgresiones de concurrencia. La actualización sobrescribe los cambios introducidos por el usuario, que no se podrán restablecer desde los valores originales de las tablas de datos. En este punto, los valores actuales y originales de la tabla de datos son idénticos, ya que el valor de la propiedad TableAdapter.AcceptChangesDuringFill es True. Es posible almacenar los valores originales antes de actualizar y dejar que el usuario los restablezca para compararlos, pero el código requerido para esta operación es complicado y poco elegante. Los valores originales no actualizados también pueden producir posteriores transgresiones de concurrencia. El mejor sistema es interrogar al servidor por los datos actuales cuando se produzcan errores durante la actualización online. Eso significa que las actualizaciones del usuario se han de procesar fila por fila y no por batches: '1. Delete Order Details records If Not DelDetails Is Nothing Then Order_DetailsTableAdapter.Update(DelDetails) Order_DetailsTableAdapter.FillOrder_Details(NorthwindDataSet.Order_Details, _ strCustomerID)

187

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 188

Bases de datos con Visual Basic intChanges += DelDetails.Count End If '2. Delete Orders records If Not DelOrders Is Nothing Then DelOrders.TableName = "DelOrders" OrdersTableAdapter.Update(DelOrders) OrdersTableAdapter.FillOrders(NorthwindDataSet.Orders, _ strCustomerID) intChanges += 1 End If '3. Insert New Customers records If Not NewCustomers Is Nothing Then CustomersTableAdapter.Update(NewCustomers) CustomersTableAdapter.GetCustomerOrders(NorthwindDataSet.Customers, _ strCustomerID) intChanges += NewCustomers.Count End If

Si durante una actualización se produce una DBConcurrencyException, puede examinar la fila de datos afectada para comprobar sus valores originales y los corrientes. No obstante, la fila contiene el valor original que causó el error, no el valor del servidor. La fila de datos que arrojó una excepción durante un borrado no se puede leer, si intenta hacerlo obtendrá una DeletedRowInaccessibleException.

5.4.1 Obtener datos actuales del servidor Tanto actualizar como borrar requiere los datos actuales del servidor para encontrar los valores de campo en conflicto, y para eso es necesario el valor OrderID de la fila Orders o el OrderID y ProductID de la fila Order Details que se están procesando. A modo de ejemplo, el siguiente extracto de la función UpdateBaseTables restablece los valores OrderID y ProductID de un DataView de la fila actual de la tabla temporal DelDetails creada con un parámetro DataViewRowState.Deleted. Para obtener los valores de filas marcadas con Deleted hay que crear un objeto DataView. If Not DelDetails Is Nothing Then strTableErr = "Order Details" Dim intPreviousID As Integer intChanges += DelDetails.Count While DelDetails.Count > 0 'Deletion ultimately removes all rows from DelDetails, 'so use the first row when deleting 'Get the OrderID (a DataView is the only way possible with a deleted row) Dim dvDeleted As New DataView(DelDetails, Nothing, Nothing, _ DataViewRowState.Deleted) intOrderID = CInt(dvDeleted.Item(0).Item(0)) Dim intProductID As Integer = CInt(dvDeleted.Item(0).Item(1)) dvDeleted.Dispose() Dim eStatus As OrderServerStatus = _

188

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 189

Añadir código para validar datos y gestionar la concurrencia IsOrderModifiedOrDeleted(intOrderID, intProductID, intOrigID) Dim drUpdate As DataRow = DelDetails.Rows(0) If eStatus = OrderServerStatus.DeletedOnServer Then 'Don’t attempt to delete orders added by others Else If intOrderID = intPreviousID Or eStatus = _ OrderServerStatus.DeletedLocally Then 'Don’t test number of Order details Order_DetailsTableAdapter.Update(drUpdate) Else 'Only test the first deletion for an OrderID, subsequent tests mismatch If TestNumberOfDetails(intOrderID, 0, False) Then Order_DetailsTableAdapter.Update(drUpdate) Else Return False End If End If intPreviousID = intOrderID End If End While blnSkipCount = False End If

El valor de retorno False hace que el bucle vuelva al principio de la función UpdateBaseTables cuando el usuario pulsa el botón Sí para continuar con la actualización en el cuadro de mensaje que se abre después de procesar una excepción. Encontrar los valores del servidor no resuelve el error de concurrencia. Para resolverlo hay que añadir código que redefina los valores originales de la fila en cuestión, lo cual no es tarea fácil. Se puede actualizar el DataRow con los valores del servidor y aplicar el método AcceptChanges para actualizaciones, con lo cual se sobrescribe la actualización del usuario, pero entonces tampoco se podrán cambiar los valores de una fila de datos (DataRow) borrada porque no será accesible. La propiedad DataRow de un DataViewRow es de sólo lectura (read-only), por lo que este método no permite modificar los valores o el DataRowState de una fila borrada.

El siguiente diagrama de flujo sirve para bases de datos actualizables. Contiene tests para los pedidos borrados en el servidor, contar las discordancias de la tabla hijo y los errores de concurrencia. Pasando los datos borrados por todos los pasos de la función UpdateBaseTables, del primero al último, permite comprobar las filas antes de que sean potencialmente borradas en el servidor. Resolver con éxito las excepciones de concurrencia es el tema del siguiente apartado.

189

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 190

Bases de datos con Visual Basic

Los cuadros sombreados en el diagrama representan operaciones UpdateBaseTables; los cuadros con esquinas redondeadas representan la función IsOrderModifiedOrDeleted. El resto de cuadros incluyen el nombre de la función entre paréntesis.

5.4.2 Restablecer y comparar los valores de celda del servidor y el cliente El manejador DBConcurrencyException llama la función ResolveConcurrencyErrors, el cual compara los valores de fila de la tabla con los del servidor. ResolveConcurrencyErrors junto con todas las funciones y procedimientos relacionados ocupa más de 500 líneas de código, por lo que no hemos considerado adecuado incluir aquí el listado completo. El segundo diagrama de flujo es la versión simplificada de la función ResolveConcurrencyErrors.

190

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 191

Añadir código para validar datos y gestionar la concurrencia

El elemento más importante de la función ResolveConcurrencyErrors es el código que compara los valores del cliente y el servidor para generar la lista de valores y el nombre de columna de la sentencia SQL UPDATE tal como se muestran en el listado siguiente. La comparación requiere el tratamiento de los potenciales valores DBNull y las diferencias insignificantes de los valores en los tipos de datos System.Decimal y System.DateTime. 'Create the error message and complete the UPDATE statement Dim intErrors As Integer Dim strMsg As String = Nothing Dim blnHasError As Boolean For intCtr = 0 To objCurrent.Length - 1 If objCurrent(intCtr) Is Nothing Then Exit For End If

191

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 192

Bases de datos con Visual Basic If strColTypes(intCtr) = "System.Decimal" Then If objCurrent(intCtr) Is DBNull.Value Then objCurrent(intCtr) = 0 End If If objUpdate(intCtr) Is DBNull.Value Then objUpdate(intCtr) = 0 End If If CType(objCurrent(intCtr).ToString, Decimal) <> _ CType(objUpdate(intCtr).ToString, Decimal) Then blnHasError = True Else blnHasError = False End If ElseIf strColTypes(intCtr) = "System.DateTime" Then If objCurrent(intCtr) Is DBNull.Value Then objCurrent(intCtr) = "1/1/0001" End If If objUpdate(intCtr) Is DBNull.Value Then objUpdate(intCtr) = "1/1/0001" End If If CType(objCurrent(intCtr).ToString, Date) <> _ CType(objUpdate(intCtr).ToString, Date) Then blnHasError = True Else blnHasError = False End If ElseIf objCurrent(intCtr).ToString <> objUpdate(intCtr).ToString Then blnHasError = True Else blnHasError = False End If If blnHasError Then strSQL += strColNames(intCtr) + " = " If strColTypes(intCtr) = "System.String" Or strColTypes(intCtr) = _ "System.DateTime" Then strSQL += "‘" + objUpdate(intCtr).ToString + "‘, " Else strSQL += objUpdate(intCtr).ToString + ", " End If strMsg += strColNames(intCtr) + " in database is ‘" + _ objCurrent(intCtr).ToString + "‘ and your current entry is ‘" + _ objUpdate(intCtr).ToString + "‘." + vbCrLf intErrors += 1 End If Next strMsg = Replace(strMsg, "1/1/0001", "(null)") strMsg = Replace(strMsg, " 12:00:00 AM", "") strSQL = Replace(strSQL, "‘1/1/0001’", "NULL")

192

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 193

Añadir código para validar datos y gestionar la concurrencia A continuación vemos la sentencia SQL UPDATE para actualizar un cliente con cuatro campos modificados localmente, incluida la definición del valor RequiredDate en DBNull.Value escribiendo (null) en la celda: UPDATE Orders SET EmployeeID = 1, OrderDate = ‘9/3/2004 12:00:00 AM’, RequiredDate = NULL, Freight = 15.50 WHERE OrderID = 11207

La siguiente figura muestra los dos cuadros de mensaje que se abren cuando hay errores de concurrencia que no implican diferencias en la cuenta de los registros hijo. Si el contador de registros hijos difiere, todas las actualizaciones de usuario pendientes se sobreescriben.

Si pulsa el botón Sí se ejecuta la sentencia UPDATE y un cuadro de mensaje indica que la operación se ha realizado con éxito. Si pulsa el botón No, se desplega un OrderDetailsForm con los datos del usuario como referencia si se rehace la actualización (ver la figura siguiente). Ese mismo formulario es el que se abre cuando hay una discordancia en el contador de un registro hijo.

5.5 Trabajar con usuarios desconectados Cuando se trabaja con conexión permanente se puede aplicar el código de lógica de negocios que implica que los usuarios perpetuarán los cambios de la entidad de datos actual en el servidor de la base de datos antes de añadir, editar o borrar otra entidad. Uno de los objetivos principales de los juegos de datos de ADO.NET es permitir que los 193

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 194

Bases de datos con Visual Basic usuarios frecuentemente desconectados también puedan acumular adiciones, modificaciones y borrados de entidades en un DataSet que el proyecto perpetúe en formato diffgram como archivo local XML. El usuario de portátil descarga los datos que desee del servidor y después se desconecta de la red. Más tarde, se conecta de nuevo, guarda en el servidor los cambios acumulados y hace un refresco del juego de datos local cargando y mezclando los datos que han sido modificados desde la última operación. La latencia de los datos es un problema mucho más serio para los usuarios que trabajan desconectados que pueden tener cien o más actualizaciones pendientes cuando están offline un día o dos. En ese caso, reconectarse y procesar las actualizaciones inevitablemente provoca conflictos de concurrencia con las tablas de bases de datos moderadamente volátiles. Las "mejores prácticas" dictan implementar la gestión de concurrencia para todos los proyectos que soportan actualización de datos offline. Puede escoger saltarse las discordancias de número de los registros hijo y regeneración de los juegos de datos borrados, pero, de todos modos, implementar características similares a las de la función ResolveConcurrencyErrors es un requisito absolutamente imprescindible en las aplicaciones de producción.

5.5.1 Crear y gestionar juegos de datos offline A los usuarios que trabajan sin conexión se les debe proporcionar los juegos de datos y los deben mantener actualizados offline. A continuación describimos las acciones básicas necesarias para mantener al día los juegos de datos offline: )

)

)

)

)

Los usuarios nuevos desconectados deben obtener un juego de datos inicial del servidor y guardarlo en un archivo local XML diffgram. El apartado siguiente describe cómo establecer el juego de registros de la tabla base necesario para las operaciones iniciales y posteriores de carga de juegos de datos. Durante la entrada de datos offline, guardar los cambios del registros padre o las actualizaciones de los registros descendientes debe hacer que se guarde nuevo el archivo diffgram local. Guardar cada cambio garantiza que las actualizaciones no desaparecerán si hay un fallo en la aplicación o se va la luz. Cerrando el formulario principal de la aplicación debe quedar guardado el archivo diffgram local. Al reconectar, las tablas se actualizan automáticamente con los cambios realizados offline. Una vez completada la actualización, el usuario offline debe restablecer un snapshot de los contenidos de la tabla base para repoblar el juego de datos local y guardar el archivo diffgram. De lo contrario, los valores actuales sólo quedarán reflejados en los registros que ha actualizado el usuario.

En el siguiente diagrama de flujo simplificado se compara la entrada de datos y el proceso de actualización entre usuarios siempre conectados y usuarios online pero que trabajan desconectados. Los nombres de los procedimientos y las funciones aparecen entre paréntesis.

194

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 195

Añadir código para validar datos y gestionar la concurrencia

5.5.2 Activar el tratamiento de registros padre múltiples Los usuarios conectados normalmente abren un solo registro padre y restablecen los registros hijo del servidor. La mayoría de usuarios desconectados tienen que actualizar más de un registro padre y otro hijo; un vendedor o cajero puede necesitar registros offline de más de cien clientes. A continuación vemos las consideraciones más importantes en el diseño de los cambios necesarios para que un front-end de clientes siempre conectados para que pueda soportar también usuarios desconectados: 195

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 196

Bases de datos con Visual Basic )

)

)

)

)

)

)

)

)

196

Los usuarios desconectados tienen una selección de registros limitada a los disponibles en su juego de datos offline. El proyecto de ejemplo descansa en una lista, separada por comas, de valores CustomerID guardados en User.config para cargar la lista desplegable de CustomerID (los usuarios conectados tienen un cuadro combinado que les permite escribir cualquier CustomerID válido en el cuadro de texto de la lista). Si los registros padre son auto-asignados, un sencillo formulario con un cuadro de lista comprobado de registros padre permite al usuario qué registros quiere incluir. El proyecto de ejemplo no incluye este formulario. Si un supervisor asigna la lista de la tabla padre, la base de datos debe incluir una tabla de asignación que la aplicación cliente leerá para crear el juego de datos inicial y después de completar la actualización offline para manejar los cambios en la asignación. A menos que la aplicación requiera un historial completo de las transacciones de un cliente, los registros derivados pueden limitarse a un número específico de registros, un periodo de tiempo o un campo especial. Más adelante, se explica cómo utilizar consultas Fillby SELECT TOP n o procedimientos almacenados para limitar el número de registros derivados. A continuación vemos un resumen de los cambios más importantes a realizar en el código de la aplicación de ejemplo, necesarios para permitir las actualizaciones de los usuarios desconectados: El valor CustomersTableAdapter.Count puede determinar si un usuario desconectado se ha reconectado a la red. Los usuarios desconectados pueden operar en modo conectado después de aplicar sus actualizaciones offline, de modo que este no es un test fiable para actualizar el juego de datos local después de completar las actualizaciones offline. En la versión de producción, la existencia de usuarios del tipo siempre conectados queda revelada por la ausencia de un archivo local diffgram. El botón Get Orders ToolStrip no es visible en el modo sin conexión. Cambiando la propiedad SelectedIndex de la lista CustomerID llama el manejador de evento GetCustomerOrdersToolStripButton_Click, el cual filtra la CustomersBindingSource para incluir sólo la fila apropiada. Cambiar el valor de la propiedad DataSource del OrdersDataGridView de OrdersDataConnector a FK_Customers_OrdersDataConnector, no funcionará. Aquí hay que aplicar el mismo filtro a OrdersBindingSource para incluir sólo las filas del cliente seleccionado en el OrdersDataGridView. Añadir un cliente nuevo requiere definir el valor Nothing para CustomersBindingSource.Filter, o aplicar el método RemoveFilter, y proporcionar una cadena de filtro no válido a la OrdersBindingSource. Estos filtros no devuelven filas de Orders u Order Details en los DataGridViews hasta que el usuario no guarda los cambios. Si se cancela la entrada del nuevo cliente, en pantalla se mostrará el registro Customers por defecto y sus registros relacionados.

VisualBasic2005_05.qxp

02/08/2007

18:27

PÆgina 197

Añadir código para validar datos y gestionar la concurrencia )

Procesar actualizaciones offline requiere sincronizar el contenido del formulario con el registro actual de cliente llamando el procedimiento SynchronizeOfflineOrders.

197

VisualBasic2005_06.qxp

02/08/2007

16:25

PÆgina 199

Capítulo 6

La aplicación de técnicas avanzadas de los DataSets DataSets y DataGridViews vinculados son los elementos centrales en el acceso a datos de ADO.NET 2.0 y las herramientas de Visual Studio 2005. Los dos capítulos anteriores trataban sobre los aspectos básicos en torno a los DataSets y formularios Windows vinculados. Este capítulo amplía las técnicas de programación de los elementos DataSet y DataGridView con los siguientes puntos principales: )

)

Permitir las transacciones ligeras de código en la actualización de las bases de datos. Añadir columnas a las DataTables y DataGridViews desde consultas SELECT con un INNER JOIN.

)

Mostrar y manipular imágenes en las DataGridViews.

)

Generar DataSets a partir de esquemas XML existentes.

)

Editar documentos XML con DataGridViews.

)

Crear y trabajar con clases de objetos serializables.

)

Vincular DataGridViews a colecciones genéricas DataList.

Todos, excepto uno, de los proyectos de ejemplo de este capítulo utilizan las bases de datos de ejemplo Northwind para proporcionar un número suficiente de registros y variedad de tipos de datos para demostrar el rendimiento relativo de las técnicas de acceso y edición de datos que se verán. En los ejemplos con tablas base sencillas, de pocas filas y columnas, y documentos o esquemas fuente en un sencillo XML, no se tratarán los problemas de rendimiento y otros aspectos del diseño de código que se verán en este capítulo. Para los ejemplos SystemTransactions.sln y DataGridViewImages.sln debe tener instalado SQL Server 2005 o SQL Server Express con las bases de datos de ejemplo Northwind y AdventureWorks. Los demás proyectos de ejemplo funcionan con SQL Server 2000, MSDE, SQL Server 2005 o SQLExpress y la base de datos Northwind.

199

VisualBasic2005_06.qxp

02/08/2007

16:25

PÆgina 200

Bases de datos con Visual Basic

6.1 Aplicar transacciones a las actualizaciones de DataSets Casi todas las DBAs requieren en "sus" tablas de producción operaciones de actualización, entrada y eliminación que se realicen con procedimientos almacenados y relacionados en una transacción. La transacción garantiza que todas las actualizaciones de cada tabla, en una operación batch, se realizarán con éxito (commit) o fallarán (roll back) en grupo. Tal como vimos anteriormente en este libro, ADO.NET 1.0 introducía la propiedad IDbCommand.Transaction y la interfaz IdbTransaction para la actualización con transacciones de múltiples tablas. Los objetos SqlTransaction y OracleTransaction son genuinos de CLR, OleDbTransaction y OdbcTransaction son envoltorios gestionados de los componentes de transacciones basados en OLE DB y ODBC COM. El ejemplo SqlTransaction es relativamente sencillo porque utiliza un par de métodos SqlCommand.ExecuteNonQuery que actualizaban las tablas dentro de una transacción local. De todas formas, los DataSets de ADO.NET 1.x requieren mucho más código para asignar un único objeto SqlTransaction a las propiedades UpdateCommand.Transaction, InsertCommand.Transaction y DeleteCommand.Transaction de múltiples SqlDataAdapters. Un procedimiento típico de ADO.NET 1.x para actualizar tablas base a partir de modificaciones simuladas realizadas por un usuario en tablas de datos sin conexión, incluye las siguientes acciones: 1. Crear un juego de datos no tipificado con un SqlDataAdapter por cada tabla de la transacción. 2. Crear un CommandBuilder para definir la propiedad ...Command de cada DataAdapter desde la sentencia SelectCommand o desde el procedimiento almacenado. 3. Abrir una SqlConnection, poblar las tablas de datos con el método DataAdapter.Fill y cerrar la conexión a la base de datos. 4. Modificar algunas filas de cada tabla de datos a modo de prueba. 5. Declarar e iniciar un objeto SqlTransaction. 6. Abrir la conexión a la base de datos y asignar la SqlTransaction a las tres propiedades de lenguaje de gestión de datos, en inglés Data Management Language (DML), ...Command.Transaction de cada DataAdapter. 7. Invocar el método Update en cada DataAdapter para que se ejecute el ...Command apropiado para cada valor de la propiedad DataRowState de cada fila modificada –los valores son: Added, Modified o Deleted. 8. Ejecutar la transacción si no se ha producido ningún error; de lo contrario, deshacer todos los pasos realizados y cerrrar la conexión a la base de datos. El siguiente código, en el que se incluyen las operaciones que acabamos de mencionar, muestra en negrita las instrucciones directamente relacionadas con el procesamiento de la SqlTransaction: Dim trnUpdate As SqlTransaction = Nothing Dim cnNwind As New SqlConnection(My.Settings.NorthwindConnectionString) Dim dsNwind As New DataSet("dsNwind") Try

200

VisualBasic2005_06.qxp

02/08/2007

16:25

PÆgina 201

La aplicación de técnicas avanzadas de los DataSets Dim daOrders As New SqlDataAdapter("SELECT * FROM Orders " + _ "WHERE OrderID > 11077;", cnNwind) Dim cbOrders As SqlCommandBuilder = New SqlCommandBuilder(daOrders) daOrders.UpdateCommand = cbOrders.GetUpdateCommand daOrders.InsertCommand = cbOrders.GetInsertCommand daOrders.DeleteCommand = cbOrders.GetDeleteCommand Dim daDetails As New SqlDataAdapter("SELECT * FROM [Order Details] " + _ "WHERE OrderID > 11077;", cnNwind) Dim cbDetails As New SqlCommandBuilder(daDetails) daDetails.UpdateCommand = cbDetails.GetUpdateCommand daDetails.InsertCommand = cbDetails.GetInsertCommand daDetails.DeleteCommand = cbDetails.GetDeleteCommand cnNwind.Open() daOrders.Fill(dsNwind, "Orders") daDetails.Fill(dsNwind, "OrderDetails") cnNwind.Close() Dim dtOrders As DataTable = dsNwind.Tables("Orders") Dim intRow As Integer For intRow = 0 To dtOrders.Rows.Count - 1 If blnReset Then dtOrders.Rows(intRow).Item("ShippedDate") = DBNull.Value Else dtOrders.Rows(intRow).Item("ShippedDate") = Today.ToShortDateString End If Next intRow Dim dtDetails As DataTable = dsNwind.Tables("OrderDetails") For intRow = 0 To dtDetails.Rows.Count - 1 If blnReset Then dtDetails.Rows(intRow).Item("Quantity") = _ dtDetails.Rows(intRow).Item("Quantity") - 1 Else dtDetails.Rows(intRow).Item("Quantity") = _ dtDetails.Rows(intRow).Item("Quantity") + 1 End If Next intRow If chkViolateConstraint.Checked Then dtDetails.Rows(intRow - 1).Item("OrderID") = 100 End If cnNwind.Open() trnUpdate = cnNwind.BeginTransaction daOrders.UpdateCommand.Transaction = trnUpdate daOrders.InsertCommand.Transaction = trnUpdate

201

VisualBasic2005_06.qxp

02/08/2007

16:25

PÆgina 202

Bases de datos con Visual Basic daOrders.DeleteCommand.Transaction = trnUpdate daOrders.Update(dsNwind, "Orders") daDetails.UpdateCommand.Transaction = trnUpdate daDetails.InsertCommand.Transaction = trnUpdate daDetails.DeleteCommand.Transaction = trnUpdate daDetails.Update(dsNwind, "OrderDetails") trnUpdate.Commit() Catch exc As Exception If trnUpdate IsNot Nothing Then trnUpdate.Rollback() End If Finally cnNwind.Close() End Try End If Si no se define explícitamente el valor de la propiedad DataAdapter.TypeCommand con el método CommandBuilder.GetTypeCommand, tampoco se podrá incluir el comando en la transacción con el valor de la propiedad SQLDataAdapter.TypeCommand.Transaction.

El proyecto SystemTransactions.sln contiene el código de ejemplo de este apartado y los dos siguientes. El procedimiento DataAdapterTransactions de Transactions.vb, contiene el ejemplo anterior. Para ejecutar el procedimiento, abra, construya y ejecute el proyecto y, a continuación, pulse el botón Update con el cuadro de verificación Show Update in Grid seleccionado. Entionces, el código actualiza los valores ShippedDate de la tabla Orders, con los datos actuales del sistema, y suma uno al valor de Quantity en la tabla Order Details, en todos los records con un OrderID mayor que 11077 (ver figura 6.1). Pulse el botón Reset para asignar el valor Null a ShippedDate y restar uno a los valores Quantity. La implementación de IdbTransaction que los proveedores de datos originales de ADO 1.x han realizado, limitan la posibilidad de las transacciones locales a una sola base de datos. Las transacciones distribuidas, efectuadas por el Distributed Transaction Coordinator (MSDTC), toman como base el espacio de nombres System.EnterpriseServices y la herencia de ServicedComponent.

6.1.1 Simplificar el listado con System.Transactions .NET Framework 2.0 incluye el espacio de nombres System.Transactions con el que se definen varias clases de clave que mejoran las posibilidades de transacción con ADO.NET 2.0 y simplifican la programación. Las clases más usadas son TransactionScope, Transaction y CommittableTransaction. La principal ventaja que aportan las clases System.Transactions a la gestión de transacciones es el listado automático de un gestor local de fuentes (RM, Resource Manager), como SQL Server 2005, en una transacción gestionada, por defecto, por un gestor de transacción ligera –en inglés: Lightweight Transaction Manager (LTM). El listado posterior de un RM remoto promueve, de forma

202

VisualBasic2005_06.qxp

02/08/2007

16:25

PÆgina 203

La aplicación de técnicas avanzadas de los DataSets

Figura 6.1: si no ha añadido datos en las tablas Orders y Order Details de la base de datos Northwind en los capítulos anteriores, deberá hacerlo ahora para poder actualizar con SqlDataAdapters.

automática, la transacción local y la convierte en una transacción distribuida con un OleTx Transaction Manager (OTM). El listado de un RM local no soporta las transacciones promovibles, como SQLServer 2000, que también promueve las transacciones ligeras. El LTM ofrece un alto rendimiento con un consumo mínimo de recursos; la promoción a OTM y DTC implica un rendimiento y un consumo de recursos similares a los de las ServicedComponents.

6.1.2

Listar SqlDataAdapters en una transacción implícita

Para sacar partido al nuevo modelo de transacción de .NET 2.0 hay que añadir una referencia de proyecto al espacio de nombres System.Transactions y una sentencia ImportsSystem.Transactions al archivo de clase. Se puede obtener una transacción implícita alistable creando un objeto TransactionScope y asignándolo a un bloque Using...EndUsing que incluya un bloque Try...EndTry. Los métodos transaccionables, como SqlDataAdapter.Update o SqlTableAdapter.Update, que se ejecutan dentro del bloque Using, automáticamente se alistan en la transacción. Si los métodos se desarrollan con éxito, al ejecutar el método TransactionScope.Complete y deshacerse del objeto TransactionScope saliendo del bloque Using, se hace válida la transacción. Si un método arroja una excepción, al salir del bloque Using sin ejecutar el método TransactionScope.Complete, se volverá atrás en la transacción. El siguiente procedimiento remplaza las diez líneas de código del listado anterior (empezando en cnNwind.Open()), que crea el objeto SqlTransaction y alista los objetos DataAdapter.TypeCommand de la transacción: 203

VisualBasic2005_06.qxp

02/08/2007

16:25

PÆgina 204

Bases de datos con Visual Basic 'cnNwind.Open() 'Opening here disables enlistment (no transaction) Dim tsExplicit As New TransactionScope Using tsExplicit Try 'cnNwind.Open() 'Opening here uses one connection for transaction daOrders.Update(dsNwind, "Orders") daDetails.Update(dsNwind, "OrderDetails") tsExplicit.Complete() Catch exc As Exception MsgBox(exc.Message) Finally cnNwind.Close() End Try End Using

Si utiliza los DataAdapters para abrir (y cerrar) sus conexiones automáticamente, el anterior bloque Using abrirá dos conexiones en el SQL Server 2005 (normalmente SPID 51 y SPID 53) y promoverá la transacción, causando así un leve descenso en el rendimiento. Si se abre explícitamente una sola conexión (cnNwind), antes de crear la transacción implícita con el constructor TransactionScope, las transacciones quedarán desactivadas para los métodos Update. Pero si la conexión se abre explícitamente después de crear la transacción, las dos operaciones Update se ejecutarán en la misma conexión (normalmente SPID 51), maximizando así la velocidad de ejecución. Nota: Para la ejecución del ejemplo anterior con el proyecto de ejemplo SystemTransactions.sln, defina blnSysTran=True en el procedimiento DataAdapterTransactions y pulse el botón Update o Reset. Para verificar que las operaciones de Update se están efectuando, seleccione el cuadro de verificación Violate constraint (Rollback), pulse Update, y compruebe que una sola transgresión de restricción de clave foránea en la tabla Order Details vuelve atrás todos los cambios realizados en las tablas Orders y Order Details.

6.1.3 Autolistar SqlTableAdapters en una transacción implícita El código siguiente realiza una actualización transactual de dos SqltableAdapters de ADO.NET 2.0 autolistando sus métodos Update en un LTM: Dim tsImplicit As New TransactionScope Using tsImplicit Try 'Adapter opens connections automatically Me.Order_DetailsTableAdapter.Update(Me.NorthwindDataSet.Order_Details) Me.OrdersTableAdapter.Update(Me.NorthwindDataSet.Orders) tsImplicit.Complete() Catch exc As Exception 'Error handling Finally 'Adapter closes connections automatically End Try End Using

204

VisualBasic2005_06.qxp

02/08/2007

16:25

PÆgina 205

La aplicación de técnicas avanzadas de los DataSets Tal como sucede con los SqlDataAdapters de ADO.NET 1.x, los SqlTableAdapters de ADO.NET 2.0 también abren dos conexiones automáticamente y promueven así una transacción implícita. El código siguiente abre una sola conexión y la asigna a los dos SqlTableAdapters para impedir que promuevan la transacción: Dim tsImplicit As New TransactionScope Using tsImplicit Try 'Open a single connection and assign it to both SqlTableAdapters Dim cnNwind As New SqlConnection(My.Settings.NorthwindConnectionString) cnNwind.Open() Me.Order_DetailsTableAdapter.Connection = cnNwind Me.OrdersTableAdapter.Connection = cnNwind Me.Order_DetailsTableAdapter.Update(Me.NorthwindDataSet.Order_Details) Me.OrdersTableAdapter.Update(Me.NorthwindDataSet.Orders) tsImplicit.Complete() Catch exc As Exception 'Error handling Finally cnNwind.Close() End Try End Using Para abrir una sola conexión para transacciones implícitas, defina blnOpenConnection=True en el manejador del evento bindingNavigatorSaveData, modifique un record de la tabla Orders y otro, como mínimo en su tabla Order Details, y pulse el botón Save o el botón Save Data de la tabla de herramientas.

6.1.4 SQL Profiler para rastrear transacciones La herramienta Profiler de SQL Server 2005 ha sido actualizada con nuevas características tales como las transacciones promovibles. Para rastrear los eventos BEGIN TRAN, PROMOTE TRAN, COMMIT TRAN y ROLLBACK TRAN, deberá pasar esos eventos desde la categoría Transactions a la plantilla por defecto T-SQL, u otra plantilla similar de rastreo personalizada. La siguiente figura muestra el rastreo realizado por SQL Profiler de una actualización transactuada con SqlTableAdapter, con dos conexiones autogeneradas que provocan que la transacción se promueva. La figura siguiente ilustra la misma transacción pero con una sola conexión asignada explícitamente en la propiedad Connection de los dos SqlTableAdapters. La edición SQL Server Express no incluye ni soporta el uso de SQL Profiler. De todos modos, se puede usar el Component Services Manager para contabilizar instancias de las transacciones distribuidas que resultan de promover transacciones implícitas o de ejecutar transacciones explícitas y que son el tema de los apartados siguientes. La segunda figura de la página siguiente muestra el cuadro de diálogo Servicios de cmponentes con las estadísticas de 94 transacciones promovidas, generadas por el mismo proyecto 205

VisualBasic2005_06.qxp

02/08/2007

16:25

PÆgina 206

Bases de datos con Visual Basic

de ejemplo. Nótese que el tiempo de respuesta medio de las transacciones distribuidas es de unos 4 segundos. Los ítems de las transacciones sólo aparecen en la ventana Lista de transacciones cuando están activados (ver figura de la página siguiente).

6.1.5 Listar manualmente SqlTableAdapters en una transacción explícita Si prefiere el modelo de transacción "tradicional" con un alistamiento explícito de los objetos transactuados y control granular de las invocaciones de los métodos Commit o Rollback, puede utilizar el objeto CommittableTransaction, tal como se muestra en el código siguiente: Dim tsExplicit As New CommittableTransaction Try Me.Order_DetailsTableAdapter.Connection.Open() Me.OrdersTableAdapter.Connection.Open() Me.Order_DetailsTableAdapter.Connection.EnlistTransaction(tsExplicit) Me.OrdersTableAdapter.Connection.EnlistTransaction(tsExplicit) Me.Order_DetailsTableAdapter.Update(Me.NorthwindDataSet.Order_Details)

206

VisualBasic2005_06.qxp

02/08/2007

16:25

PÆgina 207

La aplicación de técnicas avanzadas de los DataSets

Me.OrdersTableAdapter.Update(Me.NorthwindDataSet.Orders) tsExplicit.Commit() Catch exc As Exception tsExplicit.Rollback() Finally Me.OrdersTableAdapter.Connection.Close() Me.Order_DetailsTableAdapter.Connection.Close() End Try

Envoltorios explícitos de transacción para las actualizaciones con SqlTableAdapter son, por defecto, las transacciones distribuidas. Las promociones se producen cuando el código lista un segundo objeto SqlTableAdapter.Connection en la transacción.

6.1.6 Definir las opciones TransactionScope y Transaction El constructor TransactionScope tiene siete sobrecargas, pero las dos siguientes son las más útiles en las transacciones de base de datos: Public Sub New(ByVal scopeOption As System.Transactions.TransactionScopeOption, ByVal scopeTimeout As System.TimeSpan) Public Sub New(ByVal scopeOption As System.Transactions.TransactionScopeOption, ByVal transactionOptions As System.Transactions.TransactionOptions)

207

VisualBasic2005_06.qxp

02/08/2007

16:25

PÆgina 208

Bases de datos con Visual Basic La enumeración TransactionScopeOption tiene los tres miembros siguientes: TransactionScopeOption.Requires TransactionScopeOption.RequiresNew TransactionScopeOption.Suppress

El valor por defecto es Requires (una transacción). Especifique Suppress si no quiere que TransactionScope utilice la transacción ambiente. A continuación vemos los dos miembros TransactionScopeOption: TransactionOption.IsolationLevel TransactionOption.Timeout

IsolationLevel es por defecto Serializable, pero puede ser cualquiera de los siete miembros que aparecieron en el primer capítulo de este libro. Sólo SQL Server 2005 soporta Snapshot en Isolation. El valor por defecto de Timeout es 1 minuto.

6.2 Añadir relaciones a los SelectCommand de la tabla de datos Los DataSets actualizan tablas individuales, pero eso no significa que no se puedan añadir relaciones al SelectCommand de una tabla. Las relaciones permiten mejorar las ediciones de los usuarios añadiendo columnas de sólo lectura desde una relación "de muchos a uno" con una tabla relacionada. Como ejemplo, si se añaden las columnas ProductName, QuantityPerUnit y UnitPrice de la tabla Products Northwind a un DataGridView de items de Order Details, se mejora la legibilidad y se minimizan los errores en la entrada de datos. La columna UnitPrice se puede utilizar para dar valores por defecto de los registros nuevos y actualizar la columna UnitPrice de la tabla Order Details cuando se realicven cambios en el ProductID. Añadir columnas desde relaciones muchos a uno (many-to-one) no es el sustituto ideal a las columnas de cuadro combinado pobladas por listas lookup. La técnica descrita anteriormente, es normalmente un método más efectivo siempre que se trabaje con formularios de entrada de datos donde el número de ítems del cuadro combinado sea inferior a 100.

El proyecto de ejemplo de esta sección, SelectCommandJoins.sln, demuestra cómo añadir relaciones a los SelectCommand y sacar partido de la relación many-to-one para simplificar la actualización de la tabla base Order Details. El proyecto empieza con una fuente de la base de datos Northwind que incluye las tablas Orders, Order Details, y Products. Los componentes de datos incluyen Orders autogenerados, Order_Details DataGridViews, TableAdapters y BindingSources. La tabla Products porporciona el ProductName y el UnitPrice necesarios para editar y crear nuevos records de Order Details. Añada ProductsTableAdapter y ProductsBindingSource a la bandeja arrastrando el icono de la tabla Products desde la ventana Origenes de datos hasta el formulario Join.vb y después borre el ProductsDataGridView que se ha añadido al formulario.

208

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 209

La aplicación de técnicas avanzadas de los DataSets

6.2.1 Añadir una relación a SelectCommand A continuación vemos los pasos para añadir un INNER JOIN entre las tablas Order Details y Products de la operación Fill: 1. En la ventana Diseñador de DataSet, pulse con el botón derecho la cabecera del TableAdapter de Order Details y seleccione Propiedades. 2. En la ventana Propiedades, expanda el nodo SelectCommand, pulse el nodo CommandText, y pulse el botón del constructor para abrir el cuadro de diálogo Generador de consultas. 3. Pulse con el botón derecho del ratón el panel de las tablas, seleccione Agregar tabla, y añada la tabla Products. 4. Seleccione las columnas ProductName, QuantityPerUnit y UnitPrice de la tabla Products. 5. Cambie dbo.Products.UnitPriceASExpr1 por dbo.Products.UnitPriceASListPrice.

6. Pulse el botón Ejecutar consulta para ver los resultados en la parrilla. 7. Pulse el botón Aceptar para cerrar el cuadro de diálogo Generador de consultas y pulse el botón No cuando le pregunten si quiere regenerar los comandos de actualización basándose en el nuevo comando de selección. 8. Pulse con el botón derecho la cabecera de Order Details y seleccione Ajustar automáticamente para mostrar las columnas ProductName, ListPrice y QuantityPerUnit (ver la figura de la página siguiente).

209

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 210

Bases de datos con Visual Basic

9. Abra la ventana Propiedades y verifique que la sentencia SQL CommandText de los nodos DeleteCommand, InsertCommand y UpdateCommand incluye sólo columnas de la tabla Order Details. A continuación vemos el valor de la propiedad CommandText de SelectCommand: SELECT dbo.[Order Details].OrderID, dbo.[Order Details].ProductID, dbo.[Order Details].UnitPrice, dbo.[Order Details].Quantity, dbo.[Order Details].Discount, dbo.Products.ProductName, dbo.Products.UnitPrice AS ListPrice, dbo.Products.QuantityPerUnit FROM dbo.[Order Details] INNER JOIN dbo.Products ON dbo.[Order Details].ProductID = dbo.Products.ProductID

6.2.2 Añadir las columnas adjuntadas con relaciones al DataGridView Las columnas de la tabla Products se han de añadir manualmente pulsando con el botón derecho el Order_DetailsDataGridView y seleccionando Editar columnas para abrir el cuadro de diálogo del mismo nombre. Pulse Añadir columnas y añada la columna ProductName detrás de ProductID. Añada las columnas QuantityPerUnit y List Price. Defina el valor True para la propiedad ReadOnly de las tres columnas y cambie el orden de las columnas por OrderID, Quantity, ProductID, ProductName, QuantityPerUnit, ListPrice, UnitPrice y Discount.

6.2.3 Proporcionar los valores por defecto y columnas de sólo lectura Para navegar por la tabla de datos Products y proporcionar valores ProductName, QuantityPerUnit y UnitPrice y comprobar, opcionalmente el valor del campo Disconti-

210

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 211

La aplicación de técnicas avanzadas de los DataSets nued field, se necesita la ProductsBindingSource que añadió anteriormente en este capítulo. Defina el valor de la propiedad AllowNew de ProductsBindingSource como False y verifique DataSource que es NorthwindDataSet y DataMember es Products. El siguiente manejador de eventos da intencionadamente valores por defecto que no son válidos y muestra un icono de error al añadir un nuevo ítem en Order Details. Private Sub Order_DetailsDataGridView_DefaultValuesNeeded(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewRowEventArgs) _ Handles Order_DetailsDataGridView.DefaultValuesNeeded 'Set invalid default values With e.Row 'Illegal Quantity .Cells(1).Value = 0 'Illegal ProductID .Cells(2).Value = 0 'ProductName .Cells(3).Value = "ProductID not selected" 'Quantity per Unit .Cells(4).Value = "Not applicable" 'ListPrice .Cells(5).Value = 0D 'UnitPrice .Cells(6).Value = 0D 'Discount .Cells(7).Value = 0D .ErrorText = "Default values: You must enter ProductID and Quantity." End With End Sub

El manejador del evento CellValueChanged muestra un icono de error para los valores no válidos de ProductID, Quantity, o ambos, y los productos con discontinuidades: Private Sub Order_DetailsDataGridView_CellValueChanged(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles Order_DetailsDataGridView.CellValueChanged If blnIsLoaded AndAlso e.ColumnIndex = 2 Then 'User edited ProductID value With Order_DetailsDataGridView 'Clear error icon .Rows(e.RowIndex).ErrorText = "" 'Get the new ProductID value Dim intProductID As Integer = _ CType(.Rows(e.RowIndex).Cells(2).Value, Integer) Dim srtQuantity As Short = CType(.Rows(e.RowIndex).Cells(1).Value,Short) If intProductID = 0 OrElse intProductID > ProductsBindingSource.Count Then 'Bad ProductID value

211

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 212

Bases de datos con Visual Basic .Rows(e.RowIndex).ErrorText = "ProductID value must be between " + _ "1 and " + ProductsBindingSource.Count.ToString Return End If 'Get the required data from the ProductsBindingSource Dim drvItem As DataRowView drvItem = CType(ProductsBindingSource(intProductID - 1), DataRowView) If CBool(drvItem.Item(9)) Then 'Discontinued products (5, 9, 17, 24, 28, 29, 42, 53) .Rows(e.RowIndex).ErrorText = "ProductID " + intProductID.ToString + _ " (" + drvItem.Item(1).ToString + ") is discontinued." Else 'ProductName .Rows(e.RowIndex).Cells(3).Value = drvItem.Item(1) 'Quantity per Unit .Rows(e.RowIndex).Cells(4).Value = drvItem.Item(4) 'ListPrice .Rows(e.RowIndex).Cells(5).Value = drvItem.Item(5) 'UnitPrice .Rows(e.RowIndex).Cells(6).Value = drvItem.Item(5) 'Discount .Rows(e.RowIndex).Cells(7).Value = 0D If srtQuantity = 0 Then .Rows(e.RowIndex).ErrorText = "Quantity of 0 is not permitted." End If End If End With End If End Sub

La siguiente figura de la página siguiente muestra el formulario Joins.vb del proyecto de ejemplo SelectCommandJoin.sln en el proceso de añadir un nuevo ítem de linea a Order Details. En el apartado siguiente veremos la finalidad de los controles situados sobre el Orders DataGridView.

6.3 Mejorar el rendimiento reduciendo el tamaño de los juegos de datos Cargar DataSets y poblar DataGridViews con registros innecesarios puede hacer bajar considerablemente el rendimiento de servidores y clientes, especialmente al reproducir los DataSets perpetuados durante largo tiempo por los usuarios desconectados. Los apartados siguientes describen cómo reducir la carga del servidor y el consumo de recursos locales, y cómo mejorar la edición de datos limitando el número de filas devueltas por las operaciones DataTableAdapter.Fill. Las consultas convencionales TOP n basadas en tipos descendientes de los valores de las columnas int identity y datetime, son útiles para la mayor parte de clientes, tanto conectados como desconectados. Las

212

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 213

La aplicación de técnicas avanzadas de los DataSets

técnicas de paginación, además, minimizan el consumo de recursos y dan acceso a los usuarios conectados a los datos más antiguos.

6.3.1 Limitar el número de filas devueltas por las consultas TOP n El método más obvio para limitar el número de records devueltos por las operaciones Fill es añadir un modificador TOP n o TOP n PERCENT y una cláusula ORDER BY apropiada a la consulta SQL del TableAdapter para el SelectCommand. Por ejemplo, la siguiente consulta SQL carga las 100 últimas filas de la tabla Orders para poblar el DataGridView del proyecto de ejemplo SelectCommandJoins.sln: SELECT TOP 100OrderID, CustomerID, EmployeeID, OrderDate, RequiredDate, ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity, ShipRegion, ShipPostalCode, ShipCountry FROM dbo.Orders ORDER BY OrderID DESC

Cuando se aplican consultas TOP n a una tabla padre, se debería hacer lo mismo con las operaciones TableAdapter.Fill en las tablas hijo. La consulta SelectCommand de Order Details, que veíamos en el apartado anterior, carga todas las filas extendidas de Order Details en el Order_DetailsDataTable, para lo cual se consumen muchos más recursos de lo necesario. Para devolver sólo las filas hijo que dependen de las filas de Orders, hay que añadir un predicado IN con un subselect, también llamado subquery, tal como se destaca en negrita en la consulta siguiente: SELECT dbo.[Order Details].OrderID, dbo.[Order Details].ProductID, dbo.[Order Details].UnitPrice, dbo.[Order Details].Quantity, dbo.[Order Details].Discount, dbo.Products.ProductName,

213

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 214

Bases de datos con Visual Basic dbo.Products.QuantityPerUnit, dbo.Products.UnitPrice AS ListPrice FROM dbo.[Order Details] INNER JOIN dbo.Products ON dbo.[Order Details].ProductID = dbo.Products.ProductID WHERE dbo.[Order Details].OrderID IN (SELECT TOP 100 dbo.Orders.OrderID FROM dbo.Orders ORDER BY dbo.Orders.OrderID DESC) SQL Server 2005 y SQL Express permiten sustituir variables bigint o float por consultas literales TOP n [PERCENT]. El ejemplo de este capítulo utiliza valores literales para asegurar la compatibilidad con SQL Server o MSDE 2000.

6.3.2 Añadir clases Partial para TableAdapters Las clases TableAdapter no están anidadas en los DataSets de ADO.NET 2.0. En su lugar, los TableAdapters tienen su propio espacio de nombres para impedir que haya nombres de clase autogenerados por duplicado. Nombres de espacios de nombres autogenerados son, por ejemplo, DataSetNameTableAdapters, como NorthwindDataSetTableAdapters, que contiene PartialPublicClassOrdersTableAdapter, PublicClassOrder_DetailsTableAdapter y PublicClassProductsTableAdapter. Sustituir sentencias dinámicas SQL SELECT por el SelectCommand que se añadió en el diseñador de consultas, implica sobrecargar el método Fill y dar el valor variable de la propiedad CommandText como segundo argumento. Si añade la signatura cargada a las clases parciales del DataSet perderá los datos añadidos cuando se regenere el Dataset. Por lo tanto, debe añadir un archivo de clase parcial al proyecto –en este ejemplo TableAdapters.vb– que contenga código similar al siguiente: Namespace NorthwindDataSetTableAdapters '************************************ 'Partial classes to set SelectCommand '************************************ Partial Class OrdersTableAdapter Public Overloads Function Fill(ByVal DataTable As NorthwindDataSet.OrdersDataTable, ByVal strSelect As String) As Integer Me.Adapter.SelectCommand = Me.CommandCollection(0) 'Replace the CommandText Me.Adapter.SelectCommand.CommandText = strSelect If (Me.ClearBeforeFill = True) Then DataTable.Clear() End If Dim returnValue As Integer = Me.Adapter.Fill(DataTable) Return returnValue End Function End Class Partial Class Order_DetailsTableAdapter Public Overloads Function Fill(ByVal DataTable As NorthwindDataSet.Order_DetailsDataTable,

214

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 215

La aplicación de técnicas avanzadas de los DataSets ByVal strSelect As String) As Integer Me.Adapter.SelectCommand = Me.CommandCollection(0) 'Replace the CommandText Me.Adapter.SelectCommand.CommandText = strSelect If (Me.ClearBeforeFill = True) Then DataTable.Clear() End If Dim returnValue As Integer = Me.Adapter.Fill(DataTable) Return returnValue End Function End Class End Namespace

Seleccionando la casilla de verificación Limit Order Details Rows del proyecto y pulsando el botón Reload Data se añade el predicado subselect a Order_DetailsDataTable.SelectCommand. Probablemente no notará una diferencia notable en el tiempo de carga de los dos tipos de consulta, ya que el predicado IN aumenta el tiempo de ejecución de la consulta. De todos modos, el predicado IN disminuye el tamaño del juego de datos perpetuado, bajando de los 824 KBytes de todas las filas de Orders a sólo 182 Kbytes para 100 filas. Pulsando el botón Save Data del Navegador de datos, los DataSet se guardan en un archivo AllDetails.xml si la casilla de verificación está deseleccionada, o en Subselect.xml en caso contrario.

6.4 Trabajar con imágenes en DataGridViews Los DataGridViews requiren una columna DataGridViewImageColumn para mostrar imágenes devueltas por las tablas que contienen gráficos almacenados como datos binarios, como las columnas image o varbinary del SQL Server. Las DataGridViewImageColumns contienen una DataGridViewImageCell en cada fila. Por defecto, las celdas sin imágenes (valores nulos) muestran el gráfico de Internet Explorer con un vínculo HTML a un archivo de imagen "missed". Las DataGridViewImageColumns comparten la mayoría de propiedades y métodos de otros tipos de datos, pero incorporan dos propiedades, Image e ImageLayout específicas de los gráficos. La propiedad Image permite especificar una imagen por defecto del archivo MyResources.resx o cualquier otro archivo de recursos. La propiedad ImageLayout permite seleccionar un miembro de la enumeración DataGridViewImageCellLayout: NotSet, Normal, Stretch o Zoom. Estos miembros corresponden aproximadamente a la enumeración SizeMode del PictureBox. Como era de esperar, Normal es el valor por defecto que centra la imagen con su resolución original.

6.4.1 Añadir columnas Image a los DataGridViews Cuando se crea una fuente de datos de una tabla con una columna image o varbinary, la ventana de Orígenes de datos muestra el nodo de la nueva columna desactivado. Si arrastra el nodo de la tabla hasta el formulario para autogenerar un DataGridView, DataSet o cualquier otro componente de datos, el DataGridView no muestra ninguna DataGridViewImageColumn para el mapa de bits. 215

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 216

Bases de datos con Visual Basic Para añadir la columna image que falta, pulse con el botón derecho el DataGridView y seleccione la opción Editar columnas para abrir el cuadro de diálogo del mismo nombre. Pulse el botón Agregar para abrir el cuadro de diálogo y, con el botón de opción Columna de enlace de datos seleccionado, seleccione la columna y pulse Agregar (ver figura siguiente). A continuación, especifique en Width un valor apropiado para el diseño del DataGridView. Otra alternativa es seleccionar Rows como valor de la propiedad AutoSizeCriteria. Defina inicialmente AllCellsExceptHeaders como valor de la propiedad AutoSizeRowsMode del DataGridView. Después de un test inicial, puede darle a la propiedad RowTemplate.Height un valor que mantenga el ratio de imagen con el valor Width de la columna.

La tabla ProductPhoto de la base de datos AdventureWorks de SQLServer 2005 proporciona la fuente de datos para el proyecto ejemplo de este apartado, DataGridViewImagesAW.sln. La tabla ProductPhoto tiene las columnas varbinary, ThumbNailPhoto y LargePhoto con 101 mapas de bits GIF; el tamaño de los mapas de bits LargePhoto para el DataGridView es de 240 por 149 píxeles. La siguiente figura muestra tres columnas de las dos primeras filas de la tabla en NormalImageLayout.

6.4.2 Manipular imágenes en DataGridView El código añadido a la clase ProductPhoto permite comprobar el efecto de los cambios ImageLayout en el aspecto las imágenes: guarde el contenido de un DataGridViewImageCell seleccionado en el correspondiente archivo LargePhotoFileName(.gif), muestre una imagen en el cuadro de imagen (PictureBox) y sustituya la imagen seleccionada por una copia del archivo que ha guardado.

6.4.3 Cambiar ImageLayout Por defecto, el ancho de la columna LargePhoto y la altura de las filas se ajustan a la dimensión de las imagenes. Para comprobar los tres modos de imagen, arrastre el borde derecho de las cabeceras de columna hasta el borde derecho del DataGridView, y 216

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 217

La aplicación de técnicas avanzadas de los DataSets

seleccione a continuación el botón Stretch para distorsionar la imagen cambiando el ratio de proporción. Seleccionando Zoom, la propiedad AutoSizeRowsMode toma el valor DataGridViewAutoSizeRowsMode.None, el cual permite manipular la altura de fila y la anchura de la columna y ver los diferentes cambios de tamaño que se pueden aplicar a la imagen manteniendo siempre la proporción de aspecto habitual del mapa de bits. Los siguientes manejadores responden al evento CheckChange de los botones de opción: Private Sub rbNormal_CheckedChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles rbNormal.CheckedChanged 'Normal layout If blnLoaded And rbNormal.Checked Then With ProductPhotoDataGridView Dim colImage As DataGridViewImageColumn = _ CType(.Columns(2), DataGridViewImageColumn) colImage.ImageLayout = DataGridViewImageCellLayout.Normal .AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.ColumnsAllRows End With End If End Sub Private Sub rbStretch_CheckedChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles rbStretch.CheckedChanged 'Stretch layout If blnLoaded And rbStretch.Checked Then With ProductPhotoDataGridView Dim colImage As DataGridViewImageColumn = _

217

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 218

Bases de datos con Visual Basic CType(.Columns(2), DataGridViewImageColumn) colImage.ImageLayout = DataGridViewImageCellLayout.Stretch .AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.ColumnsAllRows End With End If End Sub Private Sub rbZoom_CheckedChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles rbZoom.CheckedChanged 'Zoom layout If blnLoaded And rbZoom.Checked Then With ProductPhotoDataGridView Dim colImage As DataGridViewImageColumn = _ CType(.Columns(2), DataGridViewImageColumn) colImage.ImageLayout = DataGridViewImageCellLayout.Zoom .AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None End With End If End Sub

6.4.4 Guardar una imagen seleccionada, mostrarla en un PictureBox y remplazarla Manipular datos de imágenes en DataGridViews no es un proceso intuitivo. La propiedad Value de un objeto DataGridViewImageCell se basa en el tipo de datos Byte(), no en el tipo Image que cabría esperar. Hay que incrustar Value en Byte y después crear una instancia FileStream para guardar el array Byte en el correspondiente archivo LargePhotoFileName.gif. Crear una instancia MemoryStream para asignar la propiedad Image de PictureBox del formulario frmPictureBox es más eficaz que cargar el PictureBox desde el archivo guardado. Sustituir la imagen original por una copia del archivo se hace mediante el método File.ReadAllBytes para simplificar la lectura de un archivo de tamaño desconocido. Estas operaciones vienen resaltadas en negrita en el procedimiento siguiente (que es llamado por el manejador de evento bindingNavigatorSaveItem_Clickevent): Private Sub SaveGifFile() 'Save the selected file Dim strFile As String = Nothing Try With ProductPhotoDataGridView If .CurrentCell.ColumnIndex = 2 Then If Not frmPictureBox Is Nothing Then frmPictureBox.Close() End If Dim strType As String = .CurrentCell.ValueType.ToString 'Create a Byte array from the value Dim bytImage() As Byte = CType(.CurrentCell.Value, Byte())

218

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 219

La aplicación de técnicas avanzadas de los DataSets 'Specify the image file name Dim intRow As Integer = .CurrentCell.RowIndex strFile = .Rows(intRow).Cells(1).Value.ToString 'Save the image as a GIF file Dim fsImage As New FileStream("..\" + strFile, FileMode.Create) fsImage.Write(bytImage, 0, bytImage.Length) fsImage.Close() 'Create a MemoryStream and assign it as the image of a PictureBox Dim msImage As New MemoryStream(bytImage) frmPictureBox.pbBitmap.Image = Image.FromStream(msImage) If frmPictureBox.ShowDialog = Windows.Forms.DialogResult.Yes Then 'Replace the CurrentCell's image from the saved version, 'if possible If File.Exists(Application.StartupPath + "\" + strFile) Then 'The easy was to obtain a Byte array Dim bytReplace() As Byte = File.ReadAllBytes(Application.StartupPath + "\" + strFile) .CurrentCell.Value = bytReplace If AdventureWorksDataSet.HasChanges Then AdventureWorksDataSet.AcceptChanges() Dim strMsg As String = "File '" + strFile + _ " has replaced the image in row " + intRow.ToString + _ " cell 2 (" + Format(bytReplace.Length, "#,##0") + " bytes). " + _ vbCrLf + vbCrLf + "AcceptChanges has been applied to the DataSet." MsgBox(strMsg, MsgBoxStyle.Information, "Image Replaced from File") Else Dim strMsg As String = "Unable to replace image with file '" + _ strFile + "'. DataSet does not have changes." MsgBox(strMsg, MsgBoxStyle.Exclamation, "Image Not Replaced") End If End If End If Else MsgBox("Please select the image to save.", MsgBoxStyle.Exclamation, _ "No Image Selected") End If End With Catch exc As Exception

219

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 220

Bases de datos con Visual Basic With ProductPhotoDataGridView If strFile = Nothing Then Dim intRow As Integer = .CurrentCell.RowIndex strFile = .Rows(intRow).Cells(1).Value.ToString End If End With Dim strExc As String = "File '" + strFile + "' threw the following " + _ "exception: " + exc.Message MsgBox(strExc, MsgBoxStyle.Exclamation, "Exception with Image") End Try End Sub El valor de transparencia RGB no corresponde al fondo blanco, por lo que la imagen seleccionada muestra áreas sombreadas como transparentes.

6.4.5 Evitar crear imágenes desde los campos de objeto OLE en Access La base de datos Northwind de SQL Server 2000 contiene las tablas Categories y Employees que se importaron de una versión anterior de Access. La columna Picture de la tabla Categories y la columna Photo de la tabla Employees tienen tipos de datos image, pero los bitmaps de formato BMP tienen un wrapper de objetos OLE. Las imágenes aparecen en DataGridView, pero el wrapper impide que se puedan mostrar en un PictureBox ni guardar el archivo en formato BMP.

6.5 Editar documentos XML con DataSets yDataGridViews La emergencia de los documentos XML como el nuevo formato de intercambio de documentos ha creado un requerimiento para las aplicaciones cliente que permiten a los usuarios revisar, editar y crear Infosets XML. Los documentos de negocios que utilizan Infosets para representar tablas de datos con una jerarquía de una o más relaciones uno-a-muchos (one-to-many), son habituales en la gestión de relación con el cliente (en inglés: customer relationship management, CRM), gestión de cadena de suministro (supply chain management, SCM), y otras aplicaciones de negocios como BizTalk Server 2004. Estas aplicaciones intentan minimizar la intervención humana en sus procesos automatizados de workflow, pero el procesamiento manual de documentos es inevitable en la mayor parte de las actividades de negocios. Microsoft Word, Excel e InfoPath 2003, todos pueden editar documentos XML, pero los documentos jerárquicos con múltiples relaciones uno-a-muchos son difíciles de editar en Word o Excel. Access 2003 permite importar esquemas XML para crear tablas con tipos de datos asignados, establecer claves y relaciones, adjuntar y editar datos y después exportar las tablas, o una consulta a un archivo XML. De todos modos, un documento XML jerárquico exportado no guarda ninguna relación con la estructura original del documento fuente. Transformar el archivo XML para regenerar la estructura del documento original sería preocuparse más de lo necesario. InfoPath 2003 maneja la edición de documentos jerárquicos y mantiene la estructura del documento, pero sus 220

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 221

La aplicación de técnicas avanzadas de los DataSets formularios basados en HTML tienen un repertorio limitado de controles y, al igual que otros miembros de Office 2003, los usuarios de InfoPath 2003 necesitan licencia del cliente. Los usuarios acostumbrados a editar tablas de bases de datos con formularios Windows creados con alguna versión de Visual Studio, sin duda preferirán una UI similar o idéntica para editar los Infosets XML tabulares, con controles DataGridView y, donde sea necesario, con cuadros de texto vinculados u otros controles de formulario Windows. Los controles DataGridView no se pueden vincular directamente a los documentos XML, sino que primero hay que generar un juego de datos desde el esquema del documento. Si no tiene el esquema o no consigue generar el juego de datos, puede utilizar el editor XML de VS 2005 para inferir el esquema a partir de los contenidos del documento.

6.5.1 Adaptar un esquema XML existente para generar un DataSet Microsoft ha diseñado los DataSets para guardar en DataTables los datos relacionales; la representación XML de DataSets y DataTables está pensada básicamente como un mecanismo para perpetuar o tratar datos a distancia. Por lo tanto, los documentos XML que sirven de fuente a los juegos de datos, deben tener un esquema adaptable a los juegos. A continuación indicamos los aspectos más importantes a tener en cuenta cuando se utilizan esquemas existentes para generar juegos de datos tipificados: El diseñador de juegos de datos asigna el juego de datos el nombre del elemento de nivel superior (raíz o documento). Si el esquema contiene una declaración global del espacio de nombres, se convierte en el espacio de nombres del juego de datos. Los elementos subsiguientes con elementos hijos o los elementos hijo con atributos generan DataTables. Esta característica es propia de los documentos centrados en atributos, como los representantes XML de los Recordsets ADO, pero también puede hacer que se genere una tabla de datos para un atributo en lugar de una columna. Los elementos hijo que representan las columnas de la tabla deben tener tipos sencillos XSD en correspondencia con los tipos de datos del sistema NET. Los DataSets están centrados en el elemento; si en el esquema se especifican atributos para la tabla, el diseñador de juegos de datos añadirá los atributos como columnas de tabla. Los esquemas con grupos de elementos hijo anidados establecen automáticamente relaciones one-to-many entre las tablas y añaden una clave primaria TableName_Id y una columna de clave foránea por cada relación con la tabla. La clave primaria TableName_Id es una columna Int32 AutoIncrement; leer un documento XML en el juego de datos genera los valores de TableName_Id. Si los grupos de elementos hijo no están anidados, hay que especificar la relación entre las tablas en el editor de juegos de datos. Si las tablas se han de cargar de documentos XML concretos y relacionados, en el esquema no se debe especificar ninguna relación de tabla anidada.

221

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 222

Bases de datos con Visual Basic El diseñador de DataSets tiene problemas para importar esquemas secundarios que soporten espacios de nombres múltiples y elementos calificados como espacios de nombres. El diseñador de juegos de datos utiliza el XML Schema Definition Tool (Xsd.exe) para generar los juegos de datos tipificados. Xsd.exe no utiliza el atributo <xs:import>schemaLocation para cargar esquemas secundarios automáticamente. Las restricciones anteriores hacen difícil, si no imposible, generar juegos de datos tipificados desde esquemas XML complejos, para documentos de negocios estándar, como Universal Business Language (UBL) 1.0 o Human Resources XML (HR-XML). Los esquemas UBL 1.0 utilizan ampliamente las directrices <xs:import> y especifican tipos complejos para elementos que representan las columnas de las tablas. La mayoría de las aplicaciones de edición XML deben producir un documento de salida con la misma estructura que el documento fuente, lo que significa que la edición sólo debe afectar a los contenidos de los elementos. La estructura tabular de los juegos de datos permite exportar todo el contenido o las filas seleccionadas de tablas concretas a los streams o archivos XML. También se pueden generar juegos de datos desde documentos fuente relacionados con estructuras definidas en un único esquema. Si la aplicación debe reestructurar el documento de salida, se puede aplicar un XSLT transform para la versión final del documento editado. Otra alternativa es sincronizar el juego de datos con una instancia XmlDataDocument y aplicar el transform a la instancia.

6.5.2 Esquemas para documentos XML de jerarquía anidada La estructura ideal de un documento fuente de un juego de datos es un Infoset XML con una jerarquía anidada de elementos relacionados. El diseñador de juegos de datos genera DataSets automáticamente desde esquemas compatibles con los documentos anidados. El siguiente documento XML, abreviado, es un ejemplo típico de archivo XML generado al serializar un juego de objetos relacionados con los negocios en una jerarquía de tres niveles: <parentGroup> <parentField1>String ... <parentFieldN>1000 String ... 15.50 String ... 15

222

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 223

La aplicación de técnicas avanzadas de los DataSets A continuación vemos el esquema general del documento anterior, con un elemento raíz <xs:complexType> y sus <xs:complexType> que contienen a su vez un grupo de elementos de campo <xs:sequence> y otros <xs:complexType> descendientes anidados: <xs:schema attributeFormDefault= unqualified elementFormDefault= qualified xmlns:xs= http://www.w3.org/2001/XMLSchema > <xs:element name= rootElement > <xs:complexType> <xs:sequence> <xs:element maxOccurs= unbounded name= parentGroup > <xs:complexType> <xs:sequence> <xs:element name= parentField1 type= xs:string /> ... <xs:element name= parentFieldN type= xs:int /> <xs:element maxOccurs= unbounded name= childGroup > <xs:complexType> <xs:sequence> <xs:element name= childField1 type= xs:string /> ... <xs:element name= childFieldN type= xs:decimal /> <xs:element maxOccurs= unbounded name= grandChildGroup > <xs:complexType> <xs:sequence> <xs:element name= grandChildField1 type= xs:string /> ... <xs:element name= grandChildFieldN type= xs:short />

El diseñador de DataSets interpreta los grupos <xs:complexType> no raíz que tienen elementos de campo, los elementos anidados <xsd:complexType>, o ambos, como tablas de datos. Por eso, los elementos de campo deben tener tipos de datos sencillos como xs:string, xs:int o xs:decimal, o grupos <xs:complexType> que representan tablas relacionadas.

223

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 224

Bases de datos con Visual Basic Un documento fuente XML que especifica un atributo de espacio de nombres por defecto con requiere un esquema que incluya un atributo targetNamespace="documentNamespace" para el elemento <xs:schema> más alto de la jerarquía. Si su esquema tiene una estructura tan básica como la del ejemplo precedente y sólo tiene un targetNamespace o ningún espacio de nombres de documento, está de suerte. Haga los cambios que se destacan en negrita a continuación en los dos primeros elementos del esquema para indicar que el esquema representa un juego de datos tipificado: <xs:schema attributeFormDefault= unqualified elementFormDefault= qualified xmlns:xs= http://www.w3.org/2001/XMLSchema xmlns:msdata= urn:schemas-microsoft-com:xml-msdata > <xs:element name= rootElement msdata:IsDataSet= true >

Copie el archivo Schema.xsd en la carpeta del proyecto, pulse con el botón derecho el icono del archivo en el Explorador de proyectos y seleccione Añadir a proyecto, lo que generará archivos Schema.Designer.vb, Schema.xsc, y Schema.xss. Realice una doble pulsación sobre Schema.xsd para abrirlo en el Editor DataSet y mostrar la ventana Orígenes de datos. Puede añadir el juego de datos a la bandeja del diseñador arrastrando la herramienta DataSetName desde la sección de componentes ProjectName hasta el formulario, o seleccionando la herramienta DataSet desde la sección Data y seleccionando ProjectName.DataSet en la lista de juegos de datos tipificados (Typed DataSet list). En este punto, ya puede arrastrar la tabla parentGroup desde la ventana de fuentes de datos para añadir un BindingNavigator y cuadros de texto o un DataGridView para editar parentGroup, y después añadir DataGridViews para las tablas childGroup y grandchildGroup.

6.5.3 Un ejemplo de esquema anidado La siguiente figura muestra un juego de datos tipificado generado desde un esquema (NorthwindDS.xsd) para un documento XML anidado (NorthwindDS.xml) que contiene un pequeño subjuego de datos de las tablas Customers, Orders y Order Details de Northwind. Al generar el juego de datos, la columna Customers_Id de clave primaria se añade a la tabla Customers y la correspondiente columna de clave foránea Customers_Id se añade a la tabla Orders para crear la relación Customers_Orders. La tabla Orders gana una clave primaria Orders_Id para la relación Orders_Order_Details con la clave foránea Orders_Id de la tabla Order_Details. A continuación vemos el esquema NorthwindDS.xsd para el documento anidado: <xs:schema id= Northwind xmlns= xmlns:xs= http://www.w3.org/2001/XMLSchema xmlns:msdata= urn:schemas-microsoft-com:xml-msdata > <xs:element name= Northwind msdata:IsDataSet= true > <xs:complexType> <xs:choice minOccurs= 0 maxOccurs= unbounded >

224

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 225

La aplicación de técnicas avanzadas de los DataSets

<xs:element name= <xs:complexType> <xs:sequence> <xs:element name= <xs:element name= <xs:element name= <xs:element name= <xs:element name= <xs:element name= <xs:element name= <xs:element name= <xs:element name= <xs:element name= <xs:element name= <xs:element name= <xs:complexType> <xs:sequence> <xs:element name= <xs:element name= <xs:element name= <xs:element name=

Customers >

CustomerID type= xs:string /> CompanyName type= xs:string /> ContactName type= xs:string minOccurs= 0 /> ContactTitle type= xs:string minOccurs= 0 /> Address type= xs:string /> City type= xs:string /> Region type= xs:string minOccurs= 0 /> PostalCode type= xs:string minOccurs= 0 /> Country type= xs:string /> Phone type= xs:string /> Fax type= xs:string minOccurs= 0 /> Orders minOccurs= 0 maxOccurs= unbounded >

OrderID type= xs:int /> CustomerID type= xs:string /> EmployeeID type= xs:int /> OrderDate type= xs:dateTime />

225

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 226

Bases de datos con Visual Basic <xs:element name= RequiredDate type= xs:dateTime minOccurs= 0 /> <xs:element name= ShippedDate type= xs:dateTime minOccurs= 0 /> <xs:element name= ShipVia type= xs:int /> <xs:element name= Freight type= xs:decimal minOccurs= 0 /> <xs:element name= ShipName type= xs:string /> <xs:element name= ShipAddress type= xs:string /> <xs:element name= ShipCity type= xs:string /> <xs:element name= ShipRegion type= xs:string minOccurs= 0 /> <xs:element name= ShipPostalCode type= xs:string minOccurs= 0 /> <xs:element name= ShipCountry type= xs:string /> <xs:element name= Order_Details minOccurs= 0 maxOccurs= unbounded > <xs:complexType> <xs:sequence> <xs:element name= OrderID type= xs:int /> <xs:element name= ProductID type= xs:int /> <xs:element name= UnitPrice type= xs:decimal /> <xs:element name= Quantity type= xs:short /> <xs:element name= Discount type= xs:decimal />

Nótese que el esquema NorthwindDS.xsd no contiene referencias a las columnas añadidas de clave primaria y clave foránea. Generar un juego de datos desde un esquema de documento fuente anidado no modifica el esquema En el archivo NorthwindDS.Designer.vb, el método Northwind.InitClass añade esas DataColumns a las DataTables al especificar los ForeignKeyConstraints, y después añade las DataRelations con la propiedad Nested definida como True.

226

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 227

La aplicación de técnicas avanzadas de los DataSets

6.5.4 La ventana Propiedades de las columnas Para examinar las propiedades de las columnas añadidas, seleccione la columna y pulse con el botón secundario del ratón para mostrar la ventana Propiedades. La siguiente figura muestra la ventana Propiedades de la columna de clave primaria Orders_Id (izquierda) de la tabla Orders, y la columna Orders_Id de clave foránea de la tabla Order_Details (derecha).

En la ventana Propiedades puede editar el tipo de datos, el nombre de la columna y otras propiedades de cualquiera de las columnas de la tabla. Pulse la ventana con el botón derecho y seleccione Añadir para añadir una nueva columna a la tabla de datos. A modo de ejemplo, puede añadir una columna Extended a la tabla Order_Details que puede calcular con la fórmula Quantity*UnitPrice*(1 Discount). Cualquier cambio en alguno de los valores de la ventana Propiedades provoca un cambio importante en el archivo de esquema: al archivo se le añade un grupo <xs:annotation> para especificar la fuente de datos, la mayoría de los elementos adquieren una gran cantidad de atributos msprop y el tamaño del archivo aumenta considerablemnte. NorthwindDS.xsd, por ejemplo, pasa de 4 KBytes a 35 KBytes. Por lo tanto, si tiene que editar el esquema y conservar la estructura original, pulse el archivo con el botón secundario, en el Explorador de soluciones, seleccione Abrir con… y, en el cuadro de diálogo que se abre con el mismo nombre, seleccione XMLEditor. No seleccione DataSet Editor, que es la opción por defecto, ni tampoco XML Schema Editor.

6.5.5 Un esquema anidado con atributos Al añadir atributos a los elementos que generan tablas de datos se añade a la tabla una columna del mismo nombre que el atributo. Por ejemplo, un atributo del campo Order_Details definido por <xs:attributename= "totalAmount" type= "xs:decimal" use= "requi227

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 228

Bases de datos con Visual Basic red" /> añade una columna totalAmount a la tabla Order_Details. La siguiente figura muestra el esquema NWAttributes.xsd abierto en el Editor DataSet. La primera columna de cada tabla viene generada por un atributo definido en el equema e incluido en el documento fuente NWAttributes.xsd source document. Cuando se añade un atributo a una tabla, se añade también un atributo msdata:Ordinal="n" , en orden consecutivo, a cada nodo hijo que representa una columna de la tabla.

Si se añade un atributo obligatorio a un elemento hijo, como por ejemplo ProductID, el diseñador crea una tabla ProductID, y probablemente no es eso lo que usted desea.

6.5.6 Ejemplo de esquema anidado y "envuelto" (wrapped) Con los documentos XML es una práctica común diseñar juegos de elementos "envueltos" en otros grupos. Un ejemplo es envolver Customer y sus hijos en un grupo Customers, Order en un grupo Orders y Order_Detail en un grupo Order_Details para crear la estructura abreviada que vemos a continuación: GREAL ...

228

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 229

La aplicación de técnicas avanzadas de los DataSets 11061 ... <ShipCountry>USA 11061 ... 0.075


A continuacion vemos el esquema abreviado del documento fuente anterior con los elementos envolventes destacados en negrita: <xs:schema id= Customers xmlns= xmlns:xs= http://www.w3.org/2001/XMLSchema xmlns:msdata= urn:schemas-microsoft-com:xml-msdata > <xs:element name= Customers msdata:IsDataSet= true > <xs:complexType> <xs:choice minOccurs= 0 maxOccurs= unbounded > <xs:element name= Customer > <xs:complexType> <xs:sequence> <xs:element name= CustomerID type= xs:string minOccurs= 0 /> ... <xs:element name= Fax type= xs:string minOccurs= 0 /> <xs:element name= Orders minOccurs= 0 /> <xs:complexType> <xs:sequence> <xs:element name= Order minOccurs= 0 maxOccurs= unbounded > <xs:complexType> <xs:sequence> <xs:element name= OrderID type= xs:string minOccurs= 0 /> ... <xs:element name= ShipCountry type= xs:string minOccurs= 0 /> <xs:element name= Order_Details minOccurs= 0 /> <xs:complexType> <xs:sequence> <xs:element name= Order_Detail minOccurs= 0 maxOccurs= unbounded > <xs:complexType> <xs:sequence> <xs:element name= OrderID type= xs:string

229

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 230

Bases de datos con Visual Basic minOccurs= 0 /> ... <xs:element name= Discount minOccurs= 0 />

type= xs:string

El esquema CustomersDS.xsd genera dos tablas adicionales para establecer las relaciones entre los elementos Orders y Order, y Order_Details y Order_Detail. Para que el DataSet se pueda editar en DataGridViews hay que añadir relaciones entre los campos CustomersID de las tablas Customers y Orders, y los campos OrderID de las tablas Orders y Order_Details, tal como se describe más adelante en este capítulo.

6.5.7 Un ejemplo de esquema plano Los esquemas anidados pueden exportar tablas como si fueran documentos XML invocando el método DataTable.WriteXML(ExportFileName,XmlWriteMode.IgnoreSchema). Los esquemas planos añaden la capacidad de importar documentos XML concretos, que complen el esquema de DataSet para tablas relacionadas. No obstante, el diseñador de juegos de datos no añade columnas TableName_Id, ForeignKeyConstraints ni DataRelations. A continuacion, el esquema abreviado de Northwind.xsd para Northwind.xml, que es la versión plana de NorthwindDS.xml, con las claves primaria y foránea destacadas en negrita: <xs:schema id= Northwind attributeFormDefault= unqualified elementFormDefault= qualified xmlns:xs= http://www.w3.org/2001/XMLSchema xmlns:msdata= urn:schemas-microsoft-com:xml-msdata > <xs:element name= Northwind msdata:IsDataSet= true >

230

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 231

La aplicación de técnicas avanzadas de los DataSets <xs:complexType> <xs:sequence> <xs:element maxOccurs= unbounded name= Customers > <xs:complexType> <xs:sequence> <xs:element name= CustomerID type= xs:string /> ... <xs:element minOccurs= 0 name= Fax type= xs:string /> <xs:element minOccurs= 0 maxOccurs= unbounded name= Orders > <xs:complexType> <xs:sequence> <xs:element name= OrderID type= xs:int /> <xs:element name= CustomerID type= xs:string /> ... <xs:element name= ShipCountry type= xs:string /> <xs:element minOccurs= 0 maxOccurs= unbounded name= Order_Details > <xs:complexType> <xs:sequence> <xs:element name= OrderID type= xs:int /> <xs:element name= ProductID type= xs:int /> ... <xs:element name= Discount type= xs:decimal />

Para crear una versión editable de Northwind.xsd hay que seguir los siguientes pasos en la ventana del DataSet Editor: Añadir claves primarias a cada tabla de datos. Seleccionar y pulsar con el botón derecho la columna de clave primera y seleccionar a continuación Establecer clave principal para las tres tablas. Opcionalmente, seleccione Editar clave para abrir el cuadro de diálgo Restricción UNIQUE y cambiar el nombre por PK_TableName o algo similar. La tabla Order_Details tiene una clave primaria compuesta, por lo tanto pulse con el botón derecho la columna OrderID, seleccione Editar clave y marque la casilla de verificación ProductID.

231

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 232

Bases de datos con Visual Basic Pulse con el botón derecho el entorno del DataSet Editor y seleccione Agregar/Relation para abrir el cuadro de diálogo Relación con los valores por defecto para una relación entre Customers y Orders, que tendrá el nombre FK_Customers_Orders. En la lista Columnas de clave externa, cambie la entrada OrderID de la lista Columnas de clave externa por CustomerID. Seleccione de nuevo Agregar/Relation, cambie el nombre actual de la relación, FK_Customers_Orders1 por a FK_Orders_Order_Details, y seleccione Orders en la lista de la tabla padre y Order_Details en la lista de la tabla hijo. Las listas Columnas de clave y Columnas de clave externa muestran el OrderID. Si quiere que los usuarios de la aplicación puedan añadir nuevos records a Orders y Order_Details, seleccione la columna OrderID de clave primaria, seleccione Propiedades y cambie el valor de la propiedad AutoIncrement de False a True. La siguiente figura muestra el editor de juegos de datos con los pasos anteriores completados.

Al añadir las claves primarias y las relaciones a las tablas, al final del esquema se añaden los siguientes elementos <xs:unique> y <xs:keyref> del elemento Northwind: <xs:schema id= Northwind xmlns= xmlns:xs= http://www.w3.org/2001/XMLSchema xmlns:msdata= urn:schemas-microsoft-com:xml-msdata xmlns:msprop= urn:schemas-microsoft-com:xml-msprop > <xs:element name= Northwind msdata:IsDataSet= true

232

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 233

La aplicación de técnicas avanzadas de los DataSets msprop:User_DataSetName= Northwind msprop:DSGenerator_DataSetName= Northwind > ... <xs:unique name= PK_Customers msdata:PrimaryKey= true > <xs:selector xpath= .//Customers /> <xs:field xpath= CustomerID /> <xs:unique name= PK_Orders msdata:PrimaryKey= true > <xs:selector xpath= .//Orders /> <xs:field xpath= OrderID /> <xs:unique name= PK_Order_Details msdata:PrimaryKey= true > <xs:selector xpath= .//Order_Details /> <xs:field xpath= OrderID /> <xs:field xpath= ProductID /> <xs:keyref name= FK_Orders_Order_Details refer= PK_Orders msprop:rel_Generator_RelationVarName= relationFK_Orders_Order_Details msprop:rel_User_ParentTable= Orders msprop:rel_User_ChildTable= Order_Details msprop:rel_User_RelationName= FK_Orders_Order_Details msprop:rel_Generator_ParentPropName= OrdersRow msprop:rel_Generator_ChildPropName= GetOrder_DetailsRows > <xs:selector xpath= .//Order_Details /> <xs:field xpath= OrderID /> <xs:keyref name= FK_Customers_Orders refer= PK_Customers msprop:rel_Generator_RelationVarName= relationFK_Customers_Orders msprop:rel_User_ParentTable= Customers msprop:rel_User_ChildTable= Orders msprop:rel_User_RelationName= FK_Customers_Orders msprop:rel_Generator_ParentPropName= CustomersRow msprop:rel_Generator_ChildPropName= GetOrdersRows > <xs:selector xpath= .//Orders /> <xs:field xpath= CustomerID />

Los elementos <xs:unique> definen claves primarias, y los elementos <xs:keyref> especifican las restricciones de clave foránea. Los atributos msprop son referencias a las relaciones entre datos (DataRelations) añadidas por la clase parcial Northwind del archivo Northwind.Designer.vb.

6.5.8 Inferir un esquema XML para generar un juego de datos Si todavía no tiene ningún esquema para su documento fuente XML, puede elegir entre las cinco opciones siguientes para generar el esquema con VS 2005:

233

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 234

Bases de datos con Visual Basic Abra un documento fuente XML representativo en el editor de XML, seleccione XML/CreateSchema para inferir un esquema, y guárdelo en la carpeta del proyecto con el nombre SchemaName.xsd. El generador de esquemas del editor intentará inferir tipos de datos XSD examinando los valores de texto en los campos del documento fuente. Desafortunadamente, el proceso de inferencia no suele tener éxito con valores numéricos unsigned que no tienen valores decimales; les asigna tipos de datos XSD numéricos, con los valores más pequeños posibles. Por ejemplo, calcular 0 dividido entre 255 se convierte en xs:unsignedByte, 256 entre 65.535 se convierte en xs:unsignedShort, y los números con muchas cifras se convierten en xs:unsignedInt o xs:unsignedLong. A menos que tenga alguna razón para obrar de otra manera, asigne xs:int a todos los valores sin fracciones decimales. Cree un juego de datos vacío en tiempo de ejecución, invoque el método DataSet.ReadXml(DocumentFileName) y guarde el archivo del esquema invocando el método DataSet.WriteXmlSchema(SchemaFileName). Este último método genera un esquema no tipificado en el que todos los elementos tienen asignado el tipo de datos xs:string y un atributo minOccurs="0". Abra SchemaFileName.xsd en el editor XML, cambie los tipos de datos de los valores numéricos o de fecha/tiempo por el tipo apropiado xs:datatype, y elimine todos los atributos minOccurs="0" que no resulten apropiados. Genere un esquema tipificado con el proceso anterior, pero invoque el método DataSet.ReadXml(DocumentFileName,XmlReadMode.InferTypedSchema) para generar un esquema idéntico al generado por el editor XML. Abra un VS 2005 Command Prompt, navegue hasta la carpeta del proyecto y escriba xsd.exe DocumentFileName.xml para generar DocumentFileName.xsd. El esquema es idéntico al generado por el método precedente. Si no dispone de ningún documento XML representativo de todas las instancias posibles de documento XML, o si no quiere crear uno manualmente, puede usar la herramienta Microsoft XSD Inference 1.0, que encontrará en http://apps.gotdotnet.com/xmltools/xsdinference/ para generar y refinar un esquema tipificado. Debe especificar una fuente inicial para inferir el esquema inicial y después procesar los documentos fuente adicionales para refinar el esquema. Si tiene que inferir y refinar esquemas de forma rutinaria, puede utilizar el método System.Xml.Schema.InferSchema para simular la herramienta de Microsoft, XSD Inference 1.0 Tool. El siguiente código infiere un esquema para una instancia de documento inicial (Initial.xml), refina el esquema con tres instancias de documentos adicionales y escribe el esquema refinado como Initial.xsd: Private Sub InferAndRefineSchema() Dim alFiles As New ArrayList alFiles.Add(Initial.xml) alFiles.Add(Refine2.xml) alFiles.Add(Refine3.xml) alFiles.Add(Refine4.xml) Dim intCtr As Integer Dim xss As XmlSchemaSet = Nothing

234

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 235

La aplicación de técnicas avanzadas de los DataSets Dim xsi As Inference = Nothing For intCtr = 0 To alFiles.Count - 1 Dim xr As XmlReader = XmlReader.Create(alFiles(intCtr).ToString) If intCtr = 0 Then Infer(schema) xss = New XmlSchemaSet() xsi = New Inference() End If xss = xsi.InferSchema(xr) xr.Close() Next Dim strXsdFile As String = Replace(alFiles(0).ToString, .xml, .xsd) Dim xsd As XmlSchema For Each xsd In xss.Schemas() Dim sw As StreamWriter = Nothing sw = My.Computer.FileSystem.OpenTextFileWriter(strXsdFile, False) xsd.Write(sw) sw.Close() Exit For Next End Sub

6.5.9 Crear formularios de edición desde fuentes de datos XML El proceso de crear formularios de edición para documentos XML es parecido al de editar tablas de bases de datos. Después de generar un juego de datos tipificado a partir del esquema existente, arrastre la tabla de más arriba desde la ventana Orígenes de datos hasta el formulario donde quiere añadir un control DataNavigator y DataGridView o cuadros de texto para detalles. Repita el mismo proceso con los DataGridViews para las tablas relacionadas y especifique la DataRelation apropiada para generar una DataRelationBindingSource para el valor de la propiedad DataSource. A diferencia de los DataGridViews vinculados a FK_ParentTable_ChildTableBindingSources generados por tablas de bases de datos, la BindingSource se crea cuando, en la lista desplegable de la propiedad DataSource, se especifica una lista relacionada. Los dos ejemplos siguientes de proyectos ilustran los cambios necesarios para crear DataRelationBindingSource, permitir la adición de nuevos elementos en el documento y acomodar juegos de datos envueltos y anidados.

6.5.10 El proyecto de ejemplo EditNorthwindDS El proyecto EditNorthwindDS.sln está basado en el documento fuente NorthwindDS.xml y en el esquema NorthwindDS.xsd. El formulario tiene DataGridViews poblados con datos de las tablas Customers, Orders y Order_Details, tal como muestra la siguiente figura. Abra la ventana Orígenes de datos y arrastre el icono del grupo padre de Customers, el icono de su subgrupo Orders y el icono del subgrupo Order Details del grupo Orders 235

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 236

Bases de datos con Visual Basic hasta el formulario para añadir los tres DataGridViews. Añada código al manejador de evento Form_Load para poblar el juego de datos con el documento NorthwindDS.xml.

La siguiente figura muestra la lista Orígenes de datos tras realizar las operaciones anteriores y cargar el documento NorthwindDS.xml. La instrucción OrdersDataGridView..Sort(.Columns(0),System. ComponentModel.ListSortDirection.Descending) del manejador de eventos clasifica los OrderID por orden descendente. Si quiere que los usuarios puedan añadir nuevos registros a Orders y Order_Details con los valores apropiados de la columna OrderID, deberá editar el esquema y darle a la propiedad AutoIncrement de las columnas OrderID y Order_Id el valor True en el cuadro de diálogo Propiedades de ColumnName. En caso contrario, defina el valor False para la propiedad AllowUserToAddRows de DataGridViews. Puede añadir los atributos autogenerados Customers_Id, Orders_Id y Order_Details_Id como columnas de los DataGridViews. Mientras personaliza la colección Columns de los DataGridViews en el cuadro de diálogo Editar columnas, lleve las columnas autogeneradas al final de la lista SelectedColumns y defina el valor True para sus propiedades ReadOnly. Si no quiere que los usuarios puedan añadir nuevas filas, borre estas columnas de los DataGridView. Añada un botón para guardar los cambios e invoque el método NorthwindDS..WriteXml(strFile,Data.XmlWriteMode.IgnoreSchema) para guardar el documento editado con los datos. El proyecto de ejemplo guarda un archivo diffgram (NorthwindDS.xsd) antes de guardar los camibos y tiene botones para mostrar en Internet Explorer el esquema y el documento XML guardado.

236

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 237

La aplicación de técnicas avanzadas de los DataSets

Para añadir nuevas filas se necesita un procedimiento OrdersDefaultValues que llama al manejador de evento OrdersDataGridView_DefaultValuesNeeded. El código del procedimiento es similar al que vimos en el capítulo anterior para el manejador de evento DefaultValuesNeeded, pero ahora hay que añadir el valor Customers_Id para mantener la relación, tal como se destaca en negrita en el siguiente listado: Private Sub OrdersDefaultValues(ByVal rowNew As DataGridViewRow) Try With CustomersDataGridView Dim intRow As Integer = .CurrentCell.RowIndex rowNew.Cells(1).Value = .Rows(intRow).Cells(0).Value rowNew.Cells(2).Value = 0 rowNew.Cells(3).Value = Today rowNew.Cells(4).Value = Today.AddDays(14) 'Leave ShippedDate empty rowNew.Cells(6).Value = 3 'Freight defaults to 0 'CompanyName rowNew.Cells(8).Value = .Rows(intRow).Cells(1).Value 'Address to Country fields Dim intCol As Integer For intCol = 9 To 13 rowNew.Cells(intCol).Value = .Rows(intRow).Cells(intCol 5).Value

237

VisualBasic2005_06.qxp

02/08/2007

16:26

PÆgina 238

Bases de datos con Visual Basic Next 'Add the current Customers_Id value rowNew.Cells(15).Value = .Rows(intRow).Cells(11).Value OrdersDataGridView.EndEdit(DataGridViewDataErrorContexts.Commit) 'Store the autoincremented Orders_Id for Order_Details default values intNewOrder_ID = CInt(rowNew.Cells(14).Value) 'Store the autoincremented OrderID value intOrderID = CInt(rowNew.Cells(0).Value) End With Catch exc As Exception MsgBox(exc.Message + exc.StackTrace, , ) End Try End Sub

El procedimiento DetailsDefaultValues requiere una modificación similar para los valores de OrdersID y Orders_Id: Private Sub DetailsDefaultValues(ByVal rowNew As DataGridViewRow) 'Default values for Order_Details Try With OrdersDataGridView Dim intRow As Integer = .CurrentCell.RowIndex 'Add OrderID rowNew.Cells(0).Value = .Rows(intRow).Cells(0).Value 'Add Orders_Id rowNew.Cells(5).Value = .Rows(intRow).Cells(14).Value End With With Order_DetailsDataGridView rowNew.Cells(1).Value = 0 rowNew.Cells(2).Value = .Rows.Count * 10 rowNew.Cells(3).Value = .Rows.Count * 5 rowNew.Cells(4).Value = .Rows.Count * 0.01 End With Catch exc As Exception rowNew.Cells(5).Value = intNewOrder_ID Finally Order_DetailsDataGridView.EndEdit(DataGridViewDataErrorContexts.Commit) End Try End Sub

238

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 239

Capítulo 7

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0 Los formularios Windows, sus fuentes de datos, componentes y controles vinculados de la versión .NET Framework 2.0 son un desarrollo de la versión anterior .NET Framework 1.0. El ayudante y las herramientas de Visual Studio 2005 simplifican las tareas más comunes, como generar juegos de datos tipificados y diseñar formularios maestros y de detalle, pero las herramientas y el ayudante se parecen mucho a sus predecesores. La transición desde las herramientas y componentes de Visual Studio implica una modesta curva de aprendizaje para los desarrolladores .NET con más experiencia. Sustituir los obsoletos DataGrid por los nuevos DataGridWiews exige algo más de esfuerzo, pero las propiedades y el rendimiento mejorado de estos elementos justifica la complejidad de su modelo de objeto. Por otra parte, ASP.NET 2.0 representa una diferencia radical respecto a ASP.NET 1.x. La herramienta de libre desarrollo Web Matrix ASP.NET, de Microsoft, fue un éxito instantáneo y una contribución remarcable a su populardidad fue que no requería ningún prerrequisito para VS 2002 o 2003 ni los Internet Information Services (IIS). Web Matrix combina un diseñador gráfico de páginas Web y un editor de código (su nombre codificado es Venus) para ASP.NET 1.1 con un servidor Web ligero (Cassini). Venus y Cassini constituyen los fundamentos de Visual Web Developer UI y el servidor Visual Web Developer de VS 2005. La edición Express 2005 de Visual Web Developer (VWD) es el equivalente a la actualización de Web Matrix para VS 2005 UI y ASP.NET 2.0. A diferencia de las ediciones Express para un lenguaje de programación específico, la VWD 2005 Express soporta VB, C#, y J#. Este capítulo presupone que el lector ya tiene cierta experiencia en la creación y desarrollo de sitios Web controlados por datos con Active Server Pages (ASP), ASP.NET 1.x o Web Matrix. Las cadenas de conexión de los proyectos de ejemplo presuponen que se trabaja con SQLServer 2000, MSDE 2000 o SQLServer 2005, como instancia localhost por defecto y la base de datos Northwind.

239

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 240

Bases de datos con Visual Basic Si está utilizando Visual Web Developer 2005 Express Edition o una instancia de nombre SQLServer 2005, debe modificar la siguiente sección del archivo Web.config para señalar la instancia nombrada:

Cambie localhost por .\SQLExpress para usar el proveedor Shared Memory con SQL Server 2005 Express.

7.1 Las nuevas características de ASP.NET 2.0 La creación de formularios Web con VS 2005 es muy diferente a la de VS 2002 y 2003, que dependían de un directorio virtual IIS definido previamente. El cuadro de diálogo Nuevo proyecto de VS 2005 no incluye los iconos Sitio Web ASP.NET, Servicio Web ASP.NET y otros relacionados con la Web. El menú Archivo/Nuevo ofrece una selección de sitios Web que abre el cuadro de diálogo Nuevo sitio Web con una serie de iconos basados en el sistema de archivos, como Sitio Web ASP.NET, Servicio Web ASP.NET y otros iconos de plantillas.La carpeta raíz por defecto para añadir nuevos sitios Web o subcarpetas de servicios es .\WebSites. El cuadro de diálogo Seleccionar ubicación se puede abrir pulsando el botón Examinar, aceptando la opción por defecto Sistema de archivos y añadiendo un nombre de acceso más apropiado para el cuadro de texto Ubicación (ver siguiente figura).

Pulse el botón Aceptar para generar una carpeta con los ítems del proyecto, añada una carpeta vacía App_Data, un archivo de página Default.aspx y un archivo de código oculto Default.aspx.vb. Si no encuentra el archivo Default.aspx.vb, pulse con el botón derecho

240

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 241

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0 en Default.aspx en el Explorador de Soluciones y seleccione la opción Ver código para generar el archivo. Default.aspx.vb que contiene las declaraciones vacías PartialClassDefault_aspx e InheritsSystem.Web.UI.Page para el código oculto tras la página Default.aspx. Default.aspx se abre en el editor XHTML1.1 de fuentes con la directiva de página por defecto, la declaración DOCTYPE y los elementos , , ,
y
, tal como se muestran, reformateados, en la siguiente figura.

Sustituya Página sin título por un nombre más significativo y pulse el botón Ver diseñador para mostrar una página vacía en el diseñador, que sólo soporta el modo convencional HTML. La nueva versión ASP.NET 2.0 no soporta el modo de posicionamiento de elementos fijos en una parrilla, por defecto, de ASP.NET 1.x. La explicación de que falte el modo diseño de posición fija es que las ventanas flotantes soportan una gama más amplia de navegadores y controladores. Sitúe los controles en celdas de tabla para controlar el posicionamiento relativo, añada hojas de estilo en cascada (en inglés, cascading style sheet, CSS) para el posicionamiento fijo. Para definir métodos alternativos de posicionamiento de los controles Web, seleccione Herramientas/Opciones//Diseñador HTML/Posición CSS, active la casilla de verificación Cambiar la siguiente posición… y el método de posicionamiento en la lista desplegable. Seleccione Diseño/Insertar tabla para abrir el cuadro de diálogo del mismo nombre, seleccione la opción Plantilla y acepte el estilo por defecto Encabezado y, por último, pulse el botón aceptar para añadir una tabla de página completa con una cabecera y sin bordes. Escriba un título para la tabla y déle formato; seleccione la tabla entera pulsando el ángulo superior izquierdo, abra la ventana Propiedades y asigne un valor Id a la tabla (por ejemplo tblmain) y un color Web a la propiedad BgColor; el color definido cambia a su valor RGB (ver siguiente figura). 241

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 242

Bases de datos con Visual Basic

Finalmente, pulse para construir y ejecutar la parte realizada del trabajo. Pulse Aceptar en el cuadro de diálogo Depuración no habilitada para añadir un archivo Web.config al proyecto. El servidor Visual Web Developer se inicia y muestra Default.aspx en Internet Explorer. Pulse con el botón secundario el icono del servidor Web en la barra de tareas y seleccione mostrar detalles para abrir el cuadro de diálogo con las propiedades del servidor que le mostrará el puerto TCP elegido, de forma aleatoria, para la página (ver la siguiente figura).

7.1.1 El modelo de compilación de ASP.NET Una directiva de página en ASP.NET 1.1 especifica el nombre del archivo de código oculto y el nombre de la clase base del formulario, tal como vemos aquí: <%@ Page Language= vb AutoEventWireup= false Inherits= DataWebSite.Form1 %>

242

Codebehind= Default.aspx.vb

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 243

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0 Los archivos PageName.aspx.vb de ASP.NET 1.x contienen una zona de inicialización y un código de inicialización para cada control de la página. La primera vez que se abre el sitio Web de la dirección http://www.company.com/datawebsite/default.aspx, ASP.NET 1.x compila el código oculto tras la página en tiempo de ejecución y genera un juego de archivos temporales, incluyendo un archivo para la definición PublicClassDefault_aspx derivada de la clase base de WebSiteName.Form1. A continuación vemos la directiva de página de ASP.NET 2.0 para la página Default.aspx que se añadió en el apartado anterior: <%@ Page Language= VB AutoEventWireup= false Inherits= _Default %>

CodeFile= Default.aspx.vb

Con las clases parciales para la etiqueta HTML y el código oculto tras la página ya no es necesaria una clase derivada. La instrucción CodeFile especifica qué etiqueta y código en Default.aspx y qué código en Default.aspx.vb se han de compilar en una sola clase single _Default. La carpeta de proyecto no incluye la subcarpeta tradicional \bin. Construir y ejecutar una solución ASP.NET 2.0 con el servidor Web integrado genera una serie de archivos temporales en la carpeta \WINDOWS\Microsoft.NET\Framework\v2.0.BuildNumber\Temporary ASP.NET Files\websitename\random1\random2; websitename es el nombre de la carpeta en minúsculas, en este ejemplo datawebsite, y random1\random2 son dos nombres de carpeta de 8 caracteres, elegidos al azar, al igual que e7ae7f95\aa3fd637 (ver la siguiente figura). ASP.NET 1.1 genera archivos temporales con una jerarquía de carpetas similar.

La tabla siguiente describe los archivos temporales mostrados en la figura anterior. Los nombres en negrita corresponden a los archivos temporales similares generados por ASP.NET 1.1. 243

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 244

Bases de datos con Visual Basic Nombre del archivo

Descripción

App_Web_bmzetgsw.dll

Unión de archivos 0.vb, 1.vb, y 2.vb.

App_Web_bmzetgsw.pdb

Archivo de símbolos (debugging) para bmzetgsw.dll.

bmzetgsw.0.vb

PartialClassDefault_aspx para el HTML, controles del servidor y código en línea de la página (autogenerado desde Default.aspx).

bmzetgsw.1.vb

PartialClassDefault_aspx contiene código oculto (copia de Default.aspx.vb).

bmzetgsw.2.vb

PublicClassFastObjectFactory con una función no utilizada (dummy) SharedFunctionCreate_Default_aspx()AsObject.

bmzetgsw.cmdline

Parámetros Vbc.exe para compilar el conjunto.

bmzetgsw.err

Errores de compilación (vacío si la compilación tiene éxito).

bmzetgsw.outFull

Comando completo Vbc.exe (incluye línea bmzetgsw.cmd).

bmzetgsw.res

Archivo de recursos compilado que contiene el código en línea de la tabla principal.

default.aspx.cdcab7d2.compiled

Dependencias del archivo y lista de valores hash para la página (XML).

default.aspx.cdcab7d2_ CBMResult.ccu

CodeCompileUnit (contenedor para programa gráfico CodeDOM para la página).

default.aspx.cdcab7d2_ CBMResult.compiled

Lista de dependencias de archivo y valores hash de la CodeCompileUnit (XML).

default.aspx.vb.cdcab7d2.compiled

Dependencias de archivo y lista de valores hash para el código oculto tras la página (XML).

hash.web

Valor hash hexadecimal de 16 Bytes.

Compilar el código y las etiquetas HTML para cada página mejora la productividad mientras se están desarrollando páginas individuales o un sitio completo con el servidor Web integrado. Sólo las páginas modificadas se recompilan mientras se está ejecutando el proyecto. En el apartado dedicado a la publicación de sitios Web precompilados del capítulo 8, se explica cómo realizar un proyecto de formularios Web completo en un directorio virtual IIS 5.0 o IIS 6.0. Publicar un formulario Web precompilado genera un único archivo DLL en la carpeta \bin, elimina el código fuente y el retraso de compilación cuando el primer usuario abre el archivo del proyecto Default.aspx o cualquier otra página de inicio que se haya especificado.

7.1.2 Los nuevos controles (Data Controls) de ASP.NET 2.0 ASP.NET 2.0 añade unos 40 nuevos controles Web al repertorio de ASP.NET 1.1. Muchos de estos nuevos controles soportan la conectividad de datos declarativa y la vinculación de datos con poco o nada de código online o código oculto tras la página requerida. La vinculación de datos automatiza, además, las operaciones opcionales de actualización, inserción y borrado en las tablas de la base de datos y los habituales objetos y componentes de acceso a datos. Q

244

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 245

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0 A continuación, una breve descripción de los nuevos controles de servidor en ASP.NET 2.0 que soportan la vinculación de datos y las actualizaciones: Q

Q

Q

Q

Q

Los controles DataSource conectan con las bases de datos, los objetos de acceso a datos y los documentos XML tabulares y jerárquicos, incluyendo los juegos de datos tipificados serializados. Los controles DataSource proporcionan la fuente vinculante para controles bidireccionales de datos vinculados y otros controles de servidor, como las listas desplegables y los cuadros de lista, que soportan la vinculación de datos de sólo lectura. Los controles DataSource sustituyen a los controles de datos relacionados de ADO.NET 1.1, como Connections y DataAdapters. Los controles DataList muestran y editan filas DataSource de forma secuencial en una o más columnas. Los controles FormView muestran y editan una sola fila DataSource en un formulario XML convencional. Los controles GridView muestran y editan múltiples filas DataSource en una parrilla similar a los DataGridView de los formularios Windows. GridView sustituye al DataGrid de ASP.NET 1.x. Los controles DetailsView muestran y editan una sola fila DataSource en una tabla de dos columnas. Además, soportan las páginas de edición de datos maestro/hijo.

El resto de este capítulo está dedicado a estos nuevos controles que acabamos de mencionar.

7.2 Los controles DataSource La sección Datos del Cuadro de herramientas de VS 2005 sustituye las herramientas de VS 2002 y 2003 de ADO.NET 1.x: DataSet, DataView, Connection, Command y Adapter por un juego de herramientas DataSource predefinidas. Un DataSource de ASP.NET 2.0 combina los elementos requeridos para el tipo de fuente de datos que se especifique en una componente nombrada que aparece en el modo Diseño de página como DataSourceType -DataSourceName. En el modo código, un elemento guarda la definición de la fuente de datos. Para añadir un control DataSource a una página en modo diseño sólo hay que arrastrar el control desde la sección Datos del Cuadro de herramientas hasta la página en cuestión. VS 2005 proporciona los siguientes controles DataSource integrados: Q

SqlDataSource – para bases de datos cliente/servidor. El elemento incluye los atributos ConnectionString y SelectCommand que se añaden con los cuadros de diálogo Configurar origen de datos. Las fuentes de datos actualizables permiten añadir los atributos DeleteCommand, InsertCommand y UpdateCommand. A diferencia de los formularios de Windows, que limitan SqlConnection, SqlCommand y los objetos relacionados a las bases de datos del SQL Server, SqlDataSource le permite utilizar cualquier conexión que haya definido en el Explorador de servidores, o definir una conexión nueva.

Q

245

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 246

Bases de datos con Visual Basic Q

Q

Q

Q

AccessDataSource – para los archivos Access. El elemento sustituye un atributo de ruta relativa datafile= ~/App_Data/FileName.mdb por el atributo string Connection de SqlDataSource. Hay que usar el comando Agregar/Nuevo elemento para añadir el archivo FileName.mdb a la carpeta ...\ProjectName\App_Data para hacer el archivo accesible al cuadro de diálogo Configurar origen de datos del asistente del mismo nombre. AccessDataSources utiliza el proveedor de datos OLE DB Jet, por lo que se puede especificar un nombre de usuario y contraseña para hacer las bases de datos más seguras modificando la conexión en el Explorador de servidores. ObjectDataSource – para los objetos de negocios habituales, servicios Web, componentes de datos o DataSets que retornan y, opcionalmente, actualizar datos. El objeto debe soportar la interfaz IEnumerable y proporcionar al menos un método público para posibilitar la selección; los métodos para borrar, insertar y actualizar son opcionales. El archivo de definición de la clase de objeto se ha de incluir en la carpeta App_Code, o bien se han de copiar los ensambladores de objetos en la carpeta App_Assemblies. Otra alternativa es añadir una referencia a la librería de la clase compilada de objetos con el cuadro de diálogo Añadir referencia o una referencia Web para el servicio Web con el cuadro de diálogo Añadir referencia web. XmlDataSource – para datos de fuente XML, tabular o jerárquica. En ese caso, debe almacenar el archivo del documento fuente XML y, opcionalmente, su esquema XML, en la carpeta App_Data. XmlDataSource no usará el esquema, pero algunos controles vinculados podrían beneficiarse de los tipos de datos asignados a los elementos del documento fuente. Puede actualizar los datos de la fuente XML invocando el método GetXmlDocument o creando un objeto en memoria XmlDataDocument que contenga objetos editables XmlNode. Otra alternativa es utilizar expresiones XPath para actualizar los datos. SiteMapDataSource – conecta con el mapa del sitio del proyecto, el cual se crea con el objeto XmlSiteMapProvider.

Los controles de servidor integrados DataSource amplían la clase base DataSourceControl, que es a su vez la base de la interfaz IDataSource. Los controles DataSource contienen objetos nombrados DataSourceView; los controles Web vinculados a datos conectan con el DataSourceView por defecto. Se pueden crear controles habituales DataSource de servidor añadiendo código para ampliar la clase DataSourceControl. La entrada de ayuda online DataSourceControlClass incluye el código fuente para un control de servidor CsvDataSource que restablece datos de un archivo de valores separados por comas.

Cuando se arrastra a una página un control de servidor derivado de la clase base abstracta DataBoundControl –como pueden ser DataList, DetailsView, GridView, FormView o TreeView– o un control Repeater, la etiqueta inteligente de Common ControlType Tasks se abre con la lista desplegable Configurar origen de datos activada. Puede seleccionar (Ninguna), un DataSource ya existente para la página o para iniciar el asistente. Otra alternativa es arrastrar uno de los controles DataSource desde el Cuadro de herramientas y utilizarlo como fuente de datos.

246

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 247

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0 Otros controles de servidor derivados de la clase base ListControl, como DropDownList, ListBox, RadioButtonList y BulletedList, soportan la vinculación de datos simple (sólo lectura). Las etiquetas inteligentes de estos controles ofrecen una opción Configurar origen de datos que abre un cuadro de diálogo del mismo nombre. El cuadro de diálogo Configurar origen de datos tiene las mismas opciones que la etiqueta inteligente de DataBoundControl, pero el cuadro de diálogo incluye también listas desplegables para seleccionar los datos y los campos de valores de la lista.

7.3 El control DataList El control de servidor DataList es el más sencillo de los controles integrados derivados de DataBoundControl. Por defecto, DataList muestra nombres de columnas y valores para todas las filas devueltas por la sentencia SQL SelectCommand en una columna individual de etiquetas. Se pueden especificar múltiples columnas y el orden en que han de aparecer las filas en las columnas, además de otras muchas opciones de formateo. La figura siguiente muestra la página DataList.aspx de DataWebSite, con pedidos del país que seleccione en la lista desplegable, de izquierda a derecha, de arriba hacia abajo, en secuencia descendente de los valores OrderID. La primera lista define la cláusula WHERE como criterio para el país de envío (ShipCountry). La segunda lista permite seleccionar cualquier pedido en la lista del país actual; al seleccionar una fila de pedidos se muestra el valor CustomerID en un cuadro de texto no vinculado.

7.3.1 SqlDataSources para controles vinculados Un SqlDataSource para controles vinculados se crea arrastrando un control DataList, FormView, GridView, DetailsView o Repeater, desde la sección Datos del Cuadro de herramientas hasta una página Web. En este ejemplo, se empieza con la página Default.aspx

247

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 248

Bases de datos con Visual Basic 1. Copie y pegue Default.aspx; renombre la copia de Default.aspx con DataList.aspx. Abra DataList.aspx en modo código y cambie CodeFile= "Default.aspx.vb" Inherits="_Default" en la página directriz por CodeFile="DataList.aspx.vb" Inherits= "DataList". Abra DataList.aspx.vb y cambie Partial Class _Default por Partial Class DataList. Cierre la ventana del editor. 2. Pulse con el botón secundario en DataList.aspx en el Explorador de soluciones y seleccione Establecer como página de inicio. Pulse para verificar que se han añadido los datos a las página e inicie la configuración de la página. 3. Cierre IE, cambie DataList.aspx a modo diseño y arrastre un control DataList desde el Cuadro de herramientas hasta una celda vacía de la tabla DataList.aspx. Un contenedor DataList1 se añade al formulario y se abre la etiqueta inteligente DataLists Tasks. 4. Seleccione en la lista Elegir origen de datos para iniciar el Asistente para la configuración de orígenes de datos. Seleccione Base de datos en la lista Elija un tipo de origen de datos.

5. Pulse Aceptar para abrir el cuadro de diálogo Elegir la conexión de datos, seleccione una conexión ya existente a la base de datos de ejemplo Northwind o cree una nueva conección. Si quiere que el sitio se pueda extender a un servidor Web IIS que soporte conexiones anónimas, seleccione o añada una conexión que utilice seguridad SQL Server. Pulse el botón Siguiente. 6. Deje seleccionado el cuadro de verificación Sí, guardar esta conexión como, y edite el nombre de la cadena de conexión como desee. Pulse Siguiente para abrir el cuadro de diálogo Configurar la instrucción Select. 7. Seleccione la tabla Orders en la lista Nombres y marque los nueve primeros cuadros de verificación, desde OrderID hasta ShipName (ver siguiente figura). 8. Pulse el botón ORDER BY para abrir el cuadro de diálogo Agregar clausula ORDER BY, seleccione OrderID en la lista Ordenar por y, a continuación, la opción Descendente. Pulse Aceptar. 248

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 249

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0

9. Pulse el botón Avanzadas para abrir el cuadro de diálogo Opciones de generación SQL avanzadas; seleccione las casillas de verificación Generar instrucciones Insert, Update y Delete. En este ejemplo, no seleccione el cuadro Usar concurrencia optimista. Pulse Aceptar y Siguiente para abrir el cuadro de diálogo Consulta de prueba. 10. Pulse el botón Consulta de prueba para mostrar los resultados de la consulta en un DataGridView (ver siguiente figura). Pulse el botón Finalizar para mostrar el formato de diseño por defecto de una DataList, el cual consiste en cinco instancias simuladas de los datos de las columnas seleccionadas en el paso 7. Pulse para construir y mostrar la página, que aparecerá tal como se muestra en la siguiente figura. La fuente de datos dsOrders con las casillas de verificación Generate Insert, Update y Delete Statements seleccionados, añade el siguiente código fuente a la página:

249

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 250

Bases de datos con Visual Basic

”>

250

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 251

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0


7.3.2 Propiedades de control La ventana Propiedades para los controles vinculados permite especificar la fuente y otras propiedades aplicables al control. Además de las propiedades que comparten todos los controles de servidor, DataList tiene propiedades que especifican el número de columnas de lista y el flujo de datos en las columnas. Para simular el diseño de FinalDataList.aspx, pulse el control con el botón derecho y seleccione Propiedades para abrir la ventana del mismo nombre con DataList1 seleccionada y definir los valores de las propiedades que se indican en la tabla siguiente. Propiedad

Valor

Id Font\Name Font\Size RepeatColumns RepeatDirection

dlOrders Verdana 10pt 2 Horizontal

251

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 252

Bases de datos con Visual Basic A continuación, arrastre el borde derecho de dlOrders DataList hasta el borde derecho de la tabla. Los valores de propiedad anteriores generan una página con datos para pedidos en la secuencia descendiente, de arriba a abajo, de izquierda a derecha, que se muestra en la siguiente figura.

7.3.3 Plantilla de datos vinculados y formateo de datos Los DataLists introducen el concepto de plantilla para los campos de los DataBoundControls. Pulse con el botón secundario el control DataList en modo diseño, seleccione Mostrar etiqueta inteligente para abrir el panel de etiquetas ingeligentes del control y pulse Editar plantillas para abrir un formulario de edición con la plantilla Item por defecto. Las plantillas Item contienen texto HTML para los nombres de columna y controles ColumnNameLabel para mostrar los valores de columna. Reformatear la plantilla Item Para tener espacio verticalmente, puede modificar la plantilla de modo que muestre varios nombres de columnas y valores en una sola línea. Dé a la plantilla un ancho de hasta unos 500 píxeles, sitúe el cursor detrás del item OrderIDLabel, pulse Eliminar para eliminar el elemento
y sustitúyalo por dos espacios ( ). Haga lo mismo con EmployeeIDLabel, RequiredDateLabel y ShipViaLabel (ver siguiente figura). Revisar el código fuente XHTML generado Cada definición de plantilla Item añade texto HTML en ColumnName, seguido de una instrucción Label de control del servidor, con el valor de la propiedad Text especificado

252

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 253

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0

en una instrucción Eval("ColumnName") o Eval("ColumnName","FormatString"), incluida entre etiquetas de vinculación de datos (<%#...%>). Al seleccionar Finalizar edición en la plantilla, o al construir el proyecto, el código fuente se añade a la página. A continuación vemos el código fuente –modificado para una mayor legibilidad– generado por la plantilla Item de la figura anterior: OrderID:
CustomerID:
EmployeeID:
OrderDate:
RequiredDate:
ShippedDate:
ShipVia:
Freight:
ShipName:



253

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 254

Bases de datos con Visual Basic La expresión {0:d} es una cadena de formato estándar para fecha breve; {0:C} especifica el formato de la moneda. 0: representa el valor; las letras corresponden a las cadenas de formato numérico o DateTime que se aplican como argumentos del método ToString, como por ejemplo en NumericValue.ToString("C") o DateTimeValue.ToString("d").

7.3.4 Restricciones WHERE en el código fuente en los valores de controles vinculados Los elementos SqlDataSource de dsOrders devuelven todos los registros de Orders, algo no demasiado conveniente para los usuarios y que, además, consume recursos considerables de red y del servidor de la base de datos sólo en la operación de abrir la página. Una manera de limitar el número de registros devueltos por el servidor es añadir un control DropDownList con una cláusula WHERE de restricción para el DataSource de la DataList. En este ejemplo, la restricción se aplica a la columna ShipCountry de la tabla Orders; otras alternativas pueden ser EmployeeID o rangos de valores de OrderDate, como año y mes. Añadir una lista desplegable (DropDownList) poblada por una nueva fuente de datos Para añadir una lista desplegable poblada únicamente con valores de la columna ShipCountry, siga los pasos siguientes: 1. Arrastre un control DropDownList hasta la derecha del título de la celda superior de la tabla y añada unos espacios entre el titulo y el control. 2. Pulse la flecha de etiqueta inteligente para abrir el la etiqueta DropDownList, marque la casilla de verificación Habilitar AutoPostBack y pulse el vínculo Elegir origen de datos para abrir el cuadro de diálogo del mismo nombre. 3. Seleccione en la lista Seleccionar un origen de datos para abrir el Asistente para la configuración de origen de datos. Seleccione Base de datos, nombre la fuente de datos dsCountries y pulse Aceptar. En el cuadro de diálogo Elegir la conexión de datos, seleccione la cadena NorthwindConnectionString que guardó al crear la fuente de datos primaria y pulse Siguiente. 4. En el cuadro de diálogo Configurar la instrucción Select, seleccione la tabla Orders, marque la columna ShipCountry y la casilla de verificación Devolver sólo filas únicas, pulse el botón ORDER BY, aplique un orden ascendente para ShipCountry y pulse Aceptar. 5. Pulse el botón Siguiente, compruebe la consulta y pulse Finalizar para volver al cuadro de diálogo Elegir un origen de datos, que mostrará ShipCountry como campo de valores y de muestra (ver la figura siguiente). Pulse Aceptar para cerrar el cuadro de diálogo. 6. Abra la ventana Propiedades de DropDownList1 y cambie el valor de la propiedad Id por ddlCountry o algo similar. 7. Pulse y verifique que la lista desplegable muestra los países por orden alfabético. Seleccionando un país distinto de Argentina, el servidor refrescará la página con la operación de postback que se especificó en el paso 2. 254

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 255

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0

Añadir una cláusula de restricción WHERE basada en el índice seleccionado de la lista Para conectar la selección de DropDownList a una cláusula WHERE de restricción añadida a dsOrders, haga lo siguiente: 1. Pulse la flecha de la etiqueta inteligente variable dsOrders para abrir la etiqueta inteligente SqlDataSource, pulse la opción Configurar origen de datos para iniciar el Asistente para la configuración de origen de datos y pulse el botón Siguiente. 2. Seleccione NorthwindConnectionString, pulse Siguiente y repita la selección de campo de la tabla Orders y la cláusula ORDER BY para SelectCommand. 3. Pulse el botón WHERE para abrir el cuadro de diálogo Agregar claúsula WHERE, seleccione ShipCountry en la lista Columna, acepte default = Operator, y seleccione Control en la lista Origen, que mostrará el cuadro de grupo Propiedades del parámetro.

255

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 256

Bases de datos con Visual Basic 4. Seleccione ddlCountry en la lista ID de Control y, opcionalmente, añada un país como valor por defecto. 5. Pulse el botón Agregar para añadir el criterio [ShipCountry]=@ShipCountry y ddlCountry.SelectedValue como el valor de @ShipCountry. 6. Pulse Aceptar para cerrar el cuadro de diálogo, pulse Siguiente y después el botón Consulta de prueba. Revise el resultado de la consulta y pulse el botón Finalizar. 7. Pulse y compruebe que al seleccionar un país distinto de Argentina en ddlCountry, la lista se refresca con los registros apropiados. Si prefiere que sean los usuarios quienes seleccionen un país, en lugar de mostrarles registros del primer país de la lista, puede utilizar una nueva propiedad de lista en ASP.NET 2.0: AppendDataBoundItems. Abra la ventana de propiedades de ddlCountry y defina el valor True para la propiedad AppendDataBoundItems. Pulse la flecha de la etiqueta inteligente ddlCountries. Seleccione Editar elementos para abrir el cuadro de diálogo Editor de la colección ListItem, pulse Agregar y escriba [Select a Country] como valor de Text, y verá que también aparece en el cuadro de texto Value (ver figura siguiente). Pulse Aceptar y la página se reabrirá con una celda de tabla inicialmente vacía. Si cambia los paréntesis en ángulo (<>) por los cuadrados ([]), la página arrojará una excepción de seguridad cuando seleccione [Select a Country]. A continuación vemos el código fuente para los elementos ddlCountry y dsCountries de la página: [Select a Country] ”>

256

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 257

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0 Al añadir la restricción de cláusula WHERE se inserta la siguiente definición de ControlParameter en el código fuente de dsOrders: <SelectParameters>

7.3.5 Editar ítems en listas de datos Editar elementos en una lista puede ser algo pesado para los usuarios, especialmente si la lista tiene un largo número de ellos. Con el control GridView o DetailsView los datos se pueden editar mucho más rápidamente y de manera más simple, ya que es el diseñador quien crea las plantillas necesarias para editar, insertar y borrar datos. En los apartados posteriores de este capítulo se describen las propiedades de los nuevos controles GridView y DetailsView. Con ellos también habrá que escribir código para obtener los valores originales y actualizados en el manejador de evento DataList_UpdateCommand y asignarlos como valores (miembros) de (la colección) DataSource.Command.Parameters en el manejador de evento DataSource_Updating. El siguiente código oculto del archivo EditableDataList.aspx.vb del proyecto de ejemplo DataWebSite, obtiene y asigna los valores de los parámetros para actualizar un elemento seleccionado: Public Sub dlOrders_UpdateCommand(ByVal source As Object, _ ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) _ Handles dlOrders.UpdateCommand 'Read-only OrderID value Dim strOrderID As String = dlOrders.DataKeys(e.Item.ItemIndex).ToString Dim strCustomerID As String = Nothing Dim txtBox As TextBox Dim strTextBox As String = Nothing Dim intParam As Integer alParamValues = New ArrayList For intParam = 0 To dsOrders.UpdateParameters.Count - 1 strTextBox = "TextBox" + (intParam + 2).ToString txtBox = CType(e.Item.FindControl(strTextBox), TextBox) If intParam = dsOrders.UpdateParameters.Count - 1 Then '@original_OrderID alParamValues.Add(strOrderID) Else If txtBox Is Nothing Then alParamValues.Add(Nothing) Else 'Other parameter values If txtBox.Text.Contains("$") Then 'Remove currency symbol for freight alParamValues.Add(Mid(Trim(txtBox.Text), 2))

257

VisualBasic2005_07.qxp

02/08/2007

16:28

PÆgina 258

Bases de datos con Visual Basic Else alParamValues.Add(Trim(txtBox.Text)) End If End If End If Next 'Execute the Update method dsOrders.Update() 'Return to Item mode dlOrders.EditItemIndex = -1 dlOrders.DataBind() End Sub Protected Sub dsOrders_Updating(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.SqlDataSourceCommandEventArgs) _ Handles dsOrders.Updating Try Dim strUpdateCmd As String = e.Command.CommandText Dim intCtr As Integer For intCtr = 0 To e.Command.Parameters.Count - 1 Dim strName As String = e.Command.Parameters(intCtr).ParameterName If alParamValues(intCtr).ToString = "" Or alParamValues(intCtr) Is Nothing Then e.Command.Parameters(intCtr).Value = DBNull.Value Else e.Command.Parameters(intCtr).Value = alParamValues(intCtr) End If Next Catch exc As Exception 'Ignore End Try End Sub

Si decide ampliar la capacidad de edición de una lista de datos, puede añadir una plantilla EditItem a la lista copiando la plantilla Item en la plantilla EditItems y substituyendo las etiquetas por cuadros de texto. Deberá añadir botones para activar la plantilla EditItem, actualizar la DataSource, o cancelar la operación de actualización. A diferencia de los botones que se añadirán a los controles FormView en el apartado siguiente, ahora debe añadir manejadores para los eventos EditCommand, UpdateCommand, y CancelCommand en la página o en el código oculto del archivo. La siguiente figura muestra la página EditableDataList.aspx del proyecto de ejemplo con un elemento en modo edición. Para este y otros muchos ejemplos de este capítulo, todos los manejadores de evento se encuentran en el código oculto del archivo.

258

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 259

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0

7.4 El control FormView El control FormView permite diseñar libremente la plantilla Item. Por ejemplo, le permite añadir una tabla multicolumna a la plantilla y después cortar y pegar el texto por defecto de ColumnName y los controles ColumnNameLabel en las celdas de la tabla. Y puede especificar el estilo de los bordes de celda, el ancho y el color, así como colores de fondo para etiquetas. FormView es una alternativa mucho más flexible que los controles GridView o DetailsView para actualizar y añadir registros a las tablas base. El proceso de añadir un control FormView y su SqlDataSource a una página es casi idéntico al de GridView. Cuando se utiliza FormView para editar datos de una tabla base, es una buena práctica añadir todas las columnas de la tabla a la fuente de datos.

7.4.1 Paginar la fuente de datos El control FormView soporta la paginacion, lo que permite seleccionar un registro específico y mostrarlo o editarlo. Para hacer posible la paginación, abra la ventana Propiedades para FormView y asigne el valor True a la propiedad AllowPaging. PagerSettings tiene por defecto un juego de diez valores númericos secuenciales y botones para seleccionar los diez valores anteriores o siguientes, pero puede darle a la propieadad Mode del paginador el valor NumericFirstList y después escribir First y Last como valores de las propiedades FirstPageText y LastPageText. Finalmente, amplíe el nodo PagerStyle y defina los valores del nodo Font para resaltar el paginador en la parte inferior del formulario (ver la siguiente figura).

259

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 260

Bases de datos con Visual Basic

Paginar es una operación que consume muchos recursos, sobre todo en tablas con muchos registros de datos. Al pulsar cualquier botón del paginador se ejecuta SelectCommand y se restablecen todas las filas de la base de datos en el servidor que cumplen el criterio de la cláusula WHERE, si existe. Filtrar registros con una cláusula WHERE generada por las listas desplegables, normalmente es el método más efectivo para reducir el tamaño de los resultados devueltos por la consulta SelectCommand. Con los registros en orden secuencial, se puede minimizar el consumo de recursos añadiendo un modificador TOP n y una cláusula ORDER BY a la sentencia SQL del SelectCommand.

7.4.2 Remplazar los valores Null por texto específico de la columna El siguiente código oculto de la página FormView.aspx añade los elementos Pending y <Empty> text que remplazan valores nulos por valores ShippedDate, ShipRegion y ShipPostalCode ausentes en la plantilla Item: Partial Class FormView Inherits System.Web.UI.Page Protected Sub fvOrders_DataBound(ByVal sender As Object, ByVal e As System.EventArgs) _ Handles fvOrders.DataBound 'Add default values for null ShippedDate, ShipRegion, and ShipPostalCode 'Disable deletion of shipped orders Dim strUpdateCmd As String = dsFormView.UpdateCommand Try If IsDBNull(Me.fvOrders.DataItem("ShippedDate")) Then Dim lblDate As Label = CType(fvOrders.FindControl("ShippedDateLabel"), Label) If Not lblDate Is Nothing Then

260

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 261

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0 lblDate.Text = "Pending" End If 'Enable deletion of orders not shipped Dim btnDelete As Button = CType(fvOrders.FindControl("btnDelete"), Button) If Not btnDelete Is Nothing Then btnDelete.Enabled = True End If 'Temporary workaround for null date problem Dim txtDate As TextBox = CType(fvOrders.FindControl("ShippedDateTextBox"), TextBox) If Not txtDate Is Nothing Then txtDate.Text = "1/1/2099" End If Else 'Disable deletion of shipped orders Dim btnDelete As Button = CType(fvOrders.FindControl("btnDelete"), Button) If Not btnDelete Is Nothing Then 'Temporary for null date workaround Dim lblDate As Label = CType(fvOrders.FindControl("ShippedDateLabel"), Label) If lblDate.Text = "Pending" Then btnDelete.Enabled = True Else btnDelete.Enabled = False End If End If End If If IsDBNull(fvOrders.DataItem("ShipRegion")) Then Dim lblRegion As Label = CType(fvOrders.FindControl("ShipRegionLabel"), Label) If Not lblRegion Is Nothing Then lblRegion.Text = "<Empty>" End If End If If IsDBNull(fvOrders.DataItem("ShipPostalCode")) Then 'Applies to Ireland only Dim lblCode As Label = CType(fvOrders.FindControl("ShipPostalCodeLabel"), Label) If Not lblCode Is Nothing Then lblCode.Text = "<Empty>" End If End If Catch exc As Exception 'Ignore for now End Try

261

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 262

Bases de datos con Visual Basic End Sub Protected Sub dsFormView_Deleting(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.SqlDataSourceCommandEventArgs) _ Handles dsFormView.Deleting 'Test for Order Details records End Sub Protected Sub dsFormView_Updating(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.SqlDataSourceCommandEventArgs) _ Handles dsFormView.Updating 'Substitute NULL for 1/1/20599 If e.Command.Parameters("@ShippedDate").Value.ToString.Contains("1/1/2099") Then e.Command.Parameters("@ShippedDate").Value = DBNull.Value End If End Sub End Class

El evento DataBound se dispara cuando todas las filas de DataSource están pobladas. El método FormView.DataItem(ColumnName) devuelve el último valor vinculado de la fila de datos seleccionada actualmente. Las sentencias DimvarNameAsControlType = CType(FormView.FindControl(ControlId), ControlType) devuelve una referencia al control que permite definir sus valores de propiedad, por ejemplo Text o Enabled. El manejador de evento también desactiva el botón Delete para los pedidos que ya se han enviado.

7.4.3 Editar, añadir y borrar registros El control FormView es mejor elección para editar registros que DataList, ya que FormView sólo muestra un elemento. Se puede crear una plantilla EditItem o InsertItem rápidamente en el modo Código del editor copiando y pegando el nodo y sus hijos. (Para borrar una fila no se necesita ninguna plantilla). El diseñador renombra automáticamente las etiquetas con valores Id duplicados en LabelN, donde N es un número secuencial. Renombre el que ha copiado y póngale el nombre <EditItemTemplate>, y sustituya todas las instancias de Label en los nodos hijo de <EditItemTemplate> por TextBox para completar la nueva plantilla. Si la plantilla Items tiene bordes, desactive la propiedad cell border de la tabla para eliminarlos. Otra alternativa es dar a la propiedad BorderColor el valor BgColor de la tabla o el BackColor del FormView. La figura siguiente muestra el FormView de la figura anterior con la plantilla EditItem activa. Una vez finalizado el diseño de la plantilla EditItem, copie y pegue en modo Código el nodo <EditItemTemplate> y sus hijos y renombre la copia como para que se puedan añadir nuevos ítems.

262

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 263

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0

7.4.4 Añadir botones de comando Los controles FormView definen un conjunto de expresiones de modo y acción para activar las plantillas EditItem o InsertItem, cancelar operaciones de edición o inserción y ejecutar los comandos de DataSource UpdateCommand, InsertCommand o DeleteCommand. Para añadir un control Button, LinkButton o ImageButton a una plantilla y asignarles una acción hay que escribir el nombre de la acción en el cuadro de texto de la propiedad CommandName. A diferencia de otros botones similares en las plantillas DataList, para activar plantillas o actualizar, insertar o borrar ítems, no se necesita código de manejador de evento. Aquí vemos cómo utilizar los tres modos para activar las plantillas: Q

Q

Q

Edit – activa la plantilla EditItem. Añade un botón Edit o Update a la plantilla Item por defecto, con el valor edit o Edit en su propiedad CommandName. New – activa la plantilla InsertItem. Añade un botón New a la plantilla por defecto Item con el valor new o New en su propiedad CommandName. Cancel – reactiva la plantilla Item por defecto. Añade botones Cancel a las plantillas EditItem e InsertItem con el valor cancel o Cancel en su propiedad CommandName.

Las siguientes acciones son para ejecutar comandos: Q

Q

Update – ejecuta UpdateCommand de DataSource y activa la plantilla Item. Añade un botón Save o Update a la plantilla EditItem, con el valor update o Update en su propiedad CommandName. Insert – ejecuta el InsertCommand de DataSource y activa el comando Item. Añade un botón Save o Insert a la plantilla InsertItem con el valor insert o Insert en su propiedad CommandName.

263

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 264

Bases de datos con Visual Basic Q

Delete – ejecuta el DeleteCommand de DataSource. Añade un botón Delete o Remove a la plantilla Item por defecto con el valor delete o Delete en su propiedad CommandName.

La figura siguiente muestra la página FormView.aspx con la plantilla InsertItem activada y la entrada de datos parcialmente completada. Los controles de paginación quedan ocultos en el modo Insert.

7.5 El control GridView El control GridView, que substituye el DataGrid de ASP.NET 1.x, simula el control de formulario Windows DataGridView hasta cierto punto, si se considera las limitaciones respecto al navegador de los controles del servidor HTML. El proceso de añadir un GridView a un formulario es similar al de un DataList o FormView. Arrastre el control GridView hasta una página y seleccione una fuente de datos existente o especifique una nueva. La figura siguiente muestra un control GridView paginado, de sólo lectura, poblado por dsOrders SqlDataSource y con un campo Select Command autogenerado. Para añadir un campo Select Command para un control GridViews de sólo lectura se ha de seleccionar la casilla de verificación Habilitar paginación de la etiqueta inteligente GridView. Al seleccionar Habilitar paginación se añade una paginación numérica por defecto para el formulario. El cuadro de verificación Habilitar ordenación subraya y cambia el color de las cabeceras de columna para indicar el tipo de clasificación. Puede desactivar la clasificación de los campos seleccionados borrando el valor de su propiedad SortExpression en el cuadro de diálogo Campos. Los GridViews con DataSources actualizables añaden casillas de verificación Habilitar edición y Habilitar eliminación como muestra la figura posterior. Una limitación seria de los GridViews es que no permiten 264

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 265

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0 añadir nuevos elementos. Para ésto se ha de utilizar o bien un DetailsView o un FormView, que pueden estar en la misma página o en otra diferente.

Los controles GridView soportan estos siete tipos de campo: Q

CommandFields – corresponden a los modos de FormView. Los CommandFields existentes son: Select, Edit, Cancel, Update y Delete, que disparan los eventos ItemCommand, SelectedIndexChanging, SelectedIndexChanged, RowEditing, RowCancelingEdit, RowUpdating, RowUpdated, RowDeleting y RowDeleted. El control por

265

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 266

Bases de datos con Visual Basic defecto para CommandFields es un botón de texto Link. Los botones o imágenes convencionales se pueden sustituir definiendo el valor Button o Image para la propiedad ButtonType del campo. Q

BoundFields – muestran valores en controles Label por defecto. Si se pulsa el botón Editar de una fila, los controles Label de columnas editables cambian a Cuadros de texto. La anchura de los cuadros de texto es fija, para cambiarla hay que cambiar el BoundField por un TemplateField.

Q

CheckBoxFields – muestran y editan valores binarios, como 0 y 1 o False y True.

Q

ButtonFields – muestran controles Button convencionales.

Q

Q

Q

HyperlinkFields – muestran texto y proporcionan un campo adicional oculto para navegar por la página. Los campos Select command se pueden sustituir por campos de hipervínculo para cargar y editar páginas. ImageFields – muestran gráficos de las columnas image o varbinary del SQL Server, o archivos XML de imágenes codificadas. TemplateFields – permiten personalizar el formateo de los cuadros de texto o substituir otros controles, como DropDownLists, para la edición.

7.5.1 Convertir campos BoundFields en campos EditItemTemplate Los cuadros de texto con un ancho automático son suficientes para los tests iniciales, pero normalmente habrá que ajustar la anchura para que quepa un GridView, necesario para la edición de los datos. La figura siguiente muestra la página EditableGridView.aspx con una fila en el modo edición. Todas las columnas de esta página, excepto Order ID, que es de sólo lectura, son TemplateFields. Las plantillas Empl. ID y Ship Via especifican DropDownLists vinculados para definir los valores de las columnas numéricas. El cuadro de texto Cust. ID es de sólo lectura porque no es habitual reasignar un pedido a un cliente diferente. Para convertir un BoundField en un TemplateField, abra la etiqueta inteligente GridView y pulse el vínculo Editar columnas para abrir el cuadro de diálogo Campos. Seleccione en la lista Campos seleccionados el campo vinculado que quiere convertir, pulse el vínculo Convertir este informe en Template Field y pulse después Aceptar. El proceso de conversión añade un ItemTemplate con un control Label y un EditItemTemplate con un control de cuadro de texto e ítems vacíos AlternatingItemTemplate, HeaderTemplate y FooterTemplate, bajo una cabeceras ColumnName para cada una de las columnas convertidas (ver figura siguiente). A continuación vemos el código fuente reformateado para las columnas de sólo lectura Order ID y Customer ID:

266

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 267

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0

<EditItemTemplate>


(ver figura siguiente)

7.5.2 Remplazar cuadros de texto por listas desplegables para la edición Una buena práctica de diseño es proporcionar listas desplegables vinculadas para definir valores de clave foránea con un número limitado de opciones. Para sustituir un cuadro de texto por una lista desplegable, cree una fuente de datos para la lista y borre el cuadro de texto. Arrastre un control DropDownList desde el cuadro de herramientas, defina su DataSource, sus campos de muestra y de valor, y después vincule la propiedad SelectedValue a la columna de clave foránea escribiendo Bind("DataColumnName") en el cuadro de texto para la expresión de código del cuadro de diálogo ListName-

267

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 268

Bases de datos con Visual Basic

DataBindings (ver la figura siguiente). El método Bind del GridView remplaza al método Eval de los controles DataList y FormView. Veamos el código fuente reformateado de la plantilla para la columna Empl. ID: <EditItemTemplate>

268

VisualBasic2005_07.qxp

02/08/2007

16:29

PÆgina 269

Trabajar con las fuentes de datos y controles vinculados de ASP.NET 2.0

269

VisualBasic2005_08.qxp

02/08/2007

16:30

PÆgina 271

Capítulo 8

Aplicar técnicas avanzadas con ASP.NET 2.0 Este capítulo expande el horizonte de desarrollo con VB.NET VB 2005 más allá de las sencillas páginas Web que sólo contienen datos y algunos controles Web vinculados. Este capítulo trata aspectos más avanzados de ASP.NET 2.0, que le enseñarán a sacar el mejor partido de los controles Web y las funciones de VS 2005, algunos nuevos y otros actualizados, que vemos a continuación: Y

Y

Data validation: validación de datos con los controles RequiredFieldValidator, RangeValidator, RegularExpressionValidator, CompareValidator, CustomValidator y ValidationSummary. ObjectDataSources: fuentes de datos de objeto, basadas en tablas de juegos de datos tipificados, definidos en los archivos DataSet.xsd o referencias a bibliotecas de clase y objetos de negocios habituales con campos Nullable(OfDataType).

No conseguirá las habilidades necesarias para diseñar sitios Webs escalables y funcionales, con fuentes de datos de ejemplo, si se limita a unos cuadros de texto y cinco o diez filas o grupos de elementos. Con las tablas Orders y Order Details como fuentes de datos para la mayoría de los proyectos de ejemplo expuestos en este capítulo, podrá abordar muchos de los aspectos a los que tendrá que enfrentarse en el desarrollo de páginas Web de producción con VS 2005.

8.1 Validar entradas en controles vinculados a datos Validar las entradas del usuario antes de enviar los valores editados al servidor de la base de datos evita acceder innecesariamente al servidor, reduce la carga del mismo e incrementa la escalabilidad de la aplicación. Si sus controles vinculados a datos conectan con un componente lógico de acceso a datos que refuerza las reglas de negocios, la validación por parte del cliente de los campos requeridos y los formatos de datos, reduce el tráfico en la red y el consumo de recursos. Todos los formularios Web y de producción de Windows deberían permitir la validación de los datos entrados por el usuario desde el cliente, independientemente de la arquitectura de acceso de datos que se adopte. 271

VisualBasic2005_08.qxp

02/08/2007

16:30

PÆgina 272

Bases de datos con Visual Basic ASP.NET cuenta con dos métodos para validar las entradas del usuario mediante controles vinculados: los controles de validación del servidor o el código añadido a los manejadores de evento ControlId_Updating o ControlId_Inserting. Los controles de validación desde el servidor muestran mensajes de error de validación cuando el usuario escribe o selecciona un valor que no supera el test de validación y pasa al siguiente control. El código del manejador de evento puede mostrar uno o más mensajes de error de validación en un cuadro de texto, pero los mensajes de error no aparecen hasta que el usuario finaliza el proceso de edición y pulsa un botón de actualización o de entrada de datos. Retrasar la valiadación hasta el momento de enviar los datos puede ser frustrante para los usuarios, especialmente después de una larga sesión de introducción de datos.

8.2 Los controles de validación de ASP.NET 2.0 ASP.NET 2.0 cuenta con los mismos controles de validación por parte del servidor que ASP.NET 1.1. Los controles de validación no se limitan a los campos vinculados, también se puede asisgnar un validador a cualquier control de servidor que acepte la entrada de datos por parte del usuario. Para validar columnas GridView o campos DetailsView se necesita una plantilla EditItem por cada ítem que se quiere validar. A continuación damos una descripción breve de los seis controles de validación de ASP.NET: Y

Y

Y

272

RequiredFieldValidator: comprueba si el control especificado por su propiedad Id contiene algún valor que no concuerde con el de la propiedad InitialValue, que tiene un string vacío por defecto. Dando uno de los valores de una DropDownList con [SelectanItem] como primer miembro de la colección de Items y definiendo True para AppendDataBoundItems, el valor inicial InitialValue queda especificado como [SelectanItem]. Si el valor entrado no concuerda con InitialValue, el validador muestra el valor de su propiedad Text o ErrorMessage en letras rojas, a la derecha o por debajo del control. RangeValidator: comprueba si el valor de un control específico queda dentro del rango de valores definidos para las propiedades MinValue y MaxValue, cuyo tipo se especifica en la propiedad DataType. Esos dos valores pueden ser constantes String, Integer, Double, Date o Currency. El mensaje de error aparece cuando los valores introducidos o seleccionados quedan fuera del margen especificado. El control RangeValidator no comprueba los controles vacíos, por lo que hay que añadir un RequiredFieldValidator al control que se especifique. RegularExpressionValidator: comprueba que el texto del control especificado sea conforme a una expresión regular que usted debe escribir como valor de la propiedad ValidationExpression. Por ejemplo, la RegularExpression [A-Z]{5} valida una entrada CustomerID si contiene cinco letras mayúsculas. Otra alternativa, es pulsar el botón builder de ValidationExpression para seleccionar una de las expresiones regulares estándar, con formato de dirección de e-mail, URL, número de teléfono, código postal o número de la seguridad social o del carnet de identidad. Hay que añadir también un RequiredFieldValidator para comprobar los valores vacíos.

VisualBasic2005_08.qxp

02/08/2007

16:30

PÆgina 273

Aplicar técnicas avanzadas con ASP.NET 2.0 Y

Y

Y

CompareValidator: comprueba si el valor del control especificado es mayor, menor, igual, menor o igual, mayor o igual que el de otro control cuyo valor Id se especifica como valor de la propiedad ControlToCompare. CompareValidator comprueba el mismo tipo de datos que RangeValidator. Hay que añadir un RequiredFieldValidator para comprobar los valores vacíos. CustomValidator: permite comprobar el control especificado con una función de JScript o VBScript, la función ClientValidationFunction y un manejador VB.NET similar que hay que escribir para el evento OnServerValidate. La función ClientValidationFunction proporciona validación por parte del cliente y el manejador de evento OnServerValidate se ejecuta cuando el usuario envía datos a la página. ValidationSummary: proporciona la herramienta para mostrar los valores de la propiedad ErrorMessage de todos los controles de validación actuales en un solo cuadro de texto o un cuadro de lista. Otra alternativa, u opción adicional, es mostrar los errores en un cuadro de mensaje para usuarios con IE 4.0 o posteriores. El resumen de los mensajes de error no aparece hasta que el usuario envía el formulario. La propiedad CausesValidation de los controles Edit y New LinkButton, tiene por defecto el valor True, lo que activa todos los controles de la plantilla EditItem y, en el caso de DetailsView, en la plantilla InsertItem.

8.2.1 La nueva propiedad ValidationGroup ASP.NET 2.0 incorpora una nueva propiedad, ValidationGroup, con la que se pueden validar datos selectivamente mediante grupos de controles de validación en los formularios de datos que no utilizan los controles preintegrados. Por ejemplo, tal vez no quiera aplicar todos los validadores a una entrada nueva de la tabla Orders. En ese caso, defina un nombre de grupo, como EditGroup1, en la propiedad ValidationGroup de un botón de envío EditGroup1 y los controles de validación de los cuadros de texto y otros controles vinculados del formulario. Otros controles de entrada de datos con validadores en EditGroup2 actualizarán los campos restantes. Aplicar grupos de validación a controles vinculados GridView es más problemático. Los CommandFields autoinsertados no proporcionan acceso directo a sus propiedades, por lo que no se puede especifiar el nombre del ValidationGroup sin añadir un CommandField obligatorio. Si añade una plantilla InsertItem a un control FormView o DetailsView, debe añadir los controles de validación requeridos a las dos plantillas EditItem e InsertItem. El proceso es similar al de agrupar controles de validación en la edición y entrada de datos.

8.3 Otras propiedades de validación compartidas A continuación, ofrecemos una breve descripción de las propiedades de control más importantes compartidas por la mayoría de controles de validación: Y

ControlToValidate es un valor necesario de la propiedad ID para todos los controles de validación, excepto CustomValidator.

273

VisualBasic2005_08.qxp

02/08/2007

16:30

PÆgina 274

Bases de datos con Visual Basic Y

Y

Y

Y

ErrorMessage aparece junto al control asociado si no se especifica un valor para Text. Especifique siempre un valor para Text y añada el texto que quiere mostrar como ErrorMessage en los cuadros de texto o de mensaje de SummaryValidator. DisplayMode especifica el modo en que el control de validación muestra la propiedad Text. El valor por defecto, Static, guarda espacio para el mensaje bajo o junto al control, según la disponibilidad. Dynamic sólo ocupa espacio en el formulario cuando se muestra un error de validación y suele ser la opción preferida. None impide que se muestre el mensaje, excepto con el control asociado ValidationSummary, si existe. ToolTip: su texto ayuda al usuario ya que permite una descripción larga del error de validación. Por ejemplo, se puede copiar el valor de ErrorMessage en ToolTip y añadir información para ayudar al usuario a corregir el error. EnableClientScript determina si hay validación por parte del cliente o no; el valor por defecto es True. Si se define el valor False, la validación no tendrá lugar hasta que el usuario no envíe el formualrio.

A continuación, mostramos un ejemplo del código fuente reformateado para un RequiredFieldValidator: <EditItemTemplate> > ID= lblOrderDate >

La siguiente figura muestra el valor Required para la propiedad Text bajo una entrada vacía de OrderDate.

8.4 Validar ediciones en GridView En los siguientes apartados veremos cómo pedir a los usuarios que introduzcan datos en un campo específico, así cómo aplicar expresiones regulares para verificar el formato de los datos, o limitar las entradas a una serie de valores, validar datos comparando valores con los datos de otra columna o un valor calculado, y sacar el mejor partido del control ValidationSummary. Si bien un GridView vinculado a un SqlDataSource proporciona los elementos fijos de un test de control de validación, las técnicas que aprenderá a 274

VisualBasic2005_08.qxp

02/08/2007

16:30

PÆgina 275

Aplicar técnicas avanzadas con ASP.NET 2.0

continuación se aplican en cualquier control editable, independientemente del tipo de fuente de datos.

8.4.1 Añadir un campo necesario de validación a un control GridView A continuación veremos el proceso básico para añadir un control de validación a la plantilla EditItem de un GridView en modo Diseño: 1. Abra la etiqueta inteligente de GridView y pulse Editar plantillas. Pulse con el botón derecho la etiqueta inteligente ItemTemplate y seleccione la opción Editar plantillas y la columna plantilla para su validación. 2. Ajuste la anchura del control TextBox en el panel EditItemTemplate para colocar el texto de entrada, borre el valor por defecto de la propiedad Height y cambie el valor de ID por un nombre descriptivo, txtCustomerID en nuestro ejemplo. 3. Arrastre uno de los controles de validación –en nuestro ejemplo, RequiredFieldValidator– desde la sección Validation del Cuadro de herramientas hasta la derecha del cuadro de texto, el control mostrará el mensaje de error RequiredFieldValidator, en rojo por defecto. 4. Abra la ventana Propiedades del validator, asigne un nombre relacionado –como rfvCustomerID–al valor de la propiedad ID y el nombre del cuadro de texto asocia-

275

VisualBasic2005_08.qxp

02/08/2007

16:30

PÆgina 276

Bases de datos con Visual Basic do a la propiedad ControlToValidate. Cambie el valor Static de la propiedad Display por Dynamic, substituya el ErrorMessage por defecto por una descripción breve de la regla de validación, añada un mensaje corto para que aparezca bajo el cuadro de texto como valor de la propiedad Text y añada un texto opcional para ToolTip, tal como muestra la siguiente figura.

Al validar los GridViews, defina el valor Top para la propiedad ItemStyle.VerticalAlign en el cuadro de diálogo Fields. Ese ajuste alineará horizontalmente todos los cuadros de texto cada vez que se muestre un mensaje de error.

8.5 Validar entradas CustomerID con un control RegularExpressionValidator El campo CustomerID de la tabla Order requiere cinco letras mayúsculas, por lo que es un buen candidato para comprobar la validación mediante una expresión regular. La sencilla expresión [A-Z]{5} es suficiente para este test; [A-Z] especifica las letras mayúsculas de la A hasta la Z y {5} indica el número de veces que ha de aparecer la letra en el texto relacionado. Escribir expresiones regulares está más allá del alcance de este libro. El sitio Regular Expressions Library, en la dirección http://www.regexlib.com/, tiene unas 800 expresiones indexadas, con una amplia variedad de formatos de texto estándar y semi-estándar. La entrada de la ayuda online: acerca de las expresines regulares (About Regular Expressions) contiene apartados dónde se explica el funcionamiento de estas expresiones y se describe las clases del espacionombre System.Text.RegularExpressions.

276

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 277

Aplicar técnicas avanzadas con ASP.NET 2.0 Para añadir un RegularExpressionValidator a la plantilla CustomerID del GridView, siga los pasos descritos en el apartado anterior, pero arrastrando un control RegularExpressionValidator hasta la derecha del RequiredFieldValidator. Cambie adecuadamente los valores de las propiedades del RequiredFieldValidator, y escriba [A-Z]{5} como valor de ValidationExpression. Pulse el botón de construcción (builder button) en el cuadro de texto ValidationExpression y se abrirá el cuadro de diálogo Editor de expresiones regulares, con una serie de expresiones prefabricadas para europeos, norteamericanos y asiáticos (ver la siguiente figura).

A continuación vemos el código fuente reformado para el TemplateField CustomerID con los controles RequiredValueValidator y RegularExpressionValidator añadidos: <EditItemTemplate> > Required!   [ABCDE]

277

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 278

Bases de datos con Visual Basic
ID= lblCustomerID >


El ejemplo anterior ha sido elaborado para demostrar la validación de regex. En las aplicaciones de la vida real, lo mejor es definir los valores de CustomerID en una lista desplegable (DropDownList, parecida a las de las columnas EmployeeID o ShipVia del ValidatedGridView) y comprobarlos contrastando un Custom Validator con una tabla de datos con valores CustomerID válidos.

8.5.1 Comprobar los valores de EmployeeID con un control RangeValidator Los valores de clave foránea de EmployeeID deben ser entre 1 y 9. La lista desplegable ddlEmployee original, que permite a los usuarios seleccionar apellidos en una lista, evita que los usuarios seleccionen un valor EmployeeID no válido. En este ejemplo se ha incluido un item [LastName] no válido en la consulta SQL sobre dsEmployeesSqlDataSource. Veámoslo: SELECT [EmployeeID], [LastName] FROM [Employees] UNION SELECT 0, [Last Name] ORDER BY [LastName]

El control RangeValidator requiere que se seleccione un valor apropiado (Integer) para la propiedad Type y se especifiquen valores en las propiedades MinimumValue(1) y MaximumValue(9) para los datos numéricos y de fecha.

8.5.2 Aplicar RangeValidator y RegularExpressionValidator a las entradas de datos Para evitar el acceso al servidor cuando los usuarios entran fechas inexistentes o en el formato erróneo, hay que verificar la entrada con un RangeValidator que acepta fechas dentro de unos ciertos límites. Los límites que se definan dependen de la fuente de datos para ese campo, pero la mayoría de aplicaciones tendrán, probablemente, 1/1/1980 como MinimumValue y 12/31/2099 como MaximumValue. Si se especifica Date como valor de la propiedad Type, el DateTime de .NET comprueba que la fecha sea válida. Como ejemplo, 2/29/2005 o 11/31/00 provocaría un error, pero 2/29/2004 o 02/29/00, no. Por lo tanto, una buena práctica de programación es añadir un RangeValidator a todas las columnas Datetime de los cuadros de texto vinculados. Por defecto, el DateTime acepta años con dos o cuatro dígitos, y barras o guiones como separadores. Si quiere implantar un formato específico más corto, por ejemplo M/D/YYYY, deberá añadir un validador RegularExpression. La siguiente regex requiere vírgulas/comas y años escritos con cuatro dígitos: 278

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 279

Aplicar técnicas avanzadas con ASP.NET 2.0 ^((((0?[13578])|(1[02]))[\/]?((0?[1-9]|[0-2][0-9])|(3[01])))|(((0?[469])|(11))[\/] ?((0?[1-9]|[0-2][0-9])|(30)))|(0?[2][\/]?(0?[1-9]|[0-2][0-9])))[\/]?\d{4}$

La regex anterior es una modificación de una de las expresiones donadas por Cliff Schneide al sitio Regular Expressions Library. Las modificaciones impiden los separadores con guiones de unión y exigen años de cuatro dígitos. Veamos el código fuente reformateado para el OrderDate TemplateField con los controles RequiredFieldValidator, RangeValidator y RegularExpressionValidator: <EditItemTemplate> > Required! [M/D/YYYY] [M/D/YYYY] ID= lblOrderDate >

8.6 Impedir entradas ilógicas con un CompareValidator Se puede utilizar un CompareValidator para impedir las entradas numéricas y las fechas con valores que van contra las reglas de negocio (o el sentido común), por ejemplo un RequiredDate igual o inferior a la fecha OrderDate. La propiedad ControlToCompare del 279

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 280

Bases de datos con Visual Basic control CompareValidator requiere que el ID de un control tenga un tipo de datos compatibles con el del ControlToValidate especificado. Por ejemplo, para comparar un valor Integer con otro que tenga una fracción decimal, hay que especificar Double como valor de Type. En este ejemplo vamos a substituir el control CompareValidator por el RangeValidator. Especifique txtRequiredDate como valor de ControlToValidate, txtOrderDate como ControlToCompare, GreaterThan como Operator y Date como Type. A continuación, el código fuente reformateado para el RequiredDate TemplateField: <EditItemTemplate> > Required! Impossible! [M/D/YYYY] ID= lblRequiredDate >

8.6.1 Añadir un control CustomValidator Los controles CustomValidator requieren añadir un manejador de validación por parte del servidor, para el evento ValidatorName_ServerValidate y una función opcional JScript 280

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 281

Aplicar técnicas avanzadas con ASP.NET 2.0 o VBScript para la validación por parte del cliente. Este ejemplo valida las ediciones de la columna Freight y requiere una entrada de 5 o más si la columna ShippedDate contiene una fecha. El validador utiliza la "política comercial de envío mínimo de $ 5.00 y cargo adicional" de los comerciantes de Northwind. Veamos el código fuente reformateado para el Freight TemplateField, que especifica el manejador de evento cvFreight_ServerValidate por parte del servidor, y el valor de la propiedad ClientValidationFunction de VBScript por parte del cliente: <EditItemTemplate> > Required! <5=Bad! ID= lblFreight >

El siguiente manejador de evento cvFreight_ServerValidate contiene código para obtener el valor de otra columna GridView de la fila editada. El código también define el valor False para la propiedad args.IsValid siempre que haya una fecha en la columna ShippedDate y el valor de Freight sea menor que $5.00. Sub cvFreight_ServerValidate(ByVal source As Object, ByVal args As System.Web.UI.WebControls.ServerValidateEventArgs) args.IsValid = True If Val(args.Value) < 5 Then With gvOrdersEditable Dim gvrRow As GridViewRow = .Rows(.EditIndex) Dim txtShipped As TextBox = CType(gvrRow.FindControl("txtShippedDate"), TextBox) If txtShipped IsNot Nothing Then If Len(txtShipped.Text) > 4 Then

281

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 282

Bases de datos con Visual Basic args.IsValid = False End If End If End With End If End Sub

Escribir el código para la función de validación por parte del cliente es un poco más complicado. Hay que inferir el nombre del campo a verificar del valor de Document.activeElement.id, el cual devuelve gvOrdersEditable_ctl14_txtFreight del siguiente elemento activo:

Substituya txtFreight por txtShippedDate para crear el valor del atributo id para la misma fila y aplique el método Document.getElementById(ShipDateId).outerHTML para devolver el elemento ShippedDate :

Finalmente, extraiga el texto del atributo value para determinar si existe algún valor ShippedDate. A continuación, vemos la función VBScript en la sección que implementa la validación por parte del cliente: <script language= vbscript > Function ValidateFreight(source, args) If args.Value < 5 Then FreightID = Document.activeElement.id ShipDateID = Left(FreightID, InStrRev(FreightID,

_ )) & txtShippedDate ShipDate = Document.getElementById(ShipDateID).outerHTML ShipDate = Mid(ShipDate, Instr(ShipDate, value= ) + 6) ShipDate = Left(ShipDate, Instr(ShipDate, name= ) -1) If Len(ShipDate) > 4 Then args.IsValid = False Else args.IsValid = True End If End If End Function

Puede escribir código similar para realizar cálculos de validación de fechas, como substituir el control CompareValidator por el RequiredDate con un CustomValidator que requiera un mínimo de siete días de diferencia entre los valores de OrderDate RequiredDate. 282

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 283

Aplicar técnicas avanzadas con ASP.NET 2.0 CustomValidators permite mucha más flexibilidad que los validadores prefabricados, aunque para ello hay que escribir, y comprobar, los manejadores de evento y el código.

8.7 Escribir un mensaje para el control Validation Summary La página ValidatedGridView.aspx ha heredado de EditableDataGridView.aspx un cuadro de texto que muestra mensajes de error del servidor. Se puede crear un cuadro de texto similar para mostrar los valores de ErrorMessage sin corregir, añadiendo un control ValidationSummary al principio de la página. El cuadro de texto ValidationSummary sólo aparecerá cuando los usuarios envíen una página con errores sin corregir. Añada un cuadro de texto para el resumen de la validación arrastrando un control ValidationSummary y situándolo por encima del GridView, definiendo el valor SingleParagraph para la propiedad DisplayMode y HeaderText opcionalmente, y aplicando los formatos necesarios.

La siguiente figura muestra un control ValidationSummary indicando varios errores de edición. El cuadro de texto PostBacks muestra el número de sesiones postbacks, lo que permite diferenciar las validaciones del cliente de las del servidor al ejecutar el servidor Web integrado.

283

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 284

Bases de datos con Visual Basic

8.8 Validar ediciones de ProductID en el servidor Web Las ediciones o inserciones que crean valores de ProductID duplicados para un solo OrderID de la tabla Order Details arrojan excepciones de clave primaria. Order Details GridView (gvOrderDetails) tiene un control CustomValidator (cvProductID) para la columna ProductID. Este control comprueba si hay valores duplicados con ayuda del siguiente manejador de evento: Sub cvProductID_ServerValidate(ByVal source As Object, ByVal args As System.Web.UI.WebControls.ServerValidateEventArgs) Dim intRow As Integer Dim lblTest As Label = Nothing args.IsValid = True 'Test edited value for duplicate ProductID With gvOrderDetails For intRow = 0 To .Rows.Count - 1 lblTest = CType(.Rows(intRow).FindControl("lblProductID"), Label) If lblTest IsNot Nothing Then If args.Value = lblTest.Text Then args.IsValid = False Exit For End If End If Next End With End Sub

Este manejador de evento funciona correctamente porque no contiene ningún elemento <span> con un control lblProductID para la fila que se edita.

8.8.1 Test para descubrir valores duplicados de ProductID en el cliente El código por parte del cliente para invalidar ediciones que crean valore duplicados de ProductID es más complejo que el de la parte del servidor. El código HTML generado por el servidor asigna valores numéricos al atributo ID de lblProductID, como vemos en este ejemplo para la primera línea del control gvOrderDetails: <span id= gvOrderDetails_ctl03_lblProductID >5

Las modificaciones en el diseño pueden hacer cambiar el valor secuencia inicial de 03 y los registros de Orders tener diferentes números de los de Order Details. El método más seguro para comprobar la existencia de valores duplicados es iniciar la búsqueda con 01 y finalizarla cuando se encuentra un duplicado, o una vez se han comprobado todas las filas con detalles de un pedido. La siguiente función de código VBScript comprueba pedidos de menos de 98 filas: <script language="vbscript"> Function ValidateProductID(source, args)

284

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 285

Aplicar técnicas avanzadas con ASP.NET 2.0 args.IsValid = True Prefix = "gvOrderDetails_ctl" Suffix = "_lblProductID" LastRow = 99 For Ctr = 1 To 99 If Ctr < 10 Then CtlNum = "0" & Ctr Else CtlNum = Ctr End If CtlName = Prefix & CtlNum & Suffix Set objCtl = Document.getElementById(CtlName) If objCtl Is Nothing Then If Ctr > LastRow Then 'Last valid row Exit For End If Else ProductID = objCtl.innerText LastRow = Ctr If ProductID = args.Value Then args.IsValid = False Exit For End If End If Next End Function

Que haya más de 97 filas no afecta al rendimiento, ya que cada vez que se encuenetra un valor duplicado o se pasa la última fila válida, se ejecuta la sentencia ExitFor. La siguiente figura muestra la página ValidatedDetailsView.aspx con numerosas transgresiones de edición por parte del cliente, incluido un valor duplicado de ProductID.

8.9 Remplaar SqlDataSources por ObjectDataSources Las fuentes de datos Sql, o SqlDataSources, implementan una arquitectura two-tier (cliente/servidor) que normalmente es suficiente para sitios Web visitados simultáneamente por algunos cientos de usuarios, sin requisitos de lógica de negocios, o requisitos simples, y procedimientos almacenados, o consultas SELECT con cláusulas WHERE para restringir el número de filas de DataLists y DataViews. La simplicidad del acceso de datos two-tier minimiza el tiempo de desarrollo y de comprobación, pero la arquitectura cliente/servidor impone una estructura de datos relacional específica a las aplicaciones. Los procedimientos almacenados pueden mejorar el rendimiento e incorporar cambios en los nombres de tabla y de columnas, así como columnas añadidas. Los procedimientos almacenados incrementan también la seguridad de la base de datos, ya que previenen el acceso directo del cliente o por middle-tier a las tablas subyacentes; de 285

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 286

Bases de datos con Visual Basic

todos modos, estos procedimientos no alcanzan el grado de abstracción de los ObjectDataSources. Las ObjectDataSources permiten añadir un componente de capa de acceso a los datos (en inglés: DataAccessLayerComponent, DALC) entre la página Web que proporciona el UI y los procedimientos almacenados o consultas SQL que acceden a las tablas base. El componente lógico DALC que implementa el middle-tier puede, pero no es necesario, añadirse como tier físico. Los apartados siguientes describen ObjectDataSources creadas a partir de las tablas de un juego de datos tipificado.

8.9.1 ObjectDataSources a partir de DataTables El control ObjectDataSource de ASP.NET 2.0 permite vincular objetos de negocios con controles de servidor Web activados para datos (data-enabled Web server controls). La versión más sencilla de un control ObjectDataSource es un objeto DataTable de un DataSet tipificado. Un ObjectDataSource creado a partir de un DataTable no permite abstraer el control asociado de vinculación de datos a partir de los metadatos de la tabla base sub-

286

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 287

Aplicar técnicas avanzadas con ASP.NET 2.0 yacente o un procedimiento almacenado. Generar, en tiempo de compilación, una clase tipificada DataSet para soportar DataTables consume más recursos del sistema y se cobra un impuesto mayor en el rendimiento al invocar un SqlDataSource. Los ejemplos de los apartados siguientes están pensados especialmente para hacer más simple el primer contacto con los ObjectDataSources, pero no son una invitación a utilizar DataSets tipificados como DALCS en las aplicaciones Web de producción. Para añadir a un sitio Web el esquema para incorporar un DataSet tipificado, pulse con el botón derecho la carpeta App_Code del sitio, seleccione la opción Agregar nuevo elemento, seleccione DataSet en la lista Plantillas instaladas de Visual Studio del cuadro de diálogo Agregar nuevo elemento, renómbrelo con DataSet.xsd y pulse Añadir. El diseñador de esquemas XML (XML Schema designer) se abre con el diseñador TableAdapter1 vacío, por defecto. Pulse con el botón derecho TableAdapter1 designer y seleccione la opción Configurar para iniciar el Asistente para la configuración de Table adapter. El proceso de configuración de las tablas de datos ASP.NET es idéntico al de configurar las tablas de los juegos de datos tipificados de los formularios Web. Seleccionando Guardar la cadena de conexión en el archivo de configuración de la aplicación, la cadena se añade también al grupo del archivo Web.config. A diferencia de los DataSets persistentes que se añaden a los proyectos de formulario Windows, los componentes de datos de ASP.NET no añaden ningún archivo DataSetName.designer.vb al proyecto. En su lugar, al compilar el archivo DataSet.xsd se genera un archivo temporal Random.2.cs que define el Public Partial Class DataSetName.

8.9.2 Crear y asignar ObjectDataSources de un DataSet El siguiente proceso es para añadir ObjectDataSources a los controles GridViews y DetailsViews para las páginas del proyecto de ejemplo. El nuevo objeto ObjectDataSources duplica las S1l DataSources del proyecto de ejemplo, permitiendo así utilizar la plantilla existente GridViews. Añadir un ObjectDataSource a la página EditableGridView Para añadir un ObjectDataSource a la página EditableGridView, haga lo siguiente: 1. Abra la página EditableGridView.aspx en modo diseño y arrastre un ObjectDataSource desde el cuadro de herramientas que hay bajo dsOrdersEdit SqlDataSource. Esto añadirá un place-holder ObjectDataSource1 y abrirá la etiqueta inteligente Common DataSource Task. 2. Pulse el vínculo Configurar origen de datos para abrir el cuadro de diálogo del mismo nombre. Marque el cuadro de verificación Mostrar sólo componentes de datos, abra la lista desplegable y seleccione OrdersDataSetTableAdapters.OrdersTableAdapter. Pulse Siguiente para abrir el cuadro de diálogo Definir métodos de datos (figura siguiente). 3. Acepte el método por defecto GetOrders() de la pestaña SELECT, que le devolverá un objeto OrdersDataTable y no un objeto DataSet.

287

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 288

Bases de datos con Visual Basic

4. Pulse la pestaña UPDATE para verificar que ObjectDataSource es actualizable. Fíjese que el cuadro de texto Firma del método pone un prefijo Nullable a todos los parámetros UPDATE que tienen por valor un tipo de datos.

5. Pulse las pestañas INSERT y DELETE para revisar las restantes firmas del método y pulse Finalizar para completar el proceso. 6. Abra la ventana de propiedades de ObjectDataSource1 y cambie el valor de la propiedad ID por odsOrdersEdit.

288

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 289

Aplicar técnicas avanzadas con ASP.NET 2.0

Asignar odsOrdersEdit a gvOrdersEditable y verificar la operabilidad El paso final en el proceso es remplazar dsOrdersEdit por odsOrdersEdit. Abra la etiqueta inteligente de gvOrdersEditable, abra la lista Elegir origen de datos y seleccione odsOrdersEdit. Esto abrirá el cuadro de mensaje Actualizar campos y claves para "gvOrdersEditable". Pulse No para dejar intacto el GridView. Si pulsa Sí, destruirá y regenerará el GridView, lo que borrará todas las plantillas y formatos.

Si pulsa Sí por error, pulse + para deshacer el cambio en la fuente de datos. Tal vez tenga que pulsar + varias veces para devolver el GridView a su estado original.

Pulse para construir y ejecutar el proyecto. Pulse el botón Edit y actualice uno de los pedidos cambiando el valor de EmployeeID o de ShipVia. Como verá, no hay diferencia detectable en el funcionamiento del GridView con SqlDataSource en lugar de ObjataSectSource. A continuación vemos el código para odsOrdersEdit:

289

VisualBasic2005_08.qxp

02/08/2007

16:31

PÆgina 290

Bases de datos con Visual Basic TypeName= OrdersDataSetTableAdapters.OrdersTableAdapter

Las diferencias básicas entre el código fuente para ObjectDataSource odsOrdersEdit y el correspondiente SqlDataSource dsOrdersEdit son la adición del atributo TypeName="NameDataSetDataTableAdapters .NameDataTableAdapter" y la sustitución de los nombres de método en los comandos SQL.

290

Related Documents


More Documents from ""