4.7.4 Añadir código para poblar los cuadros combinados

Anuncio
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
Descargar