Está en la página 1de 297

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 El namespace System.Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.2 Proveedores de datos ADO.NET . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2 Los objetos básicos de datos ADO.NET . . . . . . . . . . . . . . . 5
1.3 Creando objetos básicos de datos ADO.NET con SqlClient 7
1.3.1 SqlDataReaders con juegos de resultados múltiples . . . . . . . . . . . . 7
1.3.2 XmlReaders con consulta FOR XML AUTO . . . . . . . . . . . . . . . . . . . 9
1.3.3 Rellenar un DataGridView con un DataReader . . . . . . . . . . . . . . . 11
1.3.4 Devolver una sola fila de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.3.5 Devolver un valor escalar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.3.6 Ejecutar peticiones que no devuelven datos . . . . . . . . . . . . . . . . . 13
1.4 Aplicar transacciones para actualizar las tablas múltiples . 14
1.5 Utilizando clases de miembros OleDb, SqlXml, y Odbc . . . 17
1.5.1 Substituir OleDb por objetos SqlClient . . . . . . . . . . . . . . . . . . . . . 18
1.5.2 Cambiar SqlConnection y SqlCommand por SqlXmlCommand . . . . 19
1.5.3 Probando el proveedor de datos Odbc . . . . . . . . . . . . . . . . . . . . . 20
1.6 Trabajando con datos DataReader y
SqlResultSet tipificados . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.7 Objetos tipificados DataSet de ADO.NET . . . . . . . . . . . . . . 22
1.7.1 Añadir un juego de datos tipificado desde un servidor SQL,
fuente de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.8 Añadir una DataGridView y BindingNavigator Controls . . 29
1.9 Persistir y reabrir el juego de datos . . . . . . . . . . . . . . . . . . 31
1.10 Cambiar de un DataViewGrid a un Details Form . . . . . . . . 32
1.11 Añadir un control de vínculo de datos relacionado . . . . . . 33

CAPÍTULO 2
Las novedades de ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . 37
2.1 Los objetos de formulario . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.1.1 Utilizar DbProviderFactories para crear proyectos
con bases de datos agnósticas . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.1.2 Restablecer los esquemas de las tablas base . . . . . . . . . . . . . . . . . 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 . . . . . . . . . . . . . 44
2.2.1 Entradas Batch en tablas de servidor SQL con
el objeto SqlBulkCopy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.2.2 Obtener las estadísticas de conexión del servidor SQL . . . . . . . . . . 50
2.3 Ejecutar comandos SQL de forma asincrónica . . . . . . . . . . 51
2.3.1 El modelo Polling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
2.3.2 El módelo Callback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
2.3.3 El modelo WaitAll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.3.4 Crear tablas de datos independientes . . . . . . . . . . . . . . . . . . . . . . 60
2.4 Utilizar tipos Nullable que soporten valores DBNull . . . . . 65
2.5 Utilizar objetos persistentes de formulario Windows de
ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
2.5.1 Comparando los diseñadores de datos de ADO.NET 1.x y 2.0 . . . 69
2.5.2 ADO.NET 1.x . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
2.5.3 ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
2.6 Añadir los controles ADO.NET que faltan . . . . . . . . . . . . . 71
2.7 Actualizar proyectos de 1.x con componentes
de ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
2.7.1 Añadir subformularios multinivel . . . . . . . . . . . . . . . . . . . . . . . . . . 72
2.8 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.2.1 Windows Server System Reference Architecture . . . . . . . . . . . . . . . 79
3.2.2 Designando aplicaciones y servicios . . . . . . . . . . . . . . . . . . . . . . . 80
3.2.3 Arquitecrura referencial para el desarrollo empresarial . . . . . . . . . . 80
3.3 Encontrar modelos para proyectos . . . . . . . . . . . . . . . . . . 80
3.3.1 Enterprise Solution Patterns Using Microsoft .NET . . . . . . . . . . . . . 81
3.3.2 Data Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
3.3.3 Modelos de sistemas distribuidos . . . . . . . . . . . . . . . . . . . . . . . . . 82
3.3.4 Modelos de integración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
3.3.5 Utilizar librerías de bloques de aplicaciones . . . . . . . . . . . . . . . . . . 83
3.4 El bloque de aplicación Data Access
(Data Access Application Block) . . . . . . . . . . . . . . . . . . . . . 84
3.4.1 El archivo de configuración de datos . . . . . . . . . . . . . . . . . . . . . . . 85
3.4.2 Código de restablecimiento de datos . . . . . . . . . . . . . . . . . . . . . . 86
3.4.3 Código de actualización de datos . . . . . . . . . . . . . . . . . . . . . . . . . 87
3.5 El cliente DataAccessQuickStart . . . . . . . . . . . . . . . . . . . . . 89

vi
VisualBasic2005_Primeras.qxp 12/08/2007 13:42 PÆgina vii

Índice general

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


3.6.1 La guía .NET Data Access Architecture Guide . . . . . . . . . . . . . . . . 91
3.6.2 Mejorando el rendimiento y la escalabilidad de la aplicación .NET 91
3.6.3 Diseñar componentes Data Tier y pasar datos por tier . . . . . . . . . 92
3.7 Aplicar las directrices de diseño para la biblioteca
de clas (Apply Class Library Design Guidelines) . . . . . . . . . 97
3.7.1 Naming Guidelines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
3.7.2 Class Member Usage Guidelines . . . . . . . . . . . . . . . . . . . . . . . . . . 98
3.8 Prepararse para la arquitectura orientada al servicio . . . . 98
3.8.1 El camino a la Arquitectura orientada al servicio (SOA) . . . . . . . . . 99
3.8.2 Implementar SOA con servicios Web . . . . . . . . . . . . . . . . . . . . . . . 100
3.8.3 Garantizar total interoperabilidad del servicio Web . . . . . . . . . . . . 101
3.8.4 Instalar y publicar el servicio Web DataSetWS . . . . . . . . . . . . . . . . 103
3.9 Use FxCop para validar el código del proyecto . . . . . . . . . 105
3.10 Automatizar Test-Driven Development . . . . . . . . . . . . . . . . 106
3.11 Ejecutar Best Practices Analyzer para SQL Server 2000 . . . 106
3.12 Applicar Best Practices específicas a los proyectos
de ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
3.12.1 Use cadenas de conexión idénticas para las conexiones de bases de datos
Pool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
3.12.2 Definir el tamaño del pool de conexión . . . . . . . . . . . . . . . . . . . . . 108
3.12.3 Guardar cadenas de conexión en archivos de configuración . . . . . 109
3.12.4 Encriptar cadenas de conexión que contienen
nombres de usuario y contraseñas . . . . . . . . . . . . . . . . . . . . . . . . 109
3.12.5 Ejecutar el SQL Server Profiler para inspeccionar
las consultas SQL y RPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
3.12.6 Evitar añadir instancias CommandBuilder en tiempo
de ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
3.12.7 Sustituir las consultas SQL Batch por procedimientos
almacenados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
3.12.8 Definir valores por defecto en los parámetros que
no son necesarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
3.12.9 Utilizar sp_executesql y parámetros con nombre para
reutilizar los Cached Query Plans . . . . . . . . . . . . . . . . . . . . . . . . . . 113
3.12.10Añadir columnas timestamp para el control de
concurrencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
3.12.11Verificar registros en test de concurrencia . . . . . . . . . . . . . . . . . . . 115
3.12.12Evitar SqlExceptions con las validaciones del cliente . . . . . . . . . . . 115

Capítulo 4
Programar TableAdapters, BindingSources
y DataGridViews . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
4.1 Diseñar un formulario básico Customer-Orders-
Order Details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117

vii
VisualBasic2005_Primeras.qxp 12/08/2007 13:42 PÆgina viii

Bases de datos con Visual Basic

4.1.1 Reducir el tamaño del DataSet . . . . . . . . . . . . . . . . . . . . . . . . . . . 118


4.1.2 Crear el origen de datos y añadir los controles . . . . . . . . . . . . . . . 119
4.1.3 Añadir métodos FillBy para cada tabla de datos . . . . . . . . . . . . . . 120
4.1.4 Modificar el código autogenerado para llenar los controles . . . . . . 122
4.1.5 Llenar el cuadro combinado con valores CustomerID . . . . . . . . . . 123
4.1.6 Limpiar la UI y el código . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
4.2 Añadir y dar formato a DataGridView . . . . . . . . . . . . . . . . 125
4.2.1 Dar formato a las columnas OrdersDataGridView . . . . . . . . . . . . . 125
4.2.2 Añadir y dar formato a una columna calculada en
Order_DetailsDataGridView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
4.2.3 Añadir la columna Extended amount . . . . . . . . . . . . . . . . . . . . . . . 127
4.2.4 Calcular y mostrar el valor Extended . . . . . . . . . . . . . . . . . . . . . . . 127
4.3 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 Manejar el evento DataErrors . . . . . . . . . . . . . . . . . . . . . . 131
4.5 Entrada de datos Streamline Heads-Down . . . . . . . . . . . . . 132
4.6 Migrar el UI a un formulario tabular . . . . . . . . . . . . . . . . . 134
4.6.1 Comprobar el proyecto OrdersByCustomersV3 . . . . . . . . . . . . . . . 135
4.6.2 Fijar los valores por defecto que faltan al añadir filas con código . 136
4.6.3 Editar un record DataGridView seleccionado en la segunda ficha . 137
4.7 Crear y vincular listas de consulta Lookup para
valores de clave primaria . . . . . . . . . . . . . . . . . . . . . . . . . . 139
4.7.1 Crear un juego de datos lookup no tipificado y
sus tablas de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
4.7.2 Rellenar el cuadro combinado cboCustomerID . . . . . . . . . . . . . . . 141
4.7.3 Sustituir los cuadros de texto de DataGridView por
cuadros combinados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
4.7.4 Añadir código para poblar los cuadros combinados
Employees y ShipVia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
4.7.5 Remplazar los valores nulos por defecto en las filas nuevas . . . . . . 143
4.7.6 Asociar cuadros combinados con cuadros de texto . . . . . . . . . . . . 144
4.8 Añadir un cuadro combinado que defina
valores adicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
4.8.1 Crear y vincular un DataView ordenado por ProductName . . . . . . 147
4.8.2 Comprobar que no haya duplicados y actualizar
la columna UnitPrice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
4.9 Añadir filas a las tablas lookup para entradas de
nuevos Customers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
4.9.1 Añadir y vincular una BindingSource CustomerID . . . . . . . . . . . . . 151
4.9.2 Comprobar la existencia de duplicados con
un DataRowView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 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 . . . . . . . . . . . . . . . . . . . . . . . . 154
4.11.2 Crear y comprobar la función UpdateBaseTables . . . . . . . . . . . . . . 155
4.11.3 Entender la generación de cambios en tablas y
las instrucciones para la actualización de las tablas base . . . . . . . . 156
4.11.4 Añadir la función UpdateBaseTables . . . . . . . . . . . . . . . . . . . . . . . 158
4.11.5 Operaciones previas de actualización . . . . . . . . . . . . . . . . . . . . . . 162
4.11.6 Invocar la función UpdateBaseTables . . . . . . . . . . . . . . . . . . . . . . . 162
4.11.7 Comprobar los valores CustomerID del servidor para
evitar duplicados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 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 Validar cuadros de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
5.1.2 Validar controles DataGridViews . . . . . . . . . . . . . . . . . . . . . . . . . . 168
5.1.3 Capturar las violaciones de restricción de clave primera
durante la entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
5.1.4 Validar valores por defecto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
5.2 Gestionar las transgresiones de concurrencia . . . . . . . . . . 173
5.2.1 Control de concurrencia y cambios de transacción
en ADO.NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
5.2.2 Propiedades ocultas de conexión y transacción . . . . . . . . . . . . . . . 175
5.2.3 La propiedad ContinueUpdateOnError . . . . . . . . . . . . . . . . . . . . . 176
5.2.4 Estrategias de control de concurrencia . . . . . . . . . . . . . . . . . . . . . 176
5.2.5 Los "vínculos perdidos" en la gestión de la concurrencia . . . . . . . . 177
5.2.6 Detectar los fallos de concurrencia en los registros hijo . . . . . . . . . 178
5.2.7 Detectar otros conflictos potenciales de concurrencia . . . . . . . . . . 179
5.2.8 Permitir a los usuarios re-crear los pedidos borrados . . . . . . . . . . . 181
5.3 Anticipar las transgresiones de restricción de
clave primaria basada en valores . . . . . . . . . . . . . . . . . . . . 184
5.4 Manejar elegantemente los errores de concurrencia . . . . . 187
5.4.1 Obtener datos actuales del servidor . . . . . . . . . . . . . . . . . . . . . . . 188
5.4.2 Restablecer y comparar los valores de celda del servidor
y el cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
5.5 Trabajar con usuarios desconectados . . . . . . . . . . . . . . . . . 193
5.5.1 Crear y gestionar juegos de datos offline . . . . . . . . . . . . . . . . . . . . 194
5.5.2 Activar el tratamiento de registros padre múltiples . . . . . . . . . . . . 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 Simplificar el listado con System.Transactions . . . . . . . . . . . . . . . . . 202


6.1.2 Listar SqlDataAdapters en una transacción implícita . . . . . . . . . . . 203
6.1.3 Autolistar SqlTableAdapters en una transacción implícita . . . . . . . . 204
6.1.4 SQL Profiler para rastrear transacciones . . . . . . . . . . . . . . . . . . . . . 205
6.1.5 Listar manualmente SqlTableAdapters en una transacción explícita 206
6.1.6 Definir las opciones TransactionScope y Transaction . . . . . . . . . . . 207
6.2 Añadir relaciones a los SelectCommand de la tabla
de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
6.2.1 Añadir una relación a SelectCommand . . . . . . . . . . . . . . . . . . . . . 209
6.2.2 Añadir las columnas adjuntadas con relaciones al DataGridView . . 210
6.2.3 Proporcionar los valores por defecto y columnas de sólo lectura . . 210
6.3 Mejorar el rendimiento reduciendo el tamaño de
los juegos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
6.3.1 Limitar el número de filas devueltas por las consultas TOP n . . . . . 213
6.3.2 Añadir clases Partial para TableAdapters . . . . . . . . . . . . . . . . . . . . 214
6.4 Trabajar con imágenes en DataGridViews . . . . . . . . . . . . . 215
6.4.1 Añadir columnas Image a los DataGridViews . . . . . . . . . . . . . . . . 215
6.4.2 Manipular imágenes en DataGridView . . . . . . . . . . . . . . . . . . . . . 216
6.4.3 Cambiar ImageLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
6.4.4 Guardar una imagen seleccionada, mostrarla en un
PictureBox y remplazarla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
6.4.5 Evitar crear imágenes desde los campos de objeto OLE en Access 220
6.5 Editar documentos XML con DataSets yDataGridViews . . . 220
6.5.1 Adaptar un esquema XML existente para generar un DataSet . . . . 221
6.5.2 Esquemas para documentos XML de jerarquía anidada . . . . . . . . . 222
6.5.3 Un ejemplo de esquema anidado . . . . . . . . . . . . . . . . . . . . . . . . . 224
6.5.4 La ventana Propiedades de las columnas . . . . . . . . . . . . . . . . . . . . 227
6.5.5 Un esquema anidado con atributos . . . . . . . . . . . . . . . . . . . . . . . . 227
6.5.6 Ejemplo de esquema anidado y "envuelto" (wrapped) . . . . . . . . . . 228
6.5.7 Un ejemplo de esquema plano . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
6.5.8 Inferir un esquema XML para generar un juego de datos . . . . . . . 233
6.5.9 Crear formularios de edición desde fuentes de datos XML . . . . . . . 235
6.5.10 El proyecto de ejemplo EditNorthwindDS . . . . . . . . . . . . . . . . . . 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 El modelo de compilación de ASP.NET . . . . . . . . . . . . . . . . . . . . . . 242
7.1.2 Los nuevos controles (Data Controls) de ASP.NET 2.0 . . . . . . . . . . 244
7.2 Los controles DataSource . . . . . . . . . . . . . . . . . . . . . . . . . . 245
7.3 El control DataList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
7.3.1 SqlDataSources para controles vinculados . . . . . . . . . . . . . . . . . . . 247

x
VisualBasic2005_Primeras.qxp 12/08/2007 13:42 PÆgina xi

Índice general

7.3.2 Propiedades de control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251


7.3.3 Plantilla de datos vinculados y formateo de datos . . . . . . . . . . . . . 252
7.3.4 Restricciones WHERE en el código fuente en los valores
de controles vinculados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
7.3.5 Editar ítems en listas de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
7.4 El control FormView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
7.4.1 Paginar la fuente de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
7.4.2 Remplazar los valores Null por texto específico de la columna . . . . 260
7.4.3 Editar, añadir y borrar registros . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
7.4.4 Añadir botones de comando . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
7.5 El control GridView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
7.5.1 Convertir campos BoundFields en campos EditItemTemplate . . . . 266
7.5.2 Remplazar cuadros de texto por listas desplegables
para la edición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267

Capítulo 8
Aplicar técnicas avanzadas con ASP.NET 2.0 . . . . . . . . . . . . 271
8.1 Validar entradas en controles vinculados a datos . . . . . . . 271
8.2 Los controles de validación de ASP.NET 2.0 . . . . . . . . . . . . 272
8.2.1 La nueva propiedad ValidationGroup . . . . . . . . . . . . . . . . . . . . . . 273
8.3 Otras propiedades de validación compartidas . . . . . . . . . . 273
8.4 Validar ediciones en GridView . . . . . . . . . . . . . . . . . . . . . . 274
8.4.1 Añadir un campo necesario de validación a un control GridView . 275
8.5 Validar entradas CustomerID con un control
RegularExpressionValidator . . . . . . . . . . . . . . . . . . . . . . . . 276
8.5.1 Comprobar los valores de EmployeeID con un control
RangeValidator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278
8.5.2 Aplicar RangeValidator y RegularExpressionValidator a
las entradas de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278
8.6 Impedir entradas ilógicas con un CompareValidator . . . . . 279
8.6.1 Añadir un control CustomValidator . . . . . . . . . . . . . . . . . . . . . . . . 280
8.7 Escribir un mensaje para el control Validation Summary . . 283
8.8 Validar ediciones de ProductID en el servidor Web . . . . . . 284
8.8.1 Test para descubrir valores duplicados de ProductID
en el cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
8.9 Remplaar SqlDataSources por ObjectDataSources . . . . . . . 285
8.9.1 ObjectDataSources a partir de DataTables . . . . . . . . . . . . . . . . . . . 286
8.9.2 Crear y asignar ObjectDataSources de un DataSet . . . . . . . . . . . . . 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 uti-
lizarlo 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 Record-
sets offline y los controles vinculados.
) Comprensión básica de la organización y el uso de los namespace de .NET Frame-
work 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 total-
mente 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 familiari-
zado 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 ter-
cera 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 reu-
tilizació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 pro-
ductividad 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 trabaja-
mos 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 embar-
go, 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 dura-
ción y el número de conexiones de los servidores de bases de datos implicadas y utili-
za 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ími-
tes 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, enumeracio-
nes 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 provee-
dores controlados de datos, por ejemplo los DbCon-
nection 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, Namespace para los cinco proveedores controlados de
System.Data.OracleClient, datos incluidos en ADO.NET 2.0; la sección siguiente
system.Data.SqlClient System.Data. describe
SqlCeClientlos
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 pro-
cesar documentos XML estructurados.

Después de añadir una referencia de proyecto a System.Data.dll, se puede escribir me-


diante 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 tra-
bajan 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 names-
pace 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 servido-
res 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 con-
troladores 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 aplicacio-
nes 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 tiem-
po 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 rela-
cionadas con la conexión. El código VB 2005 para crear un elemento .NET Connec-
tion 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 colec-
ción opcional de objetos Parameter para ejecutar consultas parametrizadas o proce-
dimientos 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 lectu-
ra 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 obje-
to 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 con-
cluir 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, Com-
mand, 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 SP-
2 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 (sam-
ple 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 local-
host por .\SQLEXPRESS en la cadena strConnconnection. Si está utilizando la instancia
MSDE 2000 de Access como servidor local, cambie Northwind por NorthwindCS. Si está uti-
lizando 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 proce-
dimiento almacenado para rellenar listas múltiples en el tratador de eventos Form-
Name_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 jue-
gos de resultados e invoca su método ExecuteReader para generar la instancia SqlData-
Reader. 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éto-
do 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 pro-
cedimiento 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ña-
dir 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:
<?xml version= 1.0 encoding= utf-8 ?>
<root>
<Shippers>
<ShipperID>1</ShipperID>
<CompanyName>Speedy Express</CompanyName>
<Phone>(503) 555-9831</Phone>
</Shippers>
<Shippers>
<ShipperID>2</ShipperID>
<CompanyName>United Package</CompanyName>
<Phone>(503) 555-3199</Phone>
</Shippers>
<Shippers>
<ShipperID>3</ShipperID>
<CompanyName>Federal Shipping</CompanyName>
<Phone>(503) 555-9931</Phone>
</Shippers>
</root>
El nuevo método SqlCommand.ExecuteXmlReader de ADO.NET 2.0, carga un objeto Sys-
tem.Xml.XmlReader con la corriente, tal como se muestra en el siguiente listado de pro-
cedimiento OpenXmlReader. XmlReader es una clase abstracta con las implementaciones
concretas XmlTextReader, XmlNodeReader, y XmlValidatingReader. El método ExecuteXml-
Reader 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, segui-
do 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 Command-
Behavior, 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 Sql-
Client 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 relle-
nar desde el punto de vista de la programación. Un DataGridView de sólo lectura pobla-
do con un DataReader tiene un comportamiento similar al control de rejilla VB6 están-
dar (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 colum-
na. 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 proce-
dimiento 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 pri-
mera 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 pro-
cedimientos almacenados que actualizan datos de tabla INSERT, UPDATE, y operacio-
nes DELETE. Tal como muestra el siguiente código OpenExecuteNonQuery, Execute-
NonQuery 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 almacena-
dos 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 propor-
ciona 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 co-
rrespondientes BeginTransaction, CommitTransaction, y RollbackTransaction. A diferencia
de las conexiones ADODB, ADO.NET permite incluir selectivamente listas de coman-
dos en una transacción activa.
A continuación vemos los pasos necesarios para ejecutar las actualizaciones por trans-
acció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 Isolation-
Level. 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 Miembro ADODB ADO.NET IsolationLevel Description


ADO.NET
Chaos adXactChaos Impide que se sobreescriban los cambios de-
pendientes de transacciones aisladas de mayor
nivel
ReadCommitted adXactReadCommitted Impide las lecturas sucias pero permite las lec-
dadXactCursorStability turas no repetibles y los phantom data (por
defecto)
ReadUncommitted adXactReadUncommitted Permite lecturas sucias, filas no repetibles, y filas
adXactBrowse fantasma
RepeatableRead adXactRepeatableRead Impide lecturas no repetibles pero permite filas
fantasma
Serializable adXactSerializable Impide lecturas sucias, lecturas no repetibles y
adXactIsolated 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 transac-
ció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 escala-
bilidad 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 pro-
gramadores 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 for-
mulario 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 relle-
nar el cuadro de lista Rowset 1 (Shippers).
Se puede sacar mayor partido de los nuevos métodos de ADO.NET 2.0 DbProviderFac-
tories.GetFactory(System.Data.Provider) y los métodos DbProviderFactory.CreateConnec-
tion y CreateCommand para generar una conexión a/desde todos los managed data provi-
der 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 instan-
cia 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 tra-
bajar 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 sen-
tencia 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 adicio-
nal 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 refe-
rencia 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 nin-
gú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 OpenOdbcData-
Reader 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.Get-
String (ColumnIndex), y Reader.GetInt32 (ColumnIndex) para extraer valores de colum-
na 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 obje-
tos SqlParameter. VS 2005 añade automáticamente parámetros SqlParameters cuan-
do 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 enumeracio-
nes 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 ningu-
na tabla de estas enumeraciones y tipos.

El siguiente listado OpenDataReaderSqlTypes muestra ejemplos de cómo se usan los típi-


cos 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ítu-
los 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 pre-
ferido 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 archi-
vo 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áti-
camente 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 estructura-
dos (tabular).
) TableAdapters rellenan y actualizan las DataTables a través de una conexión contro-
lada. 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 procedimien-
to 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 actuali-
zables 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 Delete-


Command. 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 dife-
rencia 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 North-
wind 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 cone-
xió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á buscar-
la 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 cua-
dro 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 Northwind-


DataSet 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 ejem-
plo 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 North-
windDataSet.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 North-
windDataSet, 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 Northwind-
DataSet.Designer.vb y los dos archivos vacíos NorthwindDataSet.xsc y Northwind-
DataSet.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 especifi-
car 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 for-
mulario 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 for-
mulario 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, Can-
celEdit, y EndEdit. Los conocidos métodos MoveFirst, MoveLast, MoveNext, y Move-
Previous se ocupan de la navegación por las filas. Hacer posible la navegación sig-
nifica vincular un DataGridView o añadir otros controles para manipular la
BindingSource.
) CustomersTableAdapter es el envoltorio del formulario para cualquier objeto Sql-
DataAdapter 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 Custo-
mersTableAdapter.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 Binding-
Navigator a una posición más cómoda, en la parte inferior del formulario, o tam-
bié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 formu-
lario 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 pre-
via. Los DataConnectors simplifican el código para navegar por las tablas de datos. El archi-
vo 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 borra-
dos, 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 <F5> 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 con-
tenido 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ódi-
go 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 sen-
tencia siguiente se puede substituir por Me.CustomersTableAdapter.Fill(Me.North-
windDataSet.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 colum-
na 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 deta-
lle. Borre el control DataGridView, muestre la ventana Orígenes de datos, abra la lista des-
plegable 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 con-
trol, como DataGridView, que se puede vincular al BindingAdapter relacionado. Para
añadir un control relacionado OrdersDataGridView a una copia del proyecto Genera-
tedDetailView.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 <F5> 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 rela-
cionado por debajo de los cuadros de texto vinculados del formulario para autoge-
nerar un control OrdersDataGridView.
6. Ajuste el tamaño y la posición de los controles y defina para la propiedad Or-
dersDataGridView.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 <F5> 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 pro-
piedades 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 figu-
ra 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 referen-
cial. 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, propie-
dades 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 avan-
zados 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, BindingSour-
ces, BindingNavigators y DataGridViews.
Todos los ejemplos de código SQLServer de este capítulo se pueden ejecutar con SQL-
Server 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 local-
host 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 autogenera-
do. 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 formu-
larios 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 actualiza-
ciones 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 ser-
vicios Web e interfaces remotas y streaming.
) A las tablas de datos se les puede asignar espacios-nombre y prefijos para los nom-
bre 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 formula-
rios 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 desarrolla-
dores 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-tri-
viales que puedan interactuar sin fisuras con todos los administradores de bases de
datos relacionales, para los que existen proveedores de datos controlados, no es preci-
samente algo simple. Las diferencias menores en la sintaxis SQL, tipos de datos, dialec-
tos de procedimientos almacenados, tratamiento de error, y otras características pro-
pias 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 encon-
trará 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 total-
mente 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 prove-
edores DataDirect buscan salidas para minimizar las diferencias de sintaxis SQL y comunicar-
se 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 pro-
veedor de datos, como System.Data.SqlClient, al argumento de una sentencia
DimFactoryNameAsDbProviderFactory = DbProviderFactories.GetFactory(strProvider).
2. Crear un objeto IdbConnection invocando el método DimConnectionNameAsIDbCon-
nection = FactoryName.CreateConnection().
3. Definir el valor de la propiedad ConnectionName.Connection.String.
4. Crear un objeto IdbCommand invocando el método DimCommandNameAsIDbCom-
mand = ConnectionName.CreateCommand().
5. Definir para las propiedades CommandName.CommandType (opcional) y Command-
Name.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 si-
guiente 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 devol-


ver las claves primarias correctas y las propiedades de campo relacionadas.

Si sus proyectos deben incluir independencia respecto al proveedor de datos y está dis-
puesto 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 desventa-


ja 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 proyec-
tos, que substituye el código para los controles de vinculación que muestran y actuali-

41
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 infor-
mación clave primaria como son los índices de columna y detalles de autoincremen-
tació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 devuel-
ven 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 cla-
ses 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 DataTableReader


DataReaders
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 opciona-
les de la clase SystemData.Common.SchemaOptionalTableColumn. Los campos XmlSchema-
Collection 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 propieda-
des 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 El nombre de la base de datos del servidor SQL Server 2005 que
Database contiene el conjunto de esquemas para una columna del tipo
xml (null si la columna xml no tiene esquema)
XmlSchema CollectionOwning Esquema relacional del SQL Server 2005 que contiene el conjun-
Schema to 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 de-
vuelve 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 provee-
dores 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 nom-
bre de proveedores de datos ADO.NET 2.0, y una sección system.data que añade estos provee-
dores 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 Data-
GridView 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 pro-
veedores de datos instalados. La Figura siguiente muestra una instancia por defecto de un
SQLServer 2000 (OAKLEAF-W2K3), y una instancia MSDE con nombre (OAKLEAF-
W2K3\SHAREPOINT), una instancia de SQLServer 2005 (OAKLEAF-MS18), y una ins-
tancia SQLExpress (SQLX) con nombre (OAKLEAF-MS18\SQLEXPRESS), así como pro-
veedores 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 tradicio-
nales para añadir filas muy rápidamente a las tablas del SQL Server. ADO.NET 2.0 ofre-
ce una opción alternativa: programar el nuevo objeto SqlBulkCopy. La fuente más habi-
tual 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 ejecu-
ció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 colum-
nas de las tablas base. SqlBulkCopy tiene una colección de ColumnMappings que permite defi-
nir 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 cone-
xió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.Sql-
RowsCopied 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 ope-
ració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 tran-
sacció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 con-
veniente 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 aplica-


ciones 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 des-


pué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 defini-
do 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 SqlConnec-


tion.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ódi-
go 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 ini-
cializar los datos, excepto ConnectionTime.

Recuperar las estadísticas de conexión requiere establecer de nuevo una conexión con el ser-
vidor, 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 Begin-
ExecuteNonQuery (junto con los correspondientes métodos End) para las clases Sql-
Command. Estos métodos permiten ejecutar código mientras se espera a que un coman-
do 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 fuen-
te 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 den-
tro 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 Com-
mandTimeout. 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ódi-
go se va ejecutando en el hilo del formulario, por lo que los comandos múltiples se eje-

53
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 Data-


Reader.Read son complejas, se pueden ejecutar en un hilo dedicado al nuevo objeto
BackgroundWorker. Esto permite invocar el siguiente método BeginExecuteReader inme-
diatamente 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 permi-
te entrelazar comandos con bases de datos múltiples que se ejecutan en lo mismos ser-
vidores 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 Be-
ginExecuteReader. Al pasar el comando se tiene acceso al método EndExecuteReader con
la propiedad IAsyncResult.AsyncState en el tratador callback. La siguiente figura mues-
tra 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 jue-
gos 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 comple-
ta 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 individualemen-
te 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 indi-
viduales 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 ter-
cer servidor (OAKLEAF-W2K3 y OAKLEAF-MS2K3) por RemoteServerName, y seleccio-
nar 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 opera-
ciones 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 ani-

56
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 controverti-
do. Miembros del equipo de datos de VS 2005 de Microsoft recomiendan no utilizar este mode-
lo 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 ele-
mento en cada llamada de método BeginExecuteReader. Un WaitHandle.WaitAll(wh-
Array) 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 especial-
mente 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 for-
mulario 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 basa-
dos 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 mode-
lo 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 correspon-
dientes objetos SqlDataReader.AsyncWaitHandle al array WaitHandle sin importar el or-
den. 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 <Ctrl> + <S> para guardar los cambios. La siguiente figura muestra el for-
mulario 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 com-
parten muchos métodos DataSet, como ReadXml, ReadXmlSchema, WriteXml, y Write-
XmlSchema. 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 nom-
bre 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 GetSche-
maTable, 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 esque-
ma, 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 guar-
dado 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 inde-
pendiente StandaloneDataTables.sln después de una mínima edición de la columna Con-
tactName 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 prima-
ria (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ólo-
esquema 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 proce-
dimiento 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 per-


miten 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 simi-
lares 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í valo-
res 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 pri-
vados 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=Nothing-
Then... 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 ante-
riores 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 nor-
malmente, 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 vincu-
lació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 PartialPu-
blicClassTableNameTableAdapter del juego de datos definen el tiempo de ejecución de
SqlConnections, SqlDataAdapters, y SqlTransactions para los proyectos basados en el SQL-
Server. 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 for-
mualrios 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 mos-
trar 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 DE-
LETE. El diseñador añadirá a la bandeja los objetos SqlConnection1 y Sql-
DataAdapter1.
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 mane-
jador de eventos btnFill_Click.
6. Añada un botón Update de actualización y una instrucción SqlDataAdapter1.Up-
date(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íge-
nes 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 cone-
xió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 Data-
baseNameDataSet, TableNameBindingSource, TableNameTableAdapter, y TableName-
BindingNavigator a la bandeja, y los controles TableNameDataGridView y TableName-
BindingNavigator al formulario.

El paso 3 también añade una instrucción TableNameTableAdapter.Fill(DatabaseNameData-


Set) al manejador de eventos Form1_Load y un manejador de eventos bindingNavigator-
SaveItem_Click al formulario.
A diferencia del proceso ADO.NET 1.x, usted no tiene la oportunidad de crear o selec-
cionar procedimientos almacenados para llenar o actualizar las tablas de datos creadas
por el asistente. No obstante, puede reconfigurar los DataTableAdapters para crear nue-
vos procedimientos, o utilizar los que hay guardados, abriendo el archivo DataSet-
Name.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 cone-
xió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 pro-
ceso para crear un formulario Northwind de Clientes-Pedidos. Más adelante, le mostra-
mos 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 espe-
cí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 secun-
dario 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 com-
ponente o el control en el cuadro de texto Filtro para simplificar la selección. La siguien-
te 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 pro-
yectos 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 archi-
vos 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 reem-
plazar 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 bande-
ja, ni una ventana de diseñador de esquema porque DataSourceName.xsd no está actua-
lizado 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 rela-
cionales 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 figu-
ra 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 <F5> para construir y ejecutar el proyecto, que aparecerá tal co-
mo 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 pareci-
dos 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 cate-
gorí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 sir-
ven 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 conteni-
dos 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 usua-
rios de Office crear “front ends” de bases de datos y configurar bases de datos Jet mul-
tiusuario para proyectos. Visual Basic ofrece a los desarrolladores profesionales y ama-
teurs 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 departa-
mentos TI para proporcionar aplicaciones aprobadas oficialmente según un tiempo
establecido (o cuando fuera). Los proyectos no aprobados y ad-hoc, a menudo, queda-
ban 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 cor-
poraciones centralizadas. La administración centralizada de las bases de datos y el esta-
blecimiento 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 persis-
tencia 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 regu-
larizadas, 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, amplia-
mente 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 secuen-
cia descendiente desde la arquitectura general hasta las recomendaciones específicas
para incrementar la escalabilidad, interoperabilidad, rendimiento y seguridad, y la reu-
tilizació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 des-
arrollan las aplicaciones de Microsoft puede ser una tarea a jornada completa. Los pro-
yectos convencionales de cliente-servidor dieron paso los diseños de tres niveles, basa-
dos en COM, y después a los de arquitecturas de n-niveles con componentes con-
troladas 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 aplicacio-
nes 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 mala-
fortunado 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 Inter-
national 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 continua-
ción se describen brevemente los cuatro elementos P&P:
) Arquitecturas referenciales: identifican las decisiones en cuanto al diseño y hacen
recomendaciones generales para mejorar las soluciones con componentes interco-
nexas. La guía de mejoras en los Data Services "Windows Server System Reference

78
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 blo-
que de aplicación de Data Access para .NET. Hay varias guías de diseño con docu-
mentació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 pro-
porcionan información más detallada sobre los miembros de la lista anterior, enfatizan-
do 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 distribui-
dos de aplicaciones para venta al detalle por la Web y de banca. Las arquitecturas refe-
renciales 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 Architec-
ture 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 proyec-
tos 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 (original-
mente codificada como Shadowfax) es un marco para las aplicaciones orientadas al ser-
vicio de desarrollo con SQLServer, servicios Web ASP.NET, Microsoft Message Queue
(MSMQ), y otros sistemas de “back-end”. La implementación de referencia, llamada Glo-
balBank, es el punto de partida para un portal de banca online que permite a los clien-
tes 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.antipat-
terns.com/whatisapattern/) describe la primera instancia como un evento, la segunda co-
mo 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 aplicacio-
nes “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 Deploy-
ment. "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/libra-
ry/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 tra-
vé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 poste-
rior "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 reco-
mendar 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 implemen-
tació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 vie-
nen 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 (arqui-
tectura, 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, desarro-
llo 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 gene-
rales 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 publi-
caciones 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 aplica-
ciones de empresa, en inglés: enterprise application integration (EAI) patterns. El Diccio-
nario de Comercio Electrónico (Electronic Commerce Dictionary, en http://www.tedhay-
nes.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ápida-
mente 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 mezclado-
res 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:

Entity Aggregation Function Integration


Process Integration Service-Oriented Integration

82
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 res-
tablecer 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 ser-
vicios Web y la difusión en los proyectos EAI. Más adelante en este capítulo, se exami-
nam 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 componen-
tes 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ódi-
go 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 elemen-
tos 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.Enter-
priseLibrary.BlockName.dll. Después hay que añadir referencias en el proyecto VS 2005 a
los ensamblajes apropiados. QuickStart clients implica escribir soluciones desde proyec-
tos 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 blo-
ques 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 blo-
que 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áti-
ca. A cambio, el proyecto de ejemplo DataAccessQuickStart.sln VB 2005 incluye los com-
ponentes 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 usua-
rio necesita para crear y manipular las componentes runtime de acceso de datos del ser-
vidor SQL de ADO.NET 1.x. La versión de Enterprise Library permite integrar otros blo-
ques 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 total-
mente 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 pro-
bar el proyecto actualizado DataAccessQuickStart.sln, hay que seguir estos pasos:
1. Primero debemos bajar el DAAB desde la dirección http://www.microsoft.com/down-
loads/details.aspx?FamilyId=F63D1F0A-9877-4A7B-88EC-0426B48DF275&display-
lang=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 pro-
yecto 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 Custo-
mers, 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 <F5> para crear y ejecu-
tar 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 clican-
do 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:
<?xml version=”1.0” encoding=”utf-8”?>
<dataConfiguration>
<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-31-
2004/data”>

85
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 86

Bases de datos con Visual Basic

<databaseTypes>
<databaseType name=”Sql Server”
type=”Microsoft.Practices.EnterpriseLibrary.Data.Sql.SqlDatabase,
Microsoft.Practices.EnterpriseLibrary.Data” />
</databaseTypes>
<instances>
<instance name=”DataAccessQuickStart” type=”Sql Server”
connectionString=”LocalQuickStart” />
</instances>
<connectionStrings>
<connectionString name=”LocalQuickStart”>
<parameters>
<!-- For SQL Express value=”.\SQLEXPRESS” or “localhost\SQLEXPRESS” -->
<parameter name=”server” value=”localhost” isSensitive=”false” />
<parameter name=”database” value=”EntLibQuickStarts”
isSensitive=”false” />
<parameter name=”Integrated Security” value=”True”
isSensitive=”false” />
</parameters>
</connectionString>
</connectionStrings>
</enterpriseLibrary.databaseSettings>
</xmlSerializerSection>
</dataConfiguration>

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 restable-
cer 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.Add-


OutParameteter() 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 pro-
cedimiento almacenado. El tercer argumento del método AddOutParameter es la longitud de los
datos.

Restablecer los metadatos de parámetro para el método GetStoredProceCommandWrap-


per(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 pre-
sentes 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 de-
fecto) 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 pro-
yecto 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ña-
dida, "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 = Added Row

88
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 supe-
rior (Retrieve multiple rows using a DataReader), que invoca el método SalesData.Get-
CustomerList().

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 can-
tidad 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 orien-
tadas 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 justifi-
ca 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 com-
parten 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 propie-
dades descritas en los dos capítulos anteriores, pero la mayor parte de sus recomenda-
ciones 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 actualiza-
da 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 rendi-
miento 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 mejo-
ras 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 presen-
tació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 bre-
vemente las tres capas definidas por la publicación:
) 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 contie-
nen 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, restableci-
miento, 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 actuali-
zaciones. 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 responsa-
bles 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 distribui-
das. Las BPCs pueden incorporar gestión de workflow para transacciones de larga
ejecución, que pueden requerir transacciones compensativas para invertir los cam-
bios realizados anterioremente en los almacenes de datos.

92
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ódi-
go fuente están escritos sólo en C#, lo que refuerza la creencia de que VB.NET está con-
siderado 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 expo-


ne 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 ejem-
plo de BE Order serializada:
<?xml version= 1.0 encoding= utf-8 ?>
<Order>
<OrderID>1617968</OrderID>
<CustomerID>QUICK</CustomerID>

93
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 94

Bases de datos con Visual Basic

<EmployeeID>9</EmployeeID>
<OrderDate>1996-08-15T00:00:00.0000000-07:00</OrderDate>
<RequiredDate>1996-09-02T00:00:00.0000000-07:00</RequiredDate>
<ShippedDate>1996-09-02T00:00:00.0000000-07:00</ShippedDate>
<ShipVia>3</ShipVia>
<Freight>76.07</Freight>
<ShipName>QUICK-Stop</ShipName>
<ShipAddress>Taucherstra e 10</ShipAddress>
<ShipCity>Cunewalde</ShipCity>
<ShipRegion />
<ShipPostalCode>01307</ShipPostalCode>
<ShipCountry>Germany</ShipCountry>
<OrderDetails>
<OrderDetail>
<OrderID>1617968</OrderID>
<ProductID>5</ProductID>
<UnitPrice>21.35</UnitPrice>
<Quantity>13</Quantity>
<Discount>0.18</Discount>
</OrderDetail>
<OrderDetail>
<OrderID>1617968</OrderID>
<ProductID>17</ProductID>
<UnitPrice>39</UnitPrice>
<Quantity>11</Quantity>
<Discount>0.12</Discount>
</OrderDetail>
</OrderDetails>
</Order>

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:
<?xml version= 1.0 encoding= utf-8 ?>
<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 />
</xs:restriction>
</xs:simpleType>
</xs:element>

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 />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

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 servi-
cios 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 correspon-
diente 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 detalles-
maestro.
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 restable-
ce 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 des-
pué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ña-
dir 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 situa-
ciones y con diferentes usos de las BE. Puede prescindir de los ejemplos que substitu-
yen 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 simi-
lares 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 miem-
bros, 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_ co-
mo 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 recommenda-
tion en http://www.w3.org/TR/xmlschema-0/ se utiliza el modelo camelCase en las etioque-
tas 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 cum-
plir 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 ins-
tancia 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 evi-
tar 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 priva-
do 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 architec-
ture" 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 ali-
mentació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, publi-
caciones 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 mayo-
res incentivos de Microsoft en animar a los desarrolladores a comprar SOA es promo-
ver 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 com-
plica 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 nego-
cios y las operaciones de almacenamiento de datos en una sola componente. Las
primeras aplicaciones monolíticas consistían en terminales alfanuméricas conecta-
das a bases de datos mainframe y gestores de transacciones. El PC permitió a usua-
rios 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 (procesa-
miento de búsquedas) del PC cliente al servidor con las bases de datos. Cada clien-
te 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 compo-
nentes por capas individuales. Las UI del cliente acceden a la componente de lógi-
ca 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 escala-
bles, 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 consulto-
rí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 aplica-
ció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 algu-
no 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 limi-
tació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 inte-
roperabilidad de la arquitectura n-tier descritos en el apartado anterior. A continuación,
los requisitos básicos para SOA:
) 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-sin-
cró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.

100
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án-
dares 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 propie-
dades IDE de tiempo de diseño, como por ejemplo IntelliSense. El esquema y los men-
sajes 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 esta-
do 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 (WS-
Coordination, 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 impedi-
mento 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 cer-
tificados 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 WS-
Security. 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 servi-
cio 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 independien-
temente 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 intero-
perabilidad con otros servicios Web VB.NET o C# ASP.NET proporcionados por un ser-
vidor 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 intero-
perabilidad 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 serializa-
dos, 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 casu-
santes son la referencia a s:schema y el elemento wildcard <s:any/> en los nodos de méto-
do 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 fragmen-
to 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 name= GetAllCustomersResult >
<s:complexType>
<s:sequence>
<s:element ref= s:schema />
<s:any />
</s:sequence>
</s:complexType>
</s:element>
</s:sequence>
</s:complexType>
</s:element>

Los DataSets genéricos son presumiblemente dinámicos, por lo que exponen su esque-
ma 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 proyec-
to 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 her-
cú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 xml-
ns: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 atri-
butos 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 informa-
ción detallada que las versiones ADO.NET 1.x, casi idénticas a los esquemas no tipifi-
cados de DataSets. Como ejemplo, ADO.NET 2.0 añade ueve atributos msprop:Pro-
pertyName 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 acce-
den 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 inte-
roperabilidad 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: GetAll-
Customers, 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 <F5> 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 GetOr-
dersByCustomerID, 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>+<F5> 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ódi-
go 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=Conflict-
Option.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 Command-
Builder 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 ver-
siones 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 proyec-
tos. Para activar FxCop se ha de abrir la ventana Propiedades del <nombre proyecto>, selec-
cionar 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 mensa-
jes 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 veri-
ficació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 espe-
cificados. Después se añaden instrucciones especiales al código para definir cada uni-
dad 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 regre-
sió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 dedica-
das 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 auto-
matizadas 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ác-
ticas 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, Data-


base Administration, Database Design, Deprecation, Full-Text, General Administra-
tion, SQL Server 2005 Readiness, y T-SQL. Curiosamente, BPA no incluye ninguna cate-
gorí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 con-
figurar los grupos Best Practices Groups (BPGs) para cada instancia, y especificar los gru-
pos 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 ren-
dimiento del servidor o la red, y del número de objetos en el servidor. Una vez comple-
tado 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áxi-
mo 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 pri-
mer cliente que abre una conexión, pero mejora el rendimiento en los otros nueve clien-
tes 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 Asis-
tente 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:
<configuration>
<connectionStrings>
<add name= ProjectName.MySettings.ConnectionName value= ClientConnectionString />
</connectionStrings>
</configuration>

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 DataSet-
Name.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 reve-
la los nombres del servidor y la base de datos. Cualquier cadena de conexión que con-
tenga 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 –DataPro-
tectionConfigurationProvider y RSAProtectedConfigurationProvider– pensados específica-
mente para simplificar la protección de secciones específicas de los archivos Web.config.
De todos modos, una vez protegida la sección <connectionStrings> 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 senten-
cias 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 persona-
lizadas para crear trazas con la información más importante para analizar el rendimien-
to del sistema o realizar otro tipo de análisis.
En la siguiente figura vemos al Profiler mostrando en la plantilla de trazas T-SQL_Du-
ration eventos capturados por DataSetWSClient al ejecutar los métodos Web DataSetWS.
Las trazas de Profiler son igualmente útiles para comparar el rendimiento de las actua-
lizaciones con batches con el de las actualizaciones convencionales que requieren acce-
der 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 instan-
cias DeleteCommand, InsertCommand, y UpdateCommand desde la sentencia SQL de Se-
lectCommand. 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 dismi-
nución de rendimiento en tiempo de ejecución especificando el miembro CompareRow-
Version 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ódi-
go 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 sen-
tencias SQL para las cuatro operaciones. Sustituir procedimientos almacenados ya exis-
tentes 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 justifi-
car 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 sen-
tencias 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 relaciona-
dos (Northwind Orders y Order Details) en torno a un 13 por ciento más rápido que eje-
cutando 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 sen-
tencias 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 ejem-
plo, 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 con-
lleva 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 serializa-
das 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 con-
sulta 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 actualizacio-
nes y borrado de datos de las tablas blase. El optimistic concurrency control implica com-
parar 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 senten-
cia 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 produci-
dos 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) propor-
cionada específicamente para el optimistic concurrency control.
) ConflictOption.OverwriteChanges genera sentencias batch SQL parametrizadas toda-
vía más cortas o bien comandos EXECUTE para procedimientos almacenados que
no refuerzan el optimistic concurrency control. Los datos se borran y actualizan inde-
pendientemente 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 opti-
mistic 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 espe-
cificado después de inducir a propósito un error de concurrencia. Nótese los indicado-
res 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 mili-
segundos. 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 incertidum-
bre 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 usua-
rio 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 pacien-
tes reales. Si un médico A introduce un tratamiento nuevo para un paciente, las conse-
cuencias 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 relaciona-
do y borra otro que el usuario B no comprueba. Una solución verdadera implica com-
parar 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 relaciona-
das 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 jue-
gos de caracteres no hallados. Sustitya el nuevo control MaskedTextBox por uno conven-
cional 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 emple-
ados, 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 busi-
ness 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é realmen-
te introducido en el mercado.

Los formularios de entrada de datos que se crean en este capítulo empiezan con un for-
mualrio 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 DataGridView-
ComboBox 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-Orders-


Order Details
Los procesos de negocios más comunes y esenciales son tratar los Orders de los Custo-
mers, 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/activi-
dad 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 vincu-
lados 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 dema-
siado 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, con-
tiene 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 autogene-
radas 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 regis-
tro 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 con-
tiene un valor de clave foránea para el cliente. A continuación vemos una consulta sub-
select 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 autoge-
nerado 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 for-
mularios 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 por-
que 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 Adven-
tureWorks 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.SpecialOfferPro-
duct. 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 seleccio-
nar 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 cua-
dro 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 proyec-
to –en este ejemplo: Customers, Orders y Order Details.
5. Acepte el nombre por defecto del DataSet (NorthwindDataSet), y prosiga con el Asis-
tente 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 vincu-
lados a datos, y un control BindingNavigator; y a la bandeja, los iconos North-
windDataSet, CustomersBindingSource, CustomersTableAdapter, y CustomersBinding-
Navigator. 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 ban-
deja.
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 Or-
der_DetailsTableAdapter a la bandeja.
9. Pulse <F5> para construir y ejecutar el proyecto, que aparecerá tal como se mues-
tra en la figura siguiente. Muestre en pantalla algunos registros de Customers para
verificar que los dos DataGridViews muestran registros vinculados, y cierre final-
mente 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 selec-
cione la opción Agregar consulta para abrir el cuadro de diálogo Generador de crite-
rios 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 des-
pué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, cam-
bie 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 CustomersBinding-
Navigator, pulse <Ctrl> + <C>, seleccione el ToolStrip de arriba y pulse <Ctrl> + <V>
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 con-
sulta por FillByCustomerID. En el cuadro Texto de la consulta, añada WHERE Custo-
merID=@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 nom-
bre 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ódi-
go 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 GetCus-
tomerOrdersStripButton_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.FillBy-
CustomerID....
4. Cambie CustomerIDToolStripTextBox y CustomerIDToolStripTextBox1 por cboCusto-
merID, de modo que el cuadro combinado porporcione el valor del parámetro
@CustomerID para las tres instrucciones del manejador de eventos FillByCusto-
merIDToolStripButton_Click.

A continuación vemos el código final del manejador de eventos FillByCustomerIDTool-


StripButton_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 con-
tiene 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 pulsa-
ció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 FillByCustomerIDTool-
StripButton1_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 verifi-
car 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 archi-
vo NorthwindDataSet.xsd es una carga considerable con sus 129 KBytes y North-
windDataSet.Designer.vb contiene cerca de 3.300 instrucciones. Las anotaciones del es-
quema 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 con-
troles DataGridView, sea más fácil para los operadores de entrada de datos, hay que des-
activar el word-wrap, ajustar el ancho de columna, y dar formato a los valores de mone-
da 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 simpli-
fica 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álo-
go Generador de CellStyle. Seleccione la propiedad Format, abra el Cuadro de diálogo de for-
mato 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 tie-
nen 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 propie-
dad SortMode con el valor NotSortable eliminará el padding derecho de las cabeceras de colum-
na necesario para acomodar las flechas de direccionamiento. Tal vez tenga que definir la propie-
dad 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 des-
active el ordenamiento por columna. Una práctica común en Quantity es dar informa-
ció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 propie-
dades, 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 adi-
cionales 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 compro-
bar 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 Data-


GridViews 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 valo-
res 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 excep-
to 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 contie-
ne más información de la que la mayoría de usuarios desean conocer sobre el proble-
ma. Añadir un manejador de eventos delegado DataGridView.DataErrors permite subs-
tituir el mensaje por defecto "The following exception occurred in the DataGridView",
seguido del string StackTrace, por otro mensaje más apropiado. El mensaje que devuel-
ve 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 sofistica-


dos 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 heads-
down 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 formu-
larios 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:
) Proporcionar claves de aceleración (<Alt> + <Tecla>) 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 redu-
ce 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 <Alt> + <NumberKey>. Si se queda sin claves alfabéticas relacionadas,
puede especificar otras combinaciones, como <Ctrl> + <Mayús> + <Tecla> con un
manejador de eventos KeyDown.
) Evite los gráficos demasiado coloridos, los logotipos de compañías y otros elemen-
tos 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 cua-
dros 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

132
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 impre-
sos para el proyecto.
) No añada menús a menos que los necesite para imprimir o guardar archivos loca-
les. 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 cla-
ves 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 prefe-
rencias del operador. Es aconsejable que sustituya el modo por defecto EditOnKey-
strokeOrF2 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 Default-
ValuesNeeded.
) 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 acostumbra-
dos 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 pro-


porcionar 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 anterior-
mente 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_Details-
DataGridView.

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 tabu-
lar. 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 corres-
pondiente, que suele ser la primera. Este paso implica cambios en el botón asocia-
do de código del manejador de eventos.
6. Cambie la propiedad BackColor de todas las etiquetas a Transparent para que coin-
cidan 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 modificacio-
nes 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 envia-
do 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 guar-
dar 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 fle-
cha hacia abajo selecciona Orders anteriores. Si se ha comprobado Edit en la ficha,
pulsando <F2> cuando haya seleccionado un pedido no enviado, se abre la segun-
da 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_DefaultValues-
Needed 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_De-


tailsDataGridView_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 sin-
croniza 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 pri-
maria 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 tie-
nen valores ShippedDate no nulos al pulsar <F2>:

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 cua-
dros de texto de DataGridView vinculadas a valores de clave primaria o foránea necesi-
tan 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 pro-
piedad DataSource de la tabla de datos, el valor de ValueMember de la clave prima-
ria, 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 Data-
GridView, deberá invocar el método DataBindings.Add con un nuevo objeto New-
Binding 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 defi-
nir los valores de propiedad requeridos en tiempo de diseño y mantener automática-
mente 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 pue-
den 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 mane-
jador 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ógi-
cos para ser substituidos por cuadros combinados. Para remplazar una columna cua-
dro 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 figu-
ra 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 combi-
nados 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 dife-
rirá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 even-
to 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 pedi-
do 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 conser-
var 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 combina-
dos 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 cua-
dros de texto. Para actualizar los cuadros de texto con los cambios de los cuadros com-
binados, 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 combina-
dos 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 menu-
do 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 valo-
res 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 apa-
recen en el pedido de la columna vinculada a la tabla de datos. Eso no representa nin-
gú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 servi-
dor 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 procedimien-
to 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 con-
sulta 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 pun-
tero 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 cua-
dro 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éto-
dos 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 valida-
das 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 mejo-
res 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 requie-
re 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 tipi-
ficados. 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ña-
dir 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 actualizacio-
nes 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 sumi-
nistro 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 jerar-
quía de la relación, y las actualizaciones y adiciones deben ocurrer en orden descenden-
te, 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álo-
go 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.Up-
dateRule, 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 resulta-
do 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érfa-
nos los registros.
) SetDefault define el valor de la clave foránea de la tabla hijo con el valor por defec-
to 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 acep-
tar 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 actualiza-
das 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 opera-
ciones 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 cam-
bios 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 reco-
mendada 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ña-
dir 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
<dsChanges>.

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 destaca-
mos en negrita en el código siguiente después de llamar al método EndEdit del mane-
jador 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 pro-
yecto:
.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ñadi-
do 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 dupli-
cados 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 compo-
nentes 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 for-
mularios Windows o DataGridViews, es una tarea relativamente simple. Para tener con-
troles 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 excla-
mación blanco dentro de un círculo rojo. Los DataGridViews tienen un detector de erro-
res integrado que hace todavía más simple la validación de los valores de celda. La vali-
dació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 ejem-
plo, cuando se añade una fila nueva a una tabla con una clave primaria basada en valo-
res, 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 pri-
meros 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 ejecu-
tan 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 concu-
rrencia 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 Update-
BaseTables del proyecto (final) OrdersByCustomerV3.sln, y los correspondientes maneja-
dores de evento para las operaciones de actualización. Si bien los errores de concurren-
cia 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 prin-
cipal tiene tres botones que provocan errores de concurrencia cuando se escriben actua-
lizaciones 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 actuali-
zan 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 verifica-
ció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 jue-
gos 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 predece-
sor. 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 nive-
les, 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
<Tab> o <Mayús> + <Tab> ocurren dentro de la siguiente secuencia de eventos: Enter,
GotFocus, Leave, Validating, Validated, y LostFocus. Las operaciones con el ratón y el méto-
do 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 obje-
to 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_Va-
lidating 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 charac-
ters"
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 <Tab> 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 vali-
dació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 con-
trol 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 DataGrid-
Views también disparan eventos CellValidating, CellValidated, RowValidating, y RowValida-
ted. 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_Cell-
Validating 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_Cell-
EndEdit, 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 duplica-
do. 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 Data-
GridView 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 pro-
posed 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ña-
dir 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 reali-
zadas 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 edi-
ción de datos juega un papel cada vez más importante para determinar la disponibili-
dad de los datos. La disponibilidad de los datos es el factor determinante en la escala-
bilidad 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 com-
prueba 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 anterior-
mente 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 concu-
rrencia 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 opti-


mista 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 pue-
den 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 cone-
xión de ADODB proporcionan un sofisticado mecanismo para manejar las transgresio-
nes 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=ad-
FilterConflictingRecords se obtiene el conjunto de registros que contienen errores de con-
currencia. 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 pen-
dientes.

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 senten-
cia 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 decima-
les en la mantisa de Discount values son el resultado de utilizar el valor real (single-pre-
cision 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 impor-
tantes 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 sen-
tencias 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 pro-
piedades TableAdapter.Transaction y TransactionConnection como FriendPropertyTrans-
action() 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 pri-


vadas, por lo que ya no se puede implementar transacciones directamente con los jue-
gos 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 alma-
cenados. 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 concu-
rrencia 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 excla-
mación cuando se muestran en DataGridViews. Para resolver los errores hay que escri-
bir un número substancial de líneas de código de lógica de negocios.
La propiedad ContinueUpdateOnError es útil principalmente en las actualizaciones sen-
cillas 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 con-
sultor, 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 es-
pecificación más importantes a la hora de manejar las transgresiones de concurrencia:
) ¿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

176
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 por-
tátil frecuentemente desconectados, no necesitan actualizar múltiples entidades
antes de reconectarse a la red y guardar sus cambios. Las actualizaciones de múl-
tiples entidades implican procesar la actualización fila por fila para permitir el con-
trol 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 cam-
bios 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 conflic-
tos 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 des-
plegable, o una página tabular, para tratar las transgresiones en estructuras com-
plejas de datos.
) ¿Debería una sola trasgresión de concurrencia impedir cualquier cambio de actua-
lización o simplemente deshacer los realizados hasta ahora? Para deshacer los rea-
lizados 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 con-
creta 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 borra-
do todos los datos del pedido del servidor? El proceso de recrear el pedido es rela-
tivamente sencillo, pero habría que comprobar que el pedido se ha borrado real-
mente 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 bo-
rrado, 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 detec-
tan 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 borra-
dos 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 medi-
camento 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 diferen-
cia 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 Function TestNumberOfDetails(ByVal intOrderID As Integer, _
ByVal intAdded As Integer) As Boolean
Dim strConn As String = My.Settings.NorthwindConnection
Dim cnNwind As New SqlConnection(strConn)
Dim cmCurrent As New SqlCommand("", cnNwind)
Dim intCurrent As Integer
Try
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 traba-
ja 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 concu-
rrencia, 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 DataGrid-

179
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 apli-


car 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 inclu-
ye una copia del pedido borrado, se puede añadir código para poder reinstaurar el
pedido borrado con las actualizaciones realizadas. La función IsOrderModifiedOrDele-
ted, 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 fun-
ció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 inten-
tar 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 fun-
ció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 con-
cordantes 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 discor-
dante 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 almace-
nar 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 elegan-
te. Los valores originales no actualizados también pueden producir posteriores trans-
gresiones 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 obs-
tante, 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 Or-
derID y ProductID de un DataView de la fila actual de la tabla temporal DelDetails crea-
da con un parámetro DataViewRowState.Deleted. Para obtener los valores de filas marca-
das 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 Update-
BaseTables 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 valo-
res 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 usua-
rio, 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éto-
do 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 concu-
rrencia 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. ResolveConcurrency-
Errors 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 nom-
bre 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 erro-
res 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 dis-
cordancia 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, modifi-


caciones 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, guar-
da 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 traba-
jan 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 inevi-
tablemente provoca conflictos de concurrencia con las tablas de bases de datos mode-
radamente volátiles. Las "mejores prácticas" dictan implementar la gestión de concu-
rrencia 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 imprescin-
dible 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ási-
cas 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 des-
cribe cómo establecer el juego de registros de la tabla base necesario para las ope-
raciones 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 archi-
vo 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 pro-


ceso de actualización entre usuarios siempre conectados y usuarios online pero que tra-
bajan 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 importan-
tes 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

) Los usuarios desconectados tienen una selección de registros limitada a los dispo-
nibles 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 com-
binado 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 ini-
cial 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 desconecta-
do 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 actuali-
zaciones 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 Get-
CustomerOrdersToolStripButton_Click, el cual filtra la CustomersBindingSource para
incluir sólo la fila apropiada.
) Cambiar el valor de la propiedad DataSource del OrdersDataGridView de Orders-
DataConnector 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 CustomersBin-
dingSource.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.

196
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 SynchronizeOffline-
Orders.

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 tra-
taban sobre los aspectos básicos en torno a los DataSets y formularios Windows vincu-
lados. 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 tra-
tará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 Adventu-
reWorks. 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 actualiza-
ción, entrada y eliminación que se realicen con procedimientos almacenados y relacio-
nados 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 pro-
piedad 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 modi-
ficaciones simuladas realizadas por un usuario en tablas de datos sin conexión, inclu-
ye 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 DataAdap-
ter 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 propieda-
des 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, desha-
cer 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éto-


do 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 Transaction-
Scope, 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 ges-
tionada, 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 rendi-
miento 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 refe-
rencia 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 Transac-
tionScope 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 rendi-
miento. Si se abre explícitamente una sola conexión (cnNwind), antes de crear la trans-
acción implícita con el constructor TransactionScope, las transacciones quedarán desac-
tivadas 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 realiza-
dos 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ísti-
cas 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 Pro-
filer de una actualización transactuada con SqlTableAdapter, con dos conexiones autoge-
neradas 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 propie-
dad 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 eje-
cutar transacciones explícitas y que son el tema de los apartados siguientes. La segun-
da 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ódi-
go 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 miem-
bros 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ña-
dir relaciones al SelectCommand de una tabla. Las relaciones permiten mejorar las edi-
ciones 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 erro-
res 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 simplifi-
car 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 DataGrid-
Views, 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 des-
pué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 actua-
lizació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 cua-
dro de diálogo del mismo nombre. Pulse Añadir columnas y añada la columna Product-
Name 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ítu-


lo. 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 reprodu-
cir 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 lite-
rales 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 autogenera-
dos 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 argu-
mento. 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 pulsan-
do el botón Reload Data se añade el predicado subselect a Order_DetailsDataTable.Se-
lectCommand. 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 per-
petuado, 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 archi-
vo 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 bina-
rios, como las columnas image o varbinary del SQL Server. Las DataGridViewImageCo-
lumns 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 recur-
sos. La propiedad ImageLayout permite seleccionar un miembro de la enumeración
DataGridViewImageCellLayout: NotSet, Normal, Stretch o Zoom. Estos miembros corres-
ponden 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 arras-
tra 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 figu-
ra 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 pro-
piedad 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 proporcio-


na la fuente de datos para el proyecto ejemplo de este apartado, DataGridViewIma-
gesAW.sln. La tabla ProductPhoto tiene las columnas varbinary, ThumbNailPhoto y Large-
Photo 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 DataGridViewImage-
Cell 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 apli-
car 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 propie-
dad 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 procedimien-
to siguiente (que es llamado por el manejador de evento bindingNavigatorSaveI-
tem_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(Appli-
cation.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 selecciona-
da 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 guar-
dar 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 uti-
lizan Infosets para representar tablas de datos con una jerarquía de una o más relacio-
nes 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 inevita-
ble 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 des-
pués exportar las tablas, o una consulta a un archivo XML. De todos modos, un docu-
mento XML jerárquico exportado no guarda ninguna relación con la estructura origi-
nal 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én-
tica 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 docu-
mento. 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 docu-
mento.

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 meca-
nismo 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 atri-
butos, 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 es-
quema 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 tipi-
ficados desde esquemas XML complejos, para documentos de negocios estándar, como
Universal Business Language (UBL) 1.0 o Human Resources XML (HR-XML). Los esque-
mas UBL 1.0 utilizan ampliamente las directrices <xs:import> y especifican tipos com-
plejos para elementos que representan las columnas de las tablas.
La mayoría de las aplicaciones de edición XML deben producir un documento de sali-
da 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 concre-
tas 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:
<rootElement>
<parentGroup>
<parentField1>String</parentField1>
...
<parentFieldN>1000</parentFieldN>
<childGroup>
<childField1>String</childField1>
...
<childFieldN>15.50</childFieldN>
<grandchildGroup>
<grandchildField1>String</grandchildField1>
...
<grandchildFieldN>15</grandchildFieldN>
</grandchildGroup>
</childGroup>
</parentGroup>
</rootElement>

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:
<?xml version= 1.0 encoding= utf-8 ?>
<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 />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

El diseñador de DataSets interpreta los grupos <xs:complexType> no raíz que tienen ele-
mentos 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 relacio-
nadas.

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 <rootElementxmlns= documentNamespace> 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 documen-
to, 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 gene-
rará 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 selec-
cionando 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 edi-
tar parentGroup, y después añadir DataGridViews para las tablas childGroup y grand-
childGroup.

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:
<?xml version= 1.0 encoding= utf-8 ?>
<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= Customers >


<xs:complexType>
<xs:sequence>
<xs:element name= CustomerID type= xs:string />
<xs:element name= CompanyName type= xs:string />
<xs:element name= ContactName type= xs:string minOccurs= 0 />
<xs:element name= ContactTitle type= xs:string minOccurs= 0 />
<xs:element name= Address type= xs:string />
<xs:element name= City type= xs:string />
<xs:element name= Region type= xs:string minOccurs= 0 />
<xs:element name= PostalCode type= xs:string minOccurs= 0 />
<xs:element name= Country type= xs:string />
<xs:element name= Phone type= xs:string />
<xs:element name= Fax type= xs:string minOccurs= 0 />
<xs:element name= Orders minOccurs= 0 maxOccurs= unbounded >
<xs:complexType>
<xs:sequence>
<xs:element name= OrderID type= xs:int />
<xs:element name= CustomerID type= xs:string />
<xs:element name= EmployeeID type= xs:int />
<xs:element name= 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 />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>

Nótese que el esquema NorthwindDS.xsd no contiene referencias a las columnas añadi-


das 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.De-
signer.vb, el método Northwind.InitClass añade esas DataColumns a las DataTables al espe-
cificar 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 siguien-
te 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 cal-
cular con la fórmula Quantity*UnitPrice*(1 Discount).
Cualquier cambio en alguno de los valores de la ventana Propiedades provoca un cam-
bio importante en el archivo de esquema: al archivo se le añade un grupo <xs:annota-
tion> 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 se-
cundario, en el Explorador de soluciones, seleccione Abrir con… y, en el cuadro de diálo-
go 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 Or-
der_Details definido por <xs:attributename= "totalAmount" type= "xs:decimal" use= "requi-

227
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 "envuel-
tos" 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:
<Customers>
<Customer>
<CustomerID>GREAL</CustomerID>
...
<Fax></Fax>
<Orders>
<Order>

228
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 229

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

<OrderID>11061</OrderID>
...
<ShipCountry>USA</ShipCountry>
<Order_Details>
<Order_Detail>
<OrderID>11061</OrderID>
...
<Discount>0.075</Discount>
</Order_Detail>
</Order_Details>
</Order>
</Orders>
</Customer>
<Customers>
A continuacion vemos el esquema abreviado del documento fuente anterior con los elementos envol-
ventes destacados en negrita:
<?xml version= 1.0 encoding= utf-8 ?>
<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 type= xs:string
minOccurs= 0 />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>

El esquema CustomersDS.xsd genera dos tablas adicionales para establecer las relacio-
nes 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 invo-
cando 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:
<?xml version= 1.0 encoding= utf-8 ?>
<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:sequence>
</xs:complexType>
</xs:element>
<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:sequence>
</xs:complexType>
</xs:element>
<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 />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

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 dere-
cho 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ál-
go 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 verifi-
cació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 exter-
na 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 com-
pletados.

Al añadir las claves primarias y las relaciones a las tablas, al final del esquema se aña-
den 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>
<xs:unique name= PK_Orders msdata:PrimaryKey= true >
<xs:selector xpath= .//Orders />
<xs:field xpath= OrderID />
</xs:unique>
<xs:unique name= PK_Order_Details msdata:PrimaryKey= true >
<xs:selector xpath= .//Order_Details />
<xs:field xpath= OrderID />
<xs:field xpath= ProductID />
</xs:unique>
<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>
<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 />
</xs:keyref>
</xs:element>
</xs:schema>

Los elementos <xs:unique> definen claves primarias, y los elementos <xs:keyref> especi-
fican las restricciones de clave foránea. Los atributos msprop son referencias a las rela-
ciones 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éri-
cos 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 Data-
Set.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 Da-
taSet.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én-
tico al generado por el método precedente.
Si no dispone de ningún documento XML representativo de todas las instancias posi-
bles de documento XML, o si no quiere crear uno manualmente, puede usar la herra-
mienta 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 ini-
cial (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 edi-
tar 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 cua-
dros 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 ante-
riores 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 pue-
dan 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 AllowUserTo-
AddRows 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 autogenera-
das 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 colum-
nas de los DataGridView. Añada un botón para guardar los cambios e invoque el méto-
do 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 procedi-
miento 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 modes-
ta 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 ins-
tantá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 codi-
ficado 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 equi-
valente 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:
<connectionStrings>
<add name= NorthwindConnection connectionString= Server=localhost;Integrated
Security=True;Database=Northwind providerName= System.Data.SqlClient />
</connectionStrings>

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 ocul-
to 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 gene-


rar 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 <html>, <head>, <body>, <form> y
<div>, 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ña-
dor para mostrar una página vacía en el diseñador, que sólo soporta el modo conven-
cional 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, casca-
ding 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 pulsan-
do 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 <F5> 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.con-
fig 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 Codebehind= Default.aspx.vb
Inherits= DataWebSite.Form1 %>

242
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 ante-
rior:
<%@ Page Language= VB AutoEventWireup= false CodeFile= Default.aspx.vb
Inherits= _Default %>

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ódi-
go 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 nom-
bres 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 servi-
dor 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_ CodeCompileUnit (contenedor para programa gráfico
CBMResult.ccu CodeDOM para la página).
default.aspx.cdcab7d2_ Lista de dependencias de archivo y valores hash de la
CBMResult.compiled 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 servi-
dor Web integrado. Sólo las páginas modificadas se recompilan mientras se está ejecu-
tando el proyecto. En el apartado dedicado a la publicación de sitios Web precompila-
dos 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 cual-
quier 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 obje-
tos y componentes de acceso a datos.

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 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 vin-
culante para controles bidireccionales de datos vinculados y otros controles de ser-
vidor, como las listas desplegables y los cuadros de lista, que soportan la vincula-
ció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.
Q Los controles DataList muestran y editan filas DataSource de forma secuencial en
una o más columnas.
Q Los controles FormView muestran y editan una sola fila DataSource en un formula-
rio XML convencional.
Q Los controles GridView muestran y editan múltiples filas DataSource en una parri-
lla similar a los DataGridView de los formularios Windows. GridView sustituye al
DataGrid de ASP.NET 1.x.
Q 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 men-
cionar.

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 combi-
na 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 <asp:DataSourceType> guarda la defi-
nició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 <asp:SqlDataSour-
ce> 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 per-
mite utilizar cualquier conexión que haya definido en el Explorador de servidores, o
definir una conexión nueva.

245
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 246

Bases de datos con Visual Basic

Q AccessDataSource – para los archivos Access. El elemento <asp:AccessDataSource>


sustituye un atributo de ruta relativa datafile= ~/App_Data/FileName.mdb por el atri-
buto 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.
Q ObjectDataSource – para los objetos de negocios habituales, servicios Web, compo-
nentes de datos o DataSets que retornan y, opcionalmente, actualizar datos. El obje-
to debe soportar la interfaz IEnumerable y proporcionar al menos un método públi-
co 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 car-
peta 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.
Q 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 ele-
mentos del documento fuente. Puede actualizar los datos de la fuente XML invo-
cando el método GetXmlDocument o creando un objeto en memoria
XmlDataDocument que contenga objetos editables XmlNode. Otra alternativa es uti-
lizar expresiones XPath para actualizar los datos.
Q 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 DataSource-
Control, que es a su vez la base de la interfaz IDataSource. Los controles DataSource con-
tienen objetos nombrados DataSourceView; los controles Web vinculados a datos conec-
tan 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 abs-
tracta 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 <Nuevo origen de datos> para ini-
ciar 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 lec-
tura). 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 indi-
vidual 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 pedi-
dos 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 herra-
mientas 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 selec-
cione Establecer como página de inicio. Pulse <F5> 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 contene-
dor DataList1 se añade al formulario y se abre la etiqueta inteligente DataLists Tasks.
4. Seleccione <Nuevo origen de datos> 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 seguri-
dad 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 Descen-
dente. 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 selecciona-
das en el paso 7.
Pulse <F5> 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

<asp:SqlDataSource ID=”dsOrders” Runat=”server”


DeleteCommand=”DELETE FROM [Orders] WHERE [OrderID] = @original_OrderID”
InsertCommand=”INSERT INTO [Orders] ([CustomerID], [EmployeeID], [OrderDate],
[RequiredDate], [ShippedDate], [ShipVia], [Freight], [ShipName])
VALUES (@CustomerID, @EmployeeID, @OrderDate, @RequiredDate, @ShippedDate,
@ShipVia, @Freight, @ShipName)”
SelectCommand=”SELECT [OrderID], [CustomerID], [EmployeeID], [OrderDate],
[RequiredDate], [ShippedDate], [ShipVia], [Freight], [ShipName]
FROM [Orders] ORDER BY [OrderID] DESC”
UpdateCommand=”UPDATE [Orders] SET [CustomerID] = @CustomerID,
[EmployeeID] = @EmployeeID, [OrderDate] = @OrderDate,
[RequiredDate] = @RequiredDate, [ShippedDate] = @ShippedDate,
[ShipVia] = @ShipVia, [Freight] = @Freight, [ShipName] = @ShipName
WHERE [OrderID] = @original_OrderID”
ConnectionString=”<%$ ConnectionStrings:NorthwindConnection %>”>
<DeleteParameters>
<asp:Parameter Type=”Int32” Name=”OrderID”></asp:Parameter>
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Type=”String” Name=”CustomerID”></asp:Parameter>
<asp:Parameter Type=”Int32” Name=”EmployeeID”></asp:Parameter>
<asp:Parameter Type=”DateTime” Name=”OrderDate”></asp:Parameter>
<asp:Parameter Type=”DateTime” Name=”RequiredDate”></asp:Parameter>
<asp:Parameter Type=”DateTime” Name=”ShippedDate”></asp:Parameter>
<asp:Parameter Type=”Int32” Name=”ShipVia”></asp:Parameter>
<asp:Parameter Type=”Decimal” Name=”Freight”></asp:Parameter>
<asp:Parameter Type=”String” Name=”ShipName”></asp:Parameter>
<asp:Parameter Type=”Int32” Name=”OrderID”></asp:Parameter>
</UpdateParameters>
<InsertParameters>
<asp:Parameter Type=”String” Name=”CustomerID”></asp:Parameter>

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

<asp:Parameter Type=”Int32” Name=”EmployeeID”></asp:Parameter>


<asp:Parameter Type=”DateTime” Name=”OrderDate”></asp:Parameter>
<asp:Parameter Type=”DateTime” Name=”RequiredDate”></asp:Parameter>
<asp:Parameter Type=”DateTime” Name=”ShippedDate”></asp:Parameter>
<asp:Parameter Type=”Int32” Name=”ShipVia”></asp:Parameter>
<asp:Parameter Type=”Decimal” Name=”Freight”></asp:Parameter>
<asp:Parameter Type=”String” Name=”ShipName”></asp:Parameter>
</InsertParameters>
</asp:SqlDataSource>

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 dlOrders
Font\Name Verdana
Font\Size 10pt
RepeatColumns 2
RepeatDirection 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 <br/> y sustitúyalo por dos espacios (&nbsp;). 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"), inclui-


da 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– gene-
rado por la plantilla Item de la figura anterior:
<ItemTemplate>
OrderID: <asp:Label ID="OrderIDLabel" runat="server"
Text='<%# Eval("OrderID") %>'></asp:Label><br />
CustomerID: <asp:Label ID="CustomerIDLabel" runat="server"
Text='<%# Eval("CustomerID") %>'> </asp:Label><br />
EmployeeID: <asp:Label ID="EmployeeIDLabel" runat="server"
Text='<%# Eval("EmployeeID") %>'> </asp:Label><br />
OrderDate: <asp:Label ID="OrderDateLabel" runat="server"
Text='<%# Eval("OrderDate", “{0:d}”) %>'></asp:Label><br />
RequiredDate: <asp:Label ID="RequiredDateLabel" runat="server"
Text='<%# Eval("RequiredDate", “{0:d}”) %>'> </asp:Label><br />
ShippedDate: <asp:Label ID="ShippedDateLabel" runat="server"
Text='<%# Eval("ShippedDate", “{0:d}”) %>'> </asp:Label><br />
ShipVia: <asp:Label ID="ShipViaLabel" runat="server"
Text='<%# Eval("ShipVia") %>'></asp:Label><br />
Freight: <asp:Label ID="FreightLabel" runat="server"
Text='<%# Eval("Freight", “{0:C}”) %>'></asp:Label><br />
ShipName: <asp:Label ID="ShipNameLabel" runat="server"
Text='<%# Eval("ShipName") %>'></asp:Label><br /> <br />
</ItemTemplate>

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 consi-
derables de red y del servidor de la base de datos sólo en la operación de abrir la pági-
na. 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, mar-
que 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 <Nuevo origen de datos> 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 cua-
dro 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 <F5> y verifique que la lista desplegable muestra los países por orden alfa-
bético. Seleccionando un país distinto de Argentina, el servidor refrescará la pági-
na 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ña-
dida 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 <F5> 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 eti-
queta inteligente ddlCountries. Seleccione Editar elementos para abrir el cuadro de diálo-
go 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:
<asp:DropDownList ID=”ddlCountry” Runat=”server” DataSourceID=”dsCountries”
Width=”115px” Height=”22px” AutoPostBack=”True” DataTextField=”ShipCountry”
DataValueField=”ShipCountry” AppendDataBoundItems=”True”>
<asp:ListItem>[Select a Country]</asp:ListItem>
</asp:DropDownList>
<asp:SqlDataSource ID=”dsCountries” Runat=”server” SelectCommand=”SELECT
DISTINCT [ShipCountry] FROM [Orders] ORDER BY [ShipCountry]”
ConnectionString=”<%$ ConnectionStrings:NorthwindConnection %>”>
</asp:SqlDataSource>

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<asp:SQLDataSource...>:
<SelectParameters>
<asp:ControlParameter Name=”ShipCountry” DefaultValue=”USA” Type=”String”
ControlID=”ddlCountry” PropertyName=”SelectedValue”></asp:ControlParameter>
</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 con-
troles 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_UpdateCom-
mand y asignarlos como valores (miembros) de (la colección) DataSource.Command.Para-
meters 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 elemen-
to 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 plan-
tilla EditItem a la lista copiando la plantilla Item en la plantilla EditItems y substituyen-
do 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 manejado-
res 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 permi-
te 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 con-
troles 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én-
tico 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 espe-
cí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 boto-
nes para seleccionar los diez valores anteriores o siguientes, pero puede darle a la pro-
pieadad 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ñadien-
do 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 = "&lt;Empty&gt;"
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 = "&lt;Empty&gt;"
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 <ItemTemplate>
y sus hijos. (Para borrar una fila no se necesita ninguna plantilla). El diseñador renom-
bra automáticamente las etiquetas con valores Id duplicados en LabelN, donde N es un
número secuencial.
Renombre el <ItemTemplate> 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 <InsertItemTemplate> 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 acti-
var las plantillas EditItem o InsertItem, cancelar operaciones de edición o inserción y eje-
cutar 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 maneja-
dor de evento.
Aquí vemos cómo utilizar los tres modos para activar las plantillas:
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.
Q 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.
Q 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 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 pro-
piedad CommandName.
Q 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 propie-
dad 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 for-
mulario Windows DataGridView hasta cierto punto, si se considera las limitaciones res-
pecto 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 cam-
bia el color de las cabeceras de columna para indicar el tipo de clasificación. Puede des-
activar 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 actuali-
zables 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 exis-
tentes son: Select, Edit, Cancel, Update y Delete, que disparan los eventos
ItemCommand, SelectedIndexChanging, SelectedIndexChanged, RowEditing, Row-
CancelingEdit, 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 con-
vencionales se pueden sustituir definiendo el valor Button o Image para la propie-
dad 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 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.
Q ImageFields – muestran gráficos de las columnas image o varbinary del SQL Server,
o archivos XML de imágenes codificadas.
Q TemplateFields – permiten personalizar el formateo de los cuadros de texto o subs-
tituir 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 cua-
dro 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 cua-
dro de texto e ítems vacíos AlternatingItemTemplate, HeaderTemplate y FooterTemplate,
bajo una cabeceras ColumnName para cada una de las columnas convertidas (ver figu-
ra siguiente).
A continuación vemos el código fuente reformateado para las columnas de sólo lectu-
ra Order ID y Customer ID:
<Columns>
<asp:BoundField ReadOnly="True" HeaderText="Order ID"
InsertVisible="False" DataField="OrderID"
SortExpression="OrderID">
<ItemStyle HorizontalAlign="Right"></ItemStyle>
</asp:BoundField>

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

<asp:TemplateField SortExpression="CustomerID" HeaderText="Cust.


ID"><EditItemTemplate>
<asp:TextBox ID="txtCustomerID" Runat="server" Width="46px" %
Text='<%# Bind("CustomerID") %>' ReadOnly="True"></asp:TextBox>
</EditItemTemplate>
<ItemStyle HorizontalAlign="Left"></ItemStyle>
<ItemTemplate>
<asp:Label Runat="server" Text='<%# Bind("CustomerID") %>'
ID="Label3"></asp:Label>
</ItemTemplate>
</asp:TemplateField>
</Columns>
(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 defi-
nir valores de clave foránea con un número limitado de opciones. Para sustituir un cua-
dro 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 propie-
dad 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:
<Columns>
<asp:TemplateField HeaderText="Empl. ID"><EditItemTemplate>
<asp:DropDownList ID="ddlEmployee" Runat="server" Height="22px"
Width="94px" DataSourceID="dsEmployees"
DataValueField="EmployeeID" DataTextField="LastName"
SelectedValue='<%# Bind("EmployeeID") %>'>
</asp:DropDownList>
</EditItemTemplate>
<ItemStyle HorizontalAlign="Center"></ItemStyle>
<ItemTemplate>
<asp:Label Runat="server" Text='<%# Bind("EmployeeID") %>'
ID="Label2"></asp:Label>
</ItemTemplate>
</asp:TemplateField>
</Columns>

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 Data validation: validación de datos con los controles RequiredFieldValidator, Ran-


geValidator, RegularExpressionValidator, CompareValidator, CustomValidator y Valida-
tionSummary.
Y ObjectDataSources: fuentes de datos de objeto, basadas en tablas de juegos de da-
tos 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 funcio-
nales, 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 conec-
tan 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, redu-
ce el tráfico en la red y el consumo de recursos. Todos los formularios Web y de pro-
ducción de Windows deberían permitir la validación de los datos entrados por el usua-
rio 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 con-
troles 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 vali-
dació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 con-
trol. 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 frus-
trante 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, tam-
bién se puede asisgnar un validador a cualquier control de servidor que acepte la entra-
da 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 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 mues-
tra el valor de su propiedad Text o ErrorMessage en letras rojas, a la derecha o por
debajo del control.
Y 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.
Y RegularExpressionValidator: comprueba que el texto del control especificado sea
conforme a una expresión regular que usted debe escribir como valor de la propie-
dad 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.

272
VisualBasic2005_08.qxp 02/08/2007 16:30 PÆgina 273

Aplicar técnicas avanzadas con ASP.NET 2.0

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 com-
prueba el mismo tipo de datos que RangeValidator. Hay que añadir un Required-
FieldValidator para comprobar los valores vacíos.
Y CustomValidator: permite comprobar el control especificado con una función de
JScript o VBScript, la función ClientValidationFunction y un manejador VB.NET simi-
lar que hay que escribir para el evento OnServerValidate. La función ClientVali-
dationFunction 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.
Y 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 mos-
trar 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 for-
mulario.
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 formu-
larios de datos que no utilizan los controles preintegrados. Por ejemplo, tal vez no quie-
ra 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 validado-
res 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 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.
Y DisplayMode especifica el modo en que el control de validación muestra la propie-
dad 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 Validation-
Summary, si existe.
Y 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.
Y 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 Requi-


redFieldValidator:

<asp:TemplateField SortExpression= OrderDate HeaderText= Order Date >


<EditItemTemplate>
<asp:TextBox ID= txtOrderDate Runat= server Width= 76px
Text= <%# Bind( OrderDate , {0:d} ) %> ></asp:TextBox>
<asp:RequiredFieldValidator ID= rfvOrderDate Runat= server
ControlToValidate= txtOrderDate ErrorMessage= OrderDate is required.
Display= Dynamic ToolTip= OrderDate is a required field.Required!
</asp:RequiredFieldValidator>
</EditItemTemplate>
<ItemStyle HorizontalAlign= Right VerticalAlign= Top ></ItemStyle>
<ItemTemplate>
<asp:Label Runat= server Text= <%# Bind( OrderDate , {0:d} ) %>
ID= lblOrderDate ></asp:Label>
</ItemTemplate>
</asp:TemplateField>

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 forma-
to 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 proporcio-
na 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, RequiredField-
Validator– 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ús-
culas 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 Ex-
pressions Library, en la dirección http://www.regexlib.com/, tiene unas 800 expresiones indexa-
das, 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 aparta-
dos dónde se explica el funcionamiento de estas expresiones y se describe las clases del espacio-
nombre 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 RegularExpression-
Validator hasta la derecha del RequiredFieldValidator. Cambie adecuadamente los valores
de las propiedades del RequiredFieldValidator, y escriba [A-Z]{5} como valor de Valida-
tionExpression. 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:

<asp:TemplateField SortExpression= CustomerID HeaderText= Cust. ID >


<EditItemTemplate>
<asp:TextBox ID= txtCustomerID Runat= server Width= 52px
Text= <%# Bind( CustomerID ) %> ></asp:TextBox>
<asp:RequiredFieldValidator ID= rfvCustomerID Runat= server
ErrorMessage= CustomerID is required. Display= Dynamic
ControlToValidate= txtCustomerID
ToolTip= CustomerID is a required field. > Required!
</asp:RequiredFieldValidator>&nbsp;
<asp:RegularExpressionValidator ID= revCustomerID Runat= server
ErrorMessage= CustomerID must be 5 capital letters.
Display= Dynamic ControlToValidate= txtCustomerID
ValidationExpression= [A-Z]{5}
ToolTip= CustomerID must be 5 capital letters. >[ABCDE]

277
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 278

Bases de datos con Visual Basic

</asp:RegularExpressionValidator>
</EditItemTemplate>
<ItemStyle HorizontalAlign= Left VerticalAlign= Top ></ItemStyle>
<ItemTemplate>
<asp:Label Runat= server Text= <%# Bind( CustomerID ) %>
ID= lblCustomerID ></asp:Label>
</ItemTemplate>
</asp:TemplateField>

El ejemplo anterior ha sido elaborado para demostrar la validación de regex. En las apli-
caciones de la vida real, lo mejor es definir los valores de CustomerID en una lista des-
plegable (DropDownList, parecida a las de las columnas EmployeeID o ShipVia del Vali-
datedGridView) 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 dsEmployeesSqlData-
Source. 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 Ma-
ximumValue(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áli-
da. 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 Date-
Time 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 separado-
res 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 contro-
les RequiredFieldValidator, RangeValidator y RegularExpressionValidator:

<asp:TemplateField SortExpression= OrderDate HeaderText= Order Date >


<EditItemTemplate>
<asp:TextBox ID= txtOrderDate Runat= server Width= 76px
Text= <%# Bind( OrderDate , {0:d} ) %> ></asp:TextBox>
<asp:RequiredFieldValidator ID= rfvOrderDate Runat= server
ControlToValidate= txtOrderDate
ErrorMessage= OrderDate is required. Display= Dynamic
ToolTip= OrderDate is a required field. >Required!
</asp:RequiredFieldValidator>
<asp:RangeValidator ID= rvOrderDate Runat= server
ToolTip= Dates must be in ShortDate format (MM/DD/YYYY)
ControlToValidate= txtOrderDate
ErrorMessage= Dates must be in M/D/YYYY format.
MinimumValue= 1/1/1980 MaximumValue= 12/31/2099 Type= Date
Display= Dynamic >[M/D/YYYY]
</asp:RangeValidator>
<asp:RegularExpressionValidator ID= revOrderDate Runat= server
ToolTip= Date format must be M/D/YYYY and date must be valid.
Display= Dynamic ErrorMessage= Date format must be M/D/YYYY.
ControlToValidate= txtOrderDate
ValidationExpression= ^((((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}$
>[M/D/YYYY]
</asp:RegularExpressionValidator>
</EditItemTemplate>
<ItemStyle HorizontalAlign= Right VerticalAlign= Top ></ItemStyle>
<ItemTemplate>
<asp:Label Runat= server Text= <%# Bind( OrderDate , {0:d} ) %>
ID= lblOrderDate ></asp:Label>
</ItemTemplate>
</asp:TemplateField>

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


patibles 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 Control-
ToCompare, GreaterThan como Operator y Date como Type.

A continuación, el código fuente reformateado para el RequiredDate TemplateField:

<asp:TemplateField SortExpression= RequiredDate HeaderText= Required Date >


<EditItemTemplate>
<asp:TextBox ID= txtRequiredDate Runat= server Width= 76px
Text= <%# Bind( RequiredDate , {0:d} ) %> ></asp:TextBox>
<asp:RequiredFieldValidator ID= rfvRequiredDate Runat= server
ToolTip= RequiredDate is required and must be later than
OrderDate.
Display= Dynamic ErrorMessage= RequiredDate is required.
ControlToValidate= txtRequiredDate >Required!
</asp:RequiredFieldValidator>
<asp:CompareValidator ID= cvRequiredDate Runat= server
ToolTip= RequiredDate must be later than OrderDate. Display= Dynamic
ErrorMessage= RequiredDate must be later than OrderDate.
ControlToValidate= txtRequiredDate Operator= GreaterThan
ControlToCompare= txtOrderDate >Impossible!
</asp:CompareValidator>
<asp:RegularExpressionValidator ID= revRequiredDate Runat= server
ToolTip= Date format must be M/D/YYYY and date must be valid.
Display= Dynamic ErrorMessage= Date format must be M/D/YYYY.
ControlToValidate= txtRequiredDate
ValidationExpression= ^((((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}$
>[M/D/YYYY]
</asp:RegularExpressionValidator>
</EditItemTemplate>
<ItemStyle HorizontalAlign= Right VerticalAlign= Top ></ItemStyle>
<ItemTemplate>
<asp:Label Runat= server Text= <%# Bind( RequiredDate , {0:d} ) %>
ID= lblRequiredDate ></asp:Label>
</ItemTemplate>
</asp:TemplateField>

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 contie-
ne 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 pro-
piedad ClientValidationFunction de VBScript por parte del cliente:

<asp:TemplateField SortExpression= Freight HeaderText= Freight >


<EditItemTemplate>
<asp:TextBox ID= txtFreight Runat= server Width= 52px
Text= <%# Bind( Freight ) %> ></asp:TextBox>
<asp:RequiredFieldValidator ID= rfvFreight Runat= server
ErrorMessage= Freight is required; enter 0 if not known.
Display= Dynamic ControlToValidate= txtFreight >Required!
</asp:RequiredFieldValidator>
<asp:CustomValidator ID= cvFreight Runat= server
ToolTip= Freight for shipped order cannot be less than $5.00
ControlToValidate= txtFreight Display= Dynamic
ErrorMessage= Freight for shipped order is less than $5.00
OnServerValidate= cvFreight_ServerValidate
ClientValidationFunction= ValidateFreight >
<5=Bad!
</asp:CustomValidator>
</EditItemTemplate>
<ItemStyle HorizontalAlign= Right VerticalAlign= Top ></ItemStyle>
<ItemTemplate>
<asp:Label Runat= server Text= <%# Bind( Freight , {0:C2} ) %>
ID= lblFreight ></asp:Label>
</ItemTemplate>
</asp:TemplateField>

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 Docu-
ment.activeElement.id, el cual devuelve gvOrdersEditable_ctl14_txtFreight del siguiente
elemento <input> activo:

<input name= gvOrdersEditable$ctl14$txtFreight type= text value= 0


id= gvOrdersEditable_ctl14_txtFreight style= width:52px; />

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 devol-
ver el elemento ShippedDate <input>:

<input name= gvOrdersEditable$ctl14$txtShippedDate type= text value= 5/10/1998


id= gvOrdersEditable_ctl14_txtShippedDate style= width:76px; />

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 <head> que implementa la vali-


dació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
</script>

Puede escribir código similar para realizar cálculos de validación de fechas, como subs-
tituir el control CompareValidator por el RequiredDate con un CustomValidator que requie-
ra 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 Vali-


dationSummary y situándolo por encima del GridView, definiendo el valor SinglePara-
graph para la propiedad DisplayMode y HeaderText opcionalmente, y aplicando los for-
matos necesarios.

<asp:ValidationSummary ID= vsOrderData Runat= server Font-Size= 10pt


Font-Bold= False Font-Names= Verdana DisplayMode= SingleParagraph
ShowMessageBox= False Width= 802px Height= 18px BorderColor= DimGray
BorderStyle= Solid BackColor= White BorderWidth= 1px
HeaderText= &amp;nbsp;Validation summary:
ToolTip= This is a summary of all order data validation errors. />

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 servi-
dor 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 colum-
na ProductID. Este control comprueba si hay valores duplicados con ayuda del siguien-
te 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 elemen-


to <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</span>

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 com-
prueba 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
</script>

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 transgresio-
nes 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ánea-
mente por algunos cientos de usuarios, sin requisitos de lógica de negocios, o requisi-
tos 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 arqui-
tectura cliente/servidor impone una estructura de datos relacional específica a las apli-
caciones. Los procedimientos almacenados pueden mejorar el rendimiento e incorpo-
rar 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 Ob-


jectDataSources.

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 com-
ponente lógico DALC que implementa el middle-tier puede, pero no es necesario, aña-
dirse 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 ver-
sió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 pri-
mer 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 ele-
mento, 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ña-
dor 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 configu-


rar 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 <ConnectionStrings> del archivo Web.config. A diferencia de
los DataSets persistentes que se añaden a los proyectos de formulario Windows, los com-
ponentes 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 ObjectDataSour-
ces duplica las S1l DataSources del proyecto de ejemplo, permitiendo así utilizar la plan-
tilla 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 ObjectDataSour-


ce desde el cuadro de herramientas que hay bajo dsOrdersEdit SqlDataSource. Esto aña-
dirá un place-holder ObjectDataSource1 y abrirá la etiqueta inteligente Common Data-
Source Task.
2. Pulse el vínculo Configurar origen de datos para abrir el cuadro de diálogo del mis-
mo nombre. Marque el cuadro de verificación Mostrar sólo componentes de datos,
abra la lista desplegable y seleccione OrdersDataSetTableAdapters.OrdersTableAdap-
ter. 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 pro-
piedad 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 etique-
ta inteligente de gvOrdersEditable, abra la lista Elegir origen de datos y seleccione odsOr-
dersEdit. Esto abrirá el cuadro de mensaje Actualizar campos y claves para "gvOrdersEdi-
table". Pulse No para dejar intacto el GridView.

Si pulsa Sí, destruirá y regenerará el GridView, lo que borrará todas las plantillas y for-
matos.

Si pulsa Sí por error, pulse <Ctrl> + <Z> para deshacer el cambio en la fuente de datos. Tal vez
tenga que pulsar <Ctrl> + <Z> varias veces para devolver el GridView a su estado original.

Pulse <F5> 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 diferen-
cia detectable en el funcionamiento del GridView con SqlDataSource en lugar de Objata-
SectSource.

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

<asp:ObjectDataSource ID= odsOrdersEdit runat= server DeleteMethod= Delete


InsertMethod= Insert SelectMethod= GetOrders UpdateMethod= Update >
TypeName= OrdersDataSetTableAdapters.OrdersTableAdapter
<DeleteParameters>
<asp:Parameter Name= Original_OrderID Type= Int32 />
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Name= CustomerID Type= String />
<asp:Parameter Name= EmployeeID Type= Int32 />
<asp:Parameter Name= OrderDate Type= DateTime />
<asp:Parameter Name= RequiredDate Type= DateTime />
<asp:Parameter Name= ShippedDate Type= DateTime />
<asp:Parameter Name= ShipVia Type= Int32 />
<asp:Parameter Name= Freight Type= Decimal />
<asp:Parameter Name= ShipName Type= String />
<asp:Parameter Name= ShipAddress Type= String />
<asp:Parameter Name= ShipCity Type= String />
<asp:Parameter Name= ShipRegion Type= String />
<asp:Parameter Name= ShipPostalCode Type= String />
<asp:Parameter Name= ShipCountry Type= String />
<asp:Parameter Name= Original_OrderID Type= Int32 />
</UpdateParameters>
<InsertParameters>
<asp:Parameter Name= CustomerID Type= String />
<asp:Parameter Name= EmployeeID Type= Int32 />
<asp:Parameter Name= OrderDate Type= DateTime />
<asp:Parameter Name= RequiredDate Type= DateTime />
<asp:Parameter Name= ShippedDate Type= DateTime />
<asp:Parameter Name= ShipVia Type= Int32 />
<asp:Parameter Name= Freight Type= Decimal />
<asp:Parameter Name= ShipName Type= String />
<asp:Parameter Name= ShipAddress Type= String />
<asp:Parameter Name= ShipCity Type= String />
<asp:Parameter Name= ShipRegion Type= String />
<asp:Parameter Name= ShipPostalCode Type= String />
<asp:Parameter Name= ShipCountry Type= String />
</InsertParameters>
</asp:ObjectDataSource>

Las diferencias básicas entre el código fuente para ObjectDataSource odsOrdersEdit y el


correspondiente SqlDataSource dsOrdersEdit son la adición del atributo TypeName="Na-
meDataSetDataTableAdapters .NameDataTableAdapter" y la sustitución de los nombres de
método en los comandos SQL.

290

También podría gustarte