Bases de datos Visual Basic 6

Anuncio
VisualBasic2005_05.qxp
02/08/2007
18:27
PÆgina 197
Añadir código para validar datos y gestionar la concurrencia
)
Procesar actualizaciones offline requiere sincronizar el contenido del formulario
con el registro actual de cliente llamando el procedimiento SynchronizeOfflineOrders.
197
VisualBasic2005_06.qxp
02/08/2007
16:25
PÆgina 199
Capítulo 6
La aplicación de técnicas
avanzadas de los DataSets
DataSets y DataGridViews vinculados son los elementos centrales en el acceso a datos de
ADO.NET 2.0 y las herramientas de Visual Studio 2005. Los dos capítulos anteriores trataban sobre los aspectos básicos en torno a los DataSets y formularios Windows vinculados. Este capítulo amplía las técnicas de programación de los elementos DataSet y
DataGridView con los siguientes puntos principales:
)
)
Permitir las transacciones ligeras de código en la actualización de las bases de
datos.
Añadir columnas a las DataTables y DataGridViews desde consultas SELECT con un
INNER JOIN.
)
Mostrar y manipular imágenes en las DataGridViews.
)
Generar DataSets a partir de esquemas XML existentes.
)
Editar documentos XML con DataGridViews.
)
Crear y trabajar con clases de objetos serializables.
)
Vincular DataGridViews a colecciones genéricas DataList.
Todos, excepto uno, de los proyectos de ejemplo de este capítulo utilizan las bases de
datos de ejemplo Northwind para proporcionar un número suficiente de registros y
variedad de tipos de datos para demostrar el rendimiento relativo de las técnicas de
acceso y edición de datos que se verán. En los ejemplos con tablas base sencillas, de
pocas filas y columnas, y documentos o esquemas fuente en un sencillo XML, no se tratarán los problemas de rendimiento y otros aspectos del diseño de código que se verán
en este capítulo.
Para los ejemplos SystemTransactions.sln y DataGridViewImages.sln debe tener instalado SQL
Server 2005 o SQL Server Express con las bases de datos de ejemplo Northwind y AdventureWorks. Los demás proyectos de ejemplo funcionan con SQL Server 2000, MSDE, SQL Server
2005 o SQLExpress y la base de datos Northwind.
199
VisualBasic2005_06.qxp
02/08/2007
16:25
PÆgina 200
Bases de datos con Visual Basic
6.1 Aplicar transacciones a las actualizaciones de DataSets
Casi todas las DBAs requieren en "sus" tablas de producción operaciones de actualización, entrada y eliminación que se realicen con procedimientos almacenados y relacionados en una transacción. La transacción garantiza que todas las actualizaciones de
cada tabla, en una operación batch, se realizarán con éxito (commit) o fallarán (roll back)
en grupo. Tal como vimos anteriormente en este libro, ADO.NET 1.0 introducía la propiedad IDbCommand.Transaction y la interfaz IdbTransaction para la actualización con
transacciones de múltiples tablas. Los objetos SqlTransaction y OracleTransaction son
genuinos de CLR, OleDbTransaction y OdbcTransaction son envoltorios gestionados de
los componentes de transacciones basados en OLE DB y ODBC COM.
El ejemplo SqlTransaction es relativamente sencillo porque utiliza un par de métodos
SqlCommand.ExecuteNonQuery que actualizaban las tablas dentro de una transacción
local. De todas formas, los DataSets de ADO.NET 1.x requieren mucho más código para
asignar un único objeto SqlTransaction a las propiedades UpdateCommand.Transaction,
InsertCommand.Transaction y DeleteCommand.Transaction de múltiples SqlDataAdapters.
Un procedimiento típico de ADO.NET 1.x para actualizar tablas base a partir de modificaciones simuladas realizadas por un usuario en tablas de datos sin conexión, incluye las siguientes acciones:
1. Crear un juego de datos no tipificado con un SqlDataAdapter por cada tabla de la
transacción.
2. Crear un CommandBuilder para definir la propiedad ...Command de cada DataAdapter desde la sentencia SelectCommand o desde el procedimiento almacenado.
3. Abrir una SqlConnection, poblar las tablas de datos con el método DataAdapter.Fill
y cerrar la conexión a la base de datos.
4. Modificar algunas filas de cada tabla de datos a modo de prueba.
5. Declarar e iniciar un objeto SqlTransaction.
6. Abrir la conexión a la base de datos y asignar la SqlTransaction a las tres propiedades de lenguaje de gestión de datos, en inglés Data Management Language (DML),
...Command.Transaction de cada DataAdapter.
7. Invocar el método Update en cada DataAdapter para que se ejecute el ...Command
apropiado para cada valor de la propiedad DataRowState de cada fila modificada
–los valores son: Added, Modified o Deleted.
8. Ejecutar la transacción si no se ha producido ningún error; de lo contrario, deshacer todos los pasos realizados y cerrrar la conexión a la base de datos.
El siguiente código, en el que se incluyen las operaciones que acabamos de mencionar,
muestra en negrita las instrucciones directamente relacionadas con el procesamiento de
la SqlTransaction:
Dim trnUpdate As SqlTransaction = Nothing
Dim cnNwind As New SqlConnection(My.Settings.NorthwindConnectionString)
Dim dsNwind As New DataSet("dsNwind")
Try
200
VisualBasic2005_06.qxp
02/08/2007
16:25
PÆgina 201
La aplicación de técnicas avanzadas de los DataSets
Dim daOrders As New SqlDataAdapter("SELECT * FROM Orders " + _
"WHERE OrderID > 11077;", cnNwind)
Dim cbOrders As SqlCommandBuilder = New SqlCommandBuilder(daOrders)
daOrders.UpdateCommand = cbOrders.GetUpdateCommand
daOrders.InsertCommand = cbOrders.GetInsertCommand
daOrders.DeleteCommand = cbOrders.GetDeleteCommand
Dim daDetails As New SqlDataAdapter("SELECT * FROM [Order Details] " + _
"WHERE OrderID > 11077;", cnNwind)
Dim cbDetails As New SqlCommandBuilder(daDetails)
daDetails.UpdateCommand = cbDetails.GetUpdateCommand
daDetails.InsertCommand = cbDetails.GetInsertCommand
daDetails.DeleteCommand = cbDetails.GetDeleteCommand
cnNwind.Open()
daOrders.Fill(dsNwind, "Orders")
daDetails.Fill(dsNwind, "OrderDetails")
cnNwind.Close()
Dim dtOrders As DataTable = dsNwind.Tables("Orders")
Dim intRow As Integer
For intRow = 0 To dtOrders.Rows.Count - 1
If blnReset Then
dtOrders.Rows(intRow).Item("ShippedDate") = DBNull.Value
Else
dtOrders.Rows(intRow).Item("ShippedDate") =
Today.ToShortDateString
End If
Next intRow
Dim dtDetails As DataTable = dsNwind.Tables("OrderDetails")
For intRow = 0 To dtDetails.Rows.Count - 1
If blnReset Then
dtDetails.Rows(intRow).Item("Quantity") = _
dtDetails.Rows(intRow).Item("Quantity") - 1
Else
dtDetails.Rows(intRow).Item("Quantity") = _
dtDetails.Rows(intRow).Item("Quantity") + 1
End If
Next intRow
If chkViolateConstraint.Checked Then
dtDetails.Rows(intRow - 1).Item("OrderID") = 100
End If
cnNwind.Open()
trnUpdate = cnNwind.BeginTransaction
daOrders.UpdateCommand.Transaction = trnUpdate
daOrders.InsertCommand.Transaction = trnUpdate
201
VisualBasic2005_06.qxp
02/08/2007
16:25
PÆgina 202
Bases de datos con Visual Basic
daOrders.DeleteCommand.Transaction = trnUpdate
daOrders.Update(dsNwind, "Orders")
daDetails.UpdateCommand.Transaction = trnUpdate
daDetails.InsertCommand.Transaction = trnUpdate
daDetails.DeleteCommand.Transaction = trnUpdate
daDetails.Update(dsNwind, "OrderDetails")
trnUpdate.Commit()
Catch exc As Exception
If trnUpdate IsNot Nothing Then
trnUpdate.Rollback()
End If
Finally
cnNwind.Close()
End Try
End If
Si no se define explícitamente el valor de la propiedad DataAdapter.TypeCommand con el método CommandBuilder.GetTypeCommand, tampoco se podrá incluir el comando en la transacción
con el valor de la propiedad SQLDataAdapter.TypeCommand.Transaction.
El proyecto SystemTransactions.sln contiene el código de ejemplo de este apartado y los
dos siguientes. El procedimiento DataAdapterTransactions de Transactions.vb, contiene el
ejemplo anterior. Para ejecutar el procedimiento, abra, construya y ejecute el proyecto
y, a continuación, pulse el botón Update con el cuadro de verificación Show Update in
Grid seleccionado. Entionces, el código actualiza los valores ShippedDate de la tabla
Orders, con los datos actuales del sistema, y suma uno al valor de Quantity en la tabla
Order Details, en todos los records con un OrderID mayor que 11077 (ver figura 6.1).
Pulse el botón Reset para asignar el valor Null a ShippedDate y restar uno a los valores
Quantity.
La implementación de IdbTransaction que los proveedores de datos originales de ADO
1.x han realizado, limitan la posibilidad de las transacciones locales a una sola base de
datos. Las transacciones distribuidas, efectuadas por el Distributed Transaction
Coordinator (MSDTC), toman como base el espacio de nombres System.EnterpriseServices
y la herencia de ServicedComponent.
6.1.1 Simplificar el listado con System.Transactions
.NET Framework 2.0 incluye el espacio de nombres System.Transactions con el que se
definen varias clases de clave que mejoran las posibilidades de transacción con
ADO.NET 2.0 y simplifican la programación. Las clases más usadas son TransactionScope, Transaction y CommittableTransaction. La principal ventaja que aportan las clases
System.Transactions a la gestión de transacciones es el listado automático de un gestor
local de fuentes (RM, Resource Manager), como SQL Server 2005, en una transacción gestionada, por defecto, por un gestor de transacción ligera –en inglés: Lightweight
Transaction Manager (LTM). El listado posterior de un RM remoto promueve, de forma
202
VisualBasic2005_06.qxp
02/08/2007
16:25
PÆgina 203
La aplicación de técnicas avanzadas de los DataSets
Figura 6.1: si no ha añadido datos en las tablas Orders y Order
Details de la base de datos Northwind en los capítulos anteriores,
deberá hacerlo ahora para poder actualizar con SqlDataAdapters.
automática, la transacción local y la convierte en una transacción distribuida con un
OleTx Transaction Manager (OTM).
El listado de un RM local no soporta las transacciones promovibles, como SQLServer
2000, que también promueve las transacciones ligeras. El LTM ofrece un alto rendimiento con un consumo mínimo de recursos; la promoción a OTM y DTC implica un
rendimiento y un consumo de recursos similares a los de las ServicedComponents.
6.1.2
Listar SqlDataAdapters en una transacción implícita
Para sacar partido al nuevo modelo de transacción de .NET 2.0 hay que añadir una referencia de proyecto al espacio de nombres System.Transactions y una sentencia
ImportsSystem.Transactions al archivo de clase. Se puede obtener una transacción implícita alistable creando un objeto TransactionScope y asignándolo a un bloque Using...EndUsing que incluya un bloque Try...EndTry. Los métodos transaccionables, como
SqlDataAdapter.Update o SqlTableAdapter.Update, que se ejecutan dentro del bloque
Using, automáticamente se alistan en la transacción. Si los métodos se desarrollan con
éxito, al ejecutar el método TransactionScope.Complete y deshacerse del objeto TransactionScope saliendo del bloque Using, se hace válida la transacción. Si un método arroja
una excepción, al salir del bloque Using sin ejecutar el método TransactionScope.Complete, se volverá atrás en la transacción.
El siguiente procedimiento remplaza las diez líneas de código del listado anterior
(empezando en cnNwind.Open()), que crea el objeto SqlTransaction y alista los objetos
DataAdapter.TypeCommand de la transacción:
203
VisualBasic2005_06.qxp
02/08/2007
16:25
PÆgina 204
Bases de datos con Visual Basic
'cnNwind.Open() 'Opening here disables enlistment (no transaction)
Dim tsExplicit As New TransactionScope
Using tsExplicit
Try
'cnNwind.Open() 'Opening here uses one connection for transaction
daOrders.Update(dsNwind, "Orders")
daDetails.Update(dsNwind, "OrderDetails")
tsExplicit.Complete()
Catch exc As Exception
MsgBox(exc.Message)
Finally
cnNwind.Close()
End Try
End Using
Si utiliza los DataAdapters para abrir (y cerrar) sus conexiones automáticamente, el
anterior bloque Using abrirá dos conexiones en el SQL Server 2005 (normalmente SPID
51 y SPID 53) y promoverá la transacción, causando así un leve descenso en el rendimiento. Si se abre explícitamente una sola conexión (cnNwind), antes de crear la transacción implícita con el constructor TransactionScope, las transacciones quedarán desactivadas para los métodos Update. Pero si la conexión se abre explícitamente después de
crear la transacción, las dos operaciones Update se ejecutarán en la misma conexión
(normalmente SPID 51), maximizando así la velocidad de ejecución.
Nota: Para la ejecución del ejemplo anterior con el proyecto de ejemplo SystemTransactions.sln,
defina blnSysTran=True en el procedimiento DataAdapterTransactions y pulse el botón Update
o Reset. Para verificar que las operaciones de Update se están efectuando, seleccione el cuadro de
verificación Violate constraint (Rollback), pulse Update, y compruebe que una sola transgresión
de restricción de clave foránea en la tabla Order Details vuelve atrás todos los cambios realizados en las tablas Orders y Order Details.
6.1.3 Autolistar SqlTableAdapters en una transacción implícita
El código siguiente realiza una actualización transactual de dos SqltableAdapters de
ADO.NET 2.0 autolistando sus métodos Update en un LTM:
Dim tsImplicit As New TransactionScope
Using tsImplicit
Try
'Adapter opens connections automatically
Me.Order_DetailsTableAdapter.Update(Me.NorthwindDataSet.Order_Details)
Me.OrdersTableAdapter.Update(Me.NorthwindDataSet.Orders)
tsImplicit.Complete()
Catch exc As Exception
'Error handling
Finally
'Adapter closes connections automatically
End Try
End Using
204
VisualBasic2005_06.qxp
02/08/2007
16:25
PÆgina 205
La aplicación de técnicas avanzadas de los DataSets
Tal como sucede con los SqlDataAdapters de ADO.NET 1.x, los SqlTableAdapters de
ADO.NET 2.0 también abren dos conexiones automáticamente y promueven así una
transacción implícita. El código siguiente abre una sola conexión y la asigna a los dos
SqlTableAdapters para impedir que promuevan la transacción:
Dim tsImplicit As New TransactionScope
Using tsImplicit
Try
'Open a single connection and assign it to both SqlTableAdapters
Dim cnNwind As New
SqlConnection(My.Settings.NorthwindConnectionString)
cnNwind.Open()
Me.Order_DetailsTableAdapter.Connection = cnNwind
Me.OrdersTableAdapter.Connection = cnNwind
Me.Order_DetailsTableAdapter.Update(Me.NorthwindDataSet.Order_Details)
Me.OrdersTableAdapter.Update(Me.NorthwindDataSet.Orders)
tsImplicit.Complete()
Catch exc As Exception
'Error handling
Finally
cnNwind.Close()
End Try
End Using
Para abrir una sola conexión para transacciones implícitas, defina blnOpenConnection=True en
el manejador del evento bindingNavigatorSaveData, modifique un record de la tabla Orders y
otro, como mínimo en su tabla Order Details, y pulse el botón Save o el botón Save Data de la
tabla de herramientas.
6.1.4 SQL Profiler para rastrear transacciones
La herramienta Profiler de SQL Server 2005 ha sido actualizada con nuevas características tales como las transacciones promovibles. Para rastrear los eventos BEGIN TRAN,
PROMOTE TRAN, COMMIT TRAN y ROLLBACK TRAN, deberá pasar esos eventos
desde la categoría Transactions a la plantilla por defecto T-SQL, u otra plantilla similar
de rastreo personalizada. La siguiente figura muestra el rastreo realizado por SQL Profiler de una actualización transactuada con SqlTableAdapter, con dos conexiones autogeneradas que provocan que la transacción se promueva. La figura siguiente ilustra la
misma transacción pero con una sola conexión asignada explícitamente en la propiedad Connection de los dos SqlTableAdapters.
La edición SQL Server Express no incluye ni soporta el uso de SQL Profiler. De todos
modos, se puede usar el Component Services Manager para contabilizar instancias de las
transacciones distribuidas que resultan de promover transacciones implícitas o de ejecutar transacciones explícitas y que son el tema de los apartados siguientes. La segunda figura de la página siguiente muestra el cuadro de diálogo Servicios de cmponentes
con las estadísticas de 94 transacciones promovidas, generadas por el mismo proyecto
205
VisualBasic2005_06.qxp
02/08/2007
16:25
PÆgina 206
Bases de datos con Visual Basic
de ejemplo. Nótese que el tiempo de respuesta medio de las transacciones distribuidas
es de unos 4 segundos. Los ítems de las transacciones sólo aparecen en la ventana Lista
de transacciones cuando están activados (ver figura de la página siguiente).
6.1.5 Listar manualmente SqlTableAdapters en una transacción explícita
Si prefiere el modelo de transacción "tradicional" con un alistamiento explícito de los
objetos transactuados y control granular de las invocaciones de los métodos Commit o
Rollback, puede utilizar el objeto CommittableTransaction, tal como se muestra en el código siguiente:
Dim tsExplicit As New CommittableTransaction
Try
Me.Order_DetailsTableAdapter.Connection.Open()
Me.OrdersTableAdapter.Connection.Open()
Me.Order_DetailsTableAdapter.Connection.EnlistTransaction(tsExplicit)
Me.OrdersTableAdapter.Connection.EnlistTransaction(tsExplicit)
Me.Order_DetailsTableAdapter.Update(Me.NorthwindDataSet.Order_Details)
206
VisualBasic2005_06.qxp
02/08/2007
16:25
PÆgina 207
La aplicación de técnicas avanzadas de los DataSets
Me.OrdersTableAdapter.Update(Me.NorthwindDataSet.Orders)
tsExplicit.Commit()
Catch exc As Exception
tsExplicit.Rollback()
Finally
Me.OrdersTableAdapter.Connection.Close()
Me.Order_DetailsTableAdapter.Connection.Close()
End Try
Envoltorios explícitos de transacción para las actualizaciones con SqlTableAdapter son,
por defecto, las transacciones distribuidas. Las promociones se producen cuando el
código lista un segundo objeto SqlTableAdapter.Connection en la transacción.
6.1.6 Definir las opciones TransactionScope y Transaction
El constructor TransactionScope tiene siete sobrecargas, pero las dos siguientes son las
más útiles en las transacciones de base de datos:
Public Sub New(ByVal scopeOption As System.Transactions.TransactionScopeOption,
ByVal scopeTimeout As System.TimeSpan)
Public Sub New(ByVal scopeOption As System.Transactions.TransactionScopeOption,
ByVal transactionOptions As System.Transactions.TransactionOptions)
207
VisualBasic2005_06.qxp
02/08/2007
16:25
PÆgina 208
Bases de datos con Visual Basic
La enumeración TransactionScopeOption tiene los tres miembros siguientes:
TransactionScopeOption.Requires
TransactionScopeOption.RequiresNew
TransactionScopeOption.Suppress
El valor por defecto es Requires (una transacción). Especifique Suppress si no quiere que
TransactionScope utilice la transacción ambiente. A continuación vemos los dos miembros TransactionScopeOption:
TransactionOption.IsolationLevel
TransactionOption.Timeout
IsolationLevel es por defecto Serializable, pero puede ser cualquiera de los siete miembros
que aparecieron en el primer capítulo de este libro. Sólo SQL Server 2005 soporta
Snapshot en Isolation. El valor por defecto de Timeout es 1 minuto.
6.2 Añadir relaciones a los SelectCommand de la tabla
de datos
Los DataSets actualizan tablas individuales, pero eso no significa que no se puedan añadir relaciones al SelectCommand de una tabla. Las relaciones permiten mejorar las ediciones de los usuarios añadiendo columnas de sólo lectura desde una relación "de
muchos a uno" con una tabla relacionada. Como ejemplo, si se añaden las columnas
ProductName, QuantityPerUnit y UnitPrice de la tabla Products Northwind a un
DataGridView de items de Order Details, se mejora la legibilidad y se minimizan los errores en la entrada de datos. La columna UnitPrice se puede utilizar para dar valores por
defecto de los registros nuevos y actualizar la columna UnitPrice de la tabla Order
Details cuando se realicven cambios en el ProductID.
Añadir columnas desde relaciones muchos a uno (many-to-one) no es el sustituto ideal a las
columnas de cuadro combinado pobladas por listas lookup. La técnica descrita anteriormente, es
normalmente un método más efectivo siempre que se trabaje con formularios de entrada de datos
donde el número de ítems del cuadro combinado sea inferior a 100.
El proyecto de ejemplo de esta sección, SelectCommandJoins.sln, demuestra cómo añadir
relaciones a los SelectCommand y sacar partido de la relación many-to-one para simplificar la actualización de la tabla base Order Details. El proyecto empieza con una fuente
de la base de datos Northwind que incluye las tablas Orders, Order Details, y Products.
Los componentes de datos incluyen Orders autogenerados, Order_Details DataGridViews, TableAdapters y BindingSources. La tabla Products porporciona el ProductName y el
UnitPrice necesarios para editar y crear nuevos records de Order Details.
Añada ProductsTableAdapter y ProductsBindingSource a la bandeja arrastrando el icono
de la tabla Products desde la ventana Origenes de datos hasta el formulario Join.vb y después borre el ProductsDataGridView que se ha añadido al formulario.
208
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 209
La aplicación de técnicas avanzadas de los DataSets
6.2.1 Añadir una relación a SelectCommand
A continuación vemos los pasos para añadir un INNER JOIN entre las tablas Order
Details y Products de la operación Fill:
1. En la ventana Diseñador de DataSet, pulse con el botón derecho la cabecera del
TableAdapter de Order Details y seleccione Propiedades.
2. En la ventana Propiedades, expanda el nodo SelectCommand, pulse el nodo
CommandText, y pulse el botón del constructor para abrir el cuadro de diálogo
Generador de consultas.
3. Pulse con el botón derecho del ratón el panel de las tablas, seleccione Agregar tabla,
y añada la tabla Products.
4. Seleccione las columnas ProductName, QuantityPerUnit y UnitPrice de la tabla
Products.
5. Cambie dbo.Products.UnitPriceASExpr1 por dbo.Products.UnitPriceASListPrice.
6. Pulse el botón Ejecutar consulta para ver los resultados en la parrilla.
7. Pulse el botón Aceptar para cerrar el cuadro de diálogo Generador de consultas y
pulse el botón No cuando le pregunten si quiere regenerar los comandos de actualización basándose en el nuevo comando de selección.
8. Pulse con el botón derecho la cabecera de Order Details y seleccione Ajustar automáticamente para mostrar las columnas ProductName, ListPrice y QuantityPerUnit (ver
la figura de la página siguiente).
209
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 210
Bases de datos con Visual Basic
9. Abra la ventana Propiedades y verifique que la sentencia SQL CommandText de los
nodos DeleteCommand, InsertCommand y UpdateCommand incluye sólo columnas de
la tabla Order Details.
A continuación vemos el valor de la propiedad CommandText de SelectCommand:
SELECT dbo.[Order Details].OrderID, dbo.[Order Details].ProductID,
dbo.[Order Details].UnitPrice, dbo.[Order Details].Quantity,
dbo.[Order Details].Discount, dbo.Products.ProductName,
dbo.Products.UnitPrice AS ListPrice, dbo.Products.QuantityPerUnit
FROM dbo.[Order Details] INNER JOIN
dbo.Products ON dbo.[Order Details].ProductID = dbo.Products.ProductID
6.2.2 Añadir las columnas adjuntadas con relaciones al DataGridView
Las columnas de la tabla Products se han de añadir manualmente pulsando con el botón
derecho el Order_DetailsDataGridView y seleccionando Editar columnas para abrir el cuadro de diálogo del mismo nombre. Pulse Añadir columnas y añada la columna ProductName detrás de ProductID. Añada las columnas QuantityPerUnit y List Price. Defina el
valor True para la propiedad ReadOnly de las tres columnas y cambie el orden de las
columnas por OrderID, Quantity, ProductID, ProductName, QuantityPerUnit, ListPrice,
UnitPrice y Discount.
6.2.3 Proporcionar los valores por defecto y columnas de sólo lectura
Para navegar por la tabla de datos Products y proporcionar valores ProductName,
QuantityPerUnit y UnitPrice y comprobar, opcionalmente el valor del campo Disconti-
210
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 211
La aplicación de técnicas avanzadas de los DataSets
nued field, se necesita la ProductsBindingSource que añadió anteriormente en este capítulo. Defina el valor de la propiedad AllowNew de ProductsBindingSource como False y
verifique DataSource que es NorthwindDataSet y DataMember es Products.
El siguiente manejador de eventos da intencionadamente valores por defecto que no
son válidos y muestra un icono de error al añadir un nuevo ítem en Order Details.
Private Sub Order_DetailsDataGridView_DefaultValuesNeeded(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DataGridViewRowEventArgs) _
Handles Order_DetailsDataGridView.DefaultValuesNeeded
'Set invalid default values
With e.Row
'Illegal Quantity
.Cells(1).Value = 0
'Illegal ProductID
.Cells(2).Value = 0
'ProductName
.Cells(3).Value = "ProductID not selected"
'Quantity per Unit
.Cells(4).Value = "Not applicable"
'ListPrice
.Cells(5).Value = 0D
'UnitPrice
.Cells(6).Value = 0D
'Discount
.Cells(7).Value = 0D
.ErrorText = "Default values: You must enter ProductID and Quantity."
End With
End Sub
El manejador del evento CellValueChanged muestra un icono de error para los valores
no válidos de ProductID, Quantity, o ambos, y los productos con discontinuidades:
Private Sub Order_DetailsDataGridView_CellValueChanged(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _
Handles Order_DetailsDataGridView.CellValueChanged
If blnIsLoaded AndAlso e.ColumnIndex = 2 Then
'User edited ProductID value
With Order_DetailsDataGridView
'Clear error icon
.Rows(e.RowIndex).ErrorText = ""
'Get the new ProductID value
Dim intProductID As Integer = _
CType(.Rows(e.RowIndex).Cells(2).Value, Integer)
Dim srtQuantity As Short = CType(.Rows(e.RowIndex).Cells(1).Value,Short)
If intProductID = 0 OrElse intProductID > ProductsBindingSource.Count
Then
'Bad ProductID value
211
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 212
Bases de datos con Visual Basic
.Rows(e.RowIndex).ErrorText = "ProductID value must be between " + _
"1 and " + ProductsBindingSource.Count.ToString
Return
End If
'Get the required data from the ProductsBindingSource
Dim drvItem As DataRowView
drvItem = CType(ProductsBindingSource(intProductID - 1), DataRowView)
If CBool(drvItem.Item(9)) Then
'Discontinued products (5, 9, 17, 24, 28, 29, 42, 53)
.Rows(e.RowIndex).ErrorText = "ProductID " +
intProductID.ToString + _
" (" + drvItem.Item(1).ToString + ") is discontinued."
Else
'ProductName
.Rows(e.RowIndex).Cells(3).Value = drvItem.Item(1)
'Quantity per Unit
.Rows(e.RowIndex).Cells(4).Value = drvItem.Item(4)
'ListPrice
.Rows(e.RowIndex).Cells(5).Value = drvItem.Item(5)
'UnitPrice
.Rows(e.RowIndex).Cells(6).Value = drvItem.Item(5)
'Discount
.Rows(e.RowIndex).Cells(7).Value = 0D
If srtQuantity = 0 Then
.Rows(e.RowIndex).ErrorText = "Quantity of 0 is not permitted."
End If
End If
End With
End If
End Sub
La siguiente figura de la página siguiente muestra el formulario Joins.vb del proyecto
de ejemplo SelectCommandJoin.sln en el proceso de añadir un nuevo ítem de linea a
Order Details. En el apartado siguiente veremos la finalidad de los controles situados
sobre el Orders DataGridView.
6.3 Mejorar el rendimiento reduciendo el tamaño de
los juegos de datos
Cargar DataSets y poblar DataGridViews con registros innecesarios puede hacer bajar
considerablemente el rendimiento de servidores y clientes, especialmente al reproducir los DataSets perpetuados durante largo tiempo por los usuarios desconectados. Los
apartados siguientes describen cómo reducir la carga del servidor y el consumo de
recursos locales, y cómo mejorar la edición de datos limitando el número de filas
devueltas por las operaciones DataTableAdapter.Fill. Las consultas convencionales TOP
n basadas en tipos descendientes de los valores de las columnas int identity y datetime,
son útiles para la mayor parte de clientes, tanto conectados como desconectados. Las
212
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 213
La aplicación de técnicas avanzadas de los DataSets
técnicas de paginación, además, minimizan el consumo de recursos y dan acceso a los
usuarios conectados a los datos más antiguos.
6.3.1 Limitar el número de filas devueltas por las consultas TOP n
El método más obvio para limitar el número de records devueltos por las operaciones
Fill es añadir un modificador TOP n o TOP n PERCENT y una cláusula ORDER BY
apropiada a la consulta SQL del TableAdapter para el SelectCommand. Por ejemplo, la
siguiente consulta SQL carga las 100 últimas filas de la tabla Orders para poblar el
DataGridView del proyecto de ejemplo SelectCommandJoins.sln:
SELECT TOP 100OrderID, CustomerID, EmployeeID, OrderDate, RequiredDate,
ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity, ShipRegion,
ShipPostalCode, ShipCountry
FROM dbo.Orders ORDER BY OrderID DESC
Cuando se aplican consultas TOP n a una tabla padre, se debería hacer lo mismo con
las operaciones TableAdapter.Fill en las tablas hijo. La consulta SelectCommand de Order
Details, que veíamos en el apartado anterior, carga todas las filas extendidas de Order
Details en el Order_DetailsDataTable, para lo cual se consumen muchos más recursos de
lo necesario. Para devolver sólo las filas hijo que dependen de las filas de Orders, hay
que añadir un predicado IN con un subselect, también llamado subquery, tal como se
destaca en negrita en la consulta siguiente:
SELECT dbo.[Order Details].OrderID, dbo.[Order Details].ProductID,
dbo.[Order Details].UnitPrice, dbo.[Order Details].Quantity,
dbo.[Order Details].Discount, dbo.Products.ProductName,
213
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 214
Bases de datos con Visual Basic
dbo.Products.QuantityPerUnit, dbo.Products.UnitPrice AS ListPrice
FROM dbo.[Order Details] INNER JOIN
dbo.Products ON dbo.[Order Details].ProductID = dbo.Products.ProductID
WHERE dbo.[Order Details].OrderID IN (SELECT TOP 100 dbo.Orders.OrderID
FROM dbo.Orders ORDER BY dbo.Orders.OrderID DESC)
SQL Server 2005 y SQL Express permiten sustituir variables bigint o float por consultas literales TOP n [PERCENT]. El ejemplo de este capítulo utiliza valores literales para asegurar la
compatibilidad con SQL Server o MSDE 2000.
6.3.2 Añadir clases Partial para TableAdapters
Las clases TableAdapter no están anidadas en los DataSets de ADO.NET 2.0. En su lugar,
los TableAdapters tienen su propio espacio de nombres para impedir que haya nombres
de clase autogenerados por duplicado. Nombres de espacios de nombres autogenerados son, por ejemplo, DataSetNameTableAdapters, como NorthwindDataSetTableAdapters,
que contiene PartialPublicClassOrdersTableAdapter, PublicClassOrder_DetailsTableAdapter
y PublicClassProductsTableAdapter. Sustituir sentencias dinámicas SQL SELECT por el
SelectCommand que se añadió en el diseñador de consultas, implica sobrecargar el
método Fill y dar el valor variable de la propiedad CommandText como segundo argumento. Si añade la signatura cargada a las clases parciales del DataSet perderá los datos
añadidos cuando se regenere el Dataset. Por lo tanto, debe añadir un archivo de clase
parcial al proyecto –en este ejemplo TableAdapters.vb– que contenga código similar al
siguiente:
Namespace NorthwindDataSetTableAdapters
'************************************
'Partial classes to set SelectCommand
'************************************
Partial Class OrdersTableAdapter
Public Overloads Function Fill(ByVal DataTable As
NorthwindDataSet.OrdersDataTable, ByVal strSelect As String) As Integer
Me.Adapter.SelectCommand = Me.CommandCollection(0)
'Replace the CommandText
Me.Adapter.SelectCommand.CommandText = strSelect
If (Me.ClearBeforeFill = True) Then
DataTable.Clear()
End If
Dim returnValue As Integer = Me.Adapter.Fill(DataTable)
Return returnValue
End Function
End Class
Partial Class Order_DetailsTableAdapter
Public Overloads Function Fill(ByVal DataTable As
NorthwindDataSet.Order_DetailsDataTable,
214
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 215
La aplicación de técnicas avanzadas de los DataSets
ByVal strSelect As String) As Integer
Me.Adapter.SelectCommand = Me.CommandCollection(0)
'Replace the CommandText
Me.Adapter.SelectCommand.CommandText = strSelect
If (Me.ClearBeforeFill = True) Then
DataTable.Clear()
End If
Dim returnValue As Integer = Me.Adapter.Fill(DataTable)
Return returnValue
End Function
End Class
End Namespace
Seleccionando la casilla de verificación Limit Order Details Rows del proyecto y pulsando el botón Reload Data se añade el predicado subselect a Order_DetailsDataTable.SelectCommand. Probablemente no notará una diferencia notable en el tiempo de carga de
los dos tipos de consulta, ya que el predicado IN aumenta el tiempo de ejecución de la
consulta. De todos modos, el predicado IN disminuye el tamaño del juego de datos perpetuado, bajando de los 824 KBytes de todas las filas de Orders a sólo 182 Kbytes para
100 filas.
Pulsando el botón Save Data del Navegador de datos, los DataSet se guardan en un archivo AllDetails.xml si la casilla de verificación está deseleccionada, o en Subselect.xml en
caso contrario.
6.4 Trabajar con imágenes en DataGridViews
Los DataGridViews requiren una columna DataGridViewImageColumn para mostrar imágenes devueltas por las tablas que contienen gráficos almacenados como datos binarios, como las columnas image o varbinary del SQL Server. Las DataGridViewImageColumns contienen una DataGridViewImageCell en cada fila. Por defecto, las celdas sin
imágenes (valores nulos) muestran el gráfico de Internet Explorer con un vínculo HTML
a un archivo de imagen "missed". Las DataGridViewImageColumns comparten la mayoría
de propiedades y métodos de otros tipos de datos, pero incorporan dos propiedades,
Image e ImageLayout específicas de los gráficos. La propiedad Image permite especificar
una imagen por defecto del archivo MyResources.resx o cualquier otro archivo de recursos. La propiedad ImageLayout permite seleccionar un miembro de la enumeración
DataGridViewImageCellLayout: NotSet, Normal, Stretch o Zoom. Estos miembros corresponden aproximadamente a la enumeración SizeMode del PictureBox. Como era de
esperar, Normal es el valor por defecto que centra la imagen con su resolución original.
6.4.1 Añadir columnas Image a los DataGridViews
Cuando se crea una fuente de datos de una tabla con una columna image o varbinary, la
ventana de Orígenes de datos muestra el nodo de la nueva columna desactivado. Si arrastra el nodo de la tabla hasta el formulario para autogenerar un DataGridView, DataSet o
cualquier otro componente de datos, el DataGridView no muestra ninguna
DataGridViewImageColumn para el mapa de bits.
215
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 216
Bases de datos con Visual Basic
Para añadir la columna image que falta, pulse con el botón derecho el DataGridView y
seleccione la opción Editar columnas para abrir el cuadro de diálogo del mismo nombre.
Pulse el botón Agregar para abrir el cuadro de diálogo y, con el botón de opción
Columna de enlace de datos seleccionado, seleccione la columna y pulse Agregar (ver figura siguiente). A continuación, especifique en Width un valor apropiado para el diseño
del DataGridView. Otra alternativa es seleccionar Rows como valor de la propiedad
AutoSizeCriteria. Defina inicialmente AllCellsExceptHeaders como valor de la propiedad
AutoSizeRowsMode del DataGridView. Después de un test inicial, puede darle a la propiedad RowTemplate.Height un valor que mantenga el ratio de imagen con el valor Width
de la columna.
La tabla ProductPhoto de la base de datos AdventureWorks de SQLServer 2005 proporciona la fuente de datos para el proyecto ejemplo de este apartado, DataGridViewImagesAW.sln. La tabla ProductPhoto tiene las columnas varbinary, ThumbNailPhoto y LargePhoto con 101 mapas de bits GIF; el tamaño de los mapas de bits LargePhoto para el
DataGridView es de 240 por 149 píxeles. La siguiente figura muestra tres columnas de
las dos primeras filas de la tabla en NormalImageLayout.
6.4.2 Manipular imágenes en DataGridView
El código añadido a la clase ProductPhoto permite comprobar el efecto de los cambios
ImageLayout en el aspecto las imágenes: guarde el contenido de un DataGridViewImageCell seleccionado en el correspondiente archivo LargePhotoFileName(.gif), muestre una
imagen en el cuadro de imagen (PictureBox) y sustituya la imagen seleccionada por una
copia del archivo que ha guardado.
6.4.3 Cambiar ImageLayout
Por defecto, el ancho de la columna LargePhoto y la altura de las filas se ajustan a la
dimensión de las imagenes. Para comprobar los tres modos de imagen, arrastre el
borde derecho de las cabeceras de columna hasta el borde derecho del DataGridView, y
216
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 217
La aplicación de técnicas avanzadas de los DataSets
seleccione a continuación el botón Stretch para distorsionar la imagen cambiando el
ratio de proporción. Seleccionando Zoom, la propiedad AutoSizeRowsMode toma el
valor DataGridViewAutoSizeRowsMode.None, el cual permite manipular la altura de fila
y la anchura de la columna y ver los diferentes cambios de tamaño que se pueden aplicar a la imagen manteniendo siempre la proporción de aspecto habitual del mapa de
bits. Los siguientes manejadores responden al evento CheckChange de los botones de
opción:
Private Sub rbNormal_CheckedChanged(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles rbNormal.CheckedChanged
'Normal layout
If blnLoaded And rbNormal.Checked Then
With ProductPhotoDataGridView
Dim colImage As DataGridViewImageColumn = _
CType(.Columns(2), DataGridViewImageColumn)
colImage.ImageLayout = DataGridViewImageCellLayout.Normal
.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.ColumnsAllRows
End With
End If
End Sub
Private Sub rbStretch_CheckedChanged(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles rbStretch.CheckedChanged
'Stretch layout
If blnLoaded And rbStretch.Checked Then
With ProductPhotoDataGridView
Dim colImage As DataGridViewImageColumn = _
217
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 218
Bases de datos con Visual Basic
CType(.Columns(2), DataGridViewImageColumn)
colImage.ImageLayout = DataGridViewImageCellLayout.Stretch
.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.ColumnsAllRows
End With
End If
End Sub
Private Sub rbZoom_CheckedChanged(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles rbZoom.CheckedChanged
'Zoom layout
If blnLoaded And rbZoom.Checked Then
With ProductPhotoDataGridView
Dim colImage As DataGridViewImageColumn = _
CType(.Columns(2), DataGridViewImageColumn)
colImage.ImageLayout = DataGridViewImageCellLayout.Zoom
.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None
End With
End If
End Sub
6.4.4 Guardar una imagen seleccionada, mostrarla en un PictureBox
y remplazarla
Manipular datos de imágenes en DataGridViews no es un proceso intuitivo. La propiedad Value de un objeto DataGridViewImageCell se basa en el tipo de datos Byte(), no en
el tipo Image que cabría esperar. Hay que incrustar Value en Byte y después crear una
instancia FileStream para guardar el array Byte en el correspondiente archivo
LargePhotoFileName.gif. Crear una instancia MemoryStream para asignar la propiedad
Image de PictureBox del formulario frmPictureBox es más eficaz que cargar el PictureBox
desde el archivo guardado. Sustituir la imagen original por una copia del archivo se
hace mediante el método File.ReadAllBytes para simplificar la lectura de un archivo de
tamaño desconocido. Estas operaciones vienen resaltadas en negrita en el procedimiento siguiente (que es llamado por el manejador de evento bindingNavigatorSaveItem_Clickevent):
Private Sub SaveGifFile()
'Save the selected file
Dim strFile As String = Nothing
Try
With ProductPhotoDataGridView
If .CurrentCell.ColumnIndex = 2 Then
If Not frmPictureBox Is Nothing Then
frmPictureBox.Close()
End If
Dim strType As String = .CurrentCell.ValueType.ToString
'Create a Byte array from the value
Dim bytImage() As Byte = CType(.CurrentCell.Value, Byte())
218
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 219
La aplicación de técnicas avanzadas de los DataSets
'Specify the image file name
Dim intRow As Integer = .CurrentCell.RowIndex
strFile = .Rows(intRow).Cells(1).Value.ToString
'Save the image as a GIF file
Dim fsImage As New FileStream("..\" + strFile, FileMode.Create)
fsImage.Write(bytImage, 0, bytImage.Length)
fsImage.Close()
'Create a MemoryStream and assign it as the image of a PictureBox
Dim msImage As New MemoryStream(bytImage)
frmPictureBox.pbBitmap.Image = Image.FromStream(msImage)
If frmPictureBox.ShowDialog = Windows.Forms.DialogResult.Yes Then
'Replace the CurrentCell's image from the saved version,
'if possible
If File.Exists(Application.StartupPath + "\" + strFile) Then
'The easy was to obtain a Byte array
Dim bytReplace() As Byte = File.ReadAllBytes(Application.StartupPath + "\" + strFile)
.CurrentCell.Value = bytReplace
If AdventureWorksDataSet.HasChanges Then
AdventureWorksDataSet.AcceptChanges()
Dim strMsg As String = "File '" + strFile + _
" has replaced the image in row " +
intRow.ToString + _
" cell 2 (" + Format(bytReplace.Length,
"#,##0") + " bytes). " + _
vbCrLf + vbCrLf + "AcceptChanges has been
applied to the DataSet."
MsgBox(strMsg, MsgBoxStyle.Information, "Image
Replaced from File")
Else
Dim strMsg As String = "Unable to replace image
with file '" + _
strFile + "'. DataSet does not have changes."
MsgBox(strMsg, MsgBoxStyle.Exclamation, "Image
Not Replaced")
End If
End If
End If
Else
MsgBox("Please select the image to save.",
MsgBoxStyle.Exclamation, _
"No Image Selected")
End If
End With
Catch exc As Exception
219
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 220
Bases de datos con Visual Basic
With ProductPhotoDataGridView
If strFile = Nothing Then
Dim intRow As Integer = .CurrentCell.RowIndex
strFile = .Rows(intRow).Cells(1).Value.ToString
End If
End With
Dim strExc As String = "File '" + strFile + "' threw the following "
+ _
"exception: " + exc.Message
MsgBox(strExc, MsgBoxStyle.Exclamation, "Exception with Image")
End Try
End Sub
El valor de transparencia RGB no corresponde al fondo blanco, por lo que la imagen seleccionada muestra áreas sombreadas como transparentes.
6.4.5 Evitar crear imágenes desde los campos de objeto OLE en Access
La base de datos Northwind de SQL Server 2000 contiene las tablas Categories y Employees
que se importaron de una versión anterior de Access. La columna Picture de la tabla
Categories y la columna Photo de la tabla Employees tienen tipos de datos image, pero los
bitmaps de formato BMP tienen un wrapper de objetos OLE. Las imágenes aparecen en
DataGridView, pero el wrapper impide que se puedan mostrar en un PictureBox ni guardar el archivo en formato BMP.
6.5 Editar documentos XML con DataSets yDataGridViews
La emergencia de los documentos XML como el nuevo formato de intercambio de
documentos ha creado un requerimiento para las aplicaciones cliente que permiten a
los usuarios revisar, editar y crear Infosets XML. Los documentos de negocios que utilizan Infosets para representar tablas de datos con una jerarquía de una o más relaciones uno-a-muchos (one-to-many), son habituales en la gestión de relación con el cliente
(en inglés: customer relationship management, CRM), gestión de cadena de suministro
(supply chain management, SCM), y otras aplicaciones de negocios como BizTalk Server
2004. Estas aplicaciones intentan minimizar la intervención humana en sus procesos
automatizados de workflow, pero el procesamiento manual de documentos es inevitable en la mayor parte de las actividades de negocios.
Microsoft Word, Excel e InfoPath 2003, todos pueden editar documentos XML, pero los
documentos jerárquicos con múltiples relaciones uno-a-muchos son difíciles de editar
en Word o Excel. Access 2003 permite importar esquemas XML para crear tablas con
tipos de datos asignados, establecer claves y relaciones, adjuntar y editar datos y después exportar las tablas, o una consulta a un archivo XML. De todos modos, un documento XML jerárquico exportado no guarda ninguna relación con la estructura original del documento fuente. Transformar el archivo XML para regenerar la estructura
del documento original sería preocuparse más de lo necesario. InfoPath 2003 maneja la
edición de documentos jerárquicos y mantiene la estructura del documento, pero sus
220
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 221
La aplicación de técnicas avanzadas de los DataSets
formularios basados en HTML tienen un repertorio limitado de controles y, al igual
que otros miembros de Office 2003, los usuarios de InfoPath 2003 necesitan licencia del
cliente.
Los usuarios acostumbrados a editar tablas de bases de datos con formularios Windows
creados con alguna versión de Visual Studio, sin duda preferirán una UI similar o idéntica para editar los Infosets XML tabulares, con controles DataGridView y, donde sea
necesario, con cuadros de texto vinculados u otros controles de formulario Windows.
Los controles DataGridView no se pueden vincular directamente a los documentos
XML, sino que primero hay que generar un juego de datos desde el esquema del documento. Si no tiene el esquema o no consigue generar el juego de datos, puede utilizar
el editor XML de VS 2005 para inferir el esquema a partir de los contenidos del documento.
6.5.1 Adaptar un esquema XML existente para generar un DataSet
Microsoft ha diseñado los DataSets para guardar en DataTables los datos relacionales; la
representación XML de DataSets y DataTables está pensada básicamente como un mecanismo para perpetuar o tratar datos a distancia. Por lo tanto, los documentos XML que
sirven de fuente a los juegos de datos, deben tener un esquema adaptable a los juegos.
A continuación indicamos los aspectos más importantes a tener en cuenta cuando se
utilizan esquemas existentes para generar juegos de datos tipificados:
El diseñador de juegos de datos asigna el juego de datos el nombre del elemento de
nivel superior (raíz o documento). Si el esquema contiene una declaración global del
espacio de nombres, se convierte en el espacio de nombres del juego de datos.
Los elementos subsiguientes con elementos hijos o los elementos hijo con atributos
generan DataTables. Esta característica es propia de los documentos centrados en atributos, como los representantes XML de los Recordsets ADO, pero también puede hacer
que se genere una tabla de datos para un atributo en lugar de una columna.
Los elementos hijo que representan las columnas de la tabla deben tener tipos sencillos
XSD en correspondencia con los tipos de datos del sistema NET.
Los DataSets están centrados en el elemento; si en el esquema se especifican atributos
para la tabla, el diseñador de juegos de datos añadirá los atributos como columnas de
tabla.
Los esquemas con grupos de elementos hijo anidados establecen automáticamente
relaciones one-to-many entre las tablas y añaden una clave primaria TableName_Id y una
columna de clave foránea por cada relación con la tabla. La clave primaria TableName_Id
es una columna Int32 AutoIncrement; leer un documento XML en el juego de datos
genera los valores de TableName_Id.
Si los grupos de elementos hijo no están anidados, hay que especificar la relación entre
las tablas en el editor de juegos de datos.
Si las tablas se han de cargar de documentos XML concretos y relacionados, en el esquema no se debe especificar ninguna relación de tabla anidada.
221
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 222
Bases de datos con Visual Basic
El diseñador de DataSets tiene problemas para importar esquemas secundarios que
soporten espacios de nombres múltiples y elementos calificados como espacios de
nombres. El diseñador de juegos de datos utiliza el XML Schema Definition Tool (Xsd.exe)
para generar los juegos de datos tipificados. Xsd.exe no utiliza el atributo
<xs:import>schemaLocation para cargar esquemas secundarios automáticamente.
Las restricciones anteriores hacen difícil, si no imposible, generar juegos de datos tipificados desde esquemas XML complejos, para documentos de negocios estándar, como
Universal Business Language (UBL) 1.0 o Human Resources XML (HR-XML). Los esquemas UBL 1.0 utilizan ampliamente las directrices <xs:import> y especifican tipos complejos para elementos que representan las columnas de las tablas.
La mayoría de las aplicaciones de edición XML deben producir un documento de salida con la misma estructura que el documento fuente, lo que significa que la edición
sólo debe afectar a los contenidos de los elementos. La estructura tabular de los juegos
de datos permite exportar todo el contenido o las filas seleccionadas de tablas concretas a los streams o archivos XML. También se pueden generar juegos de datos desde
documentos fuente relacionados con estructuras definidas en un único esquema.
Si la aplicación debe reestructurar el documento de salida, se puede aplicar un XSLT
transform para la versión final del documento editado. Otra alternativa es sincronizar el
juego de datos con una instancia XmlDataDocument y aplicar el transform a la instancia.
6.5.2 Esquemas para documentos XML de jerarquía anidada
La estructura ideal de un documento fuente de un juego de datos es un Infoset XML con
una jerarquía anidada de elementos relacionados. El diseñador de juegos de datos
genera DataSets automáticamente desde esquemas compatibles con los documentos
anidados. El siguiente documento XML, abreviado, es un ejemplo típico de archivo
XML generado al serializar un juego de objetos relacionados con los negocios en una
jerarquía de tres niveles:
<rootElement>
<parentGroup>
<parentField1>String</parentField1>
...
<parentFieldN>1000</parentFieldN>
<childGroup>
<childField1>String</childField1>
...
<childFieldN>15.50</childFieldN>
<grandchildGroup>
<grandchildField1>String</grandchildField1>
...
<grandchildFieldN>15</grandchildFieldN>
</grandchildGroup>
</childGroup>
</parentGroup>
</rootElement>
222
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 223
La aplicación de técnicas avanzadas de los DataSets
A continuación vemos el esquema general del documento anterior, con un elemento
raíz <xs:complexType> y sus <xs:complexType> que contienen a su vez un grupo de
elementos de campo <xs:sequence> y otros <xs:complexType> descendientes anidados:
<?xml version= 1.0 encoding= utf-8 ?>
<xs:schema attributeFormDefault= unqualified elementFormDefault= qualified
xmlns:xs= http://www.w3.org/2001/XMLSchema >
<xs:element name= rootElement >
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs= unbounded name= parentGroup >
<xs:complexType>
<xs:sequence>
<xs:element name= parentField1 type= xs:string />
...
<xs:element name= parentFieldN type= xs:int />
<xs:element maxOccurs= unbounded name= childGroup >
<xs:complexType>
<xs:sequence>
<xs:element name= childField1 type= xs:string />
...
<xs:element name= childFieldN type= xs:decimal />
<xs:element maxOccurs= unbounded name= grandChildGroup >
<xs:complexType>
<xs:sequence>
<xs:element name= grandChildField1 type= xs:string />
...
<xs:element name= grandChildFieldN type= xs:short />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
El diseñador de DataSets interpreta los grupos <xs:complexType> no raíz que tienen elementos de campo, los elementos anidados <xsd:complexType>, o ambos, como tablas de
datos. Por eso, los elementos de campo deben tener tipos de datos sencillos como
xs:string, xs:int o xs:decimal, o grupos <xs:complexType> que representan tablas relacionadas.
223
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 224
Bases de datos con Visual Basic
Un documento fuente XML que especifica un atributo de espacio de nombres por
defecto con <rootElementxmlns= documentNamespace> requiere un esquema que incluya
un atributo targetNamespace="documentNamespace" para el elemento <xs:schema> más
alto de la jerarquía. Si su esquema tiene una estructura tan básica como la del ejemplo
precedente y sólo tiene un targetNamespace o ningún espacio de nombres de documento, está de suerte. Haga los cambios que se destacan en negrita a continuación en los
dos primeros elementos del esquema para indicar que el esquema representa un juego
de datos tipificado:
<xs:schema attributeFormDefault= unqualified elementFormDefault= qualified
xmlns:xs= http://www.w3.org/2001/XMLSchema
xmlns:msdata= urn:schemas-microsoft-com:xml-msdata >
<xs:element name= rootElement msdata:IsDataSet= true >
Copie el archivo Schema.xsd en la carpeta del proyecto, pulse con el botón derecho el
icono del archivo en el Explorador de proyectos y seleccione Añadir a proyecto, lo que generará archivos Schema.Designer.vb, Schema.xsc, y Schema.xss. Realice una doble pulsación
sobre Schema.xsd para abrirlo en el Editor DataSet y mostrar la ventana Orígenes de datos.
Puede añadir el juego de datos a la bandeja del diseñador arrastrando la herramienta
DataSetName desde la sección de componentes ProjectName hasta el formulario, o seleccionando la herramienta DataSet desde la sección Data y seleccionando
ProjectName.DataSet en la lista de juegos de datos tipificados (Typed DataSet list).
En este punto, ya puede arrastrar la tabla parentGroup desde la ventana de fuentes de
datos para añadir un BindingNavigator y cuadros de texto o un DataGridView para editar parentGroup, y después añadir DataGridViews para las tablas childGroup y grandchildGroup.
6.5.3 Un ejemplo de esquema anidado
La siguiente figura muestra un juego de datos tipificado generado desde un esquema
(NorthwindDS.xsd) para un documento XML anidado (NorthwindDS.xml) que contiene
un pequeño subjuego de datos de las tablas Customers, Orders y Order Details de
Northwind.
Al generar el juego de datos, la columna Customers_Id de clave primaria se añade a la
tabla Customers y la correspondiente columna de clave foránea Customers_Id se añade a
la tabla Orders para crear la relación Customers_Orders. La tabla Orders gana una clave
primaria Orders_Id para la relación Orders_Order_Details con la clave foránea Orders_Id
de la tabla Order_Details. A continuación vemos el esquema NorthwindDS.xsd para el
documento anidado:
<?xml version= 1.0 encoding= utf-8 ?>
<xs:schema id= Northwind xmlns=
xmlns:xs= http://www.w3.org/2001/XMLSchema
xmlns:msdata= urn:schemas-microsoft-com:xml-msdata >
<xs:element name= Northwind msdata:IsDataSet= true >
<xs:complexType>
<xs:choice minOccurs= 0 maxOccurs= unbounded >
224
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 225
La aplicación de técnicas avanzadas de los DataSets
<xs:element name=
<xs:complexType>
<xs:sequence>
<xs:element name=
<xs:element name=
<xs:element name=
<xs:element name=
<xs:element name=
<xs:element name=
<xs:element name=
<xs:element name=
<xs:element name=
<xs:element name=
<xs:element name=
<xs:element name=
<xs:complexType>
<xs:sequence>
<xs:element name=
<xs:element name=
<xs:element name=
<xs:element name=
Customers >
CustomerID type= xs:string />
CompanyName type= xs:string />
ContactName type= xs:string minOccurs= 0 />
ContactTitle type= xs:string minOccurs= 0 />
Address type= xs:string />
City type= xs:string />
Region type= xs:string minOccurs= 0 />
PostalCode type= xs:string minOccurs= 0 />
Country type= xs:string />
Phone type= xs:string />
Fax type= xs:string minOccurs= 0 />
Orders minOccurs= 0 maxOccurs= unbounded >
OrderID type= xs:int />
CustomerID type= xs:string />
EmployeeID type= xs:int
/>
OrderDate type= xs:dateTime />
225
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 226
Bases de datos con Visual Basic
<xs:element name= RequiredDate type= xs:dateTime
minOccurs= 0 />
<xs:element name= ShippedDate type= xs:dateTime
minOccurs= 0 />
<xs:element name= ShipVia type= xs:int />
<xs:element name= Freight type= xs:decimal minOccurs= 0 />
<xs:element name= ShipName type= xs:string />
<xs:element name= ShipAddress type= xs:string />
<xs:element name= ShipCity type= xs:string />
<xs:element name= ShipRegion type= xs:string minOccurs= 0 />
<xs:element name= ShipPostalCode type= xs:string
minOccurs= 0 />
<xs:element name= ShipCountry type= xs:string />
<xs:element name= Order_Details minOccurs= 0
maxOccurs= unbounded >
<xs:complexType>
<xs:sequence>
<xs:element name= OrderID type= xs:int />
<xs:element name= ProductID type= xs:int />
<xs:element name= UnitPrice type= xs:decimal />
<xs:element name= Quantity type= xs:short />
<xs:element name= Discount type= xs:decimal />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
Nótese que el esquema NorthwindDS.xsd no contiene referencias a las columnas añadidas de clave primaria y clave foránea. Generar un juego de datos desde un esquema de
documento fuente anidado no modifica el esquema En el archivo NorthwindDS.Designer.vb, el método Northwind.InitClass añade esas DataColumns a las DataTables al especificar los ForeignKeyConstraints, y después añade las DataRelations con la propiedad
Nested definida como True.
226
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 227
La aplicación de técnicas avanzadas de los DataSets
6.5.4 La ventana Propiedades de las columnas
Para examinar las propiedades de las columnas añadidas, seleccione la columna y
pulse con el botón secundario del ratón para mostrar la ventana Propiedades. La siguiente figura muestra la ventana Propiedades de la columna de clave primaria Orders_Id
(izquierda) de la tabla Orders, y la columna Orders_Id de clave foránea de la tabla
Order_Details (derecha).
En la ventana Propiedades puede editar el tipo de datos, el nombre de la columna y otras
propiedades de cualquiera de las columnas de la tabla. Pulse la ventana con el botón
derecho y seleccione Añadir para añadir una nueva columna a la tabla de datos. A modo
de ejemplo, puede añadir una columna Extended a la tabla Order_Details que puede calcular con la fórmula Quantity*UnitPrice*(1 Discount).
Cualquier cambio en alguno de los valores de la ventana Propiedades provoca un cambio importante en el archivo de esquema: al archivo se le añade un grupo <xs:annotation> para especificar la fuente de datos, la mayoría de los elementos adquieren una
gran cantidad de atributos msprop y el tamaño del archivo aumenta considerablemnte.
NorthwindDS.xsd, por ejemplo, pasa de 4 KBytes a 35 KBytes. Por lo tanto, si tiene que
editar el esquema y conservar la estructura original, pulse el archivo con el botón secundario, en el Explorador de soluciones, seleccione Abrir con… y, en el cuadro de diálogo que se abre con el mismo nombre, seleccione XMLEditor. No seleccione DataSet
Editor, que es la opción por defecto, ni tampoco XML Schema Editor.
6.5.5 Un esquema anidado con atributos
Al añadir atributos a los elementos que generan tablas de datos se añade a la tabla una
columna del mismo nombre que el atributo. Por ejemplo, un atributo del campo Order_Details definido por <xs:attributename= "totalAmount" type= "xs:decimal" use= "requi227
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 228
Bases de datos con Visual Basic
red" /> añade una columna totalAmount a la tabla Order_Details. La siguiente figura
muestra el esquema NWAttributes.xsd abierto en el Editor DataSet. La primera columna
de cada tabla viene generada por un atributo definido en el equema e incluido en el
documento fuente NWAttributes.xsd source document. Cuando se añade un atributo a
una tabla, se añade también un atributo msdata:Ordinal="n" , en orden consecutivo, a
cada nodo hijo que representa una columna de la tabla.
Si se añade un atributo obligatorio a un elemento hijo, como por ejemplo ProductID, el
diseñador crea una tabla ProductID, y probablemente no es eso lo que usted desea.
6.5.6 Ejemplo de esquema anidado y "envuelto" (wrapped)
Con los documentos XML es una práctica común diseñar juegos de elementos "envueltos" en otros grupos. Un ejemplo es envolver Customer y sus hijos en un grupo
Customers, Order en un grupo Orders y Order_Detail en un grupo Order_Details para
crear la estructura abreviada que vemos a continuación:
<Customers>
<Customer>
<CustomerID>GREAL</CustomerID>
...
<Fax></Fax>
<Orders>
<Order>
228
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 229
La aplicación de técnicas avanzadas de los DataSets
<OrderID>11061</OrderID>
...
<ShipCountry>USA</ShipCountry>
<Order_Details>
<Order_Detail>
<OrderID>11061</OrderID>
...
<Discount>0.075</Discount>
</Order_Detail>
</Order_Details>
</Order>
</Orders>
</Customer>
<Customers>
A continuacion vemos el esquema abreviado del documento fuente anterior con los elementos envolventes destacados en negrita:
<?xml version= 1.0 encoding= utf-8 ?>
<xs:schema id= Customers xmlns=
xmlns:xs= http://www.w3.org/2001/XMLSchema
xmlns:msdata= urn:schemas-microsoft-com:xml-msdata >
<xs:element name= Customers msdata:IsDataSet= true >
<xs:complexType>
<xs:choice minOccurs= 0 maxOccurs= unbounded >
<xs:element name= Customer >
<xs:complexType>
<xs:sequence>
<xs:element name= CustomerID type= xs:string minOccurs= 0 />
...
<xs:element name= Fax type= xs:string minOccurs= 0 />
<xs:element name= Orders minOccurs= 0 />
<xs:complexType>
<xs:sequence>
<xs:element name= Order minOccurs= 0 maxOccurs= unbounded >
<xs:complexType>
<xs:sequence>
<xs:element name= OrderID type= xs:string
minOccurs= 0 />
...
<xs:element name= ShipCountry type= xs:string
minOccurs= 0 />
<xs:element name= Order_Details minOccurs= 0 />
<xs:complexType>
<xs:sequence>
<xs:element name= Order_Detail
minOccurs= 0 maxOccurs= unbounded >
<xs:complexType>
<xs:sequence>
<xs:element name= OrderID type= xs:string
229
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 230
Bases de datos con Visual Basic
minOccurs= 0 />
...
<xs:element name= Discount
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>
type= xs:string
El esquema CustomersDS.xsd genera dos tablas adicionales para establecer las relaciones entre los elementos Orders y Order, y Order_Details y Order_Detail. Para que el
DataSet se pueda editar en DataGridViews hay que añadir relaciones entre los campos
CustomersID de las tablas Customers y Orders, y los campos OrderID de las tablas Orders
y Order_Details, tal como se describe más adelante en este capítulo.
6.5.7 Un ejemplo de esquema plano
Los esquemas anidados pueden exportar tablas como si fueran documentos XML invocando el método DataTable.WriteXML(ExportFileName,XmlWriteMode.IgnoreSchema). Los
esquemas planos añaden la capacidad de importar documentos XML concretos, que
complen el esquema de DataSet para tablas relacionadas. No obstante, el diseñador de
juegos de datos no añade columnas TableName_Id, ForeignKeyConstraints ni
DataRelations.
A continuacion, el esquema abreviado de Northwind.xsd para Northwind.xml, que es la
versión plana de NorthwindDS.xml, con las claves primaria y foránea destacadas en
negrita:
<?xml version= 1.0 encoding= utf-8 ?>
<xs:schema id= Northwind attributeFormDefault= unqualified
elementFormDefault= qualified xmlns:xs= http://www.w3.org/2001/XMLSchema
xmlns:msdata= urn:schemas-microsoft-com:xml-msdata >
<xs:element name= Northwind msdata:IsDataSet= true >
230
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 231
La aplicación de técnicas avanzadas de los DataSets
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs= unbounded name= Customers >
<xs:complexType>
<xs:sequence>
<xs:element name= CustomerID type= xs:string />
...
<xs:element minOccurs= 0 name= Fax type= xs:string />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element minOccurs= 0 maxOccurs= unbounded name= Orders >
<xs:complexType>
<xs:sequence>
<xs:element name= OrderID type= xs:int />
<xs:element name= CustomerID type= xs:string />
...
<xs:element name= ShipCountry type= xs:string />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element minOccurs= 0 maxOccurs= unbounded name= Order_Details >
<xs:complexType>
<xs:sequence>
<xs:element name= OrderID type= xs:int />
<xs:element name= ProductID type= xs:int />
...
<xs:element name= Discount type= xs:decimal />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Para crear una versión editable de Northwind.xsd hay que seguir los siguientes pasos en
la ventana del DataSet Editor:
Añadir claves primarias a cada tabla de datos. Seleccionar y pulsar con el botón derecho la columna de clave primera y seleccionar a continuación Establecer clave principal
para las tres tablas. Opcionalmente, seleccione Editar clave para abrir el cuadro de diálgo Restricción UNIQUE y cambiar el nombre por PK_TableName o algo similar.
La tabla Order_Details tiene una clave primaria compuesta, por lo tanto pulse con el
botón derecho la columna OrderID, seleccione Editar clave y marque la casilla de verificación ProductID.
231
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 232
Bases de datos con Visual Basic
Pulse con el botón derecho el entorno del DataSet Editor y seleccione Agregar/Relation
para abrir el cuadro de diálogo Relación con los valores por defecto para una relación
entre Customers y Orders, que tendrá el nombre FK_Customers_Orders. En la lista
Columnas de clave externa, cambie la entrada OrderID de la lista Columnas de clave externa por CustomerID.
Seleccione de nuevo Agregar/Relation, cambie el nombre actual de la relación,
FK_Customers_Orders1 por a FK_Orders_Order_Details, y seleccione Orders en la lista de
la tabla padre y Order_Details en la lista de la tabla hijo. Las listas Columnas de clave y
Columnas de clave externa muestran el OrderID.
Si quiere que los usuarios de la aplicación puedan añadir nuevos records a Orders y
Order_Details, seleccione la columna OrderID de clave primaria, seleccione Propiedades
y cambie el valor de la propiedad AutoIncrement de False a True.
La siguiente figura muestra el editor de juegos de datos con los pasos anteriores completados.
Al añadir las claves primarias y las relaciones a las tablas, al final del esquema se añaden los siguientes elementos <xs:unique> y <xs:keyref> del elemento Northwind:
<xs:schema id= Northwind xmlns=
xmlns:xs= http://www.w3.org/2001/XMLSchema
xmlns:msdata= urn:schemas-microsoft-com:xml-msdata
xmlns:msprop= urn:schemas-microsoft-com:xml-msprop >
<xs:element name= Northwind msdata:IsDataSet= true
232
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 233
La aplicación de técnicas avanzadas de los DataSets
msprop:User_DataSetName= Northwind
msprop:DSGenerator_DataSetName= Northwind >
...
<xs:unique name= PK_Customers msdata:PrimaryKey= true >
<xs:selector xpath= .//Customers />
<xs:field xpath= CustomerID />
</xs:unique>
<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> especifican las restricciones de clave foránea. Los atributos msprop son referencias a las relaciones entre datos (DataRelations) añadidas por la clase parcial Northwind del archivo
Northwind.Designer.vb.
6.5.8 Inferir un esquema XML para generar un juego de datos
Si todavía no tiene ningún esquema para su documento fuente XML, puede elegir entre
las cinco opciones siguientes para generar el esquema con VS 2005:
233
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 234
Bases de datos con Visual Basic
Abra un documento fuente XML representativo en el editor de XML, seleccione
XML/CreateSchema para inferir un esquema, y guárdelo en la carpeta del proyecto con
el nombre SchemaName.xsd. El generador de esquemas del editor intentará inferir tipos
de datos XSD examinando los valores de texto en los campos del documento fuente.
Desafortunadamente, el proceso de inferencia no suele tener éxito con valores numéricos unsigned que no tienen valores decimales; les asigna tipos de datos XSD numéricos,
con los valores más pequeños posibles. Por ejemplo, calcular 0 dividido entre 255 se
convierte en xs:unsignedByte, 256 entre 65.535 se convierte en xs:unsignedShort, y los
números con muchas cifras se convierten en xs:unsignedInt o xs:unsignedLong. A menos
que tenga alguna razón para obrar de otra manera, asigne xs:int a todos los valores sin
fracciones decimales.
Cree un juego de datos vacío en tiempo de ejecución, invoque el método DataSet.ReadXml(DocumentFileName) y guarde el archivo del esquema invocando el método
DataSet.WriteXmlSchema(SchemaFileName). Este último método genera un esquema no
tipificado en el que todos los elementos tienen asignado el tipo de datos xs:string y un
atributo minOccurs="0". Abra SchemaFileName.xsd en el editor XML, cambie los tipos de
datos de los valores numéricos o de fecha/tiempo por el tipo apropiado xs:datatype, y
elimine todos los atributos minOccurs="0" que no resulten apropiados.
Genere un esquema tipificado con el proceso anterior, pero invoque el método DataSet.ReadXml(DocumentFileName,XmlReadMode.InferTypedSchema) para generar un
esquema idéntico al generado por el editor XML.
Abra un VS 2005 Command Prompt, navegue hasta la carpeta del proyecto y escriba
xsd.exe DocumentFileName.xml para generar DocumentFileName.xsd. El esquema es idéntico al generado por el método precedente.
Si no dispone de ningún documento XML representativo de todas las instancias posibles de documento XML, o si no quiere crear uno manualmente, puede usar la herramienta Microsoft XSD Inference 1.0, que encontrará en http://apps.gotdotnet.com/xmltools/xsdinference/ para generar y refinar un esquema tipificado. Debe especificar
una fuente inicial para inferir el esquema inicial y después procesar los documentos
fuente adicionales para refinar el esquema.
Si tiene que inferir y refinar esquemas de forma rutinaria, puede utilizar el método
System.Xml.Schema.InferSchema para simular la herramienta de Microsoft, XSD Inference
1.0 Tool. El siguiente código infiere un esquema para una instancia de documento inicial (Initial.xml), refina el esquema con tres instancias de documentos adicionales y
escribe el esquema refinado como Initial.xsd:
Private Sub InferAndRefineSchema()
Dim alFiles As New ArrayList
alFiles.Add(Initial.xml)
alFiles.Add(Refine2.xml)
alFiles.Add(Refine3.xml)
alFiles.Add(Refine4.xml)
Dim intCtr As Integer
Dim xss As XmlSchemaSet = Nothing
234
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 235
La aplicación de técnicas avanzadas de los DataSets
Dim xsi As Inference = Nothing
For intCtr = 0 To alFiles.Count - 1
Dim xr As XmlReader = XmlReader.Create(alFiles(intCtr).ToString)
If intCtr = 0 Then
Infer(schema)
xss = New XmlSchemaSet()
xsi = New Inference()
End If
xss = xsi.InferSchema(xr)
xr.Close()
Next
Dim strXsdFile As String = Replace(alFiles(0).ToString, .xml, .xsd)
Dim xsd As XmlSchema
For Each xsd In xss.Schemas()
Dim sw As StreamWriter = Nothing
sw = My.Computer.FileSystem.OpenTextFileWriter(strXsdFile, False)
xsd.Write(sw)
sw.Close()
Exit For
Next
End Sub
6.5.9 Crear formularios de edición desde fuentes de datos XML
El proceso de crear formularios de edición para documentos XML es parecido al de editar tablas de bases de datos. Después de generar un juego de datos tipificado a partir
del esquema existente, arrastre la tabla de más arriba desde la ventana Orígenes de datos
hasta el formulario donde quiere añadir un control DataNavigator y DataGridView o cuadros de texto para detalles. Repita el mismo proceso con los DataGridViews para las
tablas relacionadas y especifique la DataRelation apropiada para generar una
DataRelationBindingSource para el valor de la propiedad DataSource. A diferencia de los
DataGridViews vinculados a FK_ParentTable_ChildTableBindingSources generados por
tablas de bases de datos, la BindingSource se crea cuando, en la lista desplegable de la
propiedad DataSource, se especifica una lista relacionada.
Los dos ejemplos siguientes de proyectos ilustran los cambios necesarios para crear
DataRelationBindingSource, permitir la adición de nuevos elementos en el documento y
acomodar juegos de datos envueltos y anidados.
6.5.10 El proyecto de ejemplo EditNorthwindDS
El proyecto EditNorthwindDS.sln está basado en el documento fuente NorthwindDS.xml
y en el esquema NorthwindDS.xsd. El formulario tiene DataGridViews poblados con
datos de las tablas Customers, Orders y Order_Details, tal como muestra la siguiente
figura.
Abra la ventana Orígenes de datos y arrastre el icono del grupo padre de Customers, el
icono de su subgrupo Orders y el icono del subgrupo Order Details del grupo Orders
235
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 236
Bases de datos con Visual Basic
hasta el formulario para añadir los tres DataGridViews. Añada código al manejador de
evento Form_Load para poblar el juego de datos con el documento NorthwindDS.xml.
La siguiente figura muestra la lista Orígenes de datos tras realizar las operaciones anteriores y cargar el documento NorthwindDS.xml. La instrucción OrdersDataGridView..Sort(.Columns(0),System. ComponentModel.ListSortDirection.Descending) del manejador
de eventos clasifica los OrderID por orden descendente. Si quiere que los usuarios puedan añadir nuevos registros a Orders y Order_Details con los valores apropiados de la
columna OrderID, deberá editar el esquema y darle a la propiedad AutoIncrement de las
columnas OrderID y Order_Id el valor True en el cuadro de diálogo Propiedades de
ColumnName. En caso contrario, defina el valor False para la propiedad AllowUserToAddRows de DataGridViews.
Puede añadir los atributos autogenerados Customers_Id, Orders_Id y Order_Details_Id
como columnas de los DataGridViews. Mientras personaliza la colección Columns de los
DataGridViews en el cuadro de diálogo Editar columnas, lleve las columnas autogeneradas al final de la lista SelectedColumns y defina el valor True para sus propiedades
ReadOnly. Si no quiere que los usuarios puedan añadir nuevas filas, borre estas columnas de los DataGridView. Añada un botón para guardar los cambios e invoque el método NorthwindDS..WriteXml(strFile,Data.XmlWriteMode.IgnoreSchema) para guardar el
documento editado con los datos. El proyecto de ejemplo guarda un archivo diffgram
(NorthwindDS.xsd) antes de guardar los camibos y tiene botones para mostrar en
Internet Explorer el esquema y el documento XML guardado.
236
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 237
La aplicación de técnicas avanzadas de los DataSets
Para añadir nuevas filas se necesita un procedimiento OrdersDefaultValues que llama al
manejador de evento OrdersDataGridView_DefaultValuesNeeded. El código del procedimiento es similar al que vimos en el capítulo anterior para el manejador de evento
DefaultValuesNeeded, pero ahora hay que añadir el valor Customers_Id para mantener la
relación, tal como se destaca en negrita en el siguiente listado:
Private Sub OrdersDefaultValues(ByVal rowNew As DataGridViewRow)
Try
With CustomersDataGridView
Dim intRow As Integer = .CurrentCell.RowIndex
rowNew.Cells(1).Value = .Rows(intRow).Cells(0).Value
rowNew.Cells(2).Value = 0
rowNew.Cells(3).Value = Today
rowNew.Cells(4).Value = Today.AddDays(14)
'Leave ShippedDate empty
rowNew.Cells(6).Value = 3
'Freight defaults to 0
'CompanyName
rowNew.Cells(8).Value = .Rows(intRow).Cells(1).Value
'Address to Country fields
Dim intCol As Integer
For intCol = 9 To 13
rowNew.Cells(intCol).Value = .Rows(intRow).Cells(intCol 5).Value
237
VisualBasic2005_06.qxp
02/08/2007
16:26
PÆgina 238
Bases de datos con Visual Basic
Next
'Add the current Customers_Id value
rowNew.Cells(15).Value = .Rows(intRow).Cells(11).Value
OrdersDataGridView.EndEdit(DataGridViewDataErrorContexts.Commit)
'Store the autoincremented Orders_Id for Order_Details default values
intNewOrder_ID = CInt(rowNew.Cells(14).Value)
'Store the autoincremented OrderID value
intOrderID = CInt(rowNew.Cells(0).Value)
End With
Catch exc As Exception
MsgBox(exc.Message + exc.StackTrace, , )
End Try
End Sub
El procedimiento DetailsDefaultValues requiere una modificación similar para los
valores de OrdersID y Orders_Id:
Private Sub DetailsDefaultValues(ByVal rowNew As DataGridViewRow)
'Default values for Order_Details
Try
With OrdersDataGridView
Dim intRow As Integer = .CurrentCell.RowIndex
'Add OrderID
rowNew.Cells(0).Value = .Rows(intRow).Cells(0).Value
'Add Orders_Id
rowNew.Cells(5).Value = .Rows(intRow).Cells(14).Value
End With
With Order_DetailsDataGridView
rowNew.Cells(1).Value = 0
rowNew.Cells(2).Value = .Rows.Count * 10
rowNew.Cells(3).Value = .Rows.Count * 5
rowNew.Cells(4).Value = .Rows.Count * 0.01
End With
Catch exc As Exception
rowNew.Cells(5).Value = intNewOrder_ID
Finally
Order_DetailsDataGridView.EndEdit(DataGridViewDataErrorContexts.Commit)
End Try
End Sub
238
VisualBasic2005_07.qxp
02/08/2007
16:28
PÆgina 239
Capítulo 7
Trabajar con las fuentes de datos
y controles vinculados de
ASP.NET 2.0
Los formularios Windows, sus fuentes de datos, componentes y controles vinculados de
la versión .NET Framework 2.0 son un desarrollo de la versión anterior .NET Framework
1.0. El ayudante y las herramientas de Visual Studio 2005 simplifican las tareas más
comunes, como generar juegos de datos tipificados y diseñar formularios maestros y de
detalle, pero las herramientas y el ayudante se parecen mucho a sus predecesores. La
transición desde las herramientas y componentes de Visual Studio implica una modesta curva de aprendizaje para los desarrolladores .NET con más experiencia. Sustituir los
obsoletos DataGrid por los nuevos DataGridWiews exige algo más de esfuerzo, pero las
propiedades y el rendimiento mejorado de estos elementos justifica la complejidad de
su modelo de objeto.
Por otra parte, ASP.NET 2.0 representa una diferencia radical respecto a ASP.NET 1.x.
La herramienta de libre desarrollo Web Matrix ASP.NET, de Microsoft, fue un éxito instantáneo y una contribución remarcable a su populardidad fue que no requería ningún
prerrequisito para VS 2002 o 2003 ni los Internet Information Services (IIS). Web Matrix
combina un diseñador gráfico de páginas Web y un editor de código (su nombre codificado es Venus) para ASP.NET 1.1 con un servidor Web ligero (Cassini). Venus y Cassini
constituyen los fundamentos de Visual Web Developer UI y el servidor Visual Web
Developer de VS 2005. La edición Express 2005 de Visual Web Developer (VWD) es el equivalente a la actualización de Web Matrix para VS 2005 UI y ASP.NET 2.0. A diferencia
de las ediciones Express para un lenguaje de programación específico, la VWD 2005
Express soporta VB, C#, y J#.
Este capítulo presupone que el lector ya tiene cierta experiencia en la creación y desarrollo de
sitios Web controlados por datos con Active Server Pages (ASP), ASP.NET 1.x o Web Matrix.
Las cadenas de conexión de los proyectos de ejemplo presuponen que se trabaja con SQLServer
2000, MSDE 2000 o SQLServer 2005, como instancia localhost por defecto y la base de datos
Northwind.
239
VisualBasic2005_07.qxp
02/08/2007
16:28
PÆgina 240
Bases de datos con Visual Basic
Si está utilizando Visual Web Developer 2005 Express Edition o una instancia de nombre
SQLServer 2005, debe modificar la siguiente sección del archivo Web.config para señalar
la instancia nombrada:
<connectionStrings>
<add name= NorthwindConnection connectionString= Server=localhost;Integrated
Security=True;Database=Northwind providerName= System.Data.SqlClient />
</connectionStrings>
Cambie localhost por .\SQLExpress para usar el proveedor Shared Memory con SQL
Server 2005 Express.
7.1 Las nuevas características de ASP.NET 2.0
La creación de formularios Web con VS 2005 es muy diferente a la de VS 2002 y 2003,
que dependían de un directorio virtual IIS definido previamente. El cuadro de diálogo
Nuevo proyecto de VS 2005 no incluye los iconos Sitio Web ASP.NET, Servicio Web
ASP.NET y otros relacionados con la Web. El menú Archivo/Nuevo ofrece una selección
de sitios Web que abre el cuadro de diálogo Nuevo sitio Web con una serie de iconos
basados en el sistema de archivos, como Sitio Web ASP.NET, Servicio Web ASP.NET y
otros iconos de plantillas.La carpeta raíz por defecto para añadir nuevos sitios Web o
subcarpetas de servicios es .\WebSites. El cuadro de diálogo Seleccionar ubicación se
puede abrir pulsando el botón Examinar, aceptando la opción por defecto Sistema de
archivos y añadiendo un nombre de acceso más apropiado para el cuadro de texto
Ubicación (ver siguiente figura).
Pulse el botón Aceptar para generar una carpeta con los ítems del proyecto, añada una
carpeta vacía App_Data, un archivo de página Default.aspx y un archivo de código oculto Default.aspx.vb. Si no encuentra el archivo Default.aspx.vb, pulse con el botón derecho
240
Descargar