18. ADO元件

當處理儲存在記憶體中一大塊資料時(例如有關客戶的所有資訊),正規的COM元件非常好用。但當從資料庫中讀取和寫入資料時,COM元件就變得不好用了。雖然可實作以自定的Load和Save方法為基礎的persistence機制(或以Microsoft Visual Basic 6中心的persistable類別為基礎),但這意味著對元件作者和使用元件的程式設計師而言有大量的額外工作。

Visual Basic 6提供對於在ADO中新的連結能力問題的新解決方案。本章中,筆者會說明如何建立從資料庫中讀取資料的資料來源類別。也會說明如何建立客戶端的類別,其將自己連結到資料來源以便獲得資料,且當另一記錄成為目前的記錄時,會自動地接收訊息。然後將這些類別轉成COM元件,以便於更容易地重複使用之。也會說明如何建立ADO資料控制項的自定版本,一個在Visual Basic 5不可能辦到的功能(其連結能力只能建立資料消費者控制項,而不能做到資料來源的部分)。對於所有自建的資料感知類別可以完全的被當作Visual Basic自行提供的資料感知元件使用,好比DataEnvironment設計師和ADO資料控制項。

資料來源類別
 

要建立資料來源類別,需要以下的基本步驟。首先,新增一個Microsoft ActiveX Data Objects 2.0 (or 2.1) Library的引用。然後,設定類別的DataSourceBehavior屬性為1-vbDataSource,其會自動增加Microsoft Data Source Interfaces type library (Msdatsrc.tlb)的引用。現在可使用此類別的GetDataMember事件、DataMembers屬性及DataMemberChanged方法。可在任一型態專案的Private類別或ActiveX DLL專案的Public類別中將DataSourceBehavior屬性設為1-vbDataSource,但在ActiveX EXE專案卻無法如法炮製,因為資料來源介面無法跨程序來進行。

當新增一個類別模組到專案時,也可藉由選擇適當的範本來建立一個資料來源類別:此一情況下,可以得到一個含有骨架程式的類別,但還是必須手動地新增Msdatsrc.tlb library的引用。使用資料表單精靈也可建立資料來源類別。

GetDataMember事件
 

建立資料來源的關鍵在於寫在GetDataMember事件的程式碼。此事件接收DataMember參數-定義資料消費者正要求哪一個特定元素的字串-與一個Object形態的Data參數。在最簡單的情況下,可忽略第一個參數,且在Data參數中傳回一個支援所需ADO介面的物件。也可傳回在應用程式其他地方建立的ADO Recordset、資料來源類別、或OLEDBSimpleProvider類別(本章 〈OLE DB Simple Providers〉 節會講到)。

在隨書光碟內有個建立在ArrayDataSource類別上的範例程式原始程式。此類別的目的是使用連結控制項來瀏覽二維陣列變數的內容:載入資料到陣列中,將此陣列當成類別SetArray方法的參數來呼叫,然後以DataGrid或其他資料感知元件控制項(data-aware control)來顯示內容。使用者可修正已存在的資料、刪除記錄,甚至新增一筆新記錄。當完成編輯時,可呼叫類別的GetArray方法來取得陣列的新內容。

ArrayDataSource類別如同大多數的資料來源類別般,包含一個ADO Recordset物件。SetArray方法建立Recordset、新增欄位(欄位名已傳遞給Fields陣列參數),然後用SetArray方法Values陣列參數內的資料來填滿Recordset:

Private rs As ADODB.Recordset      ' Module-level variable
Sub SetArray(Values As Variant, Fields As Variant)
    Dim row As Long, col As Long
    ' Build a new ADO Recordset.
    If Not (rs Is Nothing) Then
        If rs.Status = adStateOpen Then rs.Close
    End If
    Set rs = New ADODB.Recordset
    ' Create the Fields collection.
    For col = LBound(Fields) To UBound(Fields)
        rs.Fields.Append Fields(col), adBSTR
    Next
    ' Move data from the array to the Recordset.
    rs.Open
    For row = LBound(Values) To UBound(Values)
        rs.AddNew
        For col = 0 To UBound(Values, 2)
            rs(col) = Values(row, col)
        Next
    Next
    rs.MoveFirst
    ' Inform consumers that the data has changed.
    DataMemberChanged ""
End Sub

DataMemberChanged方法的呼叫通知連結控制項(廣義來講,為資料消費者),新資料是可用的。SetArray方法的兩個參數都宣告為Variant形態,所以可傳遞任何資料型態的陣列。建立Recordset後,在GetDataMember事件中可被傳回。當資料消費者要求資料或當DataMemberChanged方法被呼叫時,會第一次引發此事件:

' Return the Recordset to the data consumer.
Private Sub Class_GetDataMember(DataMember As String, Data As Object)
    Set Data = Recordset
End Sub
' Provides "Safe" access to the Recordset, 
' in that it raises a meaningful error if the Recordset is set to Nothing.
Property Get Recordset() As ADODB.Recordset
    If rs Is Nothing Then
        Err.Raise 1001, , "No data array has been provided"
    Else
        Set Recordset = rs
    End If
End Property

透過公開的Recordset屬性,事件程序可引用私有的rs變數;在呼叫SetArray方法前,若指定資料來源給連結控制項,會導致一個有意義的錯誤訊息,不同於標準的「物件變數或With區域變數未設定」錯誤訊息。資料來源類別應要涵蓋所有預期從ADO來源得來的屬性及方法,包括所有Movexxxx方法、AddNew方法和 Delete方法、EOF屬性及BOF屬性等。以下程式碼簡單地透過Recordset屬性來代表內部的rs變數,其確保適當的錯誤檢查會執行:

' Partial listing of properties and methods
Public Property Get EOF() As Boolean
    EOF = Recordset.EOF
End Property
Public Property Get BOF() As Boolean
    BOF = Recordset.BOF
End Property
Public Property Get RecordCount() As Long
    RecordCount = Recordset.RecordCount
End Property
Sub MoveFirst()
    Recordset.MoveFirst
End Sub
Sub MovePrevious()
    Recordset.MovePrevious
End Sub
' And so on...

當應用程式要求時,類別中的程式碼需要轉換Recordset內的資料為Variant陣列。轉換過程發生在GetArray方法內。

Function GetArray() As Variant
    Dim numFields As Long, row As Long, col As Long
    Dim Bookmark As Variant
    ' Remember the current record pointer.
    Bookmark = Recordset.Bookmark
    
    ' Create the result array, and fill it with data from the Recordset.
    numFields = rs.Fields.Count
    ReDim Values(0 To rs.RecordCount - 1, 0 To numFields - 1) As String
    ' Fill the array with data from the Recordset.
    rs.MoveFirst
    For row = 0 To rs.RecordCount - 1
        For col = 0 To numFields - 1
            Values(row, col) = rs(col)
        Next
        rs.MoveNext
    Next
    GetArray = Values
    ' Restore the record pointer.
    rs.Bookmark = Bookmark
End Function

在隨書光碟中有此一類別的完整描述,其可支援其他屬性,包括BOFAction和EOFAction屬性,其讓類別的行為模式類似資料控制項。為了測試ArrayDataSource類別,建立有三個TextBox控制項和一系列導覽按鈕的表單,如圖18-1。然後在Form_Load事件程序新增下列程式碼:

Dim MyData As New ArrayDataSource           ' Module-level variable
Private Sub Form_Load()
    ReDim Fields(0 To 2) As String           ' Create the Fields array.
    Fields(0) = "ID"
    Fields(1) = "Name"
    Fields(2) = "Department"
    
    ReDim Values(0 To 3, 0 To 2) As String   ' Create the Values array.
    Values(0, 0) = 100                       ' ID field
    Values(0, 1) = "Christine Johnson"       ' Name field
    Values(0, 2) = "Marketing"               ' Department field
    ' Fill other records (omitted...)
    MyData.SetArray Values, Fields           ' Initialize the data source.
    ' Bind the controls.
    Set txtID.DataSource = MyData
    txtID.DataField = "ID"
    Set txtName.DataSource = MyData
    txtName.DataField = "Name"
    Set txtDepartment.DataSource = MyData
    txtDepartment.DataField = "Department"
End Sub

當應用程式需要擷取使用者編輯過的資料時,可呼叫GetArray方法:

Dim Values() As String
Values = MyData.GetArray()


 

圖18-1 用戶端表單用來測試ArrayDataSource類別

DataMember屬性的支援
 

ArrayDataSource類別是以Visual Basic 6建立的資料來源中最簡單的類型,其無須考慮傳遞給GetDataMember事件的DataMember參數。可藉由在連結控制項中新增對於DataMember屬性的支援來增強你的類別。所必須做的是根據接收到的DataMember,建立並傳回一個不相同的Recordset。

這裡有個名為FileTextDataSource的資料來源類別,其連接自身消費者到以分號區隔的文字檔。若要連接一或多個控制項到此類別,必須在控制項的DataMember屬性中指定文字檔名。

' Code in the client form
Dim MyData As New TextFileDataSource

Private Sub Form_Load()
    ' This is the path for data files.
    MyData.FilePath = DB_PATH
    ' Bind the text controls. (Their DataField was set at design time.)
    Dim ctrl As Control
    For Each ctrl In Controls
        If TypeOf ctrl Is TextBox Then
            ctrl.DataMember = "Publishers"
            Set ctrl.DataSource = MyData
        End If
    Next
End Sub

TextFileDataSource類別模組比較簡單的ArrayDataSource類別包含更多的程式碼,不過大部分為分析此一文字檔,並將其內容搬到此私有的Recordset中。文字檔的第一行假定為以分號區隔的欄位名稱:

Const DEFAULT_EXT = ".DAT"        ' Default extension for text files
Private rs As ADODB.Recordset
Private m_DataMember As String, m_File As String, m_FilePath As String
Private Sub Class_GetDataMember(DataMember As String, Data As Object)
    If DataMember = "" Then Exit Sub
    ' Re-create the Recordset only if necessary.
    If DataMember <> m_DataMember Or (rs Is Nothing) Then
        LoadRecordset DataMember
    End If
    Set Data = rs
End Sub
Private Sub LoadRecordset(ByVal DataMember As String)
    Dim File As String, fnum As Integer
    Dim row As Long, col As Long, Text As String
    Dim Lines() As String, Values() As String

    On Error GoTo ErrorHandler
    File = m_FilePath & DataMember
    If InStr(File, ".") = 0 Then File = File & DEFAULT_EXT

    ' Read the contents of the file in memory.
    fnum = FreeFile()
    Open File For Input As #fnum
    Text = Input$(LOF(fnum), #fnum)
    Close #fnum
    
    ' Close the current Recordset, and create a new one.
    CloseRecordset
    Set rs = New ADODB.Recordset
    ' Convert the long string into an array of records.
    Lines() = Split(Text, vbCrLf)
    ' Get the field names, and append them to the Fields collection.
    Values() = Split(Lines(0), ";")
    For col = 0 To UBound(Values)
        rs.Fields.Append Values(col), adBSTR
    Next
    ' Read the actual values, and append them to the Recordset.
    rs.Open
    For row = 1 To UBound(Lines)
        rs.AddNew
        Values() = Split(Lines(row), ";")
        For col = 0 To UBound(Values)
            rs(col) = Values(col)
        Next
    Next
    rs.MoveFirst
    
    ' Remember DataMember and File for the next time.
    m_DataMember = DataMember
    m_File = File
    Exit Sub
ErrorHandler:
    Err.Raise 1001, , "Unable to load data from " & DataMember
End Sub
' If the Recordset is still open, close it.
Private Sub CloseRecordset()
    If Not (rs Is Nothing) Then rs.Close
    m_DataMember = ""
End Sub

Visual Basic文件建議當多個消費者要求相同的DataMember時,則傳回相同的Recordset。基於此一理由,類別將DataMember參數存在m_DataMember變數,且只在絕對需要時才重新載入文字檔。然而,當筆者追蹤原始程式碼時,發現當應用程式指定類別的實體給第一個連結控制項的DataSource屬性時,GetDataMember事件被呼叫時,只有一次其DataMember參數值為非空字串。之後每次,此事件都接收到空字串。

在隨書光碟的TextFileDataSource類別包括許多在此尚無機會說明的其他特質。圖18-2顯示的範例程式,載入兩個表單、一個以記錄型態表現的文字檔及一個以資料表顯示的文字檔。因為在這兩個表單中的控制項都連接到同一個TextFileDataSource類別中,每次在某表單中移動記錄指標或編輯欄位值時,另一個表單的控制項會立即更新。此一類別也有Flush方法,此方法將新值寫入磁碟中。在Class_Terminate事件中Flush方法會自動執行,所以當最後一個表單載出且資料來源物件被釋放時,Flush方法會自動更新資料檔。


 

圖18-2 TextFileDataSource類別的範例程式開啟以相同資料檔為主但不同型態的畫面。若視窗使用相同類別的實體,其會自動地進行同步。

TextFileDataSource類別也提供如何新增欄位到DataMembers集合去通知資料消費者(data consumers)關於DataMembers。此類別模組在Property Let FilePath程序中實作此一特質,其載入在特定目錄下所有資料檔案的集合:

Public Property Let FilePath(ByVal newValue As String)
    If newValue <> m_FilePath Then
        m_FilePath = newValue
        If m_FilePath <> "" And Right$(m_FilePath, 1) <> "\" Then
            m_FilePath = m_FilePath & "\"
        End If
        RefreshDataMembers
    End If
End Property
' Rebuild the DataMembers collection.
Private Sub RefreshDataMembers()
    Dim File As String
    DataMembers.Clear
    ' Load all the file names in the directory.
    File = Dir$(m_FilePath & "*" & DEFAULT_EXT)
    Do While Len(File)
        ' Drop the default extension.
        DataMembers.Add Left$(File, Len(File) - Len(DEFAULT_EXT))
        File = Dir$()
    Loop
End Sub

TextFileDataSource類別在執行階段連接到消費者。因此無法填滿DataMembers集合,因為用戶端沒有獲得此資訊。不過當建立如資料來源般的ActiveX控制項時,此技巧變成有用的,因為所有有效的DataMembers元件列表會正確地出現在連接到ActiveX控制項的屬性視窗。

自製ActiveX資料控制項
 

建立一個自製資料控制項是簡單的,因為ActiveX控制項可以完全如資料來源類別與COM元件般運作。所以可建立一個符合所需的使用者介面,如圖18-3,設定DataSourceBehavior屬性為1-vbDataSource,並且新增開發者期望的資料控制項的所有屬性和方法,例如ConnectionString、RecordSource、EOFAction及BOFAction。若完全複製ADO資料介面,甚至可以用自己的資料控制項替換標準的ADO控制項,而不需要改變表單內任何程式碼。


 

圖18-3 資料控制項包括新增和刪除記錄的按鈕。

連接到一般ADO來源的資料控制項不需要自行製造ADO Recordset。如之前所講的資料來源類別。相反地,其會自我建立以Public屬性值為基礎的ADO Connection物件和ADO Recordset物件,然後在GetDataMember事件中傳遞Recordset給消費者。以下是MyDataControl UserControl模組的部分程式碼。(完整的程式碼在隨書光碟中)。

Private cn As ADODB.Connection, rs As ADODB.Recordset
Private CnIsInvalid As Boolean, RsIsInvalid As Boolean

Private Sub UserControl_GetDataMember(DataMember As String, Data As Object)
    On Error GoTo Error_Handler
    ' Re-create the connection if necessary.
    If cn Is Nothing Or CnIsInvalid Then
        ' If the Recordset and the connection are open, close them.
        CloseConnection
        ' Validate the ConnectionString property.
        If Trim$(m_ConnectionString) = "" Then
            Err.Raise 1001, , "ConnectionString can't be an empty string"
        Else
            ' Open the connection.
            Set cn = New ADODB.Connection
            If m_Provider <> "" Then cn.Provider = m_Provider
            cn.Open m_ConnectionString
            CnIsInvalid = False
        End If
    End If
    ' Re-create the Recordset if necessary.
    If rs Is Nothing Or RsIsInvalid Then
        Set rs = New ADODB.Recordset
        rs.CursorLocation = m_CursorLocation
        rs.Open RecordSource, cn, CursorType, LockType, CommandType
        rs.MoveFirst
        RsIsInvalid = False
    End If
    ' Return the Recordset to the data consumer.
    Set Data = rs
    Exit Sub

Error_Handler:
    Err.Raise Err.Number, Ambient.DisplayName, Err.Description
    CloseConnection
End Sub
' Close the Recordset and the connection in the correct way.
Private Sub CloseRecordset()
    If Not rs Is Nothing Then
        If rs.State <> adStateClosed Then rs.Close
        Set rs = Nothing
    End If
End Sub
Private Sub CloseConnection()
    CloseRecordset
    If Not cn Is Nothing Then
        If cn.State <> adStateClosed Then cn.Close
        Set cn = Nothing
    End If
End Sub

對於瀏覽包含在UserControl模組的Recordset的程式而言,此自定的資料控制項也與資料來源類別不同。在MyDataControl模組內,此六個瀏覽按鈕屬於cmdMove控制項陣列,其乃方便管理:

Private Sub cmdMove_Click(Index As Integer)
    If rs Is Nothing Then Exit Sub    ' Exit if the Recordset doesn't exist.
    Select Case Index
        Case 0
            rs.MoveFirst
        Case 1
            If rs.BOF Then
                Select Case BOFAction
                    Case mdcBOFActionEnum.mdcBOFActionMoveFirst
                        rs.MoveFirst
                    Case mdcBOFActionEnum.mdcBOFActionBOF
                        ' Do nothing.
                End Select
            Else
                rs.MovePrevious
            End If
        Case 2
            If rs.EOF = False Then rs.MoveNext
            If rs.EOF = True Then
                Select Case EOFAction
                    Case mdcEOFActionEnum.mdcEOFActionAddNew
                        rs.AddNew
                    Case mdcEOFActionEnum.mdcEOFActionMoveLast
                        rs.MoveLast
                    Case mdcEOFActionEnum.mdcEOFActionEOF
                        ' Do nothing.
                End Select
            End If
        Case 3
            rs.MoveLast
        Case 4
            rs.AddNew
        Case 5
            rs.Delete
    End Select
End Sub

每次用戶端傳遞一個值給影響Connection或Recordset的屬性,MyDataControl模組的程式碼會重設cn或rs變數為Nothing,並設定CnIsInvalid或RsIsInvalid變數為True,以便在下個GetDataMember事件中,Connection或Recordset可正確地重建。

Public Property Get ConnectionString() As String
    ConnectionString = m_ConnectionString
End Property
Public Property Let ConnectionString(ByVal New_ConnectionString As String)
    m_ConnectionString = New_ConnectionString
    PropertyChanged "ConnectionString"
    CnIsInvalid = True
End Property

記住當控制項將終結時,必須關閉Connection。

Private Sub UserControl_Terminate()
    CloseConnection
End Sub

資料消費者類別(data consumer classes)
 

對於資料消費者的解釋,我們可以說它是一個將本身與資料來源結合的類別或元件。資料消費者物件可分為以下兩種:簡易性資料消費者及複雜性資料消費者。簡易性資料消費者類別或元件將自己本身一個或多個屬性連結至資料來源目前的資料列,就如同ActiveX控制項的多重連結屬性。而複雜性消費者則可以將它的屬性連結至多個資料列,比如說一個grid控制項。

簡易性資料消費者
 

將資料由來源傳送至消費者時,消費者是一個被動的個體。而將資料由來源傳送至消費者的實際物件是BindingCollection物件。

BindingCollection物件
 

在建立BindingCollection物件之前,需要在設定引用項目對話選單中選擇Microsoft Data Binding Collection library選項。BindingCollection最重要的成員是DataSource屬性以及Add方法。要在資料來源以及資料消費者間啟動連線前,需要先指定資料來源物件給BindingCollection的DataSource屬性,然後呼叫Add方法。下面是呼叫Add方法完整的語法:

Add(BoundObj, PropertyName, DataField, [DataFormat], [Key]) As Binding

其中BoundObj為資料消費者物件,PropertyName是物件中連結至資料來源欄位的屬性名稱,DataField是資料來源中的欄位名稱,DataFormat則是StdDataFormat物件,其為一個影響資料在資料來源與資料消費者傳輸過程中影響資料傳輸格式的非必要物件,而Key是此新的連結物件在集合物件中的Key值。可重複呼叫多次Add方法來連結一個或多個消費者的多個屬性。

一般的資料來源通常是ADO Recordset物件,但也可使用DataEnvironment物件,OLE DB Simple Provider,及其他任何的資料來源類別或您在程式碼中所定義的元件。下面的範例說明如何藉由一個ADO Recordset將兩個TextBox控制項連結至資料庫表格:

Const DBPath = "C:\Program Files\Microsoft Visual Studio\Vb98\NWind.mdb"
Dim cn As New ADODB.Connection, rs As New ADODB.Recordset 
Dim bndcol As New BindingCollection
' Open the Recordset.
cn.Open "Provider=Microsoft.Jet.OLEDB.3.51;Data Source=" & DBPATH
rs.Open "Employees", cn, adOpenStatic, adLockReadOnly 
' Use the Bindingcollection object to bind two TextBox controls to the
' FirstName and LastName fields of the Employees table.
Set bndcol.DataSource = rs
bndcol.Add txtFirstName, "Text", "FirstName", , "FirstName"
bndcol.Add txtLastName, "Text", "LastName", , "LastName"

藉由設定StdDataFormat物件的Type及Format屬性,可在消費者物件中控制資料的格式,然後將格式物件透過BindCollection集合物件的Add方法的第四個參數傳送出去,如下面範例所示:

Dim DateFormat As New StdDataFormat
DateFormat.Type = fmtCustom
DateFormat.Format = "mmmm dd, yyyy"
' One StdDataFormat object can serve multiple consumers.
bndcol.Add txtBirthDate, "Text", "BirthDate", DateFormat, "BirthDate"
bndcol.Add txtHireDate, "Text", "HireDate", DateFormat, "HireDate"

如果資料來源包含多個DataMember物件,像是DataEnvironment物件裡的成員一樣,當透過BindingCollection集合物件的DataMember屬性將選擇的欄位與資料消費者連結時,實際上就如同將控制項連結至一個ADO資料控制項一樣。

BindingCollection集合物件還包含其他幾個屬性及方法,可對連結有更多的控制。UpdateMode屬性決定資料在資料來源中何時被更新:而該屬性預設值為1-vbUpdateWhenPropertyChanges,資料來源會在屬性值改變時即刻地被更新,反之另一屬性值2-vbUpdateWhenRowChanges會在資料錄指標移動到另一筆資料錄時才會將資料來源更新。當屬性值為0-vbUsePropertyAttributes,資料的更新便會取決於屬性對話盒中的UpdateImmediate選項。

當每次執行Add方法時,實際上是在集合物件中加入一個新的Binding物件。可在稍後查詢查詢Binding物件的屬性來得到關於連結方面的訊息。每個Binding物件包含下列屬性:Object(連結資料消費者的參照)、PropertyName(連結屬性的名稱)、DataField(資料來源中的欄位名稱)、DataChanged(若消費者中的資料有更動時,屬性值為True)、DataFormat(用來格式資料的StdDataFormat物件)及Key(Binding物件位於集合物件中的鍵值)。比方說,可透過下面的程式碼來察知位於消費者中的資料值是否已被更動了:

Dim bind As Binding, changed As Boolean
For Each bind in bndcol
    changed = changed Or bndcol.DataChanged
Next
If changed Then Debug.Print "Data has been changed"

若指定key值給Binding物件,便可直接地讀取或修改它的屬性值:

' Set the ForeColor of the TextBox control bound to the HireDate field.
bndcol("HireDate").Object.ForeColor = vbRed

BindingCollection的UpdateControls方法會將目前資料來源的資料錄中的值更新給所有消費者以及重設所有Binding物件的DataChanged屬性為False。

最後,可利用BindingCollection的Error事件來捕捉任何發生的錯誤。在使用BindingCollection的Error事件前,必須先使用WithEvents來宣告:

Dim WithEvents bndcol As BindingCollection
Private Sub bndcol_Error(ByVal Error As Long, ByVal Description As String,_
    ByVal Binding As MSBind.Binding, fCancelDisplay As Boolean)
    ' Deal here with binding errors.
End Sub

其中Error為錯誤代碼,Description為錯誤敘述,Binding為產生錯誤的Binding物件,及fCancelDisplay為一布林值,若不想顯示出標準的錯誤訊息的話,可將其設為False。


注意

當我們將控制項的屬性與資料來源的欄位連結時,要確定控制項在資料更動時正確地將必要的通知訊息遞送給連結機制。例如將Label或Frame控制項的Caption屬性連結至資料來源,但在程式碼中透過程式更改了Caption的屬性值卻沒有告知資料來源資料已更動。結果所更新的值必然沒有更動至資料來源裡。在此情況下,必須利用BindingCollection的DataChanged屬性強迫通知更新訊息。


資料消費者類別及元件
 

要建立一個簡易性資料消費者類別,只需要在屬性視窗中設定類別的DataBindingBehavior屬性為1-vbSimpleBound。這個設定會在類別中新增兩個方法:PropertyChange以及CanPropertyChange。

使用簡易性資料消費者類別或元件其實就像是在使用一個具有資料連結功能的ActiveX控制項。在所有被連結的屬性的Property Let副程式中,必須確定屬性可以透過呼叫CanPropertyChange函式來改變。然後呼叫PropertyCange方法來告知連結機制屬性值已的確更改了。(要注意的是CanPropertyChange方法在Visual Basic中總是會傳回True值,如同筆者在十七章中的 〈PropertyChange and CanPropertyChange方法〉 所解釋的)。下面的程式碼為隨書光碟中的範例程式片斷,其告訴了我們簡易的CEmployee資料消費者類別使用它的FirstName屬性:

' In the CEmployee class module
Dim m_FirstName As String

Property Get FirstName() As String
    FirstName = m_FirstName
End Property
Property Let FirstName(ByVal newValue As String)
    If newValue <> m_FirstName Then
        If CanPropertyChange("FirstName") Then
            m_FirstName = newValue
            PropertyChanged "FirstName"
        End If
    End If
End Property

可透過BindingCollection集合物件將資料來源裡的資料欄位與您資料消費者類別的屬性連結。連結的動作可以在使用者端的表單或模組(如同上節中所看到的)中或資料消費者類別本身中執行。通常後者的方式比較可取,因為它將程式碼封裝在類別模組中,且避免了在使用者端被散播。如果遵從這個方式的話,必須提供一個讓使用者傳遞資料來源給類別的方法:此方法可以是資料來源類別,ADO資料控制項或者是Recordset,或者是DataEnvironment物件。此類別應該可以使用此參照就如同使用其內部BindingCollection的DataSource屬性般地自然:

' In the CEmployee class module
Private bndcol As New BindingCollection

Property Get DataSource() As Object
    Set DataSource = bndcol.DataSource
End Property
Property Set DataSource(ByVal newValue As Object)
    Set bndcol = New BindingCollection
    Set bndcol.DataSource = newValue
    bndcol.Add Me, "FirstName", "FirstName", , "FirstName"
    bndcol.Add Me, "LastName", "LastName", , "LastName"
    bndcol.Add Me, "BirthDate", "BirthDate", , "BirthDate"
End Property

下面的程式碼說明使用者的表單如何連結CEmployee類別至一個Recordset:

Dim cn As New ADODB.Connection, rs As New ADODB.Recordset
Dim employee As New CEmployee
cn.Open "Provider=Microsoft.Jet.OLEDB.3.51;" _
    & "Data Source="C:\Program Files\Microsoft Visual Studio\Vb98\NWind.mdb"
rs.Open "Employees", cn, adOpenKeyset, adLockOptimistic
Set employee.DataSource = rs

當程式更動了資料消費者類別裡的某個資料連結屬性的值時,其位於資料來源裡的相關資料欄位也會更新,假設該資料來源是可更動的。但是資料來源欄位被更動的精確時間是取決於BindingCollection物件的UpdateMode設定。如果UpdateMode的值為2-vbUpdateWhenRowChanges,資料來源便只會再當其他的資料錄變成目前的資料錄時才會更新,反之若屬性值為1-vbUpdateWhenPropertyChanges的話,Recordset便會隨即地改變。如果將UpdateMode設為0-vbUsePropertyAttribute,資料來源只會在當副程式屬性對話盒中的Update Immediately屬性被勾選起來時才會被更新。


說明

即使當資料來源是連結至資料庫的ADO Recordset,但並不表示更新資料來源時資料庫也會隨即被更新,但如果新的屬性值是指定給Field的Value屬性時,便無上述的問題。在這裡有一個方法可以強迫資料庫隨即更新,那就是透過Recordset的Move方法,藉由參數0來讓整個資料庫更新。這個方式實際上並不是真的移動資料錄指標,而是重新更新目前Fields集合物件在資料庫中的內容。奇怪的是,Recordset的Update方法在此情況下並無法正常運作。


這裡還有該特色應用後所產生的奇怪之處:0-vbUpdateWhenPropertyChanges的設定似乎無法像文件上所述般地運作,且其無法隨即地更新Recordset中的值。所以當屬性改變而要更動Recordset的唯一方式是設定0-vbUsePropertyAttributes以及勾選副程式屬性對話盒中的立即更新選項。

複雜性資料消費者
 

要建立一個複雜性資料消費者其實只比建立簡易資料消費者稍微困難一些。而絕大部分困難之處只在於缺少完整的說明文件。建立複雜信資料消費者類別的第一步是設定DataBindingBehavior屬性為2-vbComplexBound。也可以採取另外一個方式,在建立一個新的類別模組時選取建立Complex Data Consumer範本。以上兩種情況裡,都會發現兩個屬性─DataMember以及DataSource ─被加進了類別模組中:

Public Property Get DataSource() As DataSource
End Property
Public Property Set DataSource(ByVal objDataSource As DataSource)
End Property

Public Property Get DataMember() As DataMember
End Property
Public Property Let DataMember(ByVal DataMember As DataMember)
End Property

當在UserControl模組中設定了DataBindingBehavior為2-vbComplexBound時,Visual Basic並不會建立此兩個屬性的範本─您得自己手動加上去。

運作起來類似複雜性資料消費者的ActiveX控制項有些類似Grid控制項。它們會包含DataMember以及DataSource屬性,但是不像類似簡易性資料消費者的ActiveX控制項,這些屬性並不是Extender屬性。不能指望副程式屬性對話盒中設定什麼來自動地連結資料欄位,並且必須自己實際讓作這兩個屬性運作。

現在需要在設定引用項目對話方塊中加入幾個新的型態函式庫。在建立複雜性資料消費者時,需要使用到Microsoft Data Source Interfaces(Msdatsrc.tlb)、Microsoft Data Binding Collection(msbind.dll),當然還需要使用到Microsoft ActiveX Data Object 2.0(或者是2.1)的函式庫。Microsoft Data Source Interface涵蓋DataSource介面,透過它可讓您的類別支援資料來源屬性,像是ADO Recordset、ADO Data控制項及DataEnvironment物件。

在隨書附贈的光碟中可以找到ProductGrid ActiveX控制項的完整程式碼,如圖18-4所示。此ActiveX控制項會建立一個ListView控制項讓您觀看Nwind.mdb資料庫中的Product表格。筆者使用了ActiveX控制項介面精靈來建立該控制項大部分的屬性以及事件,像是Font、BackColor、ForeColor、CheckBoxes、FullRowSelection及所有滑鼠及鍵盤的事件。這裡唯一必須撰寫的部分是連結機制的部分。ProductGrid模組的宣告部分包含在下面私有變數宣告中:

Private WithEvents rs As ADODB.Recordset
Private bndcol As New BindingCollection
Private m_DataMember As String

而DataMember屬性我們使用了m_DataMember私有變數以及屬性副程式Get及Let來間接處理:

Public Property Get DataMember() As String
    DataMember = m_DataMember
End Property
Public Property Let DataMember(ByVal newValue As String)
    m_DataMember = newValue
End Property

屬性副程式Property Let DataSource在此則是連結程序真正產生的地方。當該類別或控制項連結至它的資料來源時,此副程式便會被呼叫。此連結可以透過程式碼來完成,或者如果你在ActiveX控制項屬性視窗中將DataSource設定好,也可以在表單載入時完成連結的動作。下面是CustomerGrid控制項的DataSource實際運作的程式碼:

Public Property Get DataSource() As DataSource
    ' Simply delegate to the Recordset's DataMember property.
    If Not (rs Is Nothing) Then
        Set DataSource = rs.DataSource
    End If
End Property
Public Property Set DataSource(ByVal newValue As DataSource)
    If Not Ambient.UserMode Then Exit Property
    If Not (rs Is Nothing) Then
        ' If the new value equals the old one, exit right now.
        If rs.DataSource Is newValue Then Exit Property
        If (newValue Is Nothing) Then
            ' The Recordset is being closed. (The program is shutting
            ' down.)  Flush the current record.
            Select Case rs.LockType
                Case adLockBatchOptimistic
                    rs.UpdateBatch
                Case adLockOptimistic, adLockPessimistic
                    rs.Update
                Case Else
            End Select
        End If
    End If
    If Not (newValue Is Nothing) Then
        Set rs = New ADODB.Recordset     ' Re-create the Recordset.
        rs.DataMember = m_DataMember
        Set rs.DataSource = newValue
        Refresh                          ' Reload all data.
    End If
End Property


 

圖18-4 圖中表單裡的Grid控制項是ProductGrid ActiveX控制項的一個實體。

注意程式碼前面部分並無任何UserControl組成控制項的參照。事實上,可在本書幾個類別或元件的例子中重複使用它們,而無須修改半行程式碼。將資料顯示出來的程式碼部分位於Refresh方法中:

Sub Refresh()
    ' Exit if in design mode.
    If Not Ambient.UserMode Then Exit Sub    
    ' Clear the ListView, and exit if the Recordset is empty or closed.
    ListView1.ListItems.Clear
    If rs Is Nothing Then Exit Sub
    If rs.State <> adStateOpen Then Exit Sub
    ' Move to the first record, but remember the current position.
    Dim Bookmark As Variant, FldName As Variant
    Bookmark = rs.Bookmark
    rs.MoveFirst
    
    ' Load the data from the Recordset into the ListView.
    Do Until rs.EOF
        With ListView1.ListItems.Add(, , rs("ProductName"))
            .ListSubItems.Add , , rs("UnitPrice")
            .ListSubItems.Add , , rs("UnitsInStock")
            .ListSubItems.Add , , rs("UnitsOnOrder")
            ' Remember the Bookmark of this record.
            .Tag = rs.Bookmark
        End With
        rs.MoveNext
    Loop
    ' Restore the pointer to the current record.
    rs.Bookmark = Bookmark
    
    ' Bind the properties to the Recordset.
    Set bndcol = New BindingCollection
    bndcol.DataMember = m_DataMember
    Set bndcol.DataSource = rs
    For Each FldName In Array("ProductName", "UnitPrice", "UnitsInStock", _
        "UnitsOnOrder")
        bndcol.Add Me, FldName, FldName
    Next
End Sub

這是一個運用ListView一般控制項製作的資料感知Grid ActiveX控制項的簡單運用。一個更有經驗的設計方式應該會避免將整個Recordset一次載入到ListView,而應該採用一個佔存的方式來增進它的效能以及避免記憶體的消耗。

複雜性資料消費者必須做兩件事來迎合使用者的期望。第一,當使用者點選grid某一行時,它應該要將目前的資料列改變。第二,它應該將目前使用者選取的資料列反白起來。在ProductGrid控制項中,第一個要求可以透過ListView的ItemClick事件來達成﹔此程式碼利用控制項儲存Recordset每筆資料錄的BookMark屬性於每個ItemList元件的Tag屬性達成:

Private Sub ListView1_ItemClick(ByVal Item As MSComctlLib.ListItem)
    rs.Bookmark = Item.Tag
End Sub

當ListView的某一行成為了目前選取的資料錄時,要將其反白,得在Recordset的MoveComplete事件中撰寫程式碼:

Private Sub rs_MoveComplete(ByVal adReason As ADODB.EventReasonEnum, _
    ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, _
    ByVal pRecordset As ADODB.Recordset)
    Dim Item As ListItem
    ' Exit if in a BOF or EOF condition.
    If rs.EOF Or rs.BOF Then Exit Sub
    ' Highlight the item corresponding to the current record.
    For Each Item In ListView1.ListItems
        If Item.Tag = rs.Bookmark Then
            Set ListView1.SelectedItem = Item
            Exit For
        End If
    Next
    ' Ensure that the item is visible.
    If Not (ListView1.SelectedItem Is Nothing) Then
        ListView1.SelectedItem.EnsureVisible
    End If
    ListView1.Refresh
End Sub

範例中MoveComplete事件裡的程式碼避免了發自於UserControl內部的移動所引發的事件(在此情形下,該控制項已事先知道grid的哪一行應該被反白了)。

可像是在使用DataGrid或其他資料感知grid控制項般地使用ProductGrid ActiveX控制項。然而筆者發現,該連結機制中上有未完善之處。舉例來說,如果您重新整理了ADO Data控制項,一個在Visual Basic環境發展的複雜性資料消費者並不會得到任何通知。因此,如果需要改變ADO Data控制項一個或多個的屬性且執行它的Refresh方法,還必須重新指定ADO Data控制項給ProductGrid控制項的DataSource屬性:

Adodc1.ConnectionString = "Provider=Microsoft.Jet.OLEDB.3.51;" _
    & "Data Source=C:\Program Files\Microsoft VisualStudio\Vb98\NWind.mdb" 
Adodc1.Refresh
Set ProductGrid1.DataSource = Adodc1

OLE DB Simple Provider
 

Visual Basic 6提供您建構OLE DB Simple Provider的能力-亦即可註冊在系統中的元件,且其可被標準資料來源用來連接所有人格式的資料。此能力在許多方面都是有用的。例如,從MS-DOS到Microsoft Windows的應用程式部分,通常需要繼續讀取在舊格式中的資料。幸虧有用戶端的OLE DB Simple Provider,可從新程式中使用標準的描述來連接舊資料,且當部份的程式完成,並已準備轉換資料庫的資料到有標準OLE DB provider存在的SQL Server或其他大型資料引擎中,可轉換到標準的(且更有效率的)OLE DB Provider中。

在給予較大支援前,得記住Visual Basic不允許您撰寫完整的OLE DB Provider,就像微軟提出的Microsoft Jet Database Engine、SQL Server或Oracle。OLE DB Simple Provider不支援交易、Command物件和批次更新,這些是少數限制之一。這些提供者的另一問題是,他們無法包含有關結構性的資料:它們可傳回行名稱,卻無法展示行資料型態或最大長度。對於顯露於外的且被記憶體以陣列方式儲存的資料表,OLE DB Simple Providers是特殊的工具。然而,這些限制無法阻止您用OLE DB Simple Providers做有興趣的事情。例如,可建立一個用私有的方式來加密資料的提供者,或者從Microsoft Excel或Microsoft Outlook應用程式中下載資料的提供者,亦或來自透過自動化來控制其他應用程式的提供者。


說明

從OLE DB provider的遠景來看,資料消費者是元件,亦即在稍早的章節中提到的資料來源。換句話說,OLE DB provider用戶端是Visual Basic應用程式意識到資料來源的物件,就像DataEnvironment物件的ADO資料感知元件。


為描述OLE DB Simple Provider結構的概念,筆者建立一個連接到以分號做界線的文字檔之簡易提供者。此檔第一行包含所有欄位的名稱。當提供者被啟動時,它開啟資料檔且下載資料到記憶體的陣列中。此一例子與Visual Basic文件中的相似,不過筆者的解決方案更精簡更有效率,因為使用陣列中的陣列來儲存獨立的記錄。(請看 第四章 的陣列中的陣列之完整描述)程式碼是非常普遍的,且可在不同型態的提供者中循環利用大部分的方法。可在隨書光碟中發現完整的程式碼。

OLE DB Simple Provider的架構
 

有三個部分組成OLE DB Simple Provider,一個是Msdaosp.dll函式庫,Visual Basic 6就有提供(大部分是屬於OLE DB SDK),其他兩類別是寫在Visual Basic中:OLE DB Simple Provider類別和資料來源類別。

Msdaosp.dll是資料實際看到的部分。它主要的工作是增加完整OLE DB provider的所有功能,這也是您寫在Visual Basic中的OLE DB Simple Provider類別所沒有的。當.DLL檔被資料消費者所啟動時,其會載入專案所顯示的資料來源類別,且呼叫其中之一的方法。資料來源類別傳回DLL,以OLE DB Simple Provider類別為例;從那時起,.DLL檔透過OLEDBSimpleProvider介面與OLE DB Simple Provider溝通。

為實作簡易的OLE DB Simple Provider,可從建立ActiveX DLL專案來開始,且指定名稱為TextOLEDBProvider。加兩種型態的函式庫到設定引用項目對話方塊中:Microsoft Data Source Interface library (Msdatsrc.tlb)和Microsoft OLE DB Simple Provider 1.5 Library (Simpdata.tbl)。可選擇性地增加包含所有符號的錯誤程式碼之OLE DB Errors Type Library (Msdaer.dll)。

當所有參考都在適當的位置,可以增加兩個共有的類別給此專案。第一個名為TextOSP的類別模組將執行OLE DB Simple Provider;第二個TextDataSource類別模組將執行資料來源物件。讓我們瞧瞧如何建立這兩個類別。

OLE DB Simple Provider類別
 

在OLE DB Simple Provider範例專案最複雜的程式碼是TextOSP,當消費者讀取或寫入資料時,PublicNotCreatable類別模組執行所有Msdaosp.dll呼叫的函數。因為在類別和.DLL之間的溝通是透過OLEDBSimpleProvider介面來出現,在類別宣告的部分,它必須包含Implement的關鍵字。

Implements OLEDBSimpleProvider
Const DELIMITER = ";"        ' Change this at will.
Const E_FAIL = &H80004005    ' A typical error code for OLE DB providers

Dim DataArray() As Variant   ' An array of arrays
Dim RowCount As Long         ' Number of rows (records)
Dim ColCount As Long         ' Number of columns (fields)
Dim IsDirty As Boolean       ' True if data has changed
Dim m_FileName As String     ' The path of the data file

Dim Listeners As New Collection
Dim Listener As OLEDBSimpleProviderListener

DataArray是儲存資料的變數陣列。每一元素對應一個記錄且包含一字串陣列,此陣列含有所有欄位的欄位值。元素DataArray(0) 含有欄位名稱的陣列。RowCount和ColCount模組樹狀變數各自存有記錄的比數和欄位數。不論欄位是否寫入,IsDirty標籤被設為True,因此在暫停之前,類別知道它必須更新資料檔。LoadData法則載入資料檔於記憶體中,且檔案內容被分派給DataArray變數。

Sub LoadData(FileName As String)
    Dim fnum As Integer, FileText As String
    Dim records() As String, fields() As String
    Dim row As Long, col As Long

    ' Read the file in memory.
    m_FileName = FileName       ' Remember the file name for later.
    fnum = FreeFile
    On Error GoTo ErrorHandler
    Open m_FileName For Input Lock Read Write As #fnum
    FileText = Input(LOF(fnum), #fnum)
    Close #fnum
    
    ' Split the file into records and fields.
    records = Split(FileText, vbCrLf)
    RowCount = UBound(records)
    ColCount = -1
    ReDim DataArray(0 To RowCount) As Variant
    
    For row = 0 To RowCount
        fields = Split(records(row), DELIMITER)
        DataArray(row) = fields
    Next
    ' The first record sets ColCount.
    ColCount = UBound(DataArray(0)) + 1
    Exit Sub
    
ErrorHandler:
    Err.Raise E_FAIL
End Sub

SaveData法則將資料寫回文字檔中。若在Class_Terminate事件敘述中IsDirty變數為True,此一法則便自動被載入。

Sub SaveData()
    Dim fnum As Integer, FileText As String
    Dim records() As String, fields() As String
    Dim row As Long, col As Long
    
    For row = 0 To UBound(DataArray)
        FileText = FileText & Join(DataArray(row), DELIMITER) & vbCrLf
    Next
    ' Drop the last CR-LF character pair.
    FileText = Left$(FileText, Len(FileText) - 2)
    ' Write the file.
    fnum = FreeFile
    On Error GoTo ErrorHandler
    Open m_FileName For Output Lock Read Write As #fnum
    Print #fnum, FileText;
    Close #fnum
    IsDirty = False
    Exit Sub
ErrorHandler:
    Err.Raise E_FAIL
End Sub

類別模組執行OLEDBSimpleProvider介面,包含14個函數。記住LoadData方法下載資料於DataArray之後,透過此一陣列處理獨佔的資料。因此,藉由修正在LoadData和SaveData程序中的程式碼,可準備數個提供者。OLEDBSimpleProvider介面前兩個方法傳回資料來源的行列數目。

' Return the exact number of rows.
Private Function OLEDBSimpleProvider_getRowCount() As Long
    OLEDBSimpleProvider_getRowCount = RowCount
End Function
' Return the number of columns.
Private Function OLEDBSimpleProvider_getColumnCount() As Long
    OLEDBSimpleProvider_getColumnCount = ColCount
End Function

GetLocale方法傳回有關位置的資訊;若提供者無法支援網路設定,可傳回空字串。

' Return a string that determines the system's international settings
' or an empty string if the provider doesn't support different locales.
' (This one doesn't.)
Private Function OLEDBSimpleProvider_getLocale() As String
    OLEDBSimpleProvider_getLocale = ""
End Function

當您的提供者支援非同步的資料傳輸,OLEDBSimpleProvider的三個方法是有用的。在這一例子中,在isAsync方法中傳回False,所以不需要擔心其他兩種方法,即getEstimatedRows和stopTransfer,因為它們從未被呼叫。(不過在任何時候必須提供其Implements的關鍵字。)

' Return a nonzero value if the rowset is populated asynchronously.
Private Function OLEDBSimpleProvider_isAsync() As Long
    OLEDBSimpleProvider_isAsync = False
End Function
' Return the estimated number of rows or -1 if unknown.
' This method is used in asynchronous data transfers.
Private Function OLEDBSimpleProvider_getEstimatedRows() As Long
    ' The following statement is for demonstration purposes only because
    ' this method will never be called in this provider.
    OLEDBSimpleProvider_getEstimatedRows = RowCount
End Function
' Stop asynchronous transfer.
Private Sub OLEDBSimpleProvider_stopTransfer()
    ' Do nothing in this provider.
End Sub

以下兩種方法,addOLEDBSimpleProviderListener和removeOLEDBSimpleProviderListener是非常重要的。不論何時一個新的消費者連接提供者類別的例子,它們會被呼叫。提供者必須記錄所有正在傾聽此一例子的消費者,因為每當資料被新增、移動或改變提供者必須傳送通知給所有的消費者。TextOSP樣本類別以傾聽者模組集合變數來記錄所有消費者。

' Add a Listener object to the Listeners collection.
Private Sub OLEDBSimpleProvider_addOLEDBSimpleProviderListener( _
    ByVal pospIListener As MSDAOSP.OLEDBSimpleProviderListener)
    If Not (pospIListener Is Nothing) Then Listeners.Add pospIListener
End Sub
' Remove a Listener from the Listeners collection.
Private Sub OLEDBSimpleProvider_removeOLEDBSimpleProviderListener( _
    ByVal pospIListener As MSDAOSP.OLEDBSimpleProviderListener)
    Dim i As Long
    For i = 1 To Listeners.Count
        If Listeners(i) Is pospIListener Then
            Listeners.Remove I
            Exit For
        End If
    Next
End Sub

當消費者要求有關資料來源的讀寫狀態資訊時,GetRWStatus方法會被喚醒。當此一方法被呼叫且iRow= -1,必須傳回行的狀態,行數會傳給iColumn這一引數;當iColumn引數為-1,必須傳回記錄的狀態,記錄筆數會傳遞給iRow。當兩引數皆為正值,必須傳回所給定的列之欄位狀態。在所有情況下,可傳回以下其中之一的值:OSPRW_READWRITE(資料可被讀取及修改)、 OSPRW_READONLY (資料為唯讀),或OSPRW_MIXED(未確定的狀態)。在此一例子中,所有欄位都是可寫入的,所以不需要測試iRow和iCol這兩個引數。

' Return the read/write status of a value.
Private Function OLEDBSimpleProvider_getRWStatus(ByVal iRow As Long, _
    ByVal iColumn As Long) As MSDAOSP.OSPRW
    ' Make all fields read/write.
    OLEDBSimpleProvider_getRWStatus = OSPRW_READWRITE
End Function

GetVariant方法傳回已存在的值。此一方法接收format參數,此參數指出應該以何種格式傳給消費者。可能的值有OSPFORMAT_RAW (內定值,資料沒有特定格式)、 OSPFORMAT_FORMATTED(資料為包含變數的字串)或OSPFORMAT_HTML (資料為HTML字串)。在簡易的提供者中,一旦資料儲存在DataArray陣列中,format參數被忽略且資料被傳回。

' Read a value at given row and column coordinates.
Private Function OLEDBSimpleProvider_getVariant(ByVal iRow As Long, _
    ByVal iColumn As Long, ByVal format As MSDAOSP.OSPFORMAT) As Variant
    ' Use (iColumn _ 1) because the iColumn parameter is 1-based
    ' whereas values are stored in 0-based string arrays.
    OLEDBSimpleProvider_getVariant = DataArray(iRow)(iColumn - 1)
End Function

在SetVariant方法中,您被預期在Var參數中寫入一個值給區域性陣列。傳遞一值之前,必須注意對於資料即將被改變(事前的)所有傾聽者。同樣地,在製造此一傳遞後,必須確認對於資料實際被改變的(事後的)所有傾聽者。做這兩者的通知是透過儲存在傾聽者集合的OLEDBSimpleProvider物件之方法。

' Write a value at given row/column coordinates.
Private Sub OLEDBSimpleProvider_setVariant(ByVal iRow As Long, _
    ByVal iColumn As Long, ByVal format As MSDAOSP.OSPFORMAT, _
    ByVal Var As Variant)
    ' Prenotification
    For Each Listener In Listeners
        Listener.aboutToChangeCell iRow, iColumn
    Next
    DataArray(iRow)(iColumn - 1) = Var
    ' Postnotification
    For Each Listener In Listeners
        Listener.cellChanged iRow, iColumn
    Next
    IsDirty = True
End Sub

當消費者增加一筆新紀錄或刪除現存的紀錄時,InsertRows和deleteRows方法會被呼叫。幸虧陣列結構的陣列,執行這些動作是簡單的。在這兩個狀況中,必須傳遞事前和事後給所有正在傾聽此一提供者的所有消費者。

' Insert one or more rows.
Private Function OLEDBSimpleProvider_insertRows(ByVal iRow As Long, _
    ByVal cRows As Long) As Long
    Dim row As Long
    ' Validate iRow - (RowCount + 1), and account for AddNew commands.
    If iRow < 1 Or iRow > (RowCount + 1) Then Err.Raise E_FAIL
    ReDim emptyArray(0 To ColCount) As String
    ReDim Preserve DataArray(RowCount + cRows) As Variant

    ' Prenotification
    For Each Listener In Listeners
        Listener.aboutToInsertRows iRow, cRows
    Next
    ' Make room in the array.
    If iRow <= RowCount Then
        For row = RowCount To iRow Step -1
            DataArray(row + cRows) = DataArray(row)
            DataArray(row) = emptyArray
        Next
    Else
        For row = RowCount + 1 To RowCount + cRows
            DataArray(row) = emptyArray
        Next
    End If
    RowCount = RowCount + cRows

    ' Postnotification
    For Each Listener In Listeners
        Listener.insertedRows iRow, cRows
    Next
    ' Return the number of inserted rows.
    OLEDBSimpleProvider_insertRows = cRows
    IsDirty = True
End Function
' Delete one or more rows.
Private Function OLEDBSimpleProvider_deleteRows(ByVal iRow As Long, _
    ByVal cRows As Long) As Long
    Dim row As Long
    ' Validate iRow.
    If iRow < 1 Or iRow > RowCount Then Err.Raise E_FAIL
    ' Set cRows to the actual number, which can be deleted.
    If iRow + cRows > RowCount + 1 Then cRows = RowCount - iRow + 1

    ' Prenotification
    For Each Listener In Listeners
        Listener.aboutToDeleteRows iRow, cRows
    Next
    ' Shrink the array.
    For row = iRow To RowCount - cRows
        DataArray(row) = DataArray(row + cRows)
    Next
    RowCount = RowCount - cRows
    ReDim Preserve DataArray(RowCount) As Variant

    ' Postnotification
    For Each Listener In Listeners
        Listener.deletedRows iRow, cRows
    Next
    ' Return the number of deleted rows.
    OLEDBSimpleProvider_deleteRows = cRows
    IsDirty = True 
End Function

最後一個方法是Find,當消費者搜尋一個特殊的值,此方法即被載入。它接收在val參數中要搜尋的值,而在iStartRow參數中的是開始的列數,而iColumn則有必須搜尋的行數。Find方法是OLEDBSimpleProvider介面中最複雜的方法因為此方法必須計算數個旗幟和選擇權。FindFlags參數是位元碼:1-OSPFIND_UP代表從尾端到開頭搜尋資料的方式,而2-OSPFIND_CASESENSITIVE則是大小些區分的。CompType參數指出哪一狀況必須符合:1-OSPCOMP_EQ (等於)、 2-OSPCOMP_LT (小於)、 3OSPCOMP_LE (小於或等於)、4-OSPCOMP_GE (大於或等於)、 5-OSPCOMP_GT(大於)、和6-OSPCOMP_NE(不等於)。Find方法必須傳回符合狀況的列數,或傳回-1,即無符合的資料。以下方法計算所有這些不同的設定。

Private Function OLEDBSimpleProvider_Find(ByVal iRowStart As Long, _
    ByVal iColumn As Long, ByVal val As Variant, ByVal findFlags As _
    MSDAOSP.OSPFIND, ByVal compType As MSDAOSP.OSPCOMP) As Long
    Dim RowStop As Long, RowStep As Long
    Dim CaseSens As Long, StringComp As Boolean
    Dim result As Long, compResult As Integer, row As Long
    
    ' Determine the end row and the step value for the loop.
    If findFlags And OSPFIND_UP Then
        RowStop = 1: RowStep = -1
    Else
        RowStop = RowCount: RowStep = 1
    End If
    ' Determine the case-sensitive flag.
    If findFlags And OSPFIND_CASESENSITIVE Then
        CaseSens = vbBinaryCompare
    Else
        CaseSens = vbTextCompare
    End If
    ' True if we're dealing with strings
    StringComp = (VarType(val) = vbString)
    ' -1 means not found.
    result = -1
    ' iColumn is 1-based, but internal data is 0-based.
    iColumn = iColumn - 1
    
    For row = iRowStart To RowStop Step RowStep
        If StringComp Then
            ' We're comparing strings.
            compResult = StrComp(DataArray(row)(iColumn), val, CaseSens)
        Else
            ' We're comparing numbers or dates.
            compResult = Sgn(DataArray(row)(iColumn) - val)
        End If
        Select Case compType
            Case OSPCOMP_DEFAULT, OSPCOMP_EQ
                If compResult = 0 Then result = row
            Case OSPCOMP_GE
                If compResult >= 0 Then result = row
            Case OSPCOMP_GT
                If compResult > 0 Then result = row
            Case OSPCOMP_LE
                If compResult <= 0 Then result = row
            Case OSPCOMP_LT
                If compResult < 0 Then result = row
            Case OSPCOMP_NE
                If compResult <> 0 Then result = row
        End Select
        If result <> -1 Then Exit For
    Next
    ' Return the row found or -1.
    OLEDBSimpleProvider_find = result
End Function

資料來源類別
 

OLE DB Simple Provider專案包含一個公用的MultiUse類別和TextDataSource。舉例來說,當使用者使用此提供者,此類別是Msdaosp.dll的元件。TextDataSource必須有兩個公開的方法:一個為msDataSourceObject,另一個為addDataSourceListener。MsDataSourceObject方法可建立一個新的Provider類別、告知它下載一個資料檔和回應訊息給呼叫者。從這點來看,Msdaosp.dll將直接與TextOSP Provider類別溝通。以簡單的應用為例,可以在addDataSourceListener方法中傳回零。

Const E_FAIL = &H80004005
' The DataMember passed to this function is the path of the text file.
Function msDataSourceObject(DataMember As String) As OLEDBSimpleProvider
    ' Raise an error if the member is invalid.
    If DataMember = "" Then Err.Raise E_FAIL
    ' Create an instance of the OLE DB Simple Provider component,
    ' load a data file, and return the instance to the caller.    
    Dim TextOSP As New TextOSP
    TextOSP.LoadData DataMember
    Set msDataSourceObject = TextOSP
End Function
Function addDataSourceListener(ByVal pospIListener As DataSourceListener) _
    As Long
    addDataSourceListener = 0
End Function

現在你已了解所有相關的屬性與方法,可準備編譯此DLL了。不過作業尚未結束,因為現在必須將你的DLL註冊為OLE DB Simple Provider。

註冊步驟
 

為了註冊OLE DB Simple Provider,必須新增一些輸入值到Registry。通常,建立一個REG檔且包含進提供者的安裝程序中,以便於可在任何機器上以連按兩下或執行Regedit公用程式,簡易地註冊提供者。以下是隨書光碟上作為註冊提供者的檔案,名為TextOSP.Reg。

REGEDIT4
[HKEY_CLASSES_ROOT\TextOSP_VB]
@="Semicolon-delimited text files"
[HKEY_CLASSES_ROOT\TextOSP_VB\CLSID]
@="{CDC6BD0B-98FC-11D2-BAC5-0080C8F21830}"
[HKEY_CLASSES_ROOT\CLSID\{CDC6BD0B-98FC-11D2-BAC5-0080C8F21830}]
@="TextOSP_VB"
[HKEY_CLASSES_ROOT\CLSID\{CDC6BD0B-98FC-11D2-BAC5-0080C8F21830}\InprocServer32]
@="c:\\Program Files\\Common Files\\System\\OLE DB\\MSDAOSP.DLL"
"ThreadingModel"="Both"
[HKEY_CLASSES_ROOT\CLSID\{CDC6BD0B-98FC-11D2-BAC5-0080C8F21830}\ProgID]
@="TextOSP_VB.1"
[HKEY_CLASSES_ROOT\CLSID\{CDC6BD0B-98FC-11D2-BAC5-
0080C8F21830}\VersionIndependentProgID]
@="TextOSP_VB"
[HKEY_CLASSES_ROOT\CLSID\{CDC6BD0B-98FC-11D2-BAC5-
0080C8F21830}\OLE DB Provider]
@="Semicolon-delimited text files"
[HKEY_CLASSES_ROOT\CLSID\{CDC6BD0B-98FC-11D2-BAC5-
0080C8F21830}\OSP Data Object]
@="TextOLEDBProvider.TextDataSource"

在Registry.檔上每一個OLE DB Simple Provider有兩個輸入處。HKEY_CLASSES_ROOT\<YourProviderName>此處包含提供者的描述(當程式設計師要求所有的已在系統上註冊的OLE DB providers時,此字串即被顯示)和提供者的CLSID。不要將此CLSID和我們所建立的.DLL檔的CLSID搞混。這一個CLSID僅僅服務一個唯一定義的提供者。必須自行建立CLSID-譬如,如18-5所示,使用Microsoft Visual Studio所提供的Guidgen.exe。


 

圖18-5 Guidgen.exe在不同的格式上提供新的GUIDs。

HKEY_CLASSES_ROOT\CLSID\<YourProviderClsid>此處收集有關此提供者的所有其他資訊,包括Msdaosp.dll檔案路徑和資料來源類別完整的名稱,,當消費者連接到提供者時必須被提出。最後一個數值是projectname.classname,包括進Visual Basic專案中的資料來源類別名稱。

為助於建立註冊檔,準備一個暫存的名為Model_osp.reg的REG檔。可重複使用此作為所有以下程序所建立的OLE DB Simple Provider的範本:

  1. 執行Guidgen.exe,選擇 Registry Format 選項,按下 複製 鈕,然後關閉此功能。
  2. 下載Model_osp.reg到Word或純文字檔中。(若是執行Windows 95或Windows 98,注意事項pad並不是一個好抉擇,因為缺乏搜尋和取代的能力。)然後以CLSID取代所有出現「$ClsId$」的地方。
  3. 搜尋「$Description$」字串,並以提供者的文字描述替換它們-如「Semicolon-delimiter Text Files」;此字串定義列出機器上所有安裝的OLE DB providers。
  4. 以提供者的名稱替換所有「$ProviderName$」的字串;在註冊檔中有定義提供者的名字,並在ADO Connection物件ConnectionString屬性上用來當作Provider的值。隨書光碟以「TextOSP_VB」為提供者的名字。
  5. 搜尋只出現一次的「$DataSource$」字串,以在OLE DB provider專案上的資料來源完整名稱替換;在範例專案中,是「TextOLEDBProvider.TextDataSource」字串。
  6. 確定Msdaosp.dll位在C:\Program Files\Common Files\System\OLE DB;若無,修正REG檔上的InprocServer32的值將之指向正確的路徑。
  7. 將檔案另存新檔以便不修正範本REG檔。
  8. 連按兩下 REG 檔來將所需的關鍵值新增到註冊檔中。

測試OLE DB Simple Provider
 

可用剛剛建立的OLE DB Simple Provider或其他任何OLE DB provider。例如,可開啟一個Recordset並以下列程式碼循環此Recordset的記錄。

Dim cn As New ADODB.Connection, rs As New ADODB.Recordset
cn.Open "Provider=TextOSP_VB;Data Source=TextOLEDBProvider.TextDataSource"
rs.Open "C:\Employees.Txt", cn, adOpenStatic, adLockOptimistic
rs.MoveFirst
Do Until rs.EOF
    Print rs("FirstName") & " " & rs("LastName")
    rs.MoveNext
Loop
rs.Close
cn.Close

無法在Visual Basic IDE測試提供者,必須將它編譯為單一的ActiveX元件。這表示必須放棄只能在Visual Basic IDE環境下的所有偵錯工具,而只能依靠MsgBox和App.LogEvent敘述。

資料物件精靈
 

資料物件精靈是一個新增的功能,可幫助快速集合資料感類別和UserControl模組。此精靈極可能是Visual Basic 6所提供最複雜的功能。可惜,它很難用直覺來使用。本章剩下的部分主要介紹這一個功能。

準備應用精靈
 

資料物件精靈和DataEnvironment設計者彼此之間是有關連的。與輸入所有有關資料來源所需的資料不同的是,當資料物件精靈執行,在執行新增之前,必須準備DataEnvironment物件,包含一至兩個Command物件。每一Command物件有個在資料來源上的執行動作做為代表:搜尋、新增、更新、或刪除記錄,查閱資料,等等。一旦執行精靈,就不能回到Visual Basic IDE,所以需要預先準備所有的Command物件。

本節以NWind.mdb資料庫中Products資料表作為簡單例子。坦白說,當使用SQL Server和Oracle資料庫時,資料物件精靈執行最佳。即使如此,還是選擇以區域端的MDB資料庫作為實例,以便於沒有client/server系統的用戶端。執行精靈前,有以下步驟執行:

  1. 開啟資料檢視視窗,建立連結到NWind.mdb資料庫的資料(若沒有準備的話)。若想要與下面敘述同步請選擇OLE DB Provider for ODBC,而非Provider for Microsoft Jet databases。測試連接是否正常,然後選擇在Nwind資料下要建立的資料庫之子資料夾。
  2. 在資料檢視視窗下按下新增DataEnvironment按鈕以建立一個新的DataEnvironment設計者,然後刪除預設的Connection1模式,因Visual Basic會自動新增所有DataEnvironment模組到此一模式中。
  3. 從資料檢視視窗中將Products資料表拉到DataEnvironment視窗。Visual Basic自動地建立一個新的Connection1模式和一個名為Products的Command物件。此Command物件所傳回的Recordset不需要被更新,因為所有新增、更新及刪除的動作藉由其他Command物件來執行。
  4. 在DataEnvironment工具列上按下新增Command的按鈕以建立一個新的、名為Command1的Command物件,然後按下屬性鈕顯示Command的屬性對話框。改Command物件的名字為Products_Insert,選擇SQL敘述選項,並輸入以下的SQL查詢字串到可顯示多行的文字框中,如圖18-6。
     
    INSERT INTO Products(ProductName, CategoryID, SupplierID, QuantityPerUnit, 
    UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued) 
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)



     

    圖18-6 Products_Insert Command物件的屬性頁。
  5. 以tab鍵轉換屬性頁中的屬性,且指定有意義的名稱給先前提到之SQL敘述中的九個參數。每一參數應該被指定一個相對欄位的名稱-就像ProductName、CategoryID等等。大多數的參數是長整數,因此大部分的情況下都不需要修改預設值。有兩個字串參數(ProductName和QuantityPerUnit)、一個貨幣參數(UnitPrice)及一個真假參數(Discontinued)是例外的。設定字串參數的Size屬性為大於0的值;若不能,Command物件無法被正確建立。
  6. 建立另一個Command物件,稱為Products_Update,且輸入如下的SQL敘述:
     
    UPDATE Products SET ProductName = ?, CategoryID = ?, SupplierID = ?, 
    QuantityPerUnit = ?, UnitPrice = ?, UnitsInStock = ?, UnitsOnOrder = ?,
    ReorderLevel = ?, Discontinued = ? WHERE (ProductID = ?)


    然後以tab鍵轉換參數,且指派有意義的名稱和形式給所有的參數,如同第五步驟所做的。(可惜,沒有辦法在Command物件間複製和貼上動這些資訊。)

  7. 建立第四個Command物件,稱為Products_Delete,並輸入如下的SQL敘述:
     
    DELETE FROM Products WHERE ProductID = ?
  8. 然後轉換參數和指派ProductID名稱給唯一的參數,而不需要改變其他屬性。
  9. 從資料檢視視窗中提出Categories和Suppliers資料表到DataEnvironment視窗中。建立兩個Command物件,一個稱為Categories ,一個為Suppliers,資料物件精靈會用各自用兩個Command物件建立以CategoryID和SupplierID為主的查閱表。

這所有的步驟不需花費超過5或10分鐘。若願意的話,可從一個空白的標準執行檔專案開始,然後從隨書光碟的專案上下載DE1.dsr檔案。此檔案已包含所有預備被資料物件精靈所使用的Command物件。

要求建立這些Command物件的大部分時間是花費在手動輸入Products_Insert和Products_Update命令中名稱和許多參數的其他屬性上。當運作MDB資料庫時無法避免這一步驟,因為OLE DB provider無法正確地認知任何帶有參數且儲存在資料庫中的QueryDef物件。一個好消息是,當使用SQL Server可以建立連接到預存程序的Command物件。此一情況下,不需要任何幫忙,DataEnvironment設計者可以追溯參數的名稱和形式,可顯著地減少需求的時間來完成這些準備的步驟。

建立資料連結類別
 

現在準備執行資料物件精靈(Data Object Wizard)假設尚未下載精靈,從增益集選項中選擇增益集管理員命令,在可用的增益集選項中連按兩下名稱,並且按下確定鈕。現在精靈應該在增益集選項中變成可用的,然後進行以下步驟:

  1. 按下 下一頁 按鈕以跳過介紹資訊;在建立物件頁,選擇建立的物件種類。資料物件精靈可以建立資料消費類別,此類別連結到資料來源或已連結到資料消費類別(必須之前就建立好的)的UserControl模組。第一次執行精靈,別無選擇,必須選擇第一個選項:可供其他物件連結資料的Class物件。按下 下一步 進入下一頁。
  2. 在選取Data Environment的指令頁中,選擇來源Command物件-也就是,此command應該被類別使用來拿取資料。書中的例子,選擇Products,此命令直接從資料庫的資料表中恢復資料;在實際應用程式運作上,將可能選擇使用預存程序或SQL Select查詢子句來讀取資料的命令。
  3. 在定義物件類別中資料欄的資訊頁。指定哪些欄位是Recordset的主鍵值且哪些欄位不能為Null值(需要有值)。ProductID欄位為主鍵值且ProductName、SupplierID、和CategoryID欄位不能為Null值。
  4. 在定義參照表的資訊頁,可定義在來源命令中的查閱欄位。如你所知,查閱欄位的值被用來當作存取另一個資料表資料的欄位。譬如,可以透過SupplierID欄位顯示Suppliers資料表中的CompanyName欄位。在這個例子中,定義SupplierID欄位為Suppliers資料表的查詢欄位。讓精靈集結正確的程式碼,必須鍵入以下的資料到頁次中:
     
    • 選擇SupplierID作為來源欄位(在來源命令中的查詢欄位)。
       
    • 選擇Suppliers當作查詢命令。(告知精靈哪一個Command物件應該被用來對映查閱值到可讀取的字串中以顯示給使用者。)
       
    • 在顯示欄位下拉式對話框中選擇CompanyName。(這是在提供已解譯值的查閱Command物件中的欄位。)
       
    • 勾選SupplierID顯示於查詢欄位列表上。(這是在參閱命令上能顯示的查閱欄位名稱;他可能或不可能如同來源命令上的欄位名稱。)按下新增按鈕繼續進行。
       


    重複相同的四個動作來定義CategoryID為另一個查詢欄位,如18-7所示:

     

    圖18-7 定義參閱表資訊頁。新增SupplierID欄位到查閱欄位列表中之後及新增CategoryID之前。
  5. 下一頁,對映查閱欄位。定義在來源Command物件的欄位如何對映在查閱Command物件上的欄位;因為要定義兩個查閱Command物件,基於此則有兩個連貫的頁籤。因為欄位名稱與在這兩個Command物件中是相同的,精靈可以正確地自行做對映,因此在不需要修改在表格中的數值情況下,可以按「下一步」按鈕。
  6. 在定義和對應插入資料的指令頁中,若有的話,選擇DataEnvironment Command,應該用來新增新記錄到來源Recordset中。舉例來說,選擇Products_Insert Command物件。然後可以定義在來源Command的欄位和在Insert Command的參數之間的對映。因為選擇如欄位名稱般的參數名稱,精靈可以自行做正確的對映,而不必去修改對映結構(請看圖18-8)。

     

    圖18-8 當定義作為插入、更新與刪除記錄的Command物件時,對應欄位名稱與參數名稱是重要的,但大部分情況下,精靈能自動做對應。
  7. 在定義且對應更新資料的指令頁,選擇應改使用於更新來源Command的Command物件(以Products_Update Command為例)。當有一個可新增一筆新記錄且更新目前現存記錄的預存程序,也可在更新核選控制項上勾選使用插入指令來更新。此例中,精靈可以正確地對映欄位名稱與參數名稱,因此可以直接跳到下一頁。
  8. 在定義且對應刪除資料的指令頁,選擇來源Command中哪一個Command物件應該被使用來刪除一筆記錄(Products_Delete Command為例)。不需要手動地動映這些欄位;精靈會自動做對映動作。
  9. 即將完成精靈的執行。最後一頁,輸入類別的名稱並按下完成鈕。所提供的類別名稱會自動加上rscls字樣。假設輸入『Products』,精靈會建立一個名為rsclsProducts的類別模組。

先前這幾個步驟看來似乎複雜,不過在稍後的例子中可發現使用精靈實際上花費不超過雙倍的時間。當精靈完成執行,將會發現有兩個新的類別已經被加入目前的專案中:clsDow類別和rsclsProducts類別。ClsDow類別模組只包含內含數個常數的EnumSaveMode,定義可被傳遞給rsclsProducts類別的SaveMode屬性的數值:0-adImmediate,若一旦記錄指標移到下一筆記錄時即讓類別儲存來源Recordset的數值或1-adBatch,只有當呼叫此類別的Update方法時類別才更新Recordset。

建立資料連結的UserControl
 

可直接從應用程式中使用由精靈建立的rsclsProducts類別模組,但透過用戶端的UserControl來使用較方便性。一個好消息是在次要的事件上建立一個如同UserControl,再使用資料物件精靈。

  1. 再執行精靈,且在建立物件頁、選擇可連結至某現有Class物件的UserControl選項。
  2. 在下一頁,選擇資料類別來當作UserControl的資料來源(例如rsclsProducts類別)。
  3. 在選擇使用者控制項的類型頁中,決定要建立何種UserControl類型。從精靈中可選擇單一資料錄(欄位集合)、資料方格(如DataGrid般的控制項)、資料清單及下拉式清單方塊。第一次執行,選擇單一資料錄。
  4. 在下一頁,決定在UserControl物件中哪些資料庫欄位可視,及對於可視的欄位而言使用哪一類型的控制項型態。舉例而言,對於ProductID欄位應該選擇(None),因這是一個自動編號的主鍵欄位,對於使用者而言無意義,且在CategoryName和SupplierName查閱欄位上應該使用ComboBox(如圖18-9所示)
  5. 在下一頁,選擇此控制項的名字。在各種情況下,可以接受預設的名稱並按下Finish按鈕。精靈所使用的實際名稱是依據在第三步驟所選擇的UserControl類別。假設選擇Single Record類型的控制項,精靈所集合的UserControl模組稱為uctProductsSingleRecord。


 

圖18-9 資料物件精靈的對應物件類別的屬性到控制項類型頁。

現在準備在應用程式中使用此控制項。關閉UserControl模組以便於在Toolbox上的圖示可以運作、在表單上放置一個控制項且新增一些navigational按鈕,如18-10所示。以下是這些按鈕上簡單的程式碼:

Private Sub cmdPrevious_Click()
    uctProductsSingleRecord1.MovePrevious
End Sub
Private Sub cmdNext_Click()
    uctProductsSingleRecord1.MoveNext
End Sub
Private Sub cmdAddNew_Click()
    uctProductsSingleRecord1.AddRecord
End Sub
Private Sub cmdUpdate_Click()
    uctProductsSingleRecord1.Update
End Sub
Private Sub cmdDelete_Click()
    uctProductsSingleRecord1.Delete
End Sub


 

圖18-10 由Data物件所聚集之ActiveX控制項可以在父表單中以導覽按鈕做測試。

對於Microsoft Jet而言,當使用OLE DB Provider時,資料物件精靈並不特別有效率。以下是經驗之談,若想樣增加一筆新的記錄,必須設定UserControl的SaveMode屬性為1-adBatch ,因此在鍵入新的記錄時,必須喚起Update方法。當建立類別和連接到SQL Server資料庫的UserControls時,每一運作都不遲疑。

當了解此機制後,建立UserControl的其他類型是容易的。例如,重新開始此精靈與建立像DataGrid的控制項。若放置控制項在表單上,且設其GridEditable屬性值為True,將會發現不只可編輯Grid的欄位值,還可從下拉式清單中選取參閱欄位值,如圖18-11所示。這像DataList與DataCombo的控制項也不難,因為其只是列出資料,而不使用Insert、Update、Delete與Lookup Command物件。

若原本的來源Command物件是架構在具參數的查詢或預存函數上時,可建立彈性的類別與控制項,例如:

SELECT * FROM Products WHERE ProductName LIKE ?

目前狀況下,結果性的類別和UserControl模組有一個屬性,其名稱是由一連串來源Command的名字和在查詢中參數的名字所獲得(例如:Products_ProductName)。可以在設計階段中設定此屬性,一旦控制項在執行階段被建立起來,UserControl初始化內部的Recordset。或者設定ManualInitialize屬性值為True,以便使用程式碼指派此一屬性,然後手動地執行此控制項的Initxxxx方法(InitProducts為其中一個例子)。在圖18-11中,此簡單的應用使用目前的技巧來縮小顯示在表格中的記錄筆數。下列為唯一需要的程式碼:

Private Sub cmdFetch_Click()
    uctProductsDataGrid1.Products_FetchProductName = txtProductName & "%"
    uctProductsDataGrid1.InitProducts2
End Sub

資料物件精靈是一個極佳的功能,它能產生非常好的程式碼。實際上,我建議您試著了解所產生的程式碼,來學習如何從此功能中取得最大的利益,且也學習建立更好資料感知類別和UserControls的新技巧。不過,這個精靈也有某些缺點。除了之前已經提醒過的之外,最困擾的一個是UserControl模組傾向於與表單同步發生,所以常需要在表單上按右鍵,並選取更新UserControls選項。然而,比起來用精靈所花的時間是較少些。


 

圖18-11 像DataGrid的UserControls也可從下拉式表單中選擇數值。

本章可看到ADO允許建立許多新的類別型態和構成要素:資料消費者、資料來源和OLE DB Simple Providers。也可以建立以Visual Basic為主的另一種資料庫形式之組成元件,Remote Data Services (RDS)元件。當透過HTTP協定連接資料庫時,一般都會用到這些元件,基於這個理由,下一章會描述此元件的型態,並且談論到Visual Basic對於網路的前瞻性。