Documentos de Académico
Documentos de Profesional
Documentos de Cultura
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:
Autor:
© Luis Durán
Editoriales:
en coedición con:
Í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
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
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
viii
VisualBasic2005_Primeras.qxp 12/08/2007 13:42 PÆgina ix
Índice general
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
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
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
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
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.
2
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 3
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
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.
4
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 5
5
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 6
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
7
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 8
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.
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
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
.Close()
End With
Catch exc As Exception
MsgBox(exc.Message)
Finally
cnnNwind.Close()
End Try
End Sub
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
11
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 12
12
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 13
LoadDataGridView()
Application.DoEvents()
MsgBox("Click OK to continue with update.")
LoadDataGridView()
Application.DoEvents()
MsgBox("Click OK to continue with update.")
LoadDataGridView()
13
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 14
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
14
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 15
15
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 16
Try
trnCustOrder = cnnNwind.BeginTransaction(IsolationLevel.RepeatableRead)
cmdTrans.Transaction = trnCustOrder
intRecordsAffected = cmdTrans.ExecuteNonQuery
trnCustOrder.Commit()
LoadDataGridView()
Application.DoEvents()
MsgBox("Click OK to continue with transaction.")
cmdTrans.Transaction = trnCustOrder
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
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.
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
18
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 19
Hay que cerrar el primer DataReader antes de cambiar la propiedad CommandText para
reutilizar el objeto OleDbCommand
19
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 20
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
20
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 21
21
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 22
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.
22
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 23
23
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 24
24
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 25
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
26
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 27
27
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 28
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
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
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
31
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 32
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.
32
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 33
33
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 34
7. Pulse <F5> para crear y ejecutar el proyecto. El formulario aparecerá tal como
muestra la siguiente figura.
34
VisualBasic2005_01.qxp 02/08/2007 16:11 PÆgina 35
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
Si trabajamos con SQLX, deberemos cambiar la cadena de conexión de cada proyecto de local-
host a .\SQLEXPRESS.
37
VisualBasic2005_02.qxp 02/08/2007 16:14 PÆgina 38
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.
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
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.
39
VisualBasic2005_02.qxp 02/08/2007 16:14 PÆgina 40
40
VisualBasic2005_02.qxp 02/08/2007 16:14 PÆgina 41
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
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.
41
VisualBasic2005_02.qxp 02/08/2007 16:14 PÆgina 42
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.
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 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
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.
44
VisualBasic2005_02.qxp 02/08/2007 16:14 PÆgina 45
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
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
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
48
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 49
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.
49
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 50
50
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 51
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
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.
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
52
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 53
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
54
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 55
55
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 56
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.
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
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.
57
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 58
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
58
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 59
cnnCusts.Open()
astrListItems(0) = Format(timHiRes.ElapsedTime, "0.000") + " - 1
Opened Customers connection"
cnnOrders.Open()
astrListItems(2) = Format(timHiRes.ElapsedTime, "0.000") + " - 3
Opened Orders connection"
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
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.
60
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 61
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
.Load(drCusts)
.AcceptChanges()
If .PrimaryKey.Length = 0 Then
Dim acolKeys(1) As DataColumn
acolKeys(0) = .Columns(0)
.PrimaryKey = acolKeys
End If
62
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 63
LoadDataGridViews()
Catch excDT As Exception
MsgBox(excDT.Message + excDT.StackTrace, , "DataTable Load
Exception")
Finally
cnnNwind.Close()
End Try
End Sub
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
End If
If blnShowMessage Then
MsgBox(strMsg, , "XML Files Saved")
End If
btnReadXML.Enabled = True
End Sub
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.
LoadDataGridViews()
Catch excXML As Exception
MsgBox(excXML.Message + excXML.StackTrace, , "DataTable ReadXml
Exception")
End Try
End Sub
64
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 65
65
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 66
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
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
...
m_RequiredDate = value
End Set
End Property
67
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 68
...
...
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
68
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 69
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.
69
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 70
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.
70
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 71
71
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 72
72
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 73
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
A continuación, pulse <F5> para construir y ejecutar el proyecto, que aparecerá tal co-
mo se muestra en la siguiente figura.
74
VisualBasic2005_02.qxp 02/08/2007 16:15 PÆgina 75
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
77
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 78
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.
78
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 79
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.
79
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 80
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.
80
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 81
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.
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
82
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 83
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.
83
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 84
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.
84
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 85
85
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 86
<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>
86
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 87
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.
87
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 88
88
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 89
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)
89
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 90
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
90
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 91
91
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 92
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.
92
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 93
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
93
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 94
<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
95
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 96
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
97
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 98
98
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 99
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.
99
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 100
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.
100
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 101
101
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 102
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
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.
103
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 104
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
104
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 105
<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.
105
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 106
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.
106
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 107
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.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
108
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 109
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.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
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.
110
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 111
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
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.
112
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 113
113
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 114
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
114
VisualBasic2005_03.qxp 02/08/2007 16:18 PÆgina 115
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
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.
117
VisualBasic2005_04.qxp 02/08/2007 16:20 PÆgina 118
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.
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
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.
119
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 120
120
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 121
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
122
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 123
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.
123
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 124
124
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 125
125
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 126
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
127
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 128
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.
128
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 129
129
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 130
.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.
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
131
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 132
132
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 133
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
134
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 135
135
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 136
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
136
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 137
137
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 138
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
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.
139
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 140
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
End With
End If
141
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 142
142
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 143
‘ 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
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.
144
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 145
.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
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
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.
146
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 147
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.
147
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 148
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
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
149
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 150
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.
150
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 151
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.
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
151
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 152
blnIsNewCustomer = False
Else
CustomersBindingSource.CancelEdit()
End If
...
End Sub
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
153
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 154
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.
154
VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 155
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.
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
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
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
158
VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 159
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
160
VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 161
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
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>.
162
VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 163
\LookupsDataSet.xml
If File.Exists(strFile) Then
File.Delete(strFile)
End If
163
VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 164
164
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 165
Capítulo 5
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
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
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.
167
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 168
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.
168
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 169
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
169
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 170
.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.
170
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 171
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.
171
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 172
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 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.)
173
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 174
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.
174
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 175
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)) ,
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.
175
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 176
176
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 177
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.
177
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 178
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.
178
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 179
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.
179
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 180
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
180
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 181
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
181
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 182
182
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 183
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
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.
184
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 185
185
VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 186
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
Order_DetailsTableAdapter.FillOrder_Details(NorthwindDataSet.Order_Details, _
strCustomerID)
187
VisualBasic2005_05.qxp 02/08/2007 18:27 PÆgina 188
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
188
VisualBasic2005_05.qxp 02/08/2007 18:27 PÆgina 189
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
190
VisualBasic2005_05.qxp 02/08/2007 18:27 PÆgina 191
191
VisualBasic2005_05.qxp 02/08/2007 18:27 PÆgina 192
192
VisualBasic2005_05.qxp 02/08/2007 18:27 PÆgina 193
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.
193
VisualBasic2005_05.qxp 02/08/2007 18:27 PÆgina 194
194
VisualBasic2005_05.qxp 02/08/2007 18:27 PÆgina 195
195
VisualBasic2005_05.qxp 02/08/2007 18:27 PÆgina 196
) 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
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
200
VisualBasic2005_06.qxp 02/08/2007 16:25 PÆgina 201
cnNwind.Open()
daOrders.Fill(dsNwind, "Orders")
daDetails.Fill(dsNwind, "OrderDetails")
cnNwind.Close()
trnUpdate = cnNwind.BeginTransaction
daOrders.UpdateCommand.Transaction = trnUpdate
daOrders.InsertCommand.Transaction = trnUpdate
201
VisualBasic2005_06.qxp 02/08/2007 16:25 PÆgina 202
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
202
VisualBasic2005_06.qxp 02/08/2007 16:25 PÆgina 203
203
VisualBasic2005_06.qxp 02/08/2007 16:25 PÆgina 204
204
VisualBasic2005_06.qxp 02/08/2007 16:25 PÆgina 205
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.
205
VisualBasic2005_06.qxp 02/08/2007 16:25 PÆgina 206
206
VisualBasic2005_06.qxp 02/08/2007 16:25 PÆgina 207
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
207
VisualBasic2005_06.qxp 02/08/2007 16:25 PÆgina 208
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.
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.
208
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 209
209
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 210
210
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 211
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
212
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 213
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
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.
214
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 215
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.
215
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 216
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.
216
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 217
217
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 218
CType(.Columns(2), DataGridViewImageColumn)
colImage.ImageLayout = DataGridViewImageCellLayout.Stretch
.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.ColumnsAllRows
End With
End If
End Sub
218
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 219
219
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 220
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.
220
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 221
221
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 222
222
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 223
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
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.
224
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 225
225
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 226
226
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 227
227
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 228
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.
228
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 229
<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
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.
230
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 231
<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
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
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.
233
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 234
234
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 235
235
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 236
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
237
VisualBasic2005_06.qxp 02/08/2007 16:26 PÆgina 238
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
238
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 239
Capítulo 7
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
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.
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
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
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).
242
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 243
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
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.
244
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 245
245
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 246
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
247
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 248
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
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
250
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 251
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
252
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 253
253
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 254
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").
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
255
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 256
256
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 257
257
VisualBasic2005_07.qxp 02/08/2007 16:28 PÆgina 258
Else
alParamValues.Add(Trim(txtBox.Text))
End If
End If
End If
Next
'Execute the Update method
dsOrders.Update()
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
259
VisualBasic2005_07.qxp 02/08/2007 16:29 PÆgina 260
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.
260
VisualBasic2005_07.qxp 02/08/2007 16:29 PÆgina 261
lblDate.Text = "Pending"
End If
'Enable deletion of orders not shipped
Dim btnDelete As Button =
CType(fvOrders.FindControl("btnDelete"), Button)
If Not btnDelete Is Nothing Then
btnDelete.Enabled = True
End If
'Temporary workaround for null date problem
Dim txtDate As TextBox =
CType(fvOrders.FindControl("ShippedDateTextBox"), TextBox)
If Not txtDate Is Nothing Then
txtDate.Text = "1/1/2099"
End If
Else
'Disable deletion of shipped orders
Dim btnDelete As Button =
CType(fvOrders.FindControl("btnDelete"), Button)
If Not btnDelete Is Nothing Then
'Temporary for null date workaround
Dim lblDate As Label =
CType(fvOrders.FindControl("ShippedDateLabel"), Label)
If lblDate.Text = "Pending" Then
btnDelete.Enabled = True
Else
btnDelete.Enabled = False
End If
End If
End If
If IsDBNull(fvOrders.DataItem("ShipRegion")) Then
Dim lblRegion As Label =
CType(fvOrders.FindControl("ShipRegionLabel"), Label)
If Not lblRegion Is Nothing Then
lblRegion.Text = "<Empty>"
End If
End If
If IsDBNull(fvOrders.DataItem("ShipPostalCode")) Then
'Applies to Ireland only
Dim lblCode As Label =
CType(fvOrders.FindControl("ShipPostalCodeLabel"), Label)
If Not lblCode Is Nothing Then
lblCode.Text = "<Empty>"
End If
End If
Catch exc As Exception
'Ignore for now
End Try
261
VisualBasic2005_07.qxp 02/08/2007 16:29 PÆgina 262
End Sub
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.
262
VisualBasic2005_07.qxp 02/08/2007 16:29 PÆgina 263
263
VisualBasic2005_07.qxp 02/08/2007 16:29 PÆgina 264
264
VisualBasic2005_07.qxp 02/08/2007 16:29 PÆgina 265
265
VisualBasic2005_07.qxp 02/08/2007 16:29 PÆgina 266
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.
266
VisualBasic2005_07.qxp 02/08/2007 16:29 PÆgina 267
267
VisualBasic2005_07.qxp 02/08/2007 16:29 PÆgina 268
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
269
VisualBasic2005_08.qxp 02/08/2007 16:30 PÆgina 271
Capítulo 8
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:
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.
271
VisualBasic2005_08.qxp 02/08/2007 16:30 PÆgina 272
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.
272
VisualBasic2005_08.qxp 02/08/2007 16:30 PÆgina 273
273
VisualBasic2005_08.qxp 02/08/2007 16:30 PÆgina 274
La siguiente figura muestra el valor Required para la propiedad Text bajo una entrada
vacía de OrderDate.
274
VisualBasic2005_08.qxp 02/08/2007 16:30 PÆgina 275
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
276
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 277
277
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 278
</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.
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
^((((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:
279
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 280
280
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 281
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.
281
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 282
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:
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>:
Finalmente, extraiga el texto del atributo value para determinar si existe algún valor
ShippedDate.
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
283
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 284
<script language="vbscript">
Function ValidateProductID(source, args)
284
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 285
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.
285
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 286
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.
286
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 287
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.
287
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 288
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
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.
289
VisualBasic2005_08.qxp 02/08/2007 16:31 PÆgina 290
290