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