VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 142 Bases de datos con Visual Basic DataGridViewComboBoxColumn para la propiedad ColumnType, cambie el valor de HeaderName si es necesario, y defina el valor de Width para ajustarlo a la lista de ítems. En este ejemplo, cambie la cabecera de columna EmployeeID por Employee, y defina una anchura de 120 píxeles. Cambie la anchura de la columna ShipVia a 110 pixeles. La figura siguiente muestra dos cambios en la columna EmployeeID; Width queda fuera de la vista. 4.7.4 Añadir código para poblar los cuadros combinados Employees y ShipVia Antes de ejecutar el programa, debe añadir código para poblar los dos cuadros combinados nuevos, si no lo hace, se producirán muchos DataErrors durante la navegación por el DataGridView. Para encontrar los nombres de los dos cuadros combinados, DataGridViewComboBoxColumn seguido de un número entero arbitrario, busque ‘DataGridViewComboBox (incluida la comilla simple) en OrdersForm.Designer.vb para encontrar los grupos de definición de los dos cuadros combinados, destacados en negrita en las siguientes secuencias de código: ‘ DataGridViewComboBoxColumn2 ‘ Me.DataGridViewComboBoxColumn2.DataPropertyName = EmployeeID Me.DataGridViewComboBoxColumn2.DefaultCellStyle = DataGridViewCellStyle1 Me.DataGridViewComboBoxColumn2.HeaderText = Employee Me.DataGridViewComboBoxColumn2.MaxDropDownItems = 8 Me.DataGridViewComboBoxColumn2.Name = EmployeeID Me.DataGridViewComboBoxColumn2.Resizable = _ System.Windows.Forms.DataGridViewTriState.[True] Me.DataGridViewComboBoxColumn2.SortMode = _ System.Windows.Forms.DataGridViewColumnSortMode.Automatic Me.DataGridViewComboBoxColumn2.ValueType = GetType(Integer) Me.DataGridViewComboBoxColumn2.Width = 120 142 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 143 Programar TableAdapters, BindingSources y DataGridViews ‘ DataGridViewComboBoxColumn3 Me.DataGridViewComboBoxColumn3.DataPropertyName = ShipVia Me.DataGridViewComboBoxColumn3.DefaultCellStyle = DataGridViewCellStyle1 Me.DataGridViewComboBoxColumn3.HeaderText = ShipVia Me.DataGridViewComboBoxColumn3.MaxDropDownItems = 8 Me.DataGridViewComboBoxColumn3.Name = ShipVia Me.DataGridViewComboBoxColumn3.Resizable = _ System.Windows.Forms.DataGridViewTriState.[True] Me.DataGridViewComboBoxColumn3.SortMode = _ System.Windows.Forms.DataGridViewColumnSortMode.Automatic Me.DataGridViewComboBoxColumn3.ValueType = GetType(Integer) Me.DataGridViewComboBoxColumn3.Width = 110 Usando los nombres que ha descubierto, cuyos sufijos numéricos probablemente diferirán de los del código anterior, añada el código siguiente al procedimiento LoadAndBindComboBoxes: Private Sub LoadAndBindComboBoxes() ... With DataGridViewComboBoxColumn2 .DataSource = dsLookups.Tables(EmplsLookup) .DisplayMember = EmployeeName .ValueMember = EmployeeID End With ... With DataGridViewComboBoxColumn3 .DataSource = dsLookups.Tables(ShipsLookup) .DisplayMember = CompanyName .ValueMember = ShipperID End With End Sub 4.7.5 Remplazar los valores nulos por defecto en las filas nuevas Los cuadros combinados no pueden procesar valores nulos sin mostrar un error, por lo que deberá asignar un valor por defecto válido en EmployeeID en el manejador de evento SetDefaultOrderValues de la versión inicial. El valor lógico sería ‘0’ para EmployeeID con Unassigned como valor de LastName, pero eso requeriría modificar la tabla de Empleados (Employees). Una alternativa es especificar una consulta UNION para poblar el cuadro combinado con el ítem añadido. Si elige este método, cambie la sentencia SELECT para la tabla EmplsLookup por: SELECT 0, Unassigned UNION SELECT EmployeeID, LastName + FirstName AS EmployeeName FROM dbo.Employees;. , + La alternativa más sencilla es vincular por defecto todos los Orders al vicepresidente de ventas, tal como se muestra a continuación en negrita: 143 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 144 Bases de datos con Visual Basic Private Sub SetDefaultOrderValues(ByVal rowAdded As DataGridViewRow) With rowAdded .Cells(1).Value = Me.CustomerIDTextBox.Text .Cells(2).Value = 2 .Cells(3).Value = Today.ToShortDateString .Cells(4).Value = Today.AddDays(14).ToShortDateString ... End With End Sub Cuando construya y ejecute el formulario, los Orders DataGridView con un nuevo pedido añadido aparecerá tal como se muestra en la siguiente figura. 4.7.6 Asociar cuadros combinados con cuadros de texto La Ficha Edit Selected Orders necesita cuadros combinados lookup similares, pero conservar los cuadros de texto originales EmployeeID y ShipVia verifica que los valores de la columna vinculada varían al seleccionar diferentes valores de los cuadros combinados. Para este ejemplo, añada cuadros combinados llamados cboEmployeeID y cboShipVia a la ficha Edit Selected Orders y cambie el valor de su propiedad DropDownStyle por DropDownList. Añada el código siguiente para poblar y vincular los cuadros combinados de los campos EmployeeID y ShipVia de OrdersDataTable: Private Sub LoadAndBindComboBoxes() ... With cboEmployeeID .DataSource = dsLookups.Tables(EmplsLookup) 144 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 145 Programar TableAdapters, BindingSources y DataGridViews .DisplayMember = EmployeeName .ValueMember = EmployeeID .DataBindings.Clear() Any of these bindings work; BindingSource is the preferred data source .DataBindings.Add(SelectedValue, NorthwindDataSet.Orders, EmployeeID) .DataBindings.Add(New Binding(SelectedValue, NorthwindDataSet, _ Orders.EmployeeID)) .DataBindings.Add(New Binding(SelectedValue, OrdersBindingSource, _ EmployeeID, True)) End With ... With cboShipVia .DataSource = dsLookups.Tables(ShipsLookup) .DisplayMember = CompanyName .ValueMember = ShipperID .DataBindings.Clear() .DataBindings.Add(New Binding(SelectedValue, OrdersBindingSource, _ ShipVia, True)) End With ... End Sub Una peculiaridad al sincronizar cuadros combinados y cuadros de texto vinculados al mismo campo es que se hace imposible la actualización bidireccionalidad de los cuadros de texto. Para actualizar los cuadros de texto con los cambios de los cuadros combinados, añada los siguientes manejadores de eventos: Private Sub cboEmployeeID_SelectionChangeCommitted(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles cboEmployeeID.SelectionChangeCommitted EmployeeIDTextBox.Text = cboEmployeeID.SelectedValue.ToString End Sub Private Sub cboShipVia_SelectionChangeCommitted(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles cboShipVia.SelectionChangeCommitted ShipViaTextBox.Text = cboShipVia.SelectedValue.ToString End Sub Debe manejar el evento SelectionChangeCommitted, no el evento Click, el cual ocurre antes de que el cambio seleccionado sea válido. Para actualizar la selección del cuadro combinado con los cambios de los cuadros de texto, añada al código inicial la modificación destacada en negrita, y un manejador para el evento TextChanged de ShipViaTextBox: Private Sub EmployeeIDTextBox_TextChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles EmployeeIDTextBox.TextChanged With EmployeeIDTextBox If Val(.Text) > 0 And CInt(Val(.Text)) <= cboEmployeeID.Items.Count Then 145 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 146 Bases de datos con Visual Basic btnCancelPage1Changes.Enabled = True btnSavePage1Changes.Enabled = True cboEmployeeID.SelectedIndex = CInt(Val(.Text)) - 1 End If End With End Sub Private Sub ShipViaTextBox_TextChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles ShipViaTextBox.TextChanged With ShipViaTextBox If Val(.Text) > 0 And CInt(Val(.Text)) <= cboShipVia.Items.Count Then cboShipVia.SelectedIndex = CInt(Val(.Text)) - 1 btnCancelPage1Changes.Enabled = True btnSavePage1Changes.Enabled = True End If End With End Sub La siguiente figura muestra la página Edit Selected Order con los dos cuadros combinados añadidos. 4.8 Añadir un cuadro combinado que defina valores adicionales Cambiar el valor de un cuadro de texto vinculado o de un cuadro combinado a menudo tiene efectos secundarios que se deben tratar con código. A modo de ejemplo, el valor de la columna UnitPrice de Order_DetailsDataGridView se tiene que actualizar cambiando la columna ProductID. La versión inicial OrdersByCustomerV3 requiere que 146 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 147 Programar TableAdapters, BindingSources y DataGridViews el operador de entrada de datos haga referencia a una lista de correlación para los valores ProductID, ProductName, y UnitPrice. Por lo tanto, la columna ProductID necesita un cuadro combinado para mostrar los valores de ProductName, y seleccionar un item debe proporcionar el valor correcto de UnitPrice. La tabla de datos ProdsLookup incluye los datos UnitPrice, así como una columna QuantityPerUnit. Mostrar QuantityPerUnit en una columna no vinculada es opcional. 4.8.1 Crear y vincular un DataView ordenado por ProductName Para remplazar el cuadro de texto de la columna ProductID por un cuadro combinado en los dos DataGridViews hay que seguir el mismo proceso que para las columnas EmployeeID y ShipVia de la parrila Orders. Los ítems de lista EmployeeID y ShipVia aparecen en el pedido de la columna vinculada a la tabla de datos. Eso no representa ningún problema para los cuadros combinados que contienen pocos ítems de lista, pero el cuadro combinado ProductID debería estar ordenado alfabéticamente por ProductName. Ordenar los ítems requiere crear un DataView ordenado según la tabla de datos. Primero, añada las siguientes variables de formulario a la clase OrdersForm.vb: Private dvProdsLookup As DataView Private blnHasLoaded As Boolean Añada el código siguiente al procedimiento LoadAndBindComboBoxes para crear un DataView dvProdsLookup ordenado por ProductName, y poblar las listas del cuadro con datos de dvProdsLookup: Private Sub LoadAndBindComboBoxes() ... ' ProductID combo boxes ' Create a dvProdsLookup = New DataView(.Tables(3)) dvProdsLookup.Sort = ProductName With DataGridViewComboBoxColumn4 .DataSource = dvProdsLookup .DisplayMember = ProductName .ValueMember = ProductID End With With DataGridViewComboBoxColumn5 .DataSource = dvProdsLookup .DisplayMember = ProductName .ValueMember = ProductID End With blnHasLoaded = True End Sub 147 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 148 Bases de datos con Visual Basic 4.8.2 Comprobar que no haya duplicados y actualizar la columna UnitPrice La tabla Order Details tiene una clave primaria compuesta, OrderID y ProductID, para impedir que se dupliquen los items de línea existentes. Para impedir accesos al servidor que devuelvan mensajes de error de violación de clave, debería comprobar los valores nuevos o modificados de ProductID para verificar las duplicaciones e informar al operador del error. Las entradas duplicadas de ProductID arrojarán una excepción DataError cuando el operador complete la edición y vaya a la fila siugiente. De todos modos, es es una práctica más que buena capturar el error inmediatamente después de que ocurra. Si el nuevo valor de ProductID es aceptable, hay que escanear la tabla ProdsLookup para encontrar la fila correspondiente y actualizar el precio por unidad con el procedimiento siguiente, aplicable a las dos parrillas Order Details. Hay que pasar dgvDetails por referencia para obtener un puntero de la instancia activa DataGridView. Private Sub GetUnitPrice(ByVal intRow As Integer, ByVal intCol As Integer, _ ByRef dgvDetails As DataGridView) Try If intCol = 2 Then Dim intProdID As Integer = CInt(dgvDetails.Rows(intRow).Cells(2).Value) Dim decPrice As Decimal Dim intRowCtr As Integer Dim rowProd As DataRow Dim strName As String = Nothing Dim intDups As Integer With dgvDetails For intRowCtr = 0 To .Rows.Count - 1 If CInt(.Rows(intRow).Cells(2).Value) = intProdID Then intDups += 1 If intDups > 1 Then Exit For End If End If Next intRowCtr End With If intDups > 1 Then Dim strMsg As String = "ProductID " + intProdID.ToString + _ " has been added previously to this order. " + vbCrLf + vbCrLf + _ " Please select a different product or press Esc to cancel the edit." MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) Return End If 148 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 149 Programar TableAdapters, BindingSources y DataGridViews With dsLookups.Tables(3) For intRowCtr = 0 To .Rows.Count - 1 rowProd = .Rows(intRowCtr) If CInt(rowProd.Item(0)) = intProdID Then decPrice = CDec(rowProd.Item(2)) With Order_DetailsDataGridView1 .Rows(intRow).Cells(3).Value = decPrice Exit For End With End If Next intRowCtr End With End If Catch exc As Exception MsgBox(exc.Message + exc.StackTrace, , exc.Source) End Try End Sub Una alternativa a tener presente es crear un DataView con el valor de la propiedad Filter definido como ProductID=intProdID. Las expresiones Filter utilizan la sintaxis de consulta SQL WHERE (sin WHERE), por lo que los argumentos literales del string deben ir entre comillas simples. Hay que dar los valores apropiados de intRow e intCol y un puntero DataGridView al procedimiento de los manejadores de evento CellValueChanged añadidos. Private Sub Order_DetailsDataGridView_CellValueChanged(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles Order_DetailsDataGridView.CellValueChanged 'Get the UnitPrice value If blnHasLoaded Then GetUnitPrice(e.RowIndex, e.ColumnIndex, Order_DetailsDataGridView) End If End Sub Private Sub Order_DetailsDataGridView1_CellValueChanged(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles Order_DetailsDataGridView1.CellValueChanged If blnHasLoaded Then GetUnitPrice(e.RowIndex, e.ColumnIndex, Order_DetailsDataGridView1) If Not (e.ColumnIndex = 0 Or e.ColumnIndex = 5) Then 'Update the items subtotal for Quantity, ProductID, 'UnitPrice, and Discount changes GetOrderSubtotal() btnCancelPage1Changes.Enabled = True btnSavePage1Changes.Enabled = True End If End If End Sub 149 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 150 Bases de datos con Visual Basic A continuación vemos el código para el procedimiento GetOrderSubtotal, que actualiza el cuadro de texto txtSubtotal: Private Sub GetOrderSubtotal() With Order_DetailsDataGridView1 Dim decSubtotal As Decimal Dim intCtr As Integer For intCtr = 0 To .Rows.Count - 1 decSubtotal += CDec(.Rows(intCtr).Cells(5).Value) Next txtSubtotal.Text = Format(decSubtotal, "$#,##0.00") End With End Sub La siguiente figura muestra la ficha Edit Selected Order con la columna ProductID DataGridView convertida de cuadro de texto a cuadro combinado, varios items de línea añadidos a un pedido nuevo, y el valor Items Subtotal actualizado. 4.9 Añadir filas a las tablas lookup para entradas de nuevos Customers El proyecto inicial OrdersByCustomersV3 añade el item CustomerID computado al cuadro combinado cboCustomerID cuando se completa la entrada CompanyName para un nuevo cliente. No es posible añadir items a cuadros combinados cuya DataSource es un tabla de datos, por lo que hay que añadir una fila nueva a la tabla de datos CustsLookup. La manera más sencilla de conseguir tablas de datos en tiempo de ejecución es añadir una fuente vinculada (BindingSource) al formulario y utilizar sus métodos para añadir 150 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 151 Programar TableAdapters, BindingSources y DataGridViews una fila para el nuevo valor CustomerID. Las tareas de edición se manejan con los métodos AddNew, EndEdit, CancelNew, CancelEdit de la BindingSource. 4.9.1 Añadir y vincular una BindingSource CustomerID Añadir un componente BindingConnector1 del cuadro de herramientas y renombrarlo como bsCustsLookup. A continuación, añadir el código de vinculación siguiente después de la sentencia blnHasLoaded=True del procedimiento LoadAndBindComboBoxes: bsCustsLookup.DataSource = dsLookups bsCustsLookup.DataMember = “CustsLookup” ‘Test the BindingSource (optional) Dim intRows As Integer = bsCustsLookup.Count En el evento ContactNameTextBox_GotFocusevent, elimine el código siguiente que añade un cuadro combinado que provoca una “runtime exception”. .Items.Add(strCustID + “ - “ + CompanyNameTextBox.Text) 'List is sorted, so need to find the new entry '(Lists can’t be sorted when they use a DataSource) For intCtr = 0 To .Items.Count - 1 If Mid(.Items(intCtr).ToString, 1, 5) = strCustID Then .SelectedIndex = intCtr Exit For End If Next Sustituya el código borrado por el siguiente para añadir un nuevo registro al final de la tabla de datos y definir sus valores: Dim objNewRow As Object = bsCustsLookup.AddNew() Dim drvNewRow As DataRowView = CType(objNewRow, DataRowView) With drvNewRow .Item(0) = strCustID .Item(1) = strCustID + “ - “ + CompanyNameTextBox.Text .EndEdit() End With .SelectedIndex = .Items.Count - 1 Aplicar drvNewRow.EndEdit implica eliminar la fila añadida –en lugar de llamar a CancelEdit– en el manejador de eventos btnCancelCustEdit_Click. Añada la siguiente línea destacada en negrita: Private Sub btnCancelCustEdit_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnCancelCustEdit.Click Dim intCtr As Integer = CustomersBindingSource.Count If blnIsNewCustomer Then 'Remove the added (last) record dcCustsLookup.RemoveAt(dcCustsLookup.Count - 1) ClearCustomerTextBoxes() CustomersBindingSource.CancelEdit() 151 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 152 Bases de datos con Visual Basic blnIsNewCustomer = False Else CustomersBindingSource.CancelEdit() End If ... End Sub 4.9.2 Comprobar la existencia de duplicados con un DataRowView Cambiar la fuente de datos del cuadro combinado por una tabla de datos requiere modificaciones en el test para comprobar duplicados del CustomerID. La expresión .Items(intCtr).ToString del siguiente bloque de código devuelve System.Windows.Forms.ComboBox,Items.Count=94, no el string esperado CustomerID - CustomerName: For intCtr = 0 To .Items.Count - 1 If Mid(.Items(intCtr).ToString, 1, 5) = strCustID Then CompanyNameTextBox.Focus() Dim strMsg As String = "CustomerID ‘" + strCustID + _ "‘ duplicates existing entry ‘" + .Items(intCtr).ToString + "." + strHelp MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) blnIsDup = True Exit For End If Next intCtr Hay que convertir el ítem del cuadro de texto en un objeto DataRowView y comprobar el valor DataRowView.Row.Item(0), para lo cual se han de añadir al bloque anterior los siguientes cambios resaltados en negrita: For intCtr = 0 To .Items.Count - 1 Dim drvCustID As DataRowView = CType(.Items(intCtr), DataRowView) With drvCustID.Row If .Item(0).ToString = strCustID Then CompanyNameTextBox.Focus() Dim strMsg As String = "CustomerID ‘" + strCustID + _ "‘ duplicates existing entry ‘" + .Item(1).ToString + "." + strHelp MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) blnIsDup = True Exit For End If End With Next intCtr 152 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 153 Programar TableAdapters, BindingSources y DataGridViews 4.10 Aplicar reglas de negocio a las ediciones Como ya se mencionó al principio del capítulo, las reglas de negocios quedan validadas en el sentido de activadas, con valor por la aplicación del cliente en los ejemplos de este capítulo. Las reglas reforzadas en la presentación por tiers contravienen las mejores prácticas, ya que un cambio en las reglas implica desplegar una nueva versión de la aplicación para todos los usuarios del PC. Si refuerza reglas de negocios con triggers en el SQL Server o procedimientos almacenados, cada error en la entrada de datos requiere una nueva ejecución del servidor. Hay dos reglas de negocios, sin embargo, que no es probable que cambien: prohibir Orders sin ítems de línea y valores UnitPrice de valor 0,00. Añada el código siguiente al principio del manejador de eventos btnSaveOrders_Click para reforzar las dos reglas: Dim strMsg As String If Order_DetailsBindingSource.Count < 1 Then strMsg = "An new order must have at least one line item. " + _ "Please add a line item or click Cancel All Changes." MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) Return End If 'Test for $0.00 as UnitPrice Dim intRow As Integer strMsg = "A UnitPrice of $0.00 isn’t permitted. Please edit line " With Order_DetailsDataGridView1 For intRow = 0 To .Rows.Count - 2 If CDec(.Rows(intRow).Cells(3).Value) = 0D Then strMsg += (intRow + 1).ToString + "." MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) Return End If Next End With La mayoría de operadores de entrada de datos no están autorizados a dar descuentos arbitrarios a los Customers, pero tampoco se espera de ellos que sepan de memoria las tablas de descuentos. En este ejemplo se da una cantidad única de descuento aplicable a todos los productos y Customers, y cambiar un descuento para los ítems existentes con valor Discount distinto de 0,00% está prohibido. Para establecer una cantidad fija como descuento progoramada, añada el código siguiente al principio del procedimiento GetUnitPrice: If intCol = 1 Then 'Calculate fixed discounts for default 0.0% With dgvDetails If CInt(.Rows(intRow).Cells(4).Value) = 0D Then Dim intQuan As Integer = CInt(.Rows(intRow).Cells(intCol).Value) Dim decDisc As Decimal Select Case intQuan 153 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 154 Bases de datos con Visual Basic Case Is >= 100 decDisc = 0.25D Case Is >= 50 decDisc = 0.15D Case Is >= 25 decDisc = 0.1D Case Is >= 10 decDisc = 0.075D Case Is >= 5 decDisc = 0.05D End Select .Rows(intRow).Cells(4).Value = decDisc End If End With End If Quantity y ProductID son ahora los únicos valores de campo de Order Details que no están autogenerados por el usuario, por lo tanto debería definir como True el valor de la propiedad ReadOnly de las columnas UnitPrice y Discount. 4.11 Guardar los cambios en las tablas base Hasta ahora, los botones Save... de las dos fichas sólo actualizan los juegos de datos tipificados. Antes de realizar cambios en las tablas base Northwind, debería decidir una estrategia de actualización. Se pueden acumular cambios en los juegos de datos y añadir un botón para enviar todos los cambios al servidor en un batch, o enviar cambios incrementales a medida que el usuario realiza procesos de edición. Ambas alternativas conllevan el mismo número de accesos al servidor, a menos que se activen actualizaciones batch DataAdapter. Activar actualizaciones batch implica añadir sentencias Me.m_adapter.UpdateBatchSize=n al archivo DataSetName.designer.vb y no proporciona una mejora substancial del rendimiento en un entorno LAN. La mejor política para Customers bien conectados (LAN) es guardar los cambios en las tablas base cuando el operador clica cualquier botón Save... Este método minimiza la probabilidad de que haya conflictos de concurrencia y reduce la pérdida de datos en el caso de que haya un fallo en la aplicación cliente, un fallo en el hardware o en el suministro de electricidad. 4.11.1 Mantener la integridad referencial Mantener la integridad referencial implica ejecutar sentencias DELETE, UPDATE, e INSERTSQL o procedimientos almacenados para las tablas relacionadas en un orden específico. Para los procesos de borrado se require una secuencia ascendente en la jerarquía de la relación, y las actualizaciones y adiciones deben ocurrer en orden descendente, a menos que se especifiquen actualizaciones y borrados en casacada para las tablas por debajo de la tabla superior. Las relaciones Northwind FK_Order_Details_Orders y FK_Orders_Customers no tienen especificados borrados o actualizaciones en cascada. 154 VisualBasic2005_04.qxp 02/08/2007 16:21 PÆgina 155 Programar TableAdapters, BindingSources y DataGridViews Para cambiar las reglas de actualización y borrado en la ventana del diseñador del DataSet, sólo tiene que pulsar con el botón secundario del ratón la línea de relación entre las tablas padre e hijo, y seleccionar Editar relación para abrir el cuadro de diálogo Relación. El diseñador de DataSet crea una relación por defecto entre sus tablas de datos. Se puede especificar Sólo relación, Sólo relación Foreign Key y Tanto relación como restricción Foreign Key. Si especifica una restricción de clave foránea tendrá las opciones que vemos a continuación para los valores de las propiedades ForeignKeyConstraint.UpdateRule, DeleteRule, y AcceptChangesRule: ) ) ) ) Cascade (por defecto) borra los registros de la tabla hijo cuando se borran los de la tabla padre y actualiza el valor de la clave foránea de los registros hijo con el nuevo valor de clave primaria en la tabla padre. None no modifica los registros hijo cuando se borra la tabla padre o se modifica el valor de la clave primaria, lo cual arroja excepciones automáticamente. El resultado son registros hijo huérfanos, a menos que los cambios en los registros hijo se manejen con código en el bloque Catch. SetNull define una clave primaria en la tabla hijo con valor DBNull y deja huérfanos los registros. SetDefault define el valor de la clave foránea de la tabla hijo con el valor por defecto de la columna, el cual depende del tipo de datos de la columna. Los ejemplos de este capítulo no permiten borrar registros Customers ni alterar el valor de CustomerID, por lo tanto especificar Tanto relación como restricción Foreign Key y aceptar el valor None por defecto para la clave foránea FK_Orders_Customers es válido para los tres valores Reglas. La siguiente figura muestra el cuadro de diálogo Relación con el cambio aplicado. 4.11.2 Crear y comprobar la función UpdateBaseTables Independientemente de que se especifiquen actualizaciones o borrados en cascada, o ambos, para los DataSets o las tablas base, la regla general es aplicar las actualizaciones de las tablas base según la siguiente secuencia: 1. Borrar los registros de la tabla hijo. 2. Insertar, modificar y borrar los registros de la tabla padre. 3. Insertar y modificar los registros de la tabla hijo. Actuar conforme a estas reglas implica que el código de actualización de la tabla base cree un nuevo ChangeTypeDataTable para cada tipo de actualización de cada tabla base, y ejecute TableNameTableAdapter.Update (ChangeTypeDataTable) para todas las tablas de datos con cambios. Se puede generar cada tabla copiando las filas de datos actualizadas identificadas por su valor de enumeración DataRowState: Added, Modified, o Deleted. 155 VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 156 Bases de datos con Visual Basic 4.11.3 Entender la generación de cambios en tablas y las instrucciones para la actualización de las tablas base Los DataAdapters de ADO.NET 1.x requieren la expresión siguiente para generar un ChangeTypeDataTable y actualizar la table base correspondiente: Dim ChangeTypeDataTable As DataSet.DataTable= _ DataSet.DataTable.GetChanges(DataRowState.Type) If Not ChangeTypeDataTable Is Nothing Then OrdersDataAdapter.Update(ChangeTypeDataTable) End If Los TableAdapters requieren convertir ChangeTypeDataTable en el tipo DataSet.DataTable. Para poblar un ChangeTypeDataTable en ADO.NET 2.0 y actualizar la tabla base se puede seguir la siguiente instrucción genérica: Dim ChangeTypeDataTable As DataSet.DataTable= _ CType(DataSet.DataTable.GetChanges(DataRowState.Type), DataSet.DataTable) If Not ChangeTypeDataTable Is Nothing Then OrdersTableAdapter.Update(ChangeTypeDataTable) End If Proyectar el tipo es un precio muy bajo para la versatilidad que se gana con los TableAdapters añadidos. Veamos el código para conseguir filas modificadas en la OrdersDataTable y actualizar la tabla base Orders: 156 VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 157 Programar TableAdapters, BindingSources y DataGridViews Dim ModOrders As NorthwindDataSet.OrdersDataTable = _ CType(NorthwindDataSet.Orders.GetChanges(DataRowState.Modified), _ NorthwindDataSet.OrdersDataTable) If Not ModOrders Is Nothing Then OrdersTableAdapter.Update(ModOrders) End If Para actualizar las tres tablas Northwind habría que aplicar el código anterior en ocho variaciones, tal como ilustra la siguiente figura. Para incluir la posibilidad, altamente peligrosa, de borrar un record Costumer, se necesitarían nueve versiones. Utilice operaciones de copiar, pegar, editar y substituir para minimizar las entradas por teclado. 157 VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 158 Bases de datos con Visual Basic 4.11.4 Añadir la función UpdateBaseTables Es una buena práctica comprobar el código de actualización antes de introducir cambios en las tablas base. Una manera de comprobar el procedimiento de actualización es guardar en un archivo XML de formato diffgram los cambios propuestos y comprobar después ese archivo con las operaciones típicas de actualización. Otra práctica recomendada es hacer saber a los usuarios si tienen cambios pendientes –preferentemente cuántos cambios– antes de cerrar la aplicación. El código siguiente para la función UpdateBaseTables cumple todos esos objetivos: Private Function UpdateBaseTables(ByVal blnTest As Boolean) As Boolean If NorthwindDataSet.HasChanges Then Dim NewCustomers As NorthwindDataSet.CustomersDataTable = _ CType(NorthwindDataSet.Customers.GetChanges(DataRowState.Added), _ NorthwindDataSet.CustomersDataTable) Dim ModCustomers As NorthwindDataSet.CustomersDataTable = _ CType(NorthwindDataSet.Customers.GetChanges(DataRowState.Modified), _ NorthwindDataSet.CustomersDataTable) Dim DelOrders As NorthwindDataSet.OrdersDataTable = _ CType(NorthwindDataSet.Orders.GetChanges(DataRowState.Deleted), _ NorthwindDataSet.OrdersDataTable) Dim NewOrders As NorthwindDataSet.OrdersDataTable = _ CType(NorthwindDataSet.Orders.GetChanges(DataRowState.Added), _ NorthwindDataSet.OrdersDataTable) Dim ModOrders As NorthwindDataSet.OrdersDataTable = _ CType(NorthwindDataSet.Orders.GetChanges(DataRowState.Modified), _ NorthwindDataSet.OrdersDataTable) Dim DelDetails As NorthwindDataSet.Order_DetailsDataTable = _ CType(NorthwindDataSet.Order_Details.GetChanges(DataRowState.Deleted), _ NorthwindDataSet.Order_DetailsDataTable) Dim NewDetails As NorthwindDataSet.Order_DetailsDataTable = _ CType(NorthwindDataSet.Order_Details.GetChanges(DataRowState.Added), _ NorthwindDataSet.Order_DetailsDataTable) Dim ModDetails As NorthwindDataSet.Order_DetailsDataTable = _ CType(NorthwindDataSet.Order_Details.GetChanges(DataRowState.Modified), _ NorthwindDataSet.Order_DetailsDataTable) Dim dsChanges As DataSet = Nothing Dim intChanges As Integer If blnTest Then dsChanges = New DataSet dsChanges.DataSetName = "dsChanges" End If Try '1. Delete Order Details records If Not DelDetails Is Nothing Then If blnTest Then DelDetails.TableName = "DelDetails" dsChanges.Tables.Add(DelDetails) 158 VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 159 Programar TableAdapters, BindingSources y DataGridViews Else Order_DetailsTableAdapter.Update(DelDetails) End If intChanges += DelDetails.Count End If '2. Delete Orders records If Not DelOrders Is Nothing Then DelOrders.TableName = "DelOrders" If blnTest Then dsChanges.Tables.Add(DelOrders) intChanges += DelOrders.Count Else OrdersTableAdapter.Update(DelOrders) End If intChanges += 1 End If '3. Insert New Customers records If Not NewCustomers Is Nothing Then If blnTest Then NewCustomers.TableName = "NewCustomers" dsChanges.Tables.Add(NewCustomers) Else CustomersTableAdapter.Update(NewCustomers) End If intChanges += NewCustomers.Count End If '4. Update Modified Customers records If Not ModCustomers Is Nothing Then If blnTest Then ModCustomers.TableName = "ModCustomers" dsChanges.Tables.Add(ModCustomers) Else CustomersTableAdapter.Update(ModCustomers) End If intChanges += ModCustomers.Count End If '5. Insert New Orders records If Not NewOrders Is Nothing Then If blnTest Then dsChanges.Tables.Add(NewOrders) NewOrders.TableName = "NewOrders" Else OrdersTableAdapter.Update(NewOrders) End If intChanges += NewOrders.Count End If '6. Update Modified Orders records 159 VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 160 Bases de datos con Visual Basic If Not ModOrders Is Nothing Then If blnTest Then dsChanges.Tables.Add(ModOrders) ModOrders.TableName = "ModOrders" Else OrdersTableAdapter.Update(ModOrders) End If intChanges += ModOrders.Count End If '7. Insert New Order Details records If Not NewDetails Is Nothing Then If blnTest Then dsChanges.Tables.Add(NewDetails) NewDetails.TableName = "NewDetails" Else Order_DetailsTableAdapter.Update(NewDetails) End If intChanges += NewDetails.Count End If '8. Update Modified Order Details records If Not ModDetails Is Nothing Then If blnTest Then dsChanges.Tables.Add(ModDetails) ModDetails.TableName = "ModDetails" Else Order_DetailsTableAdapter.Update(ModDetails) End If intChanges += ModDetails.Count End If If blnTest Then Dim strFile As String = Application.StartupPath + _ "\DataSetUpdategram.xml" If intChanges > 0 Then dsChanges.WriteXml(strFile, XmlWriteMode.DiffGram) Dim strMsg As String = "You have update(s) pending to " + _ intChanges.ToString + " records(s)." + vbCrLf + vbCrLf + _ "Are you sure you want to quit without " + _ " saving these updates to the Northwind database?" If MsgBox(strMsg, MsgBoxStyle.Question Or MsgBoxStyle.YesNo, _ "Pending Updates Not Saved") = MsgBoxResult.Yes Then Return False Else Return True End If Else If File.Exists(strFile) Then 160 VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 161 Programar TableAdapters, BindingSources y DataGridViews File.Delete(strFile) End If End If End If Return True Catch exc As Exception MsgBox(exc.Message + exc.StackTrace, MsgBoxStyle.Exclamation, _ "Database Updates Failed") Return False Finally If Not dsChanges Is Nothing Then dsChanges.Dispose() End If If Not NewCustomers Is Nothing Then NewCustomers.Dispose() End If If Not ModCustomers Is Nothing Then ModCustomers.Dispose() End If If Not DelOrders Is Nothing Then DelOrders.Dispose() End If If Not NewOrders Is Nothing Then NewOrders.Dispose() End If If Not ModOrders Is Nothing Then ModOrders.Dispose() End If If Not DelDetails Is Nothing Then DelDetails.Dispose() End If If Not NewDetails Is Nothing Then NewDetails.Dispose() End If If Not ModDetails Is Nothing Then ModDetails.Dispose() End If End Try Else If Not blnTest Then MsgBox("There are no data updates to save.", MsgBoxStyle.Information, _ "Save Requested Without Updates") End If Return False End If End Function 161 VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 162 Bases de datos con Visual Basic 4.11.5 Operaciones previas de actualización La manera más simple de generar archivos iniciales de prueba DataSetUpdategram.xml es añadiendo un botón provisional para Test Updates en la ficha Customer Orders y añadir el siguiente manejador de evento Click: Private Sub btnTestUpdates_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles btnTestUpdates.Click 'Temporary button for testing Dim blnQuit As Boolean = UpdateBaseTables(True) End Sub Haga algunos cambios en los records de Customers, Orders, y Order Details, pulse el botónTest Updates, y examine el archivo DataSetUpdategram.xml con Internet Explorer. Compruebe que los cambios que ha hecho han quedado reflejados en el grupo <dsChanges>. 4.11.6 Invocar la función UpdateBaseTables Una vez finalizado el test de la función UpdateBaseTables con NorthwindDataSet, elimine temporalmente el botón de test e invoque la función añadiendo las líneas que destacamos en negrita en el código siguiente después de llamar al método EndEdit del manejador de evento btnSaveCustData_Click. Así: CustomersBindingSource.EndEdit() If UpdateBaseTables(False) Then NorthwindDataSet.Customers.AcceptChanges() Else Return End If Invoque la función del manejador de evento btnSaveOrders_Click para actualizar las tablas Orders y OrderDetails con el siguiente código añadido: Order_DetailsBindingSource.EndEdit() OrdersBindingSource.EndEdit() If UpdateBaseTables(False) Then NorthwindDataSet.Orders.AcceptChanges() NorthwindDataSet.Order_Details.AcceptChanges() Else Return End If Ahora añada una sentencia ImportsSystem.IO a OrdersForm.vb y, a continuación, el siguiente código por el final del manejador de evento ContactNameTextBox_GotFocus para repoblar cboCustomerID con nuevos valores CustomerID cuando se reinicie el proyecto: .SelectedIndex = .Items.Count - 1 Dim strFile As String = Application.StartupPath + _ 162 VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 163 Programar TableAdapters, BindingSources y DataGridViews \LookupsDataSet.xml If File.Exists(strFile) Then File.Delete(strFile) End If 4.11.7 Comprobar los valores CustomerID del servidor para evitar duplicados Añadir un cliente nuevo y una carpeta inicial será imposible si otro usuario ha añadido ya un cliente con el mismo CustomerID después del último refresco del DataSet dsLookups. A menos que se guarde el diagrama de la actualización y se añada código para reintentarlo con un CustomerID diferente, se habrá perdido toda la entrada. Para prevenir la pérdida de datos, debería comprobar que el nuevo CustomerID no exista ya en la tabla Customer del servidor. Añada la siguiente función CheckServerForCustID en la que se utiliza el método SqlCommand.ExecuteScalar para un chequeo rápido de duplicados en el servidor: Private Function CheckServerForCustID(ByVal strCustID As String) As Boolean Dim cnNwind As SqlConnection = Nothing Try Dim strConn As String = My.Settings.NorthwindConnection.ToString cnNwind = New SqlConnection(strConn) Dim strSQL As String = "SELECT COUNT(CustomerID) FROM Customers " + _ "WHERE CustomerID = ‘" + strCustID + "‘" Dim cmCustID As New SqlCommand(strSQL, cnNwind) cnNwind.Open() Dim intCount As Integer = CInt(cmCustID.ExecuteScalar) cnNwind.Close() If intCount > 0 Then 'Duplicate found Return True Else Return False End If Catch exc As Exception MsgBox(exc.Message + exc.StackTrace, MsgBoxStyle.Exclamation, _ "Test Duplicates Error") Return False Finally If Not cnNwind Is Nothing Then If Not cnNwind.State = ConnectionState.Closed Then cnNwind.Close() End If cnNwind.Dispose() End If End Try End Function 163 VisualBasic2005_04.qxp 02/08/2007 16:22 PÆgina 164 Bases de datos con Visual Basic Para ejecutar la función CheckServerForCustID, añada las líneas destacadas en negrita a continuación del procedimiento ContactNameTextBox_GotFocus antes del test ExitSub: If Not blnIsDup Then 'Function is in OrderFormV3.vb blnIsDup = CheckServerForCustID(strCustID) If blnIsDup Then CompanyNameTextBox.Focus() Dim strMsg As String = "CustomerID ‘" + strCustID + _ "‘ duplicates existing entry in Customers table." + strHelp MsgBox(strMsg, MsgBoxStyle.Exclamation, strTitle) End If End If If blnIsDup Then Exit Sub End If En este momento, ya ha desarrollado todo un frente bastante completo para afrontar la entrada de datos, pero todavía no está preparado para desplegarlo sin encapsular antes el proceso de entrada de Orders en una transacción y generar mensajes de error que permitan a los usuarios superar los problemas siempre que sea posible. En los ejemplos de los capítulos siguientes añadirá funciones para la gestión de transacciones y otras carcterísticas a éste y otros proyectos similares de gestión de datos, después de lo cual habrá adquirido un nuevo estatus como programador casi independiente de componentes de datos y DataGridView. 164 VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 165 Capítulo 5 Añadir código para validar datos y gestionar la concurrencia Validar las entradas de datos en cuadros de texto, sencillos controles vinculados de formularios Windows o DataGridViews, es una tarea relativamente simple. Para tener controles de formulario vinculados hay que definir un objeto ErrorProvider que especifique la posición y otros atributos del icono de error, el cual, por defecto, es un signo de exclamación blanco dentro de un círculo rojo. Los DataGridViews tienen un detector de errores integrado que hace todavía más simple la validación de los valores de celda. La validación de datos es suficiente para los frentes de bases de datos de un solo usuario, aunque tal vez sea necesaria una consulta para el servidor dentro del manejador de evento de la validación para impedir que fallen las actualizaciones de datos, por ejemplo, cuando se añade una fila nueva a una tabla con una clave primaria basada en valores, como la tabla Customers de Northwind. Si la clave primaria propuesta ya existe, se obtendrá una SqlException y habrá que realizar de nuevo la actualización. Los dos primeros apartados de este capítulo hablan de las técnicas de validación de entrada de datos con cuadros de texto vinculados y controles DataGridViews. Las aplicaciones para un frente multi-usuario –mucho más comunes que la variedad de un solo usuario– requieren una gestión explícita de la concurrencia. Las operaciones UPDATE o DELETE en tablas base del servidor realizadas con DataTableAdapters ejecutan por defecto pruebas de concurrencia basadas en valores por defecto. Si un valor de la tabla base en el servidor no concuerda con los valores originales de un DataRow, el método DataTableAdapter.Update falla y usted recibe una DBConcurrencyException. Resolver los errores de concurrencia con un proceso que sea razonablemente sencillo para los usuarios no es una tarea simple, tal como descubrirá enseguida. La mayor parte de este capítulo está dedicada a la gestión de la concurrencia. Este capítulo explica dos técnicas de gestión de concurrencia que no se encuentran en la ayuda online de Visual Studio 2005 ni en las publicaciones sobre "mejores prácticas" con ADO.NET. Las dos técnicas son: comparar el número de registros hijo del servidor con los de la tabla de datos del cliente, y restablecer un pedido que otro usuario ha borrado en el servidor. Este capítulo incluye código VB 2005 para la validación de datos y la gestión de concurrencia con un proyecto de formulario Windows de ejemplo: OrdersByCustomerTx.sln. La siguiente figura muestra el formulario principal del proyecto OrdersByCustomerTx.sln, basado en el formulario del capítulo anterior OrdersByCustomerV2, la función UpdateBaseTables del proyecto (final) OrdersByCustomerV3.sln, y los correspondientes manejadores de evento para las operaciones de actualización. Si bien los errores de concurrencia se pueden simular ejecutando dos instancias de OrdersByCustomerTx.sln, es mucho 165 VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 166 Bases de datos con Visual Basic más aconsejable desarrollar y comprobar las estrategias de gestión de concurrencia simulando conflictos en una sola instancia de proyecto. Por lo tanto, el formulario principal tiene tres botones que provocan errores de concurrencia cuando se escriben actualizaciones directamente en el servidor. Todos los proyectos de ejemplo de los capítulos anteriores presuponen que usted o el usuario del ordenador cliente tienen una conexión de red permanente al servidor de la base de datos. Este capítulo muestra cómo diseñar aplicaciones que soporten usuarios desconectados que actualizan los juegos de datos del cliente offline y después actualizan las tablas del servidor al conectarse de nuevo a la red. Seleccionando el cuadro de verificación Emulate Disconnected User en el proyecto de ejemplo, se simula el estado offline. Si hace las actualizaciones offline y después deselecciona el cuadro de verificación, el proceso de actualización se inicia automáticamente. Las técnicas de gestión de concurrencia son similares para los usuarios conectados y usuarios que se reconectan, pero hay que añadir una cantidad substancial de código para crear y manejar los juegos de datos locales del usuario desconectado. La cadena de conexión por defecto de App.config requiere la base de datos de ejemplo de Northwind para poder instalarlo en una instancia local (localhost) de SQL Server 2000, MSDE 2000, o SQL Server 2005. Si utiliza SQL Express, cambie localhost por .\SQLEXPRESS. El proyecto de ejemplo añade más de 2.500 líneas de código Visual Basic a su predecesor. La mayor parte del código añadido implementa la gestión de concurrencia. Desarrollar y comprobar las técnicas de gestión de concurrencia a nivel de producción con ADO.NET 2.0 requiere una base de datos jerarquizada en un mínimo de tres niveles, varios tipos de datos de campo y datos de ejemplo representativos, incluyendo 166 VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 167 Añadir código para validar datos y gestionar la concurrencia valores DBNull. Sencillas tablas maestras de detalles con algunas columnas y filas no serán suficientes para explicar los numerosos aspectos que describe este capítulo sobre la implementación de la gestión de concurrencia y su diseño. 5.1 Validar las entradas de datos La mayor parte de los controles de los formularios Windows “disparan” un evento Validating cuando el usuario edita un valor de control y un evento Validated después de que el valor esté editado. Los eventos de validación para operaciones con teclado como <Tab> o <Mayús> + <Tab> ocurren dentro de la siguiente secuencia de eventos: Enter, GotFocus, Leave, Validating, Validated, y LostFocus. Las operaciones con el ratón y el método Focus generan una secuencia ligeramente distinta: Enter, GotFocus, LostFocus, Leave, Validating, y Validated. Para validar el valor de un control vinculado sencillo o uno no vinculado, añada un manejador de evento ControlName_Validating con expresiones para comprobar el valor editado y generar un icono y una herramienta de error con un objeto ErrorProvider. Los iconos y herramientas de error son mucho menos “invasivos” que los cuadros de mensaje que los usuarios tienen que confirmar pulsando el botón Aceptar. 5.1.1 Validar cuadros de texto A continuación vemos un ejemplo sencillo de un manejador de evento TextBox_Validating para asegurar que el cuadro de texto CompanyName contiene al menos cinco caracteres: Private Sub CompanyNameTextBox_Validating(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) Handles CompanyNameTextBox.Validating If CompanyNameTextBox.Text.Length < 5 Then Dim strError As String = "CompanyName requires at least five characters" e.Cancel = True epCompanyName.SetError(CompanyNameTextBox, strError) Else epCompanyName.SetError(CompanyNameTextBox, String.Empty) End If End Sub El proyecto de ejemplo de este capítulo, OrdersByCustomerTx.sln, incluye los ejemplos de validación del cuadro de texto y DataGridView. Construya y ejecute el proyecto, pulse Add New Costumer, y pulse <Tab> para generar un CustomerID. Después vacíe el cuadro de texto CompanyName para mostrar el icono de error. Escriba al menos cinco caracteres en el cuadro de texto y pulse el botón Cancel Edit para finalizar el añadido de datos a Customers. Definiendo e.Cancel=True se impide que el usuario pueda salir del control sin corregir el error. La expresión epCompanyName.SetError(CompanyNameTextBox,strError) requiere 167 VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 168 Bases de datos con Visual Basic que se defina un objeto ErrorProvider en el manejador de evento FormName_Load o el constructor del formulario con código como el que sigue: Private epCompanyName As ErrorProvider() ... epCompanyName = New ErrorProvider() With epCompanyName .SetIconAlignment(CompanyNameTextBox, ErrorIconAlignment.MiddleRight) .SetIconPadding(CompanyNameTextBox, 2) .BlinkRate = 500 'half-second .BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.AlwaysBlink End With Si existe un botón Cancel o similar para salir de la entrada sin corregir el error de validación, deberá añadir una instrucción ControlName.SetError(CompanyNameTextBox,String.Empty) para eliminar el icono y que el usuario pueda obtener de nuevo el control del foco. Un control con un objeto activo ErrorProvider impide igualmente que el usuario cierre el formulario, a menos que se añada un manejador de evento FormName_Closing y se defina e.Cancel=False. 5.1.2 Validar controles DataGridViews Los DataGridViews tienen un detector de errores integrado, por lo que no es necesario añadir ningún objeto ErrorProvider para este tipo de control. Además de los eventos comunes Validating y Validated, válidos para todo el contenido del control, los DataGridViews también disparan eventos CellValidating, CellValidated, RowValidating, y RowValidated. Los eventos Cell... se disparan cuando el usuario intenta abandonar, o abandona, la celda actual, y los eventos Row... tienen lugar cuando el usuario intenta salir, o sale, de la fila actual. El evento CellValidating es el más útil de los seis eventos de validación. Las propiedades e.ColumnIndex y e.RowIndex devuelven las coordenadas de la celda con el error. Añadiendo un mensaje de error a la propiedad DataGridView.Row.ErrorText se muestra un icono en la correspondiente RowHeader y se añade un cuadro de ayuda rápida a la fila. El icono y el cuadro de ayuda rápida se pueden eliminar definiendo la propiedad DataGridView.Row.ErrorText en un string vacío del evento DataGridView_CellValidating o DataGridView_CellEndEdit. Los valores por defecto añadidos a la columna EmployeeID de nuevas filas de Orders y ProductID para nuevas filas de Order Details, causan una restricción de clave foránea SqlException si el usuario intenta guardar los cambios sin cambiar antes 0 por un valor aceptable. Este problema se resuelve definiendo valores de clave foránea en una lista desplegable; para que sea más sencillo, el proyecto ejemplo de este capítulo requiere que el usuario entre valores numéricos. El siguiente código de ejemplo para manejar los eventos DataGridView_CellValidating y DataGridView_CellValidating o DataGridView_CellEndEdit, es típico por sus sencillas expresiones de validación. Private Sub OrdersDataGridView_CellValidating(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellValidatingEventArgs) _ Handles OrdersDataGridView.CellValidating 168 VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 169 Añadir código para validar datos y gestionar la concurrencia Try 'Validate EmployeeID column value With OrdersDataGridView If .Rows.Count > 1 Then If e.ColumnIndex = 2 Then If Not e.FormattedValue.ToString = "(null)" Then If CInt(e.FormattedValue) < 1 Or CInt(e.FormattedValue) > 9 Then Dim strError As String = "EmployeeID value must be a number " + _ "between 1 and 9" .Rows(e.RowIndex).ErrorText = strError 'Prevent saving the order SaveOrdersToolStripButton.Enabled = False e.Cancel = True Else End If End If End If End If End With Catch exc As Exception MsgBox(exc.Message, MsgBoxStyle.Information, "Invalid EmployeeID Entry") End Try End Sub Private Sub OrdersDataGridView_CellEndEdit(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles OrdersDataGridView.CellEndEdit With OrdersDataGridView If e.ColumnIndex = 2 Then .Rows(e.RowIndex).ErrorText = End If End With End Sub Private Sub Order_DetailsDataGridView_CellValidating(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellValidatingEventArgs) _ Handles Order_DetailsDataGridView.CellValidating Try With Order_DetailsDataGridView Dim strError As String = Nothing If e.ColumnIndex = 2 Then If CInt(e.FormattedValue) < 1 Or CInt(e.FormattedValue) > 77 Then strError = "ProductID value must be a number between 1 and 77" 169 VisualBasic2005_05.qxp 02/08/2007 18:26 PÆgina 170 Bases de datos con Visual Basic .Rows(e.RowIndex).ErrorText = strError e.Cancel = True SaveOrdersToolStripButton.Enabled = False End If End If End With Catch exc As Exception MsgBox(exc.Message, MsgBoxStyle.Information, "Invalid ProductID Entry") End Try End Sub Ejecute el proyecto de ejemplo y pulse el botón Add New Order ToolStrip para mostrar los iconos y los mensajes de ayuda rápida. Escriba con el teclado valores aptos para EmployeeID y ProductID para eliminar los iconos de error. Pulse el botón Cancel Orders Edit para finalizar la nueva entrada de Order. 5.1.3 Capturar las violaciones de restricción de clave primera durante la entrada La tabla Order Details tiene una clave primaria compuesta: OrderID y ProductID, para que cualquier valor duplicado de ProductID arroje una excepción de restricción de clave primaria en la tabla local Order Details. Por lo tanto, el código de validación anterior para la columna ProductID se debería comprobar para que no tuviera ningún duplicado. Un método sencillo y ligero para detectar valores duplicados es crear una instancia HashTable y poblar sus pares clave/valor con el valor de ProductID formateado del DataGridView y el número de fila. Si se añade una clave ProductID duplicada, la HashTable arroja una excepción que se deberá procesar con el código que aparece en negrita en el siguiente listado: Private Sub Order_DetailsDataGridView_CellValidating(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellValidatingEventArgs) _ Handles Order_DetailsDataGridView.CellValidating Try With Order_DetailsDataGridView Dim strError As String = Nothing If e.ColumnIndex = 2 Then If CInt(e.FormattedValue) < 1 Or CInt(e.FormattedValue) > 77 Then strError = "ProductID value must be a number between 1 and 77" .Rows(e.RowIndex).ErrorText = strError e.Cancel = True SaveOrdersToolStripButton.Enabled = False Else 'Create a hashtable of ProductID values 'Adding a duplicate key value throws an exception Dim htDupes As Hashtable = New Hashtable 170