Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Professional Visual Studio 2005 (2006) [eng]

.pdf
Скачиваний:
135
Добавлен:
16.08.2013
Размер:
21.9 Mб
Скачать

Chapter 31

Figure 31-6

Figure 31-7

406

Data Binding and Object Data Sources

Saving Changes

Now that you have a usable interface, you need to add support for making changes and adding new records. If you double-click the Save icon on the CustomerBindingNavigator toolstrip, the code window opens with a code stub that would normally save changes to the Customer table. Clearly, you need to modify this method to save changes made to both the Individual table and the Contact table. The result is a method that looks like the following snippet. Remember that because Individual is a linking table between Customer and Contact, it needs to be saved last to ensure that there are no conflicts when changes are sent to the database:

Private Sub CustomerBindingNavigatorSaveItem_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _

Handles CustomerBindingNavigatorSaveItem.Click

Me.Validate()

Me.ContactBindingSource.EndEdit()

Me.ContactTableAdapter.Update(Me.AdventureWorksDataSet.Contact)

Me.CustomerBindingSource.EndEdit()

Me.CustomerTableAdapter.Update(Me.AdventureWorksDataSet.Customer)

Me.IndividualBindingSource.EndEdit()

Me.IndividualTableAdapter.Update(Me.AdventureWorksDataSet.Individual)

End Sub

If you run this, make changes to a customer, and click the Save button, an exception will be thrown because you’re currently trying to update calculated fields. You need to correct the Update and Insert methods used by the CustomerTableAdapter to prevent updates to the Account Number column, because it is a calculated field, and to automatically update the Modified Date field. Using the DataSet Designer, select the CustomerTableAdapter, open the Properties window, expand the UpdateCommand node, and click the ellipses button next to the CommandText field. This opens the Query Builder dialog that you used in the previous chapter. Uncheck the boxes in the Set column for the rowguid and AccountNumber rows. In the New Value column, change @ModifiedDate to getdate(), to automatically set the modified date to the date on which the query was executed. This should give you a query similar to the one shown in Figure 31-8.

Figure 31-8

Unfortunately, the process of making this change to the Update command causes the parameter list for this command to be reset. Most of the parameters are regenerated correctly except for the IsNull_

407

Chapter 31

TerritoryId parameter, which is used to handle cases where the TerritoryID field can be null in the database. To fix this problem, open the Parameter Collection Editor for the Update command and update the settings for the @IsNull_TerritoryId parameter as outlined in the following table (see Figure 31-9):

Property

Value

 

 

AllowObNull

True

ColumnName

 

DbType

Int32

Direction

Input

ParameterName

@IsNull_TerritoryID

Precision

0

ProviderType

Int

Scale

0

Size

0

SourceColumn

TerritoryID

SourceColumnNullMapping

False

SourceVersion

Original

 

 

Now that you’ve completed the Update command, not only can you navigate the customers; you can also make changes.

Figure 31-9

408

Data Binding and Object Data Sources

You also need to update the Insert command so it automatically generates both the modification date and the rowguid. Using the Query Builder, update the Insert command using the following table as a guide (see Figure 31-10):

Column

New Value

TerritoryID

@TerritoryID

Customer Type

@CustomerType

rowguid

NEWID()

ModifiedDate

GETDATE()

Unlike the Update method, you don’t need to change any of the parameters for this query. Both the Update and Insert queries for the Individual and Customer tables should work without modifications.

Figure 31-10

Inserting New Items

You now have a sample application that enables you to browse and make changes to an existing set of individual customers. The one missing piece is the capability to create a new customer. By default, the Add button on the BindingNavigator is automatically wired up to the AddNew method on the BindingSource, as shown earlier in this chapter. In this case, you actually need to set some default values and create entries in both the Individual and Contact tables in addition to the record that is created in the Customer table. To do this, you need to write our own logic behind the Add button.

The first step is to double-click the Add button to create an event handler for it. Make sure that you also remove the automatic wiring by setting the AddNewItem property to (None); otherwise, you will end up with two records being created every time you press the Add button. You can then modify the default event handler as follows to set initial values for the new Customer, as well as create records in the other two tables:

409

Chapter 31

Private Const cCustomerType As String = “I”

Private Sub BindingNavigatorAddNewItem_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _

Handles BindingNavigatorAddNewItem.Click

Dim drv As DataRowView

‘Create record in the Customer table

drv = TryCast(Me.CustomerBindingSource.AddNew, DataRowView) If drv Is Nothing Then Return

Dim cr As AdventureWorksDataSet.CustomerRow = _

TryCast(drv.Row, AdventureWorksDataSet.CustomerRow)

If cr Is Nothing Then Return cr.rowguid = Guid.NewGuid cr.CustomerType = cCustomerType cr.ModifiedDate = Now

‘Create record in the Contact table

drv = TryCast(Me.ContactBindingSource.AddNew, DataRowView) If drv Is Nothing Then Return

Dim ct As AdventureWorksDataSet.ContactRow = _

TryCast(drv.Row, AdventureWorksDataSet.ContactRow)

If ct Is Nothing Then Return ct.NameStyle = True ct.PasswordSalt = “” ct.PasswordHash = “” ct.rowguid = Guid.NewGuid ct.ModifiedDate = Now

‘Create record in the Individual table

drv = TryCast(Me.IndividualBindingSource.AddNew, DataRowView) If drv Is Nothing Then Return

Dim indiv As AdventureWorksDataSet.IndividualRow =

TryCast(drv.Row, AdventureWorksDataSet.IndividualRow)

indiv.CustomerRow = cr indiv.ContactRow = ct indiv.ModifiedDate = Now

End Sub

From this example, it seems that you are unnecessarily setting some of the properties — for example, PasswordSalt and PasswordHash being equal to an empty string. This is necessary to ensure that the new row meets the constraints established by the database. Because these fields cannot be set by the user, you need to ensure that they are initially set to a value that can be accepted by the database. Clearly, for a secure application, the PasswordSalt and PasswordHash would be set to appropriate values.

Running the application with this method instead of the automatically wired event handler enables you to create a new Customer record using the Add button. If you enter values for each of the fields, you can save the changes.

Validation

In the previous section, you added functionality to create a new customer record. If you don’t enter appropriate data upon creating a new record — for example, you don’t enter a first name — this record will be rejected when you click the Save button. In fact, an exception will be raised if you try to move away from this record. The schema for the AdventureWorksDataSet contains a number of constraints, such as FirstName can’t be null, which are checked when you perform certain actions, such as saving or moving

410

Data Binding and Object Data Sources

between records. If these checks fail, an exception is raised. You have two options. One, you can trap these exceptions, which is poor programming practice, as exceptions should not be used for execution control. Alternatively, you can pre-empt this by validating the data prior to the schema being checked. Earlier in the chapter, when you learned how the BindingNavigator automatically wires the AddNew method on the BindingSource, you saw that the OnAddNew method contains a call to a Validate method. This method propagates up and calls the Validate method on the active control, which returns a Boolean value that determines whether the action will proceed. This pattern is used by all the automatically wired events and should be used in the event handlers you write for the navigation buttons.

The Validate method on the active control triggers two events — Validating and Validated — that occur before and after the validation process, respectively. Because you want to control the validation process, add an event handler for the Validating event. For example, you could add an event handler for the Validating event of the First Name TextBox:

Private Sub FirstNameTextBox_Validating(ByVal sender As System.Object, _

ByVal e As System.ComponentModel.CancelEventArgs) _ Handles FirstNameTextBox.Validating

Dim firstNameTxt As TextBox = TryCast(sender, TextBox) If firstNameTxt Is Nothing Then Return

e.Cancel = firstNameTxt.Text = “”

End Sub

While this prevents users from leaving the text box until a value has been added, it doesn’t give them any idea why the application prevents them from proceeding. Luckily, the .NET Framework includes an ErrorProvider control that can be dragged onto the form from the Toolbox. This control behaves in a similar manner to the Tooltip control. For each control on the form, you can specify an Error string, which, when set, causes an icon to appear alongside the relevant control, with a suitable tooltip displaying the Error string. This is illustrated in Figure 31-11, where the Error string is set for the First Name text box.

Figure 31-11

411

Chapter 31

Clearly, you want only to set the Error string property for the First Name text box when there is no text. Following from the earlier example in which you added the event handler for the Validating event, you can modify this code to include setting the Error string:

Private Sub FirstNameTextBox_Validating(ByVal sender As System.Object, _

ByVal e As System.ComponentModel.CancelEventArgs) _ Handles FirstNameTextBox.Validating

Dim firstNameTxt As TextBox = TryCast(sender, TextBox) If firstNameTxt Is Nothing Then Return

e.Cancel = firstNameTxt.Text = “” If firstNameTxt.Text = “” Then

Me.ErrorProvider1.SetError(firstNameTxt, “First Name must be specified”)

Else

Me.ErrorProvider1.SetError(firstNameTxt, Nothing) End If

End Sub

You can imagine that having to write event handlers that validate and set the error information for each of the controls can be quite a lengthy process, so the following component, for the most part, gives you designer support:

Imports System.ComponentModel

Imports System.Drawing.Design

<ProvideProperty(“Validate”, GetType(Control))> _

Public Class ControlValidator

Inherits Component

Implements IExtenderProvider

#Region “Rules Validator” Private Structure Validator

Public Rule As Predicate(Of IRulesList.RuleParams) Public Information As ValidationAttribute

Public Sub New(ByVal r As Predicate(Of IRulesList.RuleParams), _ ByVal info As ValidationAttribute)

Me.Rule = r Me.Information = info

End Sub

End Structure

#End Region

Private m_ErrorProvider As ErrorProvider

Private rulesHash As New Dictionary(Of String, Validator)

Public controlHash As New Dictionary(Of Control, Boolean)

Public Sub New(ByVal container As IContainer) MyBase.New()

container.Add(Me) End Sub

#Region “Error provider and Rules”

Public Property ErrorProvider() As ErrorProvider

412

Data Binding and Object Data Sources

Get

Return m_ErrorProvider End Get

Set(ByVal value As ErrorProvider) m_ErrorProvider = value

End Set End Property

Public Sub AddRules(ByVal ruleslist As IRulesList)

For Each rule As Predicate(Of IRulesList.RuleParams) In ruleslist.Rules Dim attributes As ValidationAttribute() = _

TryCast(rule.Method.GetCustomAttributes _ (GetType(ValidationAttribute), True), _

ValidationAttribute())

If Not attributes Is Nothing Then

For Each attrib As ValidationAttribute In attributes rulesHash.Add(attrib.ColumnName.ToLower, _

New Validator(rule, attrib))

Next End If

Next End Sub #End Region

#Region “Extender Provider to turn validation on”

Public Function CanExtend(ByVal extendee As Object) As Boolean _

Implements System.ComponentModel.IExtenderProvider.CanExtend Return TypeOf (extendee) Is Control

End Function

Public Sub SetValidate(ByVal control As Control, _ ByVal shouldValidate As Boolean)

If shouldValidate Then

AddHandler control.Validating, AddressOf Validating End If

controlHash.Item(control) = shouldValidate End Sub

Public Function GetValidate(ByVal control As Control) As Boolean If controlHash.ContainsKey(control) Then

Return controlHash.Item(control) End If

Return False End Function

#End Region

#Region “Validation”

Private ReadOnly Property ItemError(ByVal ctrl As Control) As String Get

Try

If ctrl.DataBindings.Count = 0 Then Return “” Dim key As String =

ctrl.DataBindings.Item(0).BindingMemberInfo.BindingField Dim bs As BindingSource =

TryCast(ctrl.DataBindings.Item(0).DataSource, BindingSource)

413

Chapter 31

If bs Is Nothing Then Return “”

Dim drv As DataRowView = TryCast(bs.Current, DataRowView)

If drv Is Nothing Then Return “”

Dim valfield As String = ctrl.DataBindings.Item(0).PropertyName Dim val As Object = ctrl.GetType.GetProperty(valfield, _

New Type() {}).GetValue(ctrl, Nothing) Return ItemError(drv, key, val)

Catch ex As Exception Return “”

End Try End Get

End Property

Private ReadOnly Property ItemError(ByVal drv As DataRowView, ByVal columnName As String, ByVal newValue As Object) As String

Get

columnName = columnName.ToLower

If Not rulesHash.ContainsKey(columnName) Then Return “” Dim p As Validator = rulesHash.Item(columnName)

If p.Rule Is Nothing Then Return “”

If p.Rule(New IRulesList.RuleParams(drv.Row, newValue)) Then Return “”

If p.Information Is Nothing Then Return “”

Return p.Information.ErrorString

End Get

End Property

Private Sub Validating(ByVal sender As Object, ByVal e As CancelEventArgs) Dim err As String = InternalValidate(sender)

e.Cancel = Not (err = “”) End Sub

Private Function InternalValidate(ByVal sender As Object) As String If Me.m_ErrorProvider Is Nothing Then Return “”

Dim ctrl As Control = TryCast(sender, Control) If ctrl Is Nothing Then Return “”

If Not Me.controlHash.ContainsKey(ctrl) OrElse Not Me.controlHash.Item(ctrl) Then Return “”

Dim err As String = Me.ItemError(ctrl) Me.m_ErrorProvider.SetError(ctrl, err) Return err

End Function

Private Sub ChangedItem(ByVal sender As Object, ByVal e As EventArgs) InternalValidate(sender)

End Sub #End Region

#Region “Validation Attribute”

<AttributeUsage(AttributeTargets.Method)> _

Public Class ValidationAttribute

Inherits Attribute

Private m_ColumnName As String

414

Data Binding and Object Data Sources

Private m_ErrorString As String

Public Sub New(ByVal columnName As String, ByVal errorString As String) Me.ColumnName = columnName

Me.ErrorString = errorString End Sub

Public Property ColumnName() As String Get

Return m_ColumnName End Get

Set(ByVal value As String) m_ColumnName = value

End Set End Property

Public Property ErrorString() As String Get

Return m_ErrorString End Get

Set(ByVal value As String) m_ErrorString = value

End Set End Property

End Class #End Region

#Region “Rules Interface”

Public Interface IRulesList

Structure RuleParams

Public ExistingData As DataRow Public NewData As Object

Public Sub New(ByVal data As DataRow, ByVal newStuff As Object) Me.ExistingData = data

Me.NewData = newStuff End Sub

End Structure

ReadOnly Property Rules() As Predicate(Of RuleParams)()

End Interface

#End Region

End Class

The ControlValidator has a number of parts that work together to validate and provide error information. First, to enable validation of a control, the ControlValidator exposes an Extender Provider, which allows you to indicate whether the ControlValidator on the form should be used for validation. The right pane in Figure 31-12 shows the Properties window for the First Name text box, in which the Validate property has been set to True. When the First Name text box is validated, the ControlValidator1 control will be given the opportunity to validate the FirstName property.

415