Sử dụng Class Module và Kết nối dữ liệu SQL SERVER trong Access VBA

Chia sẻ bởi:hands
★★★★★
Quảng cáo

Chào các Bạn,

Hôm nay tôi muốn trao đổi với các Bạn về vấn đề "Sử dụng Class Module và Kết nối dữ liệu SQL SERVER trong Access VBA" như một giải pháp tối ưu cho các ứng dụng được thiết kế với VBA trong Microsoft Access. Trao đổi này có đính kèm file nguồn để làm ví dụ minh họa.

Để lấy một ví dụ cụ thể, ở đây giả định ta có nhu cầu thiết kế 1 ứng dụng Microsoft Access dùng để quản lý 1 danh bạ điện thoại.

Ứng dụng của chúng ta sẽ bao gồm 1 file dữ liệu và 1 file ứng dụng. Các Bạn có thể tạo File dữ liệu bằng Microsoft Access hoặc SQL SERVER. Ở đây tôi tạo file dữ liệu bằng SQL SERVER.
File dữ liệu đã được nạp trên 15.000 mẫu tin.
Khi file ứng dụng được nạp, ta sẽ cho kết nối với file dữ liệu bằng thủ tục Log-In.

Mục đích của tôi thông qua cách thiết kế trên nhằm:
+ minh họa khả năng của Access VBA có thể lập trình theo hướng đối tượng;
+ kết nối được với nguồn dữ liệu ngoài, ở đây là nguồn SQL SERVER;
+ có thể tạo được những Unbound Form nhằm đáp ứng nhu cầu truy xuất dữ liệu với nhiều người dùng qua mạng máy tính, đồng thời cải thiện được tốc độ xử lý dữ liệu.

Về Cấu trúc của file dữ liệu:
Với ứng dụng này ta chỉ cần có 1 file dữ liệu với 1 bảng dữ liệu. Tất nhiên các Bạn có thể tùy biến thêm nếu thấy cần.
– Tôi đặt tên file dữ liệu này là danhba
– Và tạo 1 bảng dữ liệu có tên là tblDanhsach, với các cột dữ liệu như sau:
+ Ten: tên của 1 người cụ thể trong danh bạ, kiểu dữ liệu Text
+ HoChulot: họ và chữ lót, kiểu dữ liệu Text
+ Gioitinh: xác định giới tính, kiểu dữ liệu Yes/No (mặc định là Nam, với giá trị là True)
+ Ngaysinh: ngày sinh, kiểu dữ liệu Date
+ Dtdd: số điện thoại di động, kiểu dữ liệu Text
+ Dtnha: số điện thoại ở nhà riêng, kiểu dữ liệu Text
+ Dtvp: số điện thoại ở văn phòng làm việc, kiểu dữ liệu Text

Với ứng dụng làm ví dụ sẽ cho ta biết cách:
1. Kết nối với nguồn dữ liệu bên ngoài MS. Access, ở đây là SQL SERVER
2. Viết 1 Class module như thế nào
3. Tạo 1 Unbound Form và gắn kết dữ liệu trên đó như thế nào

Trong bài sau tôi sẽ trình bày tiếp vào nội dung chính của chuyên đề này.
Rất mong các Bạn cùng tham gia nghiên cứu và trao đổi.

Nội dung các file đính kèm:
1. File ứng dụng MS. Access với định dạng mdb có mã nguồn
2. File SQL (Text) dùng để tạo database trên SQL SERVER cục bộ (local) nếu các Bạn muốn tạo.

Cũng xin trao đổi rõ thêm: File ứng dụng và file dữ liệu nêu trên mới chỉ là "sườn" còn "thô", để nó trở thành 1 ứng dụng hoàn chỉnh, chúng ta còn phải tinh chỉnh nhiều thứ; đó cũng chính là công việc mà tôi muốn mời các Bạn cùng tham gia trao đổi, qua đó chúng ta thu hoạch được những kiến thức căn bản chắc chắn hơn về chuyên đề này.

Tài liệu tham khảo:
Tài liệu tôi dùng để tham khảo chính để viết loạt bài này (bao gồm ứng dụng làm ví dụ) là loạt sách:
Beginning Access 2003 VBA, Beginning Access 2007 VBA
của Denise M. Gosnell

Link tải File ứng dụng minh họa, bản cập nhật ngày 14/7/2014:
https://www.mediafire.com/download/j5v854t46ozo5ck/qldanhba_150714.rar

www.giaiphapexcel.com/diendan/threads/s%E1%BB%AD-d%E1%BB%A5ng-class-module-v%C3%A0-k%E1%BA%BFt-n%E1%BB%91i-d%E1%BB%AF-li%E1%BB%87u-sql-server-trong-access-vba.67063/#post-411268

Chào các Bạn,
Xin nói thêm về chuyện ứng dụng và dữ liệu còn "thô":

Nói chúng "thô" bởi lẽ:

1. File dữ liệu SQL SERVER chỉ mới có các bảng dữ liệu thôi. Như vậy chúng chỉ mới là chỗ để lưu dữ liệu phát sinh, chưa làm được việc xử lý dữ liệu (ta dễ thấy một phần những việc đơn giản trong việc xử lý dữ liệu này như: lưu, xóa, trích xuất thông tin, lọc thông tin).
Bản thân SQL SERVER là 1 hệ thống quản trị cơ sở dữ liệu mạnh, chứ không chỉ đơn thuần là nơi để lưu dữ liệu. Ta sẽ bàn tới cách giao nhiệm vụ xử lý dữ liệu cho cái file dữ liệu SQL SERVER đã tạo ở trên. Hiện nay việc xử lý dữ liệu vẫn còn do file ứng dụng đãm trách thông qua các câu lệnh SQL trong các module.

2. Nếu chạy file ứng dụng đang có ta sẽ thấy khi mở Form "frmContact" (dùng để cập nhật và xem dữ liệu) sẽ còn mất 1 ít thời gian mà ta có thể cảm nhận được. Mục tiêu của chúng ta là phải làm sao cho nhanh đến mức không cảm thấy phải chờ một chút nào.
Tôi đã kiểm tra thử mở form nói trên với kết nối internet qua 1 USB 3G của Viettel (loại 7.2 Mbps) trên xe hơi đang chạy: thời gian nạp xong form mất khoảng 25 giây.

Có Bạn nào tìm được lý do nào khác không?

Khóa học Power PI – Ứng dung trong Nhân sự
Khóa học SprinGO phù hợp

Khóa học Power PI – Ứng dung trong Nhân sự

TỔNG QUAN KHÓA HỌC: POWER BI CHO NGÀNH NHÂN SỰ Khóa học Power BI cho Nhân sự được thiết kế dành riêng cho các...

Xem khóa học
★★★★★ 5 ★ 1 👤 18 ▥ 0
Quảng cáo

Bạn nên đọc

18 Responses

  1. hands says:

    Class là gì và tại sao ta nên dùng Class trong VBA?

    1. Class là gì?
    Class dùng để tạo ra những Object theo ý muốn của người thiết kế dữ liệu trong Access VBA. Thông qua Class ta có thể tạo ra được những Object với đầy đủ Properties, Method, Even tương tự như những Object có sẵn trong Access VBA.

    Với ứng dụng mẫu đính kèm, ta thấy:
    – Để quản lý đối tượng là danh sách trong danh bạ điện thoại ta tạo ra 1 Object có tên là clsDanhba, thông qua Object này ta có thể:
    + cập nhật hoặc lấy các thông tin chi tiết về từng người có trong danh bạ được lập như: Họ tên, Địa chỉ, số điện thoại, …
    + Cũng thông qua Object này ta có thể thực hiện được việc xóa, thêm mới danh sách trong danh bạ

    Xem ví dụ trong ClsDanhba trong file ứng dụng, ta tạo được 1 Object với tên là clsDanhba có đầy đủ:
    + các properties như: Danhbaid, Diachi, Dtdd, Dtnha, Dtvp
    + các method như: Delete, Save

    Ảnh sau cho thấy việc ta gọi các properties hoặc method của clsDanhba:
    2644

    2. Tại sao ta nên dùng Class trong VBA?
    – Sẽ làm cho bộ mã (VBA code) của ứng dụng gọn gàng hơn:
    + Nếu không có Class, ta sẽ phải viết và lặp lại rất nhiều đoạn code giống nhau trong ứng dụng để quản lý thông tin của Danh bạ (lấy và cập nhật thông tin chi tiết, tạo mới, xóa bớt, …), như vậy sẽ khó khăn cho việc bảo trì và làm cồng kềnh bộ mã ứng dụng, chắc chắn sẽ làm ứng dụng sử nhiều bộ nhớ máy tính hơn.
    – Cũng thông qua Class, ta chỉ cần viết mã 1 lần, sau đó có thể sử dụng Object đã tạo cho nhiều ứng dụng cùng 1 nhóm (như 1 Add-in).
    Các Bạn có thể thấy rằng, để thiết kế 1 ứng dụng quản lý công việc bán hàng chẳng hạn, nếu tạo ra 1 Object để quản lý danh sách khách hàng. Sau đó ta có thể gọi Object này ra để sử dụng trong nhiều phân hệ khác nhau như: phân hệ quan hệ khách hàng, phân hệ công nợ, …
    Rộng ra một chút, nếu tạo ra được 1 Object để quản lý các chứng từ nhập xuất phát sinh. Sau đó ta có thể gọi Object này ra để sử dụng trong các phân hệ như: phân hệ quản lý biến động kho hàng, phân hệ quản lý chế độ chiết khấu – khuyến mại, phân hệ quản lý công nợ phát sinh do việc mua bán hàng,…

    Như vậy, ta đã viết 1 lần và sử dụng ở nhiều nơi khác nhau, mà không phải viết lại bộ mã để quản lý trong từng phân hệ của ứng dụng.

    Các Bạn có để ý thấy bằng việc Bác Bill chỉ cần viết 1 lần thư viện quản lý dữ liệu ADO, ta đã có thể sử dụng thư viện ADO này trong bất kỳ ứng dụng quản trị dữ liệu nào, chỉ cần "nạp và yên tâm xài thôi". Ta viết Class cũng nhằm như vậy.
    Trong một dịp khác, chúng ta sẽ trao đổi sâu hơn về cách thiết kế một thư viện kiểu như vậy với Access VBA, còn lúc này hãy tập trung cho cái chuyên đề chính này đã.

    Vậy cách thức để tạo ra 1 Class trong Access VBA như thế nào? Xin xem bài sau sẽ rõ.

    Các Bạn có thể tham khảo giải thích chính thức của Bác Bill về Class ở link sau nhé: Source: https://msdn.microsoft.com/en-us/library/aa140954(v=office.10)

    Hôm qua, thông qua email gửi trực tiếp cho tôi một số Bạn đã phát hiện được 2 vấn đề trong file ứng dụng minh họa:
    1. Nếu bỏ trống 1 vài chi tiết trên form nhập danh sách sẽ phát sinh lỗi và không cập nhật được.
    2. Nhập vào rồi làm sao tìm, và các Bạn này muốn thêm công cụ tìm danh sách.

    Tôi đã định những vấn đề trên sẽ được bổ sung dần trong quá trình chúng ta trao đổi về chuyên đề này, song nhận thấy có ít ý kiến tham gia trao đổi, nên hôm nay tôi tải lên đây file ứng dụng đã được bổ sung 2 vấn đề trên.

    Xin tải file về từ link sau: https://www.mediafire.com/?ewyxee99pn6212i

    Xin nói rõ thêm về những bổ sung trong file ứng dụng mới này:
    1. Thay vì bổ sung thêm 1 cửa sổ tìm kiếm, tôi sử dụng ngay form fmContacts để làm việc này luôn. Khi nào cần tìm, các Bạn bấm vào nút "Nhập mới" để xóa trống các ô dữ liệu, sau đó nhập các yếu tố cần tìm vào ô tương ứng và bấm nút "Tìm kiếm"

    2. Với chi tiết "Ngày sinh", một số Bạn cho rằng có nhu cầu bỏ trống khi chưa thu thập được thông tin cá nhân này. Do vậy tôi đã thay đổi Class clsDanhba với khai báo biến tương ứng thành Variant (thay vì Date như bản trước) để cho phép bỏ trống chi tiết này.

    Rất mong các Bạn nào có thắc mắc gì xin cứ đăng ý kiến thảo luận lên diễn đàn cho mọi người cùng tham khảo sẽ có hiệu quả chung lớn hơn.

    Chiều tối hôm nay tôi sẽ đăng tiếp bài về cách thức tạo 1 Class trong Access VBA. Mời các Bạn đón đọc và tham gia trao đổi.
    Cách thức tạo 1 Class trong Access VBA

    1. Chèn 1 Class module:
    – Trong cửa sổ Database, chọn Modules và bấm nút lệnh New
    – Trong cửa sổ "Microsoft Visual Basic" đã được mở ngay sau đó, bấm menu "Insert" sổ xuống và chọn mục lệnh "Class module", 1 trang Class module được mở ra. Ta sẽ viết code trong trang Class module này để tạo ra 1 Class

    2. Viết Class:
    – Như bài 2 đã đề cập, ta dùng Class modules để thiết kế những Object theo ý riêng của mình. Mỗi Object như vậy sẽ có các property, method và cũng có thể có các event

    Với ứng dụng ta đang sử dụng để minh họa:
    + Property: tương ứng với từng cột dữ liệu trong bảng dữ liệu
    + Method: tương ứng với các tác vụ như: lưu mới hoặc cập nhật các thay đổi trong bảng dữ liệu, xóa dòng trong bảng dữ liệu

    Với Object mà ta định thiết kế để quản lý tập trung Danh bạ điện thoại (ta gán cho cái tên là clsDanhba):

    – Ta sẽ có các properties chính là các cột dữ liệu trong bảng Danh sách, đó là:
    + Tên, Họ và Chữ lót, giới tính, ngày sinh, địa chỉ, số điện thoại di dộng, số điện thoại ở nhà riêng, số điện thoại ở văn phòng làm việc.
    Như vậy, ta sẽ có 8 properties tương ứng của Object clsDanhba, tôi đặt tên là: Ten, Hochulot, Gioitinh, Ngaysinh, Diachi, Dtdd, Dtnha, Dtvp

    – Ta cũng sẽ cần có các Method:
    + Để lưu và cập nhật dữ liệu, ở đây tôi đặt tên method này là "Save"
    + Để xóa dữ liệu, , ở đây tôi đặt tên method này là "Delete"
    + Để nạp các giá trị từ các cột trong bảng dữ liệu cho các properties của clsDanhba, ở đây tôi đặt tên method này là "PopulatePropertiesFromRecordset".
    Mục đích tạo ra method này nhằm cho nạp các giá trị của bảng dữ liệu vào các ô dữ liệu tương ứng trên form.
    + Để nạp các giá trị là giá trị từ các ô dữ liệu trên form danh sách cho các properties của clsDanhba, ở đây tôi đặt tên method này là "PopulatePropertiesFromForm".
    Mục đích tạo ra method này nhằm cho ghi lại các giá trị đã nhập trên form vào bảng dữ liệu

    – Để khai báo các properties cho clsDanhba:
    + Mỗi một property ta viết 2 procedure: 1 procedure để lấy giá trị của property (Get value), và 1 procedure để gán giá trị cho property (Let value).
    Xem file minh họa với clsDanhba, ta lấy ra 1 đoạn với 4 procedure:

    [COLOR=#0000cd]'Ten[/COLOR]
    [COLOR=#006400]Public Property Get[/COLOR] Ten() As String
        On Error Resume Next
        Ten = strTen
    [COLOR=#006400]End Property[/COLOR]
    
    [COLOR=#006400]Public Property Let[/COLOR] Ten(ByVal Value As String)
        On Error Resume Next
        strTen = Value
    [COLOR=#006400]End Property[/COLOR]
    '-----------------------------------------------------------------------
    [COLOR=#0000cd]'HoChulot[/COLOR]
    [COLOR=#006400]Public Property Get[/COLOR] HoChulot() As String
        On Error Resume Next
        HoChulot = strHochulot
    [COLOR=#006400]End Property[/COLOR]
    
    [COLOR=#006400]Public Property Let[/COLOR] HoChulot(ByVal Value As String)
        On Error Resume Next
        strHochulot = Value
    [COLOR=#006400]End Property[/COLOR]

    Từ đó ta dễ dàng rút ra nhận xét về dạng chung của 1 procedure phải không các Bạn.

    – Nếu cần, trong Class module ta cũng có thể viết thêm các Event cho Object ta định quản lý, nhằm mục đích bẩy 1 sự kiện nào đó có liên quan đến Object này.
    Thông thường, ta có các Event để làm nhiệm vụ nạp Class (Class_Initialize) và đóng Class (Class_Terminate) theo dạng thức như sau:

    [COLOR=#006400]Private Sub Class_Initialize()[/COLOR]
        'viết code của Bạn ở vùng này
    [COLOR=#006400]End Sub[/COLOR] 
    
    [COLOR=#006400]
    Private Sub Class_Terminate()[/COLOR]
        'viết code của Bạn ở vùng này
    [COLOR=#006400]End Sub[/COLOR]

    Các Bạn có thể tham khảo chi tiết hướng dẫn của Bác Bill về cách viết các Event Procedure tại link sau nhé: https://msdn.microsoft.com/en-us/library/aa140935(v=office.10)

    Để hiểu rõ hơn xin mời các Bạn mở file ứng dụng minh họa và xem nội dung Class module "clsDanhba" nhé.

    Bài viết còn nữa, xin mời các bạn xem bài sau sẽ rõ.

  2. hands says:

    Chào các Bạn,

    Trước khi tiếp tục nội dung chính của chuyên đề này, tôi xin trao đổi cùng các Bạn một số vấn đề mang tính chất "bếp núc" với file ứng dụng chúng ta đang sử dụng.

    1. Vấn đề 1: Linh hoạt việc kết nối file ứng dụng với file dữ liệu bất kỳ.
    Như các Bạn đã thấy trong file ứng dụng, chúng ta có thể tùy ý kết nối đến file dữ liệu SQL SERVER bất kỳ mà ta muốn. Các thủ tục kết nối dữ liệu trong file ứng dụng này hoàn toàn không cố định phải kết nối đến 1 file dữ liệu nào cả.
    Để làm được điều đó, ứng dụng có 1 Procedure để kết nối đến file dữ liệu có thể tùy chọn được, như các Bạn thấy trong module "modQuanlyDulieu":

    [COLOR=#006400]Sub OpenDbConnection()[/COLOR]
    '
    [COLOR=#0000cd]'Co the tham khao chuoi ket noi den cac nguon du lieu khac nhau
    'tai dia chi sau: www.connectstring.com[/COLOR]
    
    On Error GoTo HandleError
        Dim vServer, vData, vUser, vPsw, vLogInDft As Boolean
    
    With Forms("frmLogIn")
            vLogInDft = !chkLogIn.Value
            If vLogInDft = True Then
                vServer = "mssql.quantribanhang.vn"
                vData = "danhba"
                vUser = "nhanvien1"
                vPsw = "Nv001"
            Else
                vServer = !txtServer
                vData = !txtData
                vUser = !txtUser
                vPsw = !txtPsw
            End If
        End With
        Set cnConn = New ADODB.Connection
        cnConn.Open _
            "Provider = sqloledb;" & _
            "Data Source=" & vServer & ";" & _
            "Initial Catalog=" & vData & ";" & _
            "User ID=" & vUser & ";" & _
            "Password=" & vPsw & ";"
    
    Exit Sub
    
    HandleError:
        GeneralErrorHandler Err.Number, Err.Description, DB_QUANLY, "OpenDbConnection"
        Exit Sub
    
    [COLOR=#006400]End Sub[/COLOR]

    Đồng thời thiết kế 1 form để LogIn vào server và file dữ liệu xác định. Form này có tên là "frmLogIn".
    Ngay trong procedure trên cũng đã tham chiếu đến các giá trị được người chạy ứng dụng khai báo trên Form này khi mở form Cập nhật Danh bạ (form "frmContacts").

    Có Bạn đã hỏi tôi, nếu muốn kết nối đến file dữ liệu thiết kế bằng Microsoft Access có được không?
    Hoàn toàn được các Bạn ạ. Chỉ cần khai báo lại đoạn sau trong procedure nêu trên:

    cnConn.Open _
            "Provider = sqloledb;" & _
            "Data Source=" & vServer & ";" & _
            "Initial Catalog=" & vData & ";" & _
            "User ID=" & vUser & ";" & _
            "Password=" & vPsw & ";"

    thành chuỗi kết nối đến dữ liệu Microsoft Access. Các Bạn có thể tra cứu chuỗi kết nối thích hợp tại trang [URL="https://www.connectstring.com"]www.connectstring.com
    Trong trường hợp này, các Bạn phải chú ý sửa lại form "frmLogIn" và các đoạn code có liên quan trong procedure nêu trên cho phù hợp nhé.

    Vấn đề 2. Xử lý những thông tin của Object bị bỏ trống (Null value) như thế nào?
    Các giá trị bị bỏ trống nói ở đây có thể là giá trị trong các ô dữ liệu trên form "frmContacts" hoặc trong bảng dữ liệu SQL SERVER.
    Để xử lý trường hợp này ứng dụng có 1 Function có tên là FixNull cũng ở bên trong module nêu trên:

    [COLOR=#006400]Function FixNull[/COLOR]([COLOR=#0000ff]varIn As Variant[/COLOR]) [COLOR=#006400]As String[/COLOR]
    
    If IsNull(varIn) Then
            FixNull = ""
        Else
            FixNull = varIn
        End If
    
    [COLOR=#006400]End Function[/COLOR]

    Và trong 2 procedure có liên quan trong Class module "clsDanhba" ứng dụng đã sử dụng Function FixNull này để khử các giá trị Null như các Bạn đã thấy:

    [COLOR=#006400]Sub PopulatePropertiesFromForm()[/COLOR]
    
    'Lay thong tin tu Form frmContacts de gan gia tri cac thuoc tinh cho objDanhba
    
    On Error GoTo HandleError
    
    With Me
            .Ten = FixNull(Forms("frmContacts")!txtTen)
            .HoChulot = FixNull(Forms("frmContacts")!txtHoChulot)
            .Diachi = FixNull(Forms("frmContacts")!txtDiachi)
            .Dtdd = FixNull(Forms("frmContacts")!txtDtdd)
            .Dtnha = FixNull(Forms("frmContacts")!txtDtnha)
            .Dtvp = FixNull(Forms("frmContacts")!txtDtvp)
            If Len(Forms("frmContacts")!txtNgaysinh) > 0 Then
                .Ngaysinh = Forms("frmContacts")!txtNgaysinh
            Else
                .Ngaysinh = Null
            End If
            .Gioitinh = Forms("frmContacts")!frmGioitinh.Value
        End With
        Exit Sub
    
    HandleError:
        GeneralErrorHandler Err.Number, Err.Description, CLS_DANHBA, "PopulatePropertiesFromForm"
        Exit Sub
    
    [COLOR=#006400]End Sub[/COLOR]

    [COLOR=#006400]Sub PopulatePropertiesFromRecordset[/COLOR]([COLOR=#0000cd]rsCont As ADODB.Recordset[/COLOR])
    
    'Lay thong tin tu Recordset rsCont de gan gia tri cac thuoc tinh cho objDanhba
    
    On Error GoTo HandleError
    
    With Me
            .DanhbaId = rsCont!DanhbaId
            .Ten = Trim(FixNull(rsCont!Ten))
            .HoChulot = Trim(FixNull(rsCont!HoChulot))
            .Diachi = Trim(FixNull(rsCont!Diachi))
            .Dtdd = Trim(FixNull(rsCont!Dtdd))
            .Dtnha = Trim(FixNull(rsCont!Dtnha))
            .Dtvp = Trim(FixNull(rsCont!Dtvp))
            If Not IsNull(rsCont!Ngaysinh) Then
                .Ngaysinh = rsCont!Ngaysinh
            Else
                .Ngaysinh = ""
            End If
            .Gioitinh = rsCont!Gioitinh
        End With
        Exit Sub
    
    HandleError:
        GeneralErrorHandler Err.Number, Err.Description, CLS_DANHBA, "PopulatePropertiesFromRecordset"
        Exit Sub
    
    [COLOR=#006400]End Sub[/COLOR]

    3. Vấn đề bẩy lỗi trong các module:
    Như các Bạn đã thấy, ứng dụng có 1 procedure để bẩy các lỗi có thể phát sinh khi chạy các thủ tục trong ứng dụng:

    [COLOR=#006400]Public Sub GeneralErrorHandler[/COLOR]([COLOR=#0000cd]lngErrNumber As Long, strErrDesc As String, strModuleSource As String, strProcedureSource As String[/COLOR])
    
    On Error Resume Next
    
    Dim strMessage As String
    
    'build the error message string from the parameters passed in
        strMessage = "An error has occurred in the application."
        strMessage = strMessage & vbCrLf & "Error Number: " & lngErrNumber
        strMessage = strMessage & vbCrLf & "Error Description: " & strErrDesc
        strMessage = strMessage & vbCrLf & "Module Source: " & strModuleSource
        strMessage = strMessage & vbCrLf & "Procedure Source: " & strProcedureSource
    
    'display the message to the user
        MsgBox strMessage, vbCritical
    
    Exit Sub
    
    [COLOR=#006400]End Sub[/COLOR]

    Và trong các procedure viết trong ứng dụng, đều có khai báo dòng bẩy lỗi tham chiếu đến procedure GeneralErrorHandler nêu trên:

    ...
    On Error GoTo HandleError
    ...
    HandleError:
        GeneralErrorHandler Err.Number, Err.Description, <[COLOR=#0000cd]Tên module[/COLOR]>, <[COLOR=#0000cd]Tên procedure[/COLOR]>
        Exit Sub

    Với cách làm như vậy, chúng ta sẽ dễ dàng quản lý được lỗi phát sinh, thậm chí xác định chính xác lỗi phát sinh ở procedure nào nằm trong module nào.

    Tạm thời xin trao đổi với các Bạn 3 chuyện bếp núc như vậy. Mời các Bạn cho thêm ý kiến nhé.

  3. hands says:

    Chào các Bạn,

    Xin trao đổi thêm một chuyện bếp núc nữa mà rất đông các Bạn khi mới sử dụng Access VBA để thực hiện các câu lệnh SQL hay mắc phải đó là:
    Vấn đề 4: Cập nhật chuỗi Unicode: Với file ứng dụng minh hoạ, trong module "modQuanlyDulieu" tại procedure "BuildSQLInsertDanhba" ta thấy có đoạn code sau:

    ...
        strSQLInsert = "INSERT INTO " & sChemaName & ".tblDanhsach(ten,hochulot, diachi,dtdd, dtnha, dtvp,ngaysinh, gioitinh)"
        strSQLInsert = strSQLInsert & " VALUES ("
        strSQLInsert = strSQLInsert & "[COLOR=#ff0000]N[/COLOR][B][COLOR=#0000cd]'[/COLOR][/B]" & objDanhba.Ten & "[B][COLOR=#0000cd]'[/COLOR][/B], "
        strSQLInsert = strSQLInsert & "[COLOR=#ff0000]N[/COLOR][B][COLOR=#0000cd]'[/COLOR][/B]" & objDanhba.HoChulot & "[B][COLOR=#0000cd]'[/COLOR][/B], "
        strSQLInsert = strSQLInsert & "[COLOR=#ff0000]N[/COLOR][B][COLOR=#0000cd]'[/COLOR][/B]" & objDanhba.Diachi & "[B][COLOR=#0000cd]'[/COLOR][/B], "
        strSQLInsert = strSQLInsert & "'" & objDanhba.Dtdd & "', "
        strSQLInsert = strSQLInsert & "'" & objDanhba.Dtnha & "', "
        strSQLInsert = strSQLInsert & "'" & objDanhba.Dtvp & "', "
    ...

    Trong đoạn code nêu trên các Bạn chú ý ký tự N màu đỏ đặt trước các biến chuỗi khi cho ghép thành câu lệnh SQL. Đó chính là quy ước để cập nhật chuỗi unicode trong trường hợp ta đang bàn đến.
    Nếu không có ký tự N đặt trước chuỗi, chuỗi unicode sẽ được lưu thành chuỗi thường, và khi lấy giá trị các chuỗi đó ra từ bảng dữ liệu ta sẽ có chuỗi không còn dấu tiếng Việt đầy đủ nữa (vì giá trị được lưu vào không còn là Unicode).

    Xin chú ý: N phải đặt trước dấu nháy trên rồi mới tới chuỗi unicode nhé. Các Bạn xem lại câu lệnh đã được phóng to lên cho dễ thấy dấu nháy trên ngay sau ký tự N nhé:

    [SIZE=5]strSQLInsert = strSQLInsert & "[COLOR=#FF0000]N[/COLOR][B][COLOR=#0000CD]'[/COLOR][/B]" & objDanhba.Ten & "[B][COLOR=#0000CD]'[/COLOR][/B], "[/SIZE]

    Hôm nay xin tiếp tục trao đổi cùng các Bạn về nội dung chính của chuyên đề này:

    Khai báo biến đối tượng để sử dụng class đã tạo như thế nào?

    Rất đơn giản, ta khai báo biến đối tượng và sau đó gán biến đối tượng đã khai báo là 1 thành phần mới của class như đoạn code dưới đây:

    Dim objDanhba As clsDanhba
    Set objDanhba = [COLOR=#ff0000]New[/COLOR] clsDanhba
    ...

    Khi khai báo và gán biến đối tượng ta phải chú ý "câu thần chú sau: mở ra xài rồi phải đóng lại", nghĩa là: khi không còn nhu cầu sử dụng biến đối tượng đã khai báo và đã gán nữa thì ta phải cho đóng lại theo cách tương tự như đoạn code bên dưới:

    ...
        rsDanhba.Close
        Set rsDanhba = Nothing
    ...

    Câu lệnh đầu "rsDanhba.Close" có tác dụng đóng Class clsDanhba lại, câu lệnh thứ hai "Set rsDanhba = Nothing" có tác dụng xoá biến đối tượng đã gán. "Đóng" và "Xoá" ở đây để giải phóng bộ nhớ máy tính đã được cấp phát để quản lý đối tượng ta đã khai báo trước đó. Như vậy là các Bạn đã rõ việc này có vai trọng như thế nào rồi phải không.

    Vấn đề ở đây là phải xác định đúng "khi nào cần dùng" và "khi nào không cần dùng nữa" để "mở" và "đóng" đúng lúc.
    Xin dẫn ra đây ví dụ cụ thể ngay trong file ứng dụng minh hoạ chúng ta đang dùng:

    Với form "frmContacts", do mục đích sử dụng form này là để cập nhật và trình bày các thông tin chi tiết của danh bạ nên ta sẽ cần phải dùng đến đối tượng ta đã tạo trong class "clsDanhba", vì vậy:

    + Khi form này được nạp lên màn hình, ta phải gán biến đối tượng danh bạ đã tạo ngay tại sự kiện "Form được nạp – Load_Event với thủ tục Form_Load".
    Đồng thời ta cũng nhận thấy rằng biến đối tượng này ta sẽ phải sử dụng đến từ lúc form này được mở (khi cần dùng đến) cho đến lúc đóng nó lại (khi không cần dùng nữa), nên ta sẽ khai báo biến đối tượng này là biến dùng chung cho tất cả các thủ tục (procedure) có trong form,
    Và chỉ đóng nó lại khi ta đóng form lại (sự kiện Unload_Event với thủ tục Form_Unload).

    Vậy ta khai báo biến đối tượng này ở đâu? Ở trong từng thủ tục bên trong form "frmContacts" chăng?

    Các Bạn xem trang code của form "frmContacts" sẽ thấy các biến dùng chung trong "nội bộ" form này được khai báo ở đầu trang code, các dòng khai báo này đều nằm bên ngoài các thủ tục (procedure) như đoạn code được trích bên dưới đây.

    Nếu khai báo trong từng thủ tục sẽ không đạt được nhu cầu sử dụng biến đối tượng ta đã nêu ở trên (xét trong trường hợp cụ thể là file ứng dụng minh hoạ mà chúng ta đang dùng), vì khi thủ tục sự kiện hoàn tất (nghĩa là sự kiện đã hoàn thành) các biến đã khai báo và được gán sẽ bị đóng lại, các thủ tục khác không thể dùng (kế thừa) chúng được.

    Option Compare Database
    Option Explicit
    
    Dim blnAddMode As Boolean
    Dim rsDanhba As ADODB.Recordset
    [COLOR=#FF0000]Dim objDanhba As clsDanhba[/COLOR]
    Const Danhba_FORM = "frmDanhba"
    Dim intCurrDanhbaRecord As Integer
    Dim rsSearch As ADODB.Recordset
    Dim RecSearch As Boolean

    Sáng hôm nay ta tạm thời trao đổi chừng ấy. Xin hẹn các Bạn sẽ bàn tiếp vào chiều hôm nay
    Có Bạn vừa gọi hỏi tôi 2 vấn đề:

    1. Có thể đổi tên class module "clsDanhba" thành tên khác (chẳng hạn như "LopDanhba") được không?

    Đổi tên khác được, nhưng phải làm thêm một việc rất mất công là phải thay đổi tất cả các tham chiếu đến class module đã đổi tên. Cũng cần nói rõ hơn, nếu đổi tên "clsDanhba" thành "LopDanhba" thì khi tham chiếu đến đối tượng tương ứng cũng phải tham chiếu theo tên đã đổi.

    Thí dụ:

    Nếu đã khai báo biến đối tượng objDanhba bằng câu khai báo: Dim objDanhba As clsDanhba
    thì cũng phải đổi dòng khai báo tên thành: Dim objDanhba As LopDanhba

    Tuy nhiên, nếu Bạn muốn chuẩn hóa công việc thiết kế ứng dụng của mình khi viết code (là yêu cầu bắt buộc của làm việc khoa học), phải tuân thủ quy tắc thống nhất trong cách đặt tên biến, tên module, tên thủ tục, … Để làm gì vậy? Để dễ nhận diện và quản lý chúng. Đừng bao giờ đặt tên tùy hứng rồi sẽ đến ngày Bạn phải trả giá rất đắt khi phải xới tung đám rừng code trong ứng dụng để tìm được đúng cái mình cần đấy.

    Về cái sự "chuẩn hóa" này, như các Bạn đã thấy trong file ứng dụng minh họa, việc đặt tên đều theo 1 quy ước thông nhất đấy nhé:
    + Tên form bắt đầu bằng tiền tố "frm", như: frmContacts, frmLogIn
    + Tên module bắt đầu bằng tiền tố "mod", như: modQuanlyDulieu, modQuanlyRecord
    + Tên class module bắt đầu bằng tiền tố "cls", như: "clsDanhba"
    + Tên 1 biến đối tượng (object variabe) bắt đầu bằng tiền tố "obj", như: objDanhba

    Bấy giờ sang vấn đề thứ hai:
    2. Từ khóa "Me" trong một số thủ tục bên trong class module "clsDanhba" có ý nghĩa như thế nào? Có phải chỉ form đang mở?

    Từ khóa "Me" mà các Bạn thấy ở một số thủ tục trong class module "clsDanhba" là để chỉ bản thân class mình đang mở đấy. Chằng hạn thủ tục sau bên trong class module này:

    [COLOR=#006400]Sub PopulatePropertiesFromRecordset[/COLOR]([COLOR=#0000cd]rsCont As ADODB.Recordset[/COLOR])
     'Lay thong tin tu Recordset rsCont de gan gia tri cac thuoc tinh cho objDanhba
    
    On Error GoTo HandleError
    
    With Me
            .DanhbaId = rsCont!DanhbaId
            .Ten = Trim(FixNull(rsCont!Ten))
            .HoChulot = Trim(FixNull(rsCont!HoChulot))
            .Diachi = Trim(FixNull(rsCont!Diachi))
            .Dtdd = Trim(FixNull(rsCont!Dtdd))
            .Dtnha = Trim(FixNull(rsCont!Dtnha))
            .Dtvp = Trim(FixNull(rsCont!Dtvp))
            If Not IsNull(rsCont!Ngaysinh) Then
                .Ngaysinh = rsCont!Ngaysinh
            Else
                .Ngaysinh = ""
            End If
            .Gioitinh = rsCont!Gioitinh
        End With
        Exit Sub
    
    HandleError:
        GeneralErrorHandler Err.Number, Err.Description, CLS_DANHBA, "PopulatePropertiesFromRecordset"
        Exit Sub
    
    [COLOR=#006400]End Sub[/COLOR]

    Nếu chú ý, bên trong 1 thủ tục đang viết trong class module này, khi Bạn nhập vào từ khóa "Me" với dấu "." liền ngay sau đó sẽ thấy 1 popup sổ xuống liệt kê tất cả các properties và method đã viết trong class module này (đó cũng chính là properties và method của đối tượng ta tự tạo thông qua class module đang mở)
    Xem ảnh chụp màn hình sau ta sẽ thấy điều đó:
    2646

    Như vậy, trong class module, từ khóa "Me" không phải chỉ form đang mở các Bạn nhé.

  4. hands says:

    Hôm nay chúng ta sẽ trao đổi tiếp tục về chuyên đề này.

    Làm sao để khai báo 1 property của Object tự tạo là Read-Only (chỉ đọc mà thôi), nghĩa là ta chỉ có thể lấy được giá trị của property này, chứ không thể gán giá trị cho nó được.
    Rất đơn giản, ta chỉ cần không khai báo thủ tục Property Let trong Class module là xong.
    Lấy ví dụ cụ thể trong file ứng dụng minh họa mà chúng ta đang khảo sát, giả định ta muốn property DanhbaId là Read-Only, ta sẽ xóa thủ tục Public Property Let DanhbaId ra khỏi clsDanhba, chính là bỏ đi thủ tục ghi dưới đây:

    Public Property Let DanhbaId(ByVal Value As Long)
        On Error Resume Next
        lngDanhbaid = Value
    End Property

    Ngược lại, nếu muốn khai báo 1 property của Object tự tạo là Write-Only (chỉ ghi mà thôi), nghĩa là ta chỉ có thể gán giá trị cho property này, chứ không thể đọc giá trị của nó ra được.
    Cũng tương tự như trên, ta chỉ cần không khai báo thủ tục Property Get trong Class module là xong.
    Với file ứng dụng minh họa, nếu ta muốn property DanhbaId là Write-Only, ta sẽ xóa thủ tục Public Property Get DanhbaId ra khỏi clsDanhba, chính là bỏ đi thủ tục ghi dưới đây:

    Public Property Get DanhbaId() As Long
        On Error Resume Next
        DanhbaId = lngDanhbaid
    End Property

    Có Bạn hỏi tôi: có thể tạo ra nhiều Object trong cùng 1 class module hay không? Câu trả lời dứt khoát là không.
    Mỗi Class module chỉ được dùng để tạo 1 Object thôi.

    Như vậy là chúng ta đã khảo sát xong những vấn đề cơ bản về cách thức tạo 1 Object theo ý riêng thông qua công cụ Class module trong Access VBA.

    Trong bài kế tiếp ta sẽ trao đổi về 1 vấn đề có liên quan là làm sao để quản lý được tất cả các thành phần riêng lẻ của 1 Object tự tạo? Kiểu như quản lý tập hợp nguyên cả cái Danh bạ, bao gồm các công việc như: thêm , xóa, đếm số lượng thành phần, …
    Rất mong các Bạn tham gia trao đổi về chuyên đề này. Các Bạn có thể trao đổi về:
    1.
    2. Những giải pháp khác của Bạn xung quanh vấn đề chúng ta đang bàn
    3. Những thắc mắc phát sinh khi chạy file ứng dụng minh họa
    4. Những nhu cầu cần đáp ứng
    Và những vấn đề có liên quan khác.

    Khi có trao đổi qua lại, chắc chắn chúng ta sẽ có điều kiện làm sáng tỏ thêm nhiều điều và quan trọng là sẽ rút ra được những kiến thức ứng dụng thiết thực.
    Hôm nay chúng ta sẽ trao đổi đến một công cụ để quản lý tập hợp các thành phần thuộc object do chúng ta tự tạo ra (bằng cách thức ta đã trao đổi trong các bài trước). Công cụ này được Access VBA gọi là Collection.

    Trong Access VBA, Collection là một Object như một tập hợp các thành phần nhiều object xác định. Chẳng hạn như tập hợp từng dòng danh sách trong cùng 1 danh bạ vậy .

    Collection trong Access VBA có 3 methods và 1 Property sau đây:

    – Methods:
    + Add
    : dùng để thêm một thành phần vào Collection tự tạo. Chúng ta có thể dễ dàng truy xuất đến thành phần bất kỳ trong Collection này thông qua một khoá chỉ định, khoá này gọi là "Key"
    + Item: dùng để truy xuất đến 1 thành phần xác định thông qua 1 chỉ số index (hay là chỉ số thứ tự) của thành phần xác định đó trong Collection tự tạo
    + Remove: dùng để xoá 1 thành phần xác định khỏi Collection tự tạo thông qua chỉ số index hoặc key tương ứng của thành phần đó.

    – Property:
    + Count: dùng để lấy tổng số thành phần đang có trong Collection tự tạo

    Trong file ứng dụng minh họa, giả định chúng ta có nhu cầu cần xử lý một danh sách thỏa một điều kiện lọc xác định nào đó.
    Cách làm khoa học nhất là sử dụng Collection để tập hợp danh sách đó lại, sau đó sẽ tùy nghi xử lý.
    Với cách làm này, việc xử lý sẽ tách tập hợp danh sách này riêng ra khỏi file dữ liệu, tránh nặng nề cho các tác vụ khác trong môi trường nhiều người dùng trong mạng máy tính. Đồng thời ta cũng được lợi là danh sách (đã được lọc) ấy sẽ được lưu trữ tạm thời trong bộ nhớ máy tính (RAM) nên việc xử lý sẽ nhanh hơn.

    Các Bạn xem ví dụ sau nhé:
    Giả định ta muốn lấy toàn bộ danh sách đã được lọc trên form "frmContacts" để ghi vào 1 bảng dữ liệu đã được tạo trước đó (giả định bảng này tên là tblDs, với 2 cột dữ liệu: Id và Ten).
    Ta sẽ phải làm 2 việc sau đây:
    1. Ta viết thủ tục sau để lấy dữ liệu vào bảng tblDs, có nội dung như sau:

    [COLOR=#006400]Sub GetDataFromCollection([/COLOR][COLOR=#0000cd]strSQL As String[/COLOR][COLOR=#006400])[/COLOR]
    '
    Dim sqlSt As String, sCri As String, vCount As Long
    Dim MyRec As ADODB.Recordset
    '
    [COLOR=#0000CD]Dim MyObj As clsDanhba[/COLOR]
    Dim TestCol As Collection
    [COLOR=#ff0000]Set TestCol = New Collection[/COLOR]
    '
    Set MyRec = ProcessRecordset(strSQL)
    [COLOR=#0000cd]'Sau đây ta sẽ duyệt MyRec để nạp các dòng danh sách cho TestCol (Collection)[/COLOR]
    Do Until MyRec.EOF
        Set MyObj = New clsDanhba
        MyObj.PopulatePropertiesFromRecordset MyRec
        TestCol.Add MyObj [COLOR=#0000cd]'Thêm thành phần vào Collection với method Add[/COLOR]
        MyRec.MoveNext
    Loop
    
    MyRec.Close
    Set MyRec = Nothing
    Set MyObj = Nothing
    '
    vCount = TestCol.Count [COLOR=#0000cd]'Lấy tổng số dòng danh sách (thành phần) đã thêm vào Collection với Property Count
    
    [/COLOR]Docmd.Setwarnings False [COLOR=#0000cd]'Dòng này được bổ sung ngày 09/7/2012 để ngăn thông báo nhắc xác nhận khi chạy câu lệnh RunSQL bên dưới.[/COLOR]
    
    DoCmd.RunSQL "DELETE * FROM tblDs"
    
    [COLOR=#0000ff]'Sau đây ta duyệt từng thành phần trong Collection TestCol để cho ghi vào bảng tblDs
    'Hoặc Bạn có thể viết code khác để làm một việc nào đó khác
    'Xin chú ý cách duyệt các thành phần của 1 Collection thông qua cấu trúc câu lệnh được sơn màu đỏ bên dưới
    [/COLOR][COLOR=#ff0000]For Each MyObj In TestCol[/COLOR]
        [B]With MyObj[/B]
            DoCmd.Hourglass True
            DoCmd.RunSQL "INSERT INTO tblDs(Id, Ten) VALUES(" & .DanhbaId & ",'" & .Ten & "')"
            DoCmd.Hourglass False
        [B]End With[/B]
    [COLOR=#ff0000]Next[/COLOR]
    Set TestCol = Nothing
    
    'Open Table tblDs
    MsgBox "Danh sach nay co tat ca la: " & vCount & " nguoi"
    DoCmd.OpenTable "tblDs", , acReadOnly
    
    Docmd.Setwarnings True [COLOR=#FF0000][/COLOR][COLOR=#0000cd]'Dòng này được bổ sung ngày 09/7/2012 để khôi phục lại việc cho hiện thông báo nhắc xác nhận khi chạy câu lệnh RunSQL (hoặc 1 Action-Query)[/COLOR][COLOR=#FF0000]
    [/COLOR] 
    End Sub

    2. Ta bổ sung thêm thiết kế cho form "frmContacts", với:
    + 1 nút lệnh có caption là "Lấy Danh sách"
    + và thêm thủ tục sự kiện Click Even cho nút lệnh này như sau:

    ...
    GetDataFromCollection GetSQL
    'Cho hien ket qua len form
    Call RunSearch
    ...

    Bạn nào muốn xem chứ ngại viết như trên thì tải về file ứng dụng từ link sau nhé:
    https://www.mediafire.com/?o5krl1bznxpanjy

    Như vậy ta đã khảo sát xong 1 trường hợp sử dụng Collection.
    Cũng xin các Bạn lưu ý: có rất nhiều cách sử dụng Collection. Ở đây tôi chỉ trình bày 1 cách, các Bạn có thể tùy trường hợp và nhu cầu cụ thể để sử dụng thích hợp.

    Trong bài sau, chúng ta sẽ tìm hiểu vấn đề nên thiết kế Form gắn kết với nguồn dữ liệu (Bound-Form) hay thiết kế Form không gắn kết với nguồn dữ liệu (UnBound-Form).

  5. hands says:

    Chào các Bạn,

    Tôi vừa tìm lại được tài liệu + ứng dụng minh họa do tôi sưu tầm trước đây trên internet đề cập đến việc tạo và sử dụng các Object tự tạo (custom objects) và Collection tự tạo (custom collections) với Access VBA. Tất cả đều bằng tiếng Anh.
    Link tải về: https://www.mediafire.com/?434diarb50ric4v

    Qua tài liệu này ta sẽ thấy 1 cách khác trong việc sử dụng Collection.

  6. hands says:

    Chào các Bạn,

    Hôm nay, xin trao đổi với các Bạn về Bound Form và UnBound Form

    1. Bound Form là gì?
    Bound Form là 1 form được gán Record Source xác định, nghĩa là Bound Form được gán với 1 nguồn dữ liệu xác định, nguồn dữ liệu đó có thể là 1 bảng (table) hoặc 1 truy vấn dữ liệu (Query), nói theo ngôn ngữ lập trình với ADO thì nguồn dữ liệu này là 1 Recordset.
    Khi tạo 1 Form trong Access, nguồn dữ liệu được khai báo thông qua property "Record Source".
    Đặc điểm của Bound Form là nguồn dữ liệu được nạp và duy trì liên tục từ lúc Form được mở cho đến khi Form được đóng lại. Mọi việc xử lý dữ liệu, từ nạp dữ liệu nguồn, duyệt dữ liệu nguồn,… hầu như đều do Access làm thay ta hết thảy.

    Với các Form được đặt ở chế độ cho phép thêm, xóa, hiệu chỉnh dữ liệu trong điều kiện nhiều người sử dụng cùng lúc (truy xuất cùng 1 nguồn dữ liệu) sẽ dễ dẫn đến tình trạng khi form đang ở tình trạng hiệu chỉnh 1 mẫu tin (Record) và chưa kết thúc công việc này sẽ dẫn đến việc Access tạm khóa mẫu tin này lại (record locked) cho đến khi kết thúc việc hiệu chỉnh dữ liệu (bằng việc cho lưu các thay đổi hoặc phục hồi lại như cũ – Undo). Khi mẫu tin bị Access tạm khóa nếu lại có ai đó cũng đồng thời hiệu chỉnh mẫu tin này, Access sẽ ngăn lại và hiện thông báo cảnh báo. Nếu ta lập trình không khéo để bẩy sự kiện truy xuất trùng này sẽ dễ dàng dẫn đến làm hỏng nguồn dữ liệu đang nạp.

    Các Bạn hình dung tình huống sau xem điều gì sẽ xảy ra nhé: nhân viên A đang mở mẫu tin xác định ra để hiệu chỉnh, và đang hiệu chỉnh nữa chừng chưa lưu lại các thay đổi thì bổng chột bụng cần phải "giải quyết" ngay. Thế là mẫu tin bị treo ở đó cho đến khi nhân viên A quay lại và có thao tác thích hợp. Mọi người khác phải đành bó tay khi cần làm gì đó với mẫu tin này. Lỡ nhân viên A quên quay lại để có thao tác chấm dứt cái sự nữa chừng bị treo lại kia thì sao? Bó tay đó các Bạn.

    2. UnBound Form là gì?
    Ngược với Bound Form, UnBound Form là form không gắn với 1 nguồn dữ liệu xác định nào cả.
    Vậy làm sao UnBound Form hiển thị được thông tin ta cần đến, và làm sao để ta có thể thêm mới hoặc hiệu chỉnh nội dung một mẫu tin nào đó trong 1 nguồn dữ liệu xác định?
    Nguyên tắc ở đây là: khi nào cần thì cho kết nối với dữ liệu nguồn, xong việc thì đóng kết nối lại.
    – Việc kết nối với dữ liệu nguồn:
    + Có nhiều kiểu kết nối tùy theo mục đích của ta cần kết nối để làm gì? Chỉ để xem hay còn để hiệu chỉnh, cập nhật lại thông tin? Tùy theo đó mà ta lựa chọn kiểu kết nối thích hợp.
    + Mặt khác, cũng cần phải xác định rõ phạm vi kết nối, để tránh chiếm dụng vô ích tài nguyên bộ nhớ của máy tính; chẳng hạn như nếu ta chỉ cần xử lý danh sách với một phạm vi lọc nào đó (danh sách theo 1 vùng địa lý xác định được ghi trong địa chỉ của khách hàng có trong danh sách chẳng hạn), tránh việc nạp hết nguồn dữ liệu lên.
    Với UnBound Form, việc truy xuất đến dữ liệu nguồn đều phải do ta tự làm lấy thông qua việc viết các thủ tục (procedure) để xử lý (từ việc nạp dữ liệu đến việc hiển thị, hiệu chỉnh, xóa, cập nhật thay đổi, …). Đây chính là đặc điểm khác với Bound Form.

    Rõ ràng, qua các đặc điểm của UnBound Form như trên đã phân tích, cho ta thấy UnBound Form thích hợp cho việc khai thác và xử lý nguồn dữ liệu trong môi trường nhiều người sử dụng cùng lúc (làm việc trên mạng máy tính), đặc biệt đối với dữ liệu có quy mô lớn (lớn về độ phức tạp và lớn về sức chứa vật lý)

    3. Vậy, nên áp dụng Bound Form hay UnBound Form? Cái nào ưu việt hơn?
    Câu trả lời ở đây là: lựa chọn phải tùy vào mục đích ta áp dụng để làm gì và trong hoàn cảnh cụ thể như thế nào?
    Nếu chỉ với dữ liệu chạy cục bộ, chỉ có một người sử dụng trong cùng 1 thời gian và quy mô dữ liệu không lớn lắm thì lựa chọn áp dung Bound Form sẽ tốt hơn, vì dễ dàng và nhanh chóng.
    Cái nào ưu việt hơn? cái nào giúp ta đạt được mục đích với chi phí ít nhất (tiền bạc, thời gian, công sức) là cái ưu việt hơn.
    Ở đây chúng ta cần suy niệm nguyên tắc của tiền nhân là "đừng bao giờ mổ gà bằng dao mổ trâu và ngược lại."

    Hẹn các Bạn trong bài sau ta sẽ tìm hiểu tiếp cách thức xử lý dữ liệu với 1 Unbound Form.

  7. hands says:

    Chào các Bạn,

    Hôm nay ta tiếp tục tìm hiểu xem cách thức xử lý dữ liệu với 1 UnBound Form như thế nào?

    Với 1 UnBound Form ta cần phải giải quyết những nhu cầu sau đây:
    1. Làm sao để nạp được nội dung dữ liệu cho hiển thị lên các ô dữ liệu trên Form?
    2. Làm sao để cập nhật các thông tin (thêm hoặc lưu các thay đổi) đang có trên Form vào nguồn dữ liệu?
    3. Làm sao để xoá 1 mẫu tin xác định?
    4. Làm sao để duyệt qua lại các mẫu tin của nguồn dữ liệu và cho hiển thị chúng trên Form (như 1 Bound Form đã có sẵn qua các nút lệnh duyệt tới lui qua Navigation Buttons)?

    Để đáp ứng được các nhu cầu nêu trên trước hết ta cần chú ý một số vấn đề mang tính nguyên tắc trong thiết kế UnBound Form như sau:

    1. Việc đặt tên các Control trong UnBound Form:
    Đặc điểm của UnBound Form là không gắn với nguồn dữ liệu xác định thông qua property "Record Source", và các Controls trong Form (TextBox, ComboBox, ListBox, …) này cũng không gắn với nguồn dữ liệu xác định thông qua property "Control Source".

    Do vậy, khi đặt tên các Controls này ta phải chú ý đặt tên sao cho thể hiện mối liên hệ trực tiếp đến các cột dữ liệu trong nguồn dữ liệu ta cần xử lý, chẳng hạn như với file ứng dụng minh hoạ, ta thấy bên trong form "frmContacts":
    + TextBox mang tên "txtTen" sẽ được dùng để hiển thị "Tên" của từng người trong danh bạ.
    + TextBox mang tên "txtHoChulot" sẽ được dùng để hiển thị "Họ và chữ lót" của từng người trong danh bạ.

    Điều đó giúp ta tránh được việc nhầm lẫn khi gán nội dung thông tin chi tiết tương ứng lên các Control này khi viết các thủ tục xử lý dữ liệu.

    2. Việc cho hiển thị thông tin trên UnBound Form:
    Với ứng dụng chạy trong môi trường nhiều người dùng (qua mạng máy tính) ta phải hết sức tiết kiệm tài nguyên của máy tính (bao gồm cả không gian trống của bộ nhớ và cường dộ làm việc của CPU), dù máy tính được trang bị mạnh đến cỡ nào đi nữa cũng không được lơ là việc tiết kiệm tài nguyên. Bởi vấn đề ở đây không chỉ là tiết kiệm thôi đâu, mà còn là vấn đề tránh xung đột khi xử lý dữ liệu.
    Do vậy, ta chỉ cho hiển thị thông tin khi cần và chỉ nạp nguồn dữ liệu trong phạm vi vừa đúng với nhu cầu cần xử lý (không được thừa hoặc thiếu). Chẳng hạn như khi mở Form, nếu không phải là nhu cầu hiển thị kết quả tìm kiếm thì ta không nên nạp bất kỳ thông tin gì lên Form, nghĩa là để Form trống ở tình trạng sẵn sàng nhận nội dung ta nhập vào. Làm như vậy việc nạp Form lên sẽ rất nhanh, có thể nói là tức thì.

    Bây giờ ta xét từng nhu cầu xử lý dữ liệu:

    1. Làm sao để nạp được nội dung dữ liệu cho hiển thị lên các ô dữ liệu trên Form?
    Thường để làm việc này ta cần phải qua các thủ tục sau:
    + Kết nối đến nguồn dữ liệu cần xử lý (Database)
    + Cho nạp tập hợp các mẫu tin trong phạm vi cần xử lý (Recordset)
    + Nạp thông tin chi tiết của mẫu tin đầu tiên vào các ô dữ liệu có liên quan trên form (Record)

    Xét ứng dụng minh hoạ với form "frmContacts" ta thấy:
    + Để kết nối đến nguồn dữ liệu (Database) ta có thủ tục "OpenDbConnection" trong module "modQuanlyDulieu"
    + Để nạp tập hợp mẫu tin hiện ta đang có thủ tục "RetrieveDanhba" trong class module "clsDanhba" như một method của Object tự tạo "clsDanhba".

    Thủ tục "RetrieveDanhba" hiện có cho nạp tập hợp mẫu tin (Recordset) là toàn bộ bảng dữ liệu "tblDanhsach". Ta cũng đã biết bảng "tblDanhsach" có trên 15.000 mẫu tin. Vậy là ta đã cho nạp hết trọi tập hợp trên 15.000 mẫu tin này.

    Vấn đề cần được quan tâm đánh giá ở đây là: ta nạp khối lượng mẫu tin to đùng như vậy để làm gì?
    Xét hết trọi các thao tác và nhu cầu hiển thị thông tin trên form "frmContacts" ta thấy:
    Ngoài việc để biết bảng dữ liệu có tổng số mẫu tin là bao nhiêu, còn lại chẳng để làm gì cho có lợi cả.
    Vậy thì hà cớ gì ta lại tiêu tốn một lượng lớn tài nguyên của máy tính cho chỉ duy nhất có 1 mục đích như vậy. Nếu muốn lấy tổng số mẫu tin trong 1 bảng dữ liệu ta chỉ cần "SELECT Count(*) FROM <tên_bảng_dữ_liệu>" là được rồi kia mà.

    Mặt khác ta cũng thấy rằng, với kiểu xài sang đó mỗi khi mở form "frmContacts" ta thấy phải mất bộn thời gian tính bằng giây thì form mới nạp xong.

    Do đó, ta cần 1 giải pháp để chỉ nạp tập hợp mẫu tin trong phạm vi cần xử lý thôi.
    Sau đây là 1 cách, theo đề nghị của tôi (Nếu các Bạn có giải pháp khác xin trao đổi thêm nhé):
    Ta sẽ phải làm mấy công việc sau đây:
    Việc thứ nhất:
    – Để bảo toàn thủ tục đang có nhằm mục đích có cái mà đối chiếu so sánh thiệt hơn giữa các giải pháp, ta sẽ viết thêm 1 thủ tục, sao cho chỉ cần nạp 1 tập hợp khoảng chừng 100 mẫu tin thôi.
    Tại sao tôi lại chọn 100 mẫu tin, mà không chọn ít hơn, thậm chí chỉ cần 1 là đủ, vì mỗi lần ta chỉ hiển thị được nội dung của 1 mẫu tin lên Form "frmContacts" thôi mà?
    Ái dà, cũng phải có lý do đầy đủ và hợp lý chứ các Bạn nhỉ.
    Là tôi nghĩ như thế này:
    Việc cần phải nạp hơn 1 mẫu tin nhằm mục đích để minh họa cho các thao tác duyệt tập hợp mẫu tin trên form "frmContacts" thông qua các nút lệnh duyệt mẫu tin tơi lui.
    Do vậy, nếu nạp ít quá sẽ khó hình dung tác dụng của các thủ tục duyệt mẫu tin.
    Thủ tục được viết thêm như sau:

    [COLOR=#006400]Function BuildSQLSelectLimitDanhba(FromId, ToId) As String[/COLOR]
    
    On Error GoTo HandleError
    
    Dim strSQLRetrieve As String
    
    sChemaName = GetSchemaTable("tblDanhsach")
    
    strSQLRetrieve = "SELECT * FROM " & sChemaName & ".tblDanhsach"
        strSQLRetrieve = strSQLRetrieve & " WHERE DanhbaId BETWEEN " & FromId & " AND " & ToId
        strSQLRetrieve = strSQLRetrieve & " ORDER BY DanhbaId"
    
    BuildSQLSelectLimitDanhba = strSQLRetrieve
    
    Exit Function
    
    HandleError:
        GeneralErrorHandler Err.Number, Err.Description, DB_QUANLY, "BuildSQLSelectLimitDanhba"
        Exit Function
    
    [COLOR=#006400]End Function[/COLOR]

    Các Bạn để ý thủ tục trên sẽ thấy ta SELECT bảng dữ liệu với 1 điều kiện trong câu lệnh WHERE xác định là chỉ chọn các mẫu tin liên tục bắt đầu từ Danhbaid = biến FromId đến DanhbaId = biến ToId.
    Và ta sẽ điều chỉnh lại thủ tục "RetrieveDanhba" trong class module "clsDanhba" cho thích hợp như sau:

    [COLOR=#006400]Function RetrieveDanhba(WithLimit As Boolean, Optional FromId, Optional ToId) As ADODB.Recordset[/COLOR]
    
    'RetrieveDanhba: Truy xuat recordset cua Danhba thong qua cau lenh strSQLStatement
    
    On Error GoTo HandleError
    
    Dim strSQLStatement As String
        Dim rsCont As New ADODB.Recordset
    
    [COLOR=#008000]   'Dòng ngay bên dưới là dòng để nạp chuỗi SELECT toàn bộ danh bạ[/COLOR]
        '[COLOR=#ff0000]strSQLStatement = BuildSQLSelectDanhba[/COLOR]
    
    [COLOR=#006400]    '==== Nay ta REM nó lại để nạp đoạn code thay thế sau đây:======[/COLOR]
        If WithLimit = True Then
            strSQLStatement = BuildSQLSelectLimitDanhba(FromId, ToId)
        Else
            strSQLStatement = BuildSQLSelectDanhba
        End If
    [COLOR=#006400]    '============== HẾT ĐOẠN CODE MỚI ===============[/COLOR]
    
    Set rsCont = ProcessRecordset(strSQLStatement)
    
    Set RetrieveDanhba = rsCont
    
    Exit Function
    
    HandleError:
        GeneralErrorHandler Err.Number, Err.Description, CLS_DANHBA, "RetrieveDanhba"
        Exit Function
    
    [COLOR=#006400]End Function[/COLOR]

    Việc thứ hai: là làm sao xem được tổng số mẫu tin trong bảng danh sách?
    Ta phân tích nhu cầu thì thấy rằng đây là nhu cầu không cần thường xuyên, vậy ta sẽ làm việc này chỉ khi nào cần thôi.
    – Tôi viết thêm thủ tục lấy tổng số mẫu tin trong bảng danh sách như sau:
    + Trước hết tôi khai báo 1 biến dùng chung cho toàn bộ ứng dụng chỉ tổng số mẫu tin trong bảng danh sách bằng câu lệnh khai báo sau:

    Public lngRecCount As Long

    Tất nhiên là dòng khai báo trên nằm ở vùng Declarations của module "modQuanlyDulieu"
    Và thủ tục được thêm như sau:

    [COLOR=#006400]Sub GetTotalRecCount()[/COLOR]
    Dim strSQLStatement As String
    Dim rsSourceRec As ADODB.Recordset
    strSQLStatement = BuildSQLSelectDanhba
    Set rsSourceRec = ProcessRecordset(strSQLStatement)
    lngRecCount = rsSourceRec.RecordCount
    Set rsSourceRec = Nothing
    [COLOR=#006400]End Sub[/COLOR]

    – Kế đó, tôi sẽ vẽ vời thêm vài nét trên hình hài của form "frmContacts" gồm có:
    + Thêm 1 ô kiểm (check-box) để ta đánh dấu chọn khi cần cho hiện tổng số mẫu tin của bảng dữ liệu.
    + Thêm 1 ô dữ liệu nữa để hiển thị "DanhbaId" của từng mẫu tin được nạp lên form. Ô này ta cho nó mờ đi bằng cách khai báo property "Enabled" là False (Hay "No").
    + Thêm một nút lệnh vào nhóm các nút lệnh duyệt mẫu tin để nạp 100 mẫu tin khác khi cần và gán nó cái nhãn (caption) là "+100 Rec" cho dễ hiểu.
    Cứ mỗi lần bấm nút lệnh này (Click_Event) ta sẽ cho chạy thủ tục Nạp thêm 1 tập hợp có 100 mẫu tin tiếp theo 100 mẫu tin đã nạp.
    Làm sao xác định là "100 mẫu tin tiếp theo"?
    Tôi chỉ cần khai báo 1 biến cục bộ trong class module của form "frmContact" (chính là cái trang code ta mở phía sau form) để chỉ DanhbaId cuối cùng của tập hợp mẫu tin đã được nạp và đang hiện hữu. Vậy là ta sẽ xác định được giá trị của 2 tham số: FromId và ToId trong thủ tục "RetrieveDanhba" nêu trên.

    Thế là ta đã thỏa mãn được yêu cầu chỉ nạp tập hợp mẫu tin trong phạm vi giới hạn cần dùng.

    Sau đây là Link tải xuống file ứng dụng đã cập nhật theo bài này:
    https://www.mediafire.com/?c9bi76gtn2dmjv9

    Đến đây bài đã dài rồi, xin hẹn các Bạn bài sau ta sẽ bàn tiếp nhé.

  8. hands says:

    Chào các Bạn,

    Tối hôm qua có Bạn gọi hỏi tôi rằng: vậy muốn nạp trở lại 100 mẫu tin trước 100 mẫu tin đang hiện hữu thì làm sao?

    Các Bạn thử làm như sau xem sao nhé:
    1. Thêm 1 nút lệnh với caption "-100 Rec"
    2. Viết thủ tục bẩy sự kiện click_event cho nút lệnh này, trong đó xác định 2 tham số FromId và ToId như sau:
    + ToId = (Id của mẫu tin cuối cùng trong tập hợp 100 mẫu tin đang hiện hữu) – 1
    + FromId = (Id của mẫu tin cuối cùng trong tập hợp 100 mẫu tin đang hiện hữu) – 100
    Về việc nạp dữ liệu nguồn cho form, chúng ta cũng cần chú ý đến việc nạp dữ liệu nguồn cho các ComboBox hoặc ListBox.
    Khi nạp dữ liệu nguồn cho các ComboBox hoặc ListBox chúng ta cũng phải tuân thủ nguyên tắc chỉ nạp dữ liệu trong phạm vi giới hạn vừa đúng với nhu cầu khai thác xử liệu.
    Sau đây tôi xin trình bày một trong những cách thức nạp dữ liệu nguồn cho ComboBox xác định tuân thủ nguyên tắc nêu trên. Cụ thể như sau:

    Nhu cầu đặt ra là: trên form cần có 1 ComboBox dùng để liệt kê sẵn danh sách tên và địa chỉ khách có trong bảng dữ liệu "tblDanhsach".
    Thay vì ta cho nạp nguồn dữ liệu cho ComboBox này 1 lần ngay khi form được mở, ta sẽ cho lọc danh sách nguồn theo 1 điều kiện xác định.
    Điều kiện lọc ở đây được ban hành bằng cách ta nhập thẳng 1 vài từ cần tìm trong tên của khách (có trong bảng danh sách), sau đó chương trình sẽ tự động nạp nguồn dữ liệu theo điều kiện lọc này. Làm như vậy ta sẽ hạn chế được khối lượng dữ liệu hữu ích cần nạp, đồng thwofi cũng làm cho việc hiện danh sách sổ xuống nhanh hơn.

    Cách làm như sau:
    – Giả định ta đặt tên ComboBox nói trên là "combo0"
    – Trong class module của form chứa ComboBox nêu trên, ta viết 1 thủ tục có nội dung như sau để thiết lập nguồn dữ liệu cho ComboBox "combo0".
    Thủ tục này có tham số "stFilter" sẽ là chuỗi ký tự lập thành điều kiện lọc do người sử dụng nhập vào tại ComboBox "combo0".

    [COLOR=green]Private Sub SetComboRowSource([COLOR=blue]stFilter[/COLOR])[/COLOR]
    Dim sqlSt As String
    Dim r As ADODB.Recordset
    
    sqlSt = "SELECT ten, diachi, danhbaid FROM " & GetSchemaTable("tblDanhsach") & ".tblDanhsach"
    sqlSt = sqlSt & [COLOR=#ff0000]" WHERE ten LIKE N'%" & stFilter & "%'"[/COLOR]
    sqlSt = sqlSt & " ORDER BY danhbaid"
    
    Set r = ProcessRecordset(sqlSt)
    [COLOR=#0000cd]
    Set Me.Combo0.Recordset = r[/COLOR]
    
    With Me.Combo0
        .BoundColumn = 1
        .ColumnCount = 3
        .ColumnWidths = "7 Cm;7 Cm;0"
    End With
    r.Close
    Set r = Nothing
    [COLOR=teal]End Sub[/COLOR]

    Các Bạn lưu ý: thay vì cho nạp chuỗi nguồn dữ liệu cho property "RowSource" của ComboBox, tôi cho nạp thuộc tính "Recordset" cho ComboBox này. Tôi làm như vậy để cho gọn gàng thôi.

    – Với ComboBox "combo0" ta viết thủ tục sự kiện Enter có nội dung như sau:

    [COLOR=teal]Private Sub Combo0_Enter()[/COLOR]
        If Len(Me.Combo0) > 0 Then SetComboRowSource Me.Combo0
    [COLOR=teal]End Sub[/COLOR]

    Tôi vừa nhận được email của 1 Bạn hỏi về vấn đề nạp nguồn dữ liệu cho ComboBox mà chúng ta đã trao đổi ở #21. Bạn ấy hỏi:
    "Tôi muốn cứ mỗi khi gõ vào 1 chuỗi thì ComboBox được lọc ngay theo chuỗi này thì phải làm sao?"
    Ở đây ta cần cân nhắc xem việc lọc nguồn dữ liệu có cần thực hiện ngay tại thời điểm "cứ mỗi khi gõ vào" hay không?
    Rõ ràng trong thực tế ta không cần đến mức tức thì "cứ mỗi khi gõ vào" như vậy. Nếu làm việc này tôi e rằng sẽ mất rất nhiều thời gian để ứng dụng nạp xong dữ liệu nguồn theo điều kiện lọc ta gõ vào.
    Do đó, tôi đề nghị 1 giải pháp như sau: chỉ khi nào ta bấm phím lệnh cho hiện danh sách sổ xuống thì lúc ấy ứng dụng hãy cho nạp dữ liệu nguồn được lọc theo chuỗi ta đã nhập vào ComboBox. Cách làm như sau:
    1. Bỏ thủ tục đáp ứng sự kiện Enter của ComboBox như ta đã làm như đã trình bày trong bài trên (#21)
    2. Viết thủ tục đáp ứng sự kiện KeyDown như sau, để mỗi khi ta bấm phím F4 hoặc tổ hợp phím (Alt+Mũi tên xuống) ứng dụng sẽ cho nạp dữ liệu nguồn được lọc theo chuỗi ta đã nhập vào ComboBox này.

    Chúng ta đã biết: phím F4 hoặc tổ hợp phím (Alt+Mũi tên xuống) dùng để cho hiện danh sách sổ xuống của ComboBox

    [COLOR=#006400]Private Sub Combo0_KeyDown([/COLOR][COLOR=#0000cd][B]KeyCode [/B]As Integer, Shift As Integer[/COLOR][COLOR=#006400])[/COLOR]
    Dim stFilter
    If KeyCode = vbKeyF4 Or (KeyCode = vbKeyDown And Shift = acAltMask) Then [COLOR=#ff0000]'Bẩy phím F4 hoặc Alt+Mũi tên xuống[/COLOR]
        stFilter = Me.Combo0.Text
        If Len(stFilter) > 0 Then
            SetComboRowSource stFilter
        End If
    End If
    [COLOR=#006400]End Sub[/COLOR]

    Về việc kết nối với dữ liệu nguồn qua mạng máy tính tôi thấy cũng cần trao đổi thêm về việc tổ chức dữ liệu sao cho việc kết nối dữ liệu được thuận lợi và hiệu quả nhất.

    Theo tôi thấy (có thể các Bạn sẽ thấy khác): Trong thực tế, không phải lúc nào chúng ta cũng cần lấy dữ liệu xuống bằng cách kết nối với dữ liệu nguồn đặt tại server; có những nguồn dữ liệu có tính ổn định nhất định (không bị thay đổi thường xuyên) ta có thể cho trích xuất với phạm vi giới hạn nhất định và cho lưu xuống máy client (máy khách cần kết nối vào server), sau đó ta sẽ cho nạp nguồn dữ liệu từ dữ liệu đã được trích xuất này. Làm như vậy ta vừa cải thiện được tốc độ truy xuất dữ liệu, vừa giảm được tải không cần thiết cho cả server và client.

    Các Bạn thử xem xét tình huống sau đây nhé:
    Với doanh nghiệp bán hàng trên phạm vi rộng, có ứng dụng chạy trên client kết nối đến dữ liệu nguồn ở server, ứng dụng này dành cho các nhân viên thị trường sử dụng trên các laptop để thực hiện nhiệm vụ "Tìm kiếm khách mua hàng và lập đơn đặt hàng theo bảng giá ấn định chung". Mỗi nhân viên thị trường đều được phân công phụ trách một phạm vi địa lý nhất định.

    Như vậy, ta có thể cho trích xuất các nguồn dữ liệu sau lưu xuống máy client để ứng dụng client sử dụng trực tiếp, không cần phải lấy từ server thông qua kết nối qua mạng:
    + Danh sách khách hàng trong phạm vi địa lý đã phân công cho từng nhân viên;
    + Danh mục hàng hoá (có bảng giá) cũng trong giới hạn cần thiết.

    Đồng thời với đó, ta sẽ có các thủ tục thích hợp để cho đồng bộ dữ liệu đang lưu tạm trên các máy Client với dữ liệu gốc trên server. Việc đồng bộ dữ liệu này sẽ được thực hiện tại thời điểm thích hợp (trong ngày hoặc trong tuần) hoặc khi có sự kiện thay đổi dữ liệu xảy ra (như đơn giá được người có thẩm quyền cập nhật mới, …).

    Các Bạn có thấy điều gì không ổn trong đề nghị trên của tôi không? Xin vui lòng góp ý trao đổi thêm.

  9. hands says:

    Trước hết tôi rất cám ơn những gì bạn đã tận tình hướng dẫn cho mọi người, nhất là những thành viên còn rất mơ hồ về Access như tôi. Bạn hướng dẫn rất cụ thể và tận tình, những bài viết của bạn thật hữu ích.

    Qua đường link của bài này khi mình tải về thì có hiện tượng như sau:

    Sau khi bấm nút "Lấy Danh Sách" thì nó hiện một thông báo:

    2652

    Nếu bấm No sẽ hiện ra lỗi:

    2651

    Chọn Debug thì lỗi đặt tại đây:

    2650

    Như vậy cần phải làm gì để bẫy lỗi này? Và nguyên nhân từ đâu?

    Xin cám ơn bạn.

    Chào Bạn,

    Xin sửa lại thủ tục có phát sinh lỗi như sau:

    + Thêm dòng sau bên trên dòng lệnh "DoCmd.RunSQL "DELETE * FROM tblDs":

    [COLOR=#0000cd]'Cho chặn lại các thông báo nhắc xác nhận của Access mỗi khi thực hiện 1 Action-Query[/COLOR]
    DoCmd.SetWarnings False

    + Và thêm dòng sau ở sau dòng cuối cùng của thủ tục nêu trên:

    [COLOR=#0000cd]'Khôi phục lại việc cho hiện các thông báo nhắc xác nhận của Access mỗi khi thực hiện 1 Action-Query[/COLOR]
    DoCmd.SetWarnings True

    Trong File bạn gửi lên, thì đã tồn tại tblDs rồi mà, như vậy mới có thông báo "Bạn định xóa 7866 dòng…?" đó thôi.

    Tôi có nhầm lẫn khi trả lời, nên đã sửa lại nội dung trả lời rồi. Bạn xem lại ở #25 nhé.

    Nhưng cho mình hỏi, mình vẫn chưa hiểu cái nút Lấy danh sách. Sau khi đặt thủ tục không hiện lên thông báo, thì tất cả những gì có trong tblDs đã bị xóa sạch. Vậy Lấy danh sách gì vậy bạn? Hỏi để biết thêm nguyên lý hoạt động của form này.
    Xin cám ơn.

    Về vấn đề Bạn hỏi về cái nút lệnh "Lấy Danh sách": đây chỉ làm ví dụ minh hoạ cho việc tạo và sử dụng 1 Collection tự tạo thôi (xem #16). Mặt khác, qua đó tôi cũng đã ngầm chuẩn bị cho nội dung như đã trao đổi ở bài #23 ở trên về việc trích xuất dữ liệu từ server cho lưu xuống máy client. Đây cũng chỉ là 1 trong rất nhiều giải pháp thôi, không phải là duy nhất.

    Còn về việc ra lệnh xoá nội dung bảng "tblDs" rồi nạp lại để làm gì? Trong tình huống này, bảng "tblDs" như là 1 bảng lưu tạm dữ liệu lên máy client để tôi dùng vào 1 việc gì đó (chẳng hạn làm nguồn cho 1 ComboBox hoặc ListBox), khi cần nạp nội dung khác thì phải xoá nội dung cũ đi để nạp lại cái mới.

    Khi xem file ứng dụng minh hoạ, các Bạn chỉ nên coi đó là trường hợp minh hoạ cho những nội dung tôi trao đổi cùng các Bạn, đừng xem đó là 1 ứng dụng hoàn chỉnh.

  10. hands says:

    Chào các Bạn,
    Có Bạn hỏi: muốn thiết kế 1 form có SubForm theo UnBound Form thì phải làm sao? Chẳng hạn như thiết kế 1 form để nhập chứng từ nhập / xuất hàng hoá (với SubForm trình bày chi tiết hàng hoá phát sinh của chứng từ).

    Do quá bận nên tôi chưa thể trao đổi cụ thể được về vấn đề này, xin hẹn các Bạn trong những ngày tới. Hôm nay chỉ xin trao đổi một số gợi ý để các Bạn tham khảo như sau:

    – Với MainForm để đăng ký các thông tin chung của chứng từ chúng ta thiết kế form và viết thủ tục để truy xuất dữ liệu có liên quan theo cách tương tự như ta đã làm trong file ứng dụng minh hoạ với Danh sách trong Danh bạ điện thoại.
    Để hiển thị thông tin hàng hoá chi tiết phát sinh của chứng từ, các Bạn có thể thiết kế theo 1 trong các cách sau:
    + Thiết kế 1 ListBox gồm có các cột dữ liệu phản ảnh thông tin chi tiết của hàng hoá
    + Hoặc thiết kế 1 Form độc lập để làm SubForm, lấy dữ liệu nguồn là Recordset được lọc theo số chứng từ phát sinh xác định, số chứng từ này ta sẽ lấy từ ô ghi số chứng từ trên MainForm. Các Bạn cần chú ý thiết lập kiểu dữ liệu của Recordset này phù hợp với nhu cầu chỉ để hiển thị thông tin thôi.
    + Thiết kế các ô để nhập dữ liệu chi tiết hàng hoá phát sinh (như: mã hàng, tên hàng, đơn vị tính, số lượng, đơn giá,…), các ô dữ liệu này cũng không gắn liền với nguồn dữ liệu xác định nào cả (nghĩa là không khai báo ControlSource). Để cập nhật thông tin nhập trên các ô này vào bảng dữ liệu có liên quan ta sẽ viết 1 thủ tục cập nhật (tương tự như thủ tục cập nhật danh sách phát sinh trong danh bạ vậy).

    Để giúp các Bạn có điều kiện test dữ liệu qua internet, tôi đã bổ sung vào database "danhba" (là nguồn dữ liệu SQL SERVER được sử dụng trong file ứng dụng minh hoạ ta đã dùng từ bài đầu đến nay) 2 bảng dữ liệu sau đây:

    1. Bảng dữ liệu để đăng ký thông tin chung của chứng từ nhập / xuất:
    – Tên bảng: "tblctunx"
    – Các Field dữ liệu:
    + Id (PK – numeric theo dạng AutoNumber)
    + soctu kiểu nchar(20)
    + ngay kiểu smalldatetime
    + msnv kiểu nchar(10) – dùng để đăng ký nghiệp vụ phát sinh là nhập hay xuất (và loại nhập xuất cụ thể nào, nếu các Bạn muốn phân biệt tới mức chi tiết như vậy)
    + mskh kiểu numeric(18,0) – dùng để đăng ký mã số khách hàng
    + tsuatvat kiểu numeric(18,0) – dùng để đăng ký thuế suất thuế VAT

    2. Bảng đăng ký thông tin chi tiết hàng hoá:
    – Tên bảng: "tblctunxct"
    – Các Fields dữ liệu:
    + Id (PK, kiểu numeric(18,0)
    + soctu (PK, nchar(20)
    + mshh (PK, numeric(18,0) – đăng ký mã số hàng hoá
    3 Field trên đều được khai báo là khoá chính của bảng (PK) để tránh trùng dữ liệu theo quy tắc: mỗi mặt hàng chỉ được đăng ký 1 dòng trong bảng.
    + dvt kiểu smallint – đăng ký đơn vị tính, tạm thời ta quy ước đơn vị tính thấp nhât với chỉ số = 1, sau này ta sẽ thiết kế bảng đăng ký hệ thống đơn vị tính cho hàng hoá (theo hướng 1 mặt hàng có thể đăng ký nhiều đơn vị tính khác nhau, các đơn vị tính này có liên quan với nhau thông qua 1 chỉ số quy số lượng về đơn vị tính thấp nhất)
    + soluong kiểu numeric(18,0)
    + dongia kiểu numeric(18,0)

    Để cho đơn giản, trước mắt ta cho nhập tự do mã số khách hàng và mã số hàng hoá; sau này ta sẽ tạo thêm 2 bảng ghi danh sách khách hàng và ghi danh mục hàng hoá.

    Mong các Bạn góp ý kiến trao đổi thêm.

  11. hands says:

    Chào các Bạn,

    Hôm nay xin trao đổi tiếp tục vấn đề đang bỏ dỡ hôm trước:
    Thiết kế 1 UnBound Form có SubForm kết nối dữ liệu tới SQL Server

    Nhu cầu ứng dụng: Ta cần 1 form để quản lý chứng từ nhập xuất kho hàng, bao gồm các chức năng: cho nhập chứng từ mới phát sinh, cho truy xuất lại chứng từ đã lập, cho cập nhật lại các thông tin của chứng từ đã nhập.

    Các bảng dữ liệu SQL Server phục vụ cho nhu cầu trên đã được tôi chuẩn bị sẵn gồm có:

    1. Bảng ghi danh mục hàng hóa: tbldmhanghoa
    Gồm các cột dữ liệu sau:
    + mshh: PK, numeric(18,0)
    + tenhanghoa: nvarchar(255)
    + xuatxu: nvarchar(50)
    + dactrung: nvarchar(255)

    2. Bảng ghi hệ thống đơn vị tính của từng mặt hàng: tbldonvitinh
    Gồm các cột dữ liệu sau:
    + mshh: PK, numeric(18,0)
    + cap: PK, smallint, đăng ký cấp của đơn vị tính
    + kihieu: nchar(10)
    + mota: nvarchar(50)
    + quycap1: numeric(18,0)
    + dongianhap: numeric(18,0)
    + dongiaxuat1: numeric(18,0)
    + dongiaxuat2: numeric(18,0)
    + dongiaxuat3: numeric(18,0)

    3. Bảng ghi các thông tin chung của chứng từ nhập xuất phát sinh: tblctunx
    Gồm các cột dữ liệu sau:
    + Id: PK, numeric(18,0)
    + soctu: nchar(20)
    + ngay: smalldatetime
    + msnv: nchar(10)
    + mskh: numeric(18,0)
    + tsuatvat: numeric(18,0)
    + nguoigiaodich: nvarchar(255)

    4. Bảng ghi các thông tin về chi tiết hàng hóa của chứng từ nhập xuất phát sinh: tblctunxct
    Gồm các cột dữ liệu sau:

    + Id: PK, numeric(18,0)
    + soctu: nchar(20)
    + mshh: numeric(18,0)
    + dvt: smallint
    + soluong: numeric(18,0)
    + dongia: numeric(18,0)
    + lacktyle: bit, đăng ký nội dung: có phải là chiết khấu theo tỷ lệ hay không?
    + mucck: decimal(18,2), đăng ký nội dung: mức chiết khấu cụ thể là bao nhiêu? Nếu là chiết khấu tỷ lệ thì nhập nguyên không có chia phần trăm (thí dụ: nếu chiết khấu với tỷ lệ là 2,5%, ta nhập 2,5)

    5. Bảng đăng ký danh mục các nghiệp vụ phát sinh: tbldmnghiepvu
    Khi lập chứng từ nhập xuất, để xác định nghiệp vụ phát sinh cụ thể (cần thống nhất mã nghiệp vụ phát sinh để tiện quản lý về sau này)
    Gồm các cột dữ liệu sau:
    + msnv: PK, nchar(5), đăng ký mã số nghiệp vụ
    + tennghiepvu: nvarchar(255)

    Sau đây là link tải file ứng dụng minh họa cập nhật ngày 16/7/2012:
    https://www.mediafire.com/?43n5qckyc3q1s18

    Với file ứng dụng minh họa này,
    – Để hiển thị nội dung thông tin chi tiết các mặt hàng trong chứng từ phát sinh, tôi thiết kế 1 Subform với nguồn dữ liệu được nạp một cách linh hoạt, không cố định, tùy thuộc vào số chứng từ đang mở trên form chính.

    – Khi thiết kế UnBound Form theo nhu cầu như trên đã nêu, theo tôi chúng ta cần phải chú ý những vấn đề sau đây:

    1. Việc nạp nguồn dữ liệu cho SubForm nên chọn nạp thông qua property "Recordset" của SubForm, điều này khác với cách hay làm thông thường là xác định thông qua thuộc tính "RecordSource".
    Các Bạn có thể thấy cách thức tôi đã làm trong file ứng dụng mẫu, để nạp nguồn dữ liệu cho SubForm tôi đã viết thủ tục sau trong module "modQuanlyDulieu":

    [B]Sub SetSourceRecForSubForm(mForm As Form, sForm As String)[/B]
        Dim SQLst As String
        Dim SQLrec As ADODB.Recordset
        Dim tblName As String
        Dim vSoCtu, stChema As String
        vSoCtu = mForm!cmbSoCtu
        If Not IsNull(vSoCtu) Then
            tblName = "tblctunxct"
            stChema = GetSchemaTable(tblName)
            SQLst = "SELECT " & stChema & ".tbldmhanghoa.tenhanghoa, " & stChema & ".tblctunxct.*"
            SQLst = SQLst & " FROM " & stChema & ".tbldmhanghoa INNER JOIN " & stChema & ".tblctunxct"
            SQLst = SQLst & " ON " & stChema & ".tbldmhanghoa.mshh=" & stChema & ".tblctunxct.mshh"
            SQLst = SQLst & " WHERE " & stChema & ".tblctunxct.soctu = '" & vSoCtu & "'"
    
    Set SQLrec = ProcessRecordset(SQLst)
    
    Set mForm(sForm).Form.Recordset = SQLrec
    
    With mForm(sForm).Form
                .Requery
                !txtId.ControlSource = "id"
                !txtMSHH.ControlSource = "mshh"
                !txtTenHanghoa.ControlSource = "tenhanghoa"
                !txtCapDvt.ControlSource = "dvt"
                !txtDvt.ControlSource = "=IIF(not isnull(dvt),flookup('kihieu','tbldonvitinh','cap=' & [dvt]),'')"
                !txtSoluong.ControlSource = "soluong"
                !txtDongia.ControlSource = "dongia"
                !chkCKTL.ControlSource = "lacktyle"
                !txtMucCK.ControlSource = "mucck"
            End With
    
    'Nhớ đóng Recordset đã gán cho SubForm bằng 2 dòng lệnh sau nhằm mục đích tiết kiệm tài nguyên hệ thống:
            SQLrec.Close
            Set SQLrec = Nothing
        End If
    [B]End Sub[/B]

    Như các Bạn đã thấy trong thủ tục trên, ngay sau khi đã gán Recordset SQLrec cho SubForm qua dòng lệnh:

    Set mForm(sForm).Form.Recordset = SQLrec

    Tôi đã cho đóng Recordset SQLrec này lại. Việc đóng Recordset SQLrec không dẫn đến việc đóng Recordset của SubForm.

    2. Chúng ta cũng cần lưu ý đến nhu cầu kép đối với nguồn dữ liệu của SubForm phải vừa cho hiển thị nội dung, vừa cho cập nhật lại hoặc xóa chi tiết hàng hóa phát sinh.
    Để đáp ứng nhu cầu trên, tôi đã cho SubForm chỉ làm nhiệm vụ hiển thị nội dung thông tin chi tiết về hàng hóa phát sinh.
    Đối với nhu cầu cập nhật lại hoặc xóa tôi cho thực hiện bằng cách:
    + Trên Form chính, tôi thiết kế các ô dữ liệu tương ứng với các cột dữ liệu của chi tiết hàng hóa cần cập nhật lại hoặc nhập mới, đồng thời viết thủ tục cho cập nhật các chi tiết này ngay trong class module của Form chính.
    Nút lệnh gọi thủ tục cập nhật này được bố trí bên phải của các ô dữ liệu tương ứng, có hình Floppy-Disk
    Nút lệnh gọi thủ tục xóa chi tiết hàng đang chọn được bố trí bên trái của các ô dữ liệu tương ứng, có hình gạch chéo màu đỏ. Muốn xóa 1 dòng chi tiết hàng nào đó, trước hết ta phải cho nạp dòng đó lên các ô dữ liệu tương ứng đang nói ở đoạn này.

    Thủ tục cập nhật về chi tiết hàng hóa của chứng từ như sau:

    [B]Sub SaveToInvoiceDetailFromForm(Optional InVoiceDetailId)[/B]
        'Luu thong tin tren form vao tblctunxCT
        'UpdateInvoiceDetail
        On Error GoTo HandleError
    
    Dim SQLst As String, tblName As String
        Dim vId
        Dim MucCK As Double, CKTL As Byte
    
    Call OpenMyConnection
    
    tblName = "tblctunxct"
        With Me
            vId = Me.txtDetailId
            MucCK = Nz(.txtMucCK)
            If IsNull(.chkCKTL) Then
                CKTL = 0
            Else
                If .chkCKTL.Value = True Then
                    CKTL = 1
                Else
                    CKTL = 0
                End If
            End If
            If Not IsNull(vId) Then
                SQLst = "UPDATE " & GetSchemaTable(tblName) & "." & tblName & " SET "
                SQLst = SQLst & " soctu ='" & Trim(.cmbSoCtu) & "',"
                SQLst = SQLst & " mshh =" & .cmbMSHH & ","
                SQLst = SQLst & " dvt =" & .cmbDvt & ","
                SQLst = SQLst & " soluong =" & .txtSoluong & ","
                SQLst = SQLst & " dongia =" & .txtDongia & ","
                SQLst = SQLst & " lacktyle =" & CKTL & ","
        '        SQLst = SQLst & " mucck =" & Format(MucCK, "#,###.0#")
                SQLst = SQLst & " mucck =" & MucCK
                SQLst = SQLst & " WHERE ("
                SQLst = SQLst & " soctu='" & Trim(Me.cmbSoCtu) & "'"
                SQLst = SQLst & " AND id=" & InVoiceDetailId
                SQLst = SQLst & ")"
            Else
                SQLst = "INSERT INTO " & GetSchemaTable(tblName) & "." & tblName
                SQLst = SQLst & "(soctu, mshh, dvt, soluong, dongia, lacktyle, mucck)"
                SQLst = SQLst & " VALUES ("
                SQLst = SQLst & " '" & Trim(.cmbSoCtu) & "',"
                SQLst = SQLst & " " & .cmbMSHH & ","
                SQLst = SQLst & " " & .cmbDvt & ","
                SQLst = SQLst & " " & .txtSoluong & ","
                SQLst = SQLst & " " & .txtDongia & ","
                SQLst = SQLst & " " & CKTL & ","
                SQLst = SQLst & " " & Nz(MucCK)
                SQLst = SQLst & ")"
            End If
        End With
    
    Debug.Print SQLst
    
    MyConn.Execute SQLst
    
    Call CloseMyConnection
    
    HandleError:
            If Err > 0 Then
                GeneralErrorHandler Err.Number, Err.Description, NhapXuat_FORM, "SaveToInvoiceDetailFromForm"
                Exit Sub
            End If
    [B]End Sub[/B]

    3. Với các ComboBox, chúng ta cũng cần cân nhắc việc nạp nguồn dữ liệu cho các ComboBox này (để có danh sách sổ xuống) sao cho phù hợp, chỉ nạp khi cần và với giới hạn xác định.
    Để đáp ứng nhu cầu này, tôi chỉ cho nạp nguồn dữ liệu cho ComboBox khi nào ta cho gọi hiện danh sách sổ xuống (thường là bằng cách bấm phím F4 hoặc Alt + phím mũi tên xuống). Do vậy, tôi viết thủ tục sau để gán nguồn dữ liệu cho ComboBox, và khai báo thủ tục sự kiện KeyDown (khi có phím bấm xuống) tại ComboBox.

    Thủ tục gán dữ liệu nguồn:

    [B]Private Sub SetComboRowSource(ComboName As String, RecSourceSt As String, stFilter As String)[/B]
        'Nap RowSource cho ComboBox có tên qua biến ComboName
    
    Dim SQLst As String
        Dim SourceRec As ADODB.Recordset
    
    SQLst = RecSourceSt & " WHERE " & stFilter 'ten LIKE N'%" & stFilter & "%'"
        Set SourceRec = ProcessRecordset(SQLst)
        Set Me(ComboName).Recordset = SourceRec
    
    SourceRec.Close
        Set SourceRec = Nothing
    [B]End Sub[/B]

    Và nội dung thủ tục bẩy sự kiện tương tự như sau (ở đây là bẩy sự kiện KeyDown của ComboBox lấy danh sách khách hàng từ nguồn là bảng tblDanhsach):

    [B]Private Sub cmbKhachhang_KeyDown(KeyCode As Integer, Shift As Integer)[/B]
        Dim srcSt As String, sCri As String
        Dim tblName As String
        Dim InputSt
        'Set RowSource For CmbKhachhang
        'SetComboRowSource
        If KeyCode = vbKeyF4 Or (KeyCode = vbKeyDown And Shift = acAltMask) Then
            InputSt = Me.cmbKhachhang.Text
            tblName = "tblDanhsach"
            srcSt = "SELECT * FROM " & GetSchemaTable(tblName) & "." & tblName
            sCri = " ten LIKE N'%" & InputSt & "%'"
    
    SetComboRowSource "cmbkhachhang", srcSt, sCri
        End If
        '
    [B]End Sub[/B]

    4. Về việc cập nhật thông tin chung của chứng từ chúng ta cũng cần cân nhắc với 2 trường hợp phân biệt là Thêm chứng từ mới hay Cập nhật lại các thay đổi của chứng từ đã lập.
    Tôi giải quyết vấn đề trên như sau:
    – Trong cấu trúc bảng tblctunx có 1 field được xác định là khóa chính (PK) là field "Id". Trên Form chính tôi bố trí 1 TextBox để nhận giá trị của field khóa chính này:
    + Khi TextBox này có giá trị xác định, nghĩa là trường hợp form đang hiển thị nội dung của 1 chứng từ xác định đang hiện hữu trong bảng tblctunx. Việc cập nhật thay đổi được thực hiện thông qua thủ tục SaveToInvoiceFromForm sau đây với biến InvoiceId xác định (trong thủ tục này InvoiceId là 1 biến tùy chọn – với từ khóa Optional phía trước)

    Thủ tục đó như sau:

    [B]Sub SaveToInvoiceFromForm(Optional InvoiceId)[/B]
        'Luu thong tin tren form vao tblctunx
        On Error GoTo HandleError
    
    Dim SQLst As String, tblName As String
        Dim vId
    
    Call OpenMyConnection
    
    tblName = "tblctunx"
    
    With Me
            vId = Me.txtId
            If Not IsNull(vId) Then 'Nếu giá trị của TextBox txtId không là Null nghĩa là Form đang hiển thị thông tin của chứng từ đang hiện hữu.
                SQLst = "UPDATE " & GetSchemaTable(tblName) & "." & tblName & " SET "
                SQLst = SQLst & " soctu ='" & .cmbSoCtu & "',"
                SQLst = SQLst & " ngay ='" & Format$(.txtNgay, "dd-mmm-yy") & "',"
                SQLst = SQLst & " msnv ='" & .cmbNghiepvu & "',"
                SQLst = SQLst & " mskh ='" & .cmbKhachhang & "',"
                SQLst = SQLst & " nguoigiaodich ='" & .txtNguoiGiaodich & "',"
                SQLst = SQLst & " tsuatvat =" & .txtTsuat
                SQLst = SQLst & " WHERE ("
                SQLst = SQLst & " soctu='" & InvoiceId & "'"
                SQLst = SQLst & ")"
            Else 'Nếu giá trị của TextBox txtId là Null nghĩa là Form đang hiển thị thông tin của chứng từ chờ lưu mới.
                If IsNull(.cmbSoCtu) Then Exit Sub
                SQLst = "INSERT INTO " & GetSchemaTable(tblName) & "." & tblName
                SQLst = SQLst & "(soctu, ngay, msnv, mskh, tsuatvat)"
                SQLst = SQLst & " VALUES ("
                SQLst = SQLst & " '" & .cmbSoCtu & "',"
                SQLst = SQLst & " '" & Format$(.txtNgay, "dd-mmm-yy") & "',"
                SQLst = SQLst & " '" & .cmbNghiepvu & "',"
                SQLst = SQLst & " '" & .cmbKhachhang & "',"
                SQLst = SQLst & " '" & .txtNguoiGiaodich & "',"
                SQLst = SQLst & " " & .txtTsuat
                SQLst = SQLst & ")"
            End If
        End With
    
    MyConn.Execute SQLst
    
    Call CloseMyConnection
    
    HandleError:
            If Err > 0 Then
                GeneralErrorHandler Err.Number, Err.Description, NhapXuat_FORM, "SaveToInvoiceFromForm"
                Exit Sub
            End If
    [B]End Sub[/B]

    Và thủ tục để nạp thông tin của chứng từ đang hiện hữu trong abrng tblctunx lên Form chính như sau:

    [B]Sub LoadInvoiceInfoToForm(SoCtuSt)[/B]
        Dim SQLst As String, SQLrec As ADODB.Recordset
        Dim KHrec As ADODB.Recordset
        Dim tblName As String, MsKH As Long
        tblName = "tblctunx"
        If IsNull(SoCtuSt) Then Exit Sub
        SQLst = "SELECT * FROM " & GetSchemaTable(tblName) & "." & tblName
        SQLst = SQLst & " WHERE soctu ='" & SoCtuSt & "'"
        Set SQLrec = ProcessRecordset(SQLst)
        '
        If SQLrec.RecordCount > 0 Then
            Set objKhachHang = New clsDanhba
            With Me
                .txtId = SQLrec!id
                .txtNgay = SQLrec!ngay
                .cmbNghiepvu = SQLrec!msnv
                .txtTsuat = SQLrec!tsuatvat
                .txtNguoiGiaodich = SQLrec!nguoigiaodich
    
    MsKH = SQLrec!MsKH
    
    SQLst = "SELECT * FROM " & GetSchemaTable("tblDanhsach") & ".tblDanhsach"
                SQLst = SQLst & " WHERE danhbaid = " & MsKH
                Set KHrec = ProcessRecordset(SQLst)
                objKhachHang.PopulatePropertiesFromRecordset KHrec
    
    .cmbKhachhang = MsKH
                .cmbKhachhang.RowSourceType = "Value List"
                .cmbKhachhang.RowSource = objKhachHang.Ten & ";" & MsKH
    
    .txtDiachi = objKhachHang.Diachi
                .txtPhone = objKhachHang.Dtvp
                .txtMasoThue = objKhachHang.Msthue
    
    KHrec.Close
                Set KHrec = Nothing
    
    'Dòng sau để cho nạp nguồn dữ liệu chi tiết hàng hóa tương ứng của chứng từ đã xác định
                 SetSourceRecForSubForm Me, "frmCtuNXCT"
    
    End With
        End If
        '
        SQLrec.Close
        Set SQLrec = Nothing
    [B]End Sub[/B]

    Còn các vấn đề có liên quan khác như: tìm và xóa chứng từ, các Bạn tự làm nhé.

    Như vậy là tôi đã trình bày xong 1 trong những cách thiết kế UnBound Form có chứa SubForm kết nối đến dữ liệu SQL Server.
    Và cũng xin nhắc lại rằng: có nhiều cách để ứng dụng cho nhu cầu này. Ở đây tôi chỉ trình bày cách dễ làm nhất thôi.

    Có Bạn nào muốn thiết kế các Object tự tạo để quản lý các chứng từ nhập xuất phát sinh kiểu như ta đã làm để quản lý Danh bạ đã đề cập trong các bài trước không? Các Bạn thử xem sao nhé.

    Cũng xin thông tin thêm về tình trạng các bảng dữ liệu mới bổ sung:
    – Danh mục hàng hóa và đơn vị tính đã được nạp sẵn trên 1.000 mặt hàng, mỗi mặt hàng đều có từ 2 đến 3 đơn vị tính.
    – Mới chỉ có vài chứng từ phát sinh

    Rất mong các Bạn tham gia trao đổi thêm.

  12. hands says:

    Chào các Bạn,
    Xin trao đổi thêm nội dung còn thiếu về file ứng dụng minh họa được cập nhật hôm nay (16/7/2012):

    1. Trên form chính "frmCtuNX":
    + Để nạp lại nội dung các chứng từ đã lưu trước đây, tại ô nhập số chứng từ xin bấm 1 vài ký tự số để lọc nhanh và cho sổ danh sách chứng từ xuống (với các chứng từ do tôi nhập đều có số 3 trong chuỗi số chứng từ, nên các Bạn nhập số 3), sau đó chọn số chứng từ xác định từ danh sách sổ xuống, chương trình sẽ cho nạp nội dung của chứng từ đó lên Form.

    + Để chọn khách hàng có sẵn từ danh sách: tại ô nhập khách hàng, cũng thao tác tương tự như trên, nghĩa là nhập vào 1 vài từ cần tìm rồi cho sổ danh sách xuống (thí dụ như nhập từ "Công ty"), sau đó chọn khách hàng thích hợp. Danh sách này truy xuất từ bảng dữ liệu lưu Danh bạ (tblDanhsach) ta đã xem xét trong các bài trước có sẵn trên 15.000 mẫu tin.

    2. Để xóa trống các ô nhập chi tiết hàng phát sinh trong chứng từ: kích kép tại ô nhập mã số hàng hóa.
    Khi chọn hoặc nhập mới số chứng từ, các ô này cũng sẽ tự động được xóa trống.

    3. Với SubForm "frmCtuNXCT": xin các Bạn chú ý các thuộc tính được khai báo trong ảnh đính kèm.
    Trong các thuộc tính này, các Bạn chú ý thuộc tính "Recordset-Type" đã được khai báo là kiểu "Snapshot".
    Với kiểu Snapshot, Recordset sẽ được đặt ở chế độ chỉ xem, không hiệu chỉnh, không thêm, không xóa được. Access sẽ dành ít tài nguyên nhất để nạp Recordset kiểu "Snapshot"

    Các Bạn có thể tham khảo các hướng dẫn của Microsoft về Recordset-Type của 1 Access Form tại link sau:
    office.microsoft.com/en-us/access-help/recordsettype-property-HA001232788.aspx]RecordsetType Property – Access – Office.com
    Và các khuyến cáo nhằm tăng khả năng truy xuất dữ liệu SQL Server của ứng dụng Access từ link sau:
    msdn.microsoft.com/en-us/library/bb188204(v=sql.90).aspx]Optimizing Microsoft Office Access Applications Linked to SQL Server

  13. hands says:

    Chào các Bạn,

    Có một Bạn đã phát hiện lỗi không cập nhật được chứng từ mới phát sinh.
    Tôi đã kiểm tra và phát hiện lỗi ở thủ tục sau, nằm bên trong Class module của Form "frmCtuNX":

    Sub SaveToInvoiceFromForm(Optional InvoiceId)
        'Luu thong tin tren form vao tblctunx
    
    'UpdateOrInsert:
        '+ True: Luu thong tin thay doi vao mau tin dang hien huu
        '+ Flase: Them mau tin moi
    
    'InvoiceId: so chung tu
        '
        On Error GoTo HandleError
    
    Dim SQLst As String, tblName As String
        Dim vId
    
    Call OpenMyConnection
    
    tblName = "tblctunx"
    
    With Me
            vId = Me.txtId
            If Not IsNull(vId) Then
                If IsNull(InvoiceId) Then Exit Sub
                SQLst = "UPDATE " & GetSchemaTable(tblName) & "." & tblName & " SET "
                SQLst = SQLst & " soctu ='" & .cmbSoCtu & "',"
                SQLst = SQLst & " ngay ='" & Format$(.txtNgay, "dd-mmm-yy") & "',"
                SQLst = SQLst & " msnv ='" & .cmbNghiepvu & "',"
    
    '[COLOR="green"]SQLst = SQLst & " mskh ='" & .cmbKhachhang & "',"[/COLOR] 'Đây là dòng sai, vì mskh có kiểu numeric nhưng ở đây có 2 dấu nháy ở 2 đầu nên thành kiểu Text
    
    [COLOR="red"]SQLst = SQLst & " mskh =" & .cmbKhachhang & ","[/COLOR] 'Đây là dòng đã được hiệu chỉnh cho đúng, bỏ dấu nháy ở 2 đầu
    
    [COLOR="blue"]SQLst = SQLst & " nguoigiaodich =N'" & .txtNguoiGiaodich & "',"[/COLOR] 'Và sẵn tiện sửa luôn dòng này để lưu được chuỗi Unicode
    
    SQLst = SQLst & " tsuatvat =" & .txtTsuat
                SQLst = SQLst & " WHERE ("
                SQLst = SQLst & " soctu='" & InvoiceId & "'"
                SQLst = SQLst & ")"
            Else
                If IsNull(.cmbSoCtu) Then Exit Sub
                SQLst = "INSERT INTO " & GetSchemaTable(tblName) & "." & tblName
                SQLst = SQLst & "(soctu, ngay, msnv, mskh, nguoigiaodich, tsuatvat)"
                SQLst = SQLst & " VALUES ("
                SQLst = SQLst & " '" & .cmbSoCtu & "',"
                SQLst = SQLst & " '" & Format$(.txtNgay, "dd-mmm-yy") & "',"
                SQLst = SQLst & " '" & .cmbNghiepvu & "',"
    
    [COLOR="red"]            SQLst = SQLst & " " & .cmbKhachhang & ","[/COLOR]
    
    [COLOR="blue"]            SQLst = SQLst & " N'" & .txtNguoiGiaodich & "',"[/COLOR]
    
    SQLst = SQLst & " " & .txtTsuat
                SQLst = SQLst & ")"
            End If
        End With
    
    MyConn.Execute SQLst
    
    Call CloseMyConnection
        '
        LoadInvoiceInfoToForm Me.cmbSoCtu
    
    HandleError:
            If Err > 0 Then
                GeneralErrorHandler Err.Number, Err.Description, NhapXuat_FORM, "SaveToInvoiceFromForm"
                Exit Sub
            End If
    End Sub

    Xin cảm ơn các Bạn đã quan tâm.
    Có Bạn nào thấy sai ở chỗ nào nữa không?
    Và lỗi ở thủ tục sau đây, cũng ở trong Class module của form "frmCtuNX":

    Private Sub SetComboRowSource(ComboName As String, RecSourceSt As String, stFilter As String)
        'Nap RowSource cho ComboBox
    
    Dim SQLst As String
        Dim SourceRec As ADODB.Recordset
    
    SQLst = RecSourceSt & " WHERE " & stFilter 'ten LIKE N'%" & stFilter & "%'"
        Set SourceRec = ProcessRecordset(SQLst)
    
    [COLOR=#008000]   'Thêm 3 dòng kế bên dưới. 
       'Tôi viết kiểu With ... End With để phòng khi phải khai báo thêm gì nữa cho ComboBox    [/COLOR]
    [B]    With Me(ComboName)
            .RowSourceType = "Table/Query"
        End With[/B]
    
    Set Me(ComboName).Recordset = SourceRec
    
    SourceRec.Close
        Set SourceRec = Nothing
    
    End Sub

    Tối hôm qua có Bạn hỏi qua email:
    Vì sao trong thủ tục "SetSourceRecForSubForm" (module modQuanlyDulieu) để gán Recordset cho SubForm tôi lại dùng câu lệnh:

    Set mForm(sForm).Form.Recordset = SQLrec

    mà không phải là:

    mForm(sForm).Form.Recordset = SQLrec

    Câu trả lời thật ngắn gọn là:
    Theo quy ước của VBA:
    + Recordset là 1 Object (các Bạn sử dụng thư viện ADO hay DAO cũng đều như vậy cả)
    + Trong thủ tục nêu trên SQLrec là 1 Recordset
    + Câu lệnh gán giá trị cho 1 biến Object phải tuân theo cú pháp: SET <Biến Object hoặc Property của Object> = Giá trị là 1 Object
    Có một số Bạn gọi điện hỏi tôi vì sao truy xuất chậm quá, không giống như lần đầu sử dụng file minh họa?
    Tôi đã kiểm tra lại và thấy tốc độ truy xuất vẫn như trước. Tôi đã cho nạp thử tiện ích VPN ảo thì thấy ứng dụng chạy chậm hẳn, lý do ở đây là khi nạp tiện ích này (và các tiện ích tương tự) máy tính của Bạn thay vì truy xuất trực tiếp đến host đang lưu file dữ liệu cần truy xuất, thì lại đi vòng qua 1 hoặc nhiều host khác nữa, nên bị chậm hẳn. Trong trường hợp này, các Bạn chỉ cần tắt hoặc DisConnect đến VPN ảo đi là nhanh trở lại.

  14. hands says:

    Chào các Bạn,

    Có Bạn bảo tôi: đã lỡ làm được tới đó rồi sao không tiện thể cho tự động đề nghị đơn giá mỗi khi chọn 1 mặt hàng hoặc chọn lại đơn vị tính?
    Thấy nhu cầu này cũng cần để thêm phần sâu sắc cho vấn đề được minh họa nên tôi đã bổ sung nhu cầu trên vào file ứng dụng được cập nhật lúc 13 giờ trưa nay. Bạn nào có nhu cầu xin tải xuống từ link sau:
    https://www.mediafire.com/?7qesy6y1ec1d50z

    Nội dung bổ sung được tôi sử dụng 1 thủ tục tự tạo thay thế cho hàm Dlookup của VBA, thủ tục này có tên là fLookup nằm trong module "modUtilities".
    Nội dung thủ tục này như sau:

    [COLOR=#006400]Function fLookup[/COLOR]([COLOR=#0000cd]WhatField [/COLOR]As String, [COLOR=#0000cd]WhatTable [/COLOR]As String, [COLOR=#0000cd]CriSt [/COLOR]As String)
        On Error GoTo xulynull
        Dim SrcRec As ADODB.Recordset
        Dim srcSt As String
    
    If Len(CriSt) = 0 Then Exit Function
    
    srcSt = "SELECT TOP 1 " & WhatField & " FROM " & GetSchemaTable(WhatTable) & "." & WhatTable
        srcSt = srcSt & " WHERE " & CriSt
        Set SrcRec = ProcessRecordset(srcSt)
    
    If SrcRec.RecordCount > 0 Then fLookup = Trim(SrcRec(WhatField))
    
    SrcRec.Close
        Set SrcRec = Nothing
    
    Exit Function
    
    xulynull:
        If Err > 0 Then fLookup = Null
        Exit Function
    [COLOR=#006400]End Function[/COLOR]

    Để giúp các Bạn có căn cứ đánh giá và tối ưu hoá hiệu quả truy xuất dữ liệu của các thủ tục đang có trong file ứng dụng minh hoạ và các thủ tục do chính các Bạn viết hoặc hiệu chỉnh, tôi đã cho nạp vào file dữ liệu trên SQL SERVER:
    + Trên 12.000 chứng từ phát sinh (trong bảng "tblctunx")
    + Với trên 48.000 chi tiết hàng hoá phát sinh (trong bảng "tblctunxct")
    Theo dõi thấy có nhiều Bạn đọc chuyên đề này, nhưng sao không thấy ý kiến gì trao đổi thêm, làm tôi thấy băn khoăn. Không biết những gì tôi trao đổi có mang đến cho các Bạn điều gì ích lợi không? Có gì chưa đúng hay sai chăng?

    Thật tình, tôi cũng chỉ muốn chứng minh rằng Microsoft Access giúp ta được rất nhiều việc, trong đó có những việc mà bấy lâu nay chúng ta tưởng, và cũng có rất nhiều người chê Access cũng tưởng lầm rằng Access chỉ làm được ba cái ứng dụng "lẹt đẹt" mang tính "local" thôi, chứ đụng tới NET là chào thua.

    Rất mong các Bạn cùng tham gia trao đổi để chúng ta cùng làm sáng tỏ những vấn đề đang thảo luận trong chuyên đề này.

  15. hands says:

    Chào các Bạn,
    Tôi vừa nhận được thắc mắc của 1 Bạn như sau:

    Chào bạn lehongduc
    Mình cũng làm ms access project kết nối sql server trong môi trường nhiều người dùng
    Và vẫn đang vướng khâu nhiều nhiều người cùng truy cập vào 1 table
    Khi nhiều người cùng tạo báo cáo và đẩy kết quả vào 1 table để đưa dữ liệu vào báo cáo dẫn tới việc đụng độ
    Ví dụ:Mình tạo báo cáo tồn kho
    User 1 thực hiện
    Delete from TB_KHO
    Insert into TB_Kho (…)
    docmd.openreport "rpKho"
    User 2 thực hiện
    Delete from TB_KHO
    Insert into TB_Kho (…)
    docmd.openreport "rpKho"
    ……….
    Trong TB_KHO mình đã có thêm cột User1,User2 để phân biệt báo cáo đc tạo bởi user nào
    Khi 1 user chạy báo cáo thì kết quả luôn đúng
    Khi nhiều user cùng chạy báo cáo kết quả lúc đúng lúc sai
    ———
    Vậy theo bạn mình phải giải quyết việc đụng độ khi nhiều user dùng chung 1 table như thế nào trong sql server
    Rất mong học hỏi thêm access+sql server từ bạn

    Các Bạn không phải mất công như vậy, chỉ cần chú ý những nội dung mang tính nguyên tắc sau đây thì sẽ giải quyết được nỗi lo ngay:

    1. Việc mở các bảng dữ liệu luôn có nhiều tùy chọn, ta có thể kể ra đây các tùy chọn thông dụng như sau:
    – Mở ra chỉ để đọc dữ liệu
    – Mở ra không chỉ để đọc mà còn để hiệu chỉnh dữ liệu hoặc ghi thêm, xóa dữ liệu, …
    Và nguyên tắc truy xuất dữ liệu tối ưu là: cần đến đâu thì mở đến đó. Nếu chỉ cần để ghi thêm mẫu tin (record) mới vào bảng dữ liệu thì tại sao ta lại mở hết trọi dữ liệu trong bảng ra? Và cần gì phải mở hết trọi với chế độ sẵn sàng hiệu chỉnh (bao gồm cả: edit, add và delete)?

    2. Khi thiết kế Form, lúc ban đầu mới làm quen với Microsoft Access ta hay bị Bác Bill "dụ khị" bằng cách thiết kế Form với kiểu gắn liền với 1 nguồn dữ liệu (là bảng dữ liệu đơn hoặc 1 truy vấn phức tạp hơn) ở chế độ sẵn sàng cho hiệu chỉnh (bao gồm cả: edit, add và delete). Cái này thuật ngữ thiết kế ứng dụng gọi là thiết kế 1 Bound Form. Bác Bill làm vậy là có lý do, vì ở giai đoạn sơ khởi làm quen với Microsoft Access chủ yếu ta làm ra những ứng dụng chỉ để 1 người dùng trên máy đơn, nó đơn giản nên dễ tiếp thu và dễ làm, vậy mới "dụ khị" được chứ.

    Thật sự, có tới 2 chế độ thiết kế Form:
    – Thiết kế Bound Form như trên đã nói
    – Hoặc thiết kế 1 UnBound Form. Với 1 UnBound Form, ta không cần gán 1 nguồn dữ liệu thường trực như với 1 Bound Form, chỉ khi nào cần tác động đến 1 bảng dữ liệu nào đó ta mới cho chạy lệnh tác động tương ứng (thông qua công cụ VBA code hoặc SQL code). Đây chính là kiểu Form mà tôi đã trình bày trong các bài trước đây.

    3. Nguyên tắc của việc thiết kế Form trong 1 ứng dụng có nhiều người dùng qua mạng cùng truy xuất 1 nguồn dữ liệu là: Nên thiết kế UnBound Form. Đó chính là bảo đảm an toàn nhất để ta khỏi phải đối đầu với nỗi lo đau cả đầu về xung đột như các Bạn đang lo ở đây. Làm vậy sẽ thêm được cái lợi là ứng dụng chạy nhanh nữa, vì tiêu tốn ít tài nguyên đó mà.

    Những điều nêu trên đều đã được tôi trình bày cụ thể trong các bài viết trước đây, các Bạn có thể đọc lại được ngay trên diễn đàn này.
    Các Bạn cũng có thể tham khảo thêm lời khuyên của Bác Bill bằng cách dùng Google với từ khóa "UnBound Form"

    4. Về vấn đề cụ thể Bạn nêu, tôi sơ bộ có nhận xét và ý kiến thế này:
    – Bảng TB_KHO là 1 bảng dữ liệu mang tính chất tạm thời để nhằm mục đích làm nguồn dữ liệu cho 1 Report theo tuỳ chọn riêng của từng User xác định.
    Có 2 cách quản lý cái bảng tạm thời này:
    + Có thể cho gắn liền với từng User đang làm việc theo kiểu "xong việc rồi bỏ" (chứ lưu lại làm chi cho nó nặng bụng mà chẳng để làm gì?)
    + Hoặc cho ghi bảng này ngay trên file ứng dụng tại máy tính của User đang làm việc (client), với điều kiện ta không thiết kế 1 Access Project File mà là 1 MDB file. Hoặc lưu trên 1 SQL SERVER cục bộ hay lưu thành file XML tại máy client.
    Cách đầu luôn luôn rối và chậm hơn cách 2.

    Chào bạn lehongduc.
    Rất cám ơn bạn đã nhiệt tình hướng dẫn. Mình cũng đang nghiên cứu về đề tài này nên rất tâm đắc với nó.
    trong ví dụ demo của bạn mình không hiểu hàm này để làm gì ?

    Function GetSchemaTable(TbName As String) As String
    Dim TbRec As New ADODB.Recordset
    Dim SQLst As String
    SQLst = "SELECT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES"
    SQLst = SQLst & " WHERE TABLE_NAME='" & TbName & "'"
    Set TbRec = ProcessRecordset(SQLst)
    GetSchemaTable = TbRec("TABLE_SCHEMA")
    TbRec.Close
    Set TbRec = Nothing
    End Function

    Và hàm này nữa : Function ProcessRecordset(strSQLStatement As String) As ADODB.Recordset

    và khi mở forms "frmContacts" thì báo lỗi ngay dòng này : "If CurrentProject.AllForms("frmlogin").IsLoaded = False Then DoCmd.OpenForm "frmlogin", , , , , acDialog"

    xin bạn giải thích giúp mình rõ. cám ơn.

    Xin trả lời các thắc mắc của Bạn như sau:

    – Function GetSchemaTable dùng để lấy Schema của bảng dữ liệu xác định (TbName)
    Còn Schema là gì? Bạn có thể tìm hiểu thông qua Google với từ khoá "Schema in SQL SERVER"
    Bạn cũng có thể tham khảo tài liệu đính kèm về Schema in SQL SERVER

    – Function ProcessRecordset dùng để xác lập giá trị cho biến Recordset thông qua câu lệnh SQL xác định (câu lệnh SQL nói ở đây chính là chuỗi strSQLStatement)

    – Về lỗi xuất hiện khi mở form "fmContacts": tôi đã kiểm tra không thấy lỗi này xuất hiện như Bạn đã gặp. Bạn kiểm tra xem có Form "frmlogin" hiện hữu trong file ứng dụng hay không? Nếu không có, Bạn cần phải tải lại file ứng dụng. Để tiện cho Bạn theo dõi tôi upload đính kèm file ứng dụng trong bài này luôn.

  16. hands says:

    chào bạn mình có một lỗi trong access là khi minh upadate csdl thì nó báo là đã có người dùng khác sửa trước mặt dù ko có ai ngoài tui..CODE]skydrive.live.com/redir?resid=6AA5E28C7FECD5C4!642&authkey=!AOJBLcFPTaZmE2s
    mong cao thủ chỉ dại

    Xin chào các Bạn,
    Theo yêu cầu của một số Bạn cần cập nhật lại file ứng dụng làm mẫu minh hoạ loạt bài này, tôi đã cho bổ sung, chỉnh lý và đã cập nhật lại link tải file này ngay trong bài số #1
    Xin các Bạn tham khảo lại bài #1 để lấy link tải về.
    Cũng theo yêu cầu của nhiều Bạn qua email, tôi sẽ có bài hướng dẫn chi tiết hơn về một số phương cách thiết kế form có subform theo dạng Unbound form (đã có thiết kế mẫu minh hoạ trong file nêu trên). Xin các Bạn vui lòng chờ đến sáng mai.
    Xin trao đổi thêm để các Bạn tiện tham khảo file ứng dụng làm mẫu minh hoạ tôi mới cập nhật.

    1. Theo đề nghị của nhiều Bạn, tôi đã cho phục hồi file SQL Server database truy xuất được qua internet, đồng thời bổ sung thêm số lượng dữ liệu nhiều hơn trước để các Bạn có thể kiểm tra được tốc độ truy xuất nhằm mục đích giúp tối ưu hoá các dòng lệnh truy xuất dữ liệu trong ứng dụng.

    2. Tôi đã chỉnh lý các form nhập dữ liệu với nội dung như sau:
    – Chỉnh lý form frmContacts giúp cho việc tìm kiếm dữ liệu đã có được thuận tiện hơn.
    Cách tìm kiếm như sau: khi muốn tìm dữ liệu theo 1 chi tiết thông tin nào đó ta chỉ cần nhập vào 1 vài ký tự có trong dòng thông tin đó (mà không cần nhập toàn bộ dòn thông tin cần tìm) tại ô tương ứng rồi bấm nút lệnh "Tìm kiếm"
    Thí dụ: để tìm những khách hàng nào có địa chỉ ở tại "Cam Ranh" (có từ này trong dòng ghi địa chỉ) ta nhập từ "Cam Ranh" vào ô ghi địa chỉ rồi bấm nút lệnh tìm kiếm.
    Code phục vụ cho việc tìm kiếm theo kiểu này như sau:

    Function BuildSQLWhere(blnPriorWhere As Boolean, strPriorWhere As String, strValue As String, strDbFieldName As String) As String

    On Error GoTo HandleError

    Dim strWhere As String

    If blnPriorWhere Then
    'add to the existing where clause
    strWhere = strPriorWhere & " AND "
    Else
    'create the where clause for the first time
    strWhere = " WHERE "
    End If

    If strDbFieldName = "Ngaysinh" Then
    strWhere = strWhere & strDbFieldName & " = '" & Format$(strValue, "dd-mmm-yy") & "' "
    Else
    'build where clause using LIKE so will find both exact
    'matches and those that start with value input by user
    If strDbFieldName = "Gioitinh" Then
    strWhere = strWhere & strDbFieldName & " = " & PadQuotes(strValue) & " "
    Else
    strWhere = strWhere & strDbFieldName & " LIKE N'%" & PadQuotes(strValue) & "%' "
    End If
    End If

    blnPriorWhere = True

    'return where clause
    BuildSQLWhere = strWhere

    Exit Function

    HandleError:
    GeneralErrorHandler Err.Number, Err.Description, DB_QUANLY, "BuildSQLWhere"
    Exit Function

    End Function

    Các Bạn chú ý đoạn code trên có dòng ghi:

    strWhere = strWhere & strDbFieldName & " LIKE N'%" & PadQuotes(strValue) & "%' "

    Đây là điều kiện lọc dữ liệu để tìm. Ta chú ý 2 dấu % đặt ở 2 đầu trong dòng trên có ý nghĩa "dữ liệu cần tìm bắt đầu và kết thúc bằng gì cũng được miễn là có sự hiện diện của từ được cung cấp bởi biến strValue là được".
    Việc sử dụng ký tự % như vậy có khác với thông thường trong Access là hay dùng dấu ? hay *, đây chính là quy ước của câu lệnh SQL trong SQL Server. Ta phải viết theo đúng quy ước của SQL Server vì ứng dụng này chủ yếu là gửi các câu lệnh SQL truy xuất dữ liệu đến SQL Server.

    – Chỉnh lý form nhập chứng từ nhập xuất phát sinh frmCtuNX:
    + Khi cần tìm chứng từ đã nhập theo số chứng từ: tại ô nhập số chứng từ (là kiểu comboBox) ta chỉ cần nhập 1 vài ký tự có trong số chứng từ đã nhập rồi bấm phím F4 sẽ được 1 danh sách sổ xuống liệt kê các số chứng từ có chứa ký tự đã nhập trong đó.
    + Khi cần chọn 1 khách hàng xác định: ta cũng làm tương tự tại ô khách hàng (cũng là 1 comboBox)
    Vấn đề cần chú ý ở đây là ta đã cho hạn chế dữ liệu hiển thị trong các comboBox trong giới hạn vừa đủ với nhu cầu.

    Còn nữa các Bạn ạ. Xin hẹn bài kế tiếp ta lại tiếp tục với những chú ý khi thiết kế form có subform theo dạng Unbound form thông qua form frmCtuNX vừa nêu ở trên.
    Các Bạn có thể tìm thấy tài liệu hướng dẫn chi tiết về vấn đề này tại trang [URL='https://bis.net.vn/'%5Dhttps://bis.net.vn

    Hoặc tải xuống bản in tài liệu này từ link sau: https://www.mediafire.com/view/bcsk4nwfiwub17j/HuongdanCauhinhSQLServerDeTruyxuatQuaInternet.doc

    [URL='https://www.mediafire.com/view/bcsk4nwfiwub17j/HuongdanCauhinhSQLServerDeTruyxuatQuaInternet.doc'%5DChúc các Bạn nhiều sức khoẻ và thành công.

  17. hands says:

    Xin chào các Bạn,

    Trong bài này xin trao đổi với các Bạn về thiết kế Unbound Form có chứa Subform. SubForm là Form nằm bên trong 1 Form khác.
    Như tôi đã trao đổi tại bài #18, trái ngược với Bound Form luôn gắn liền với 1 nguồn dữ liệu xác định (được khai báo tại thuộc tính Record Source), Unbound Form là Form không gắn với một nguồn dữ liệu nào cả.
    Đó chính là căn nguyên khiến 1 Unbound Form tránh được xung đột dữ liệu trong quá trình có nhiều người cùng truy xuất dư liệu, hoặc tuy chỉ có mỗi mình ên mần công chuyện với dữ liệu đó nhưng hổng dè đã "mở" nó ra mà quên "đóng" nó lại.
    Những điều cần chú ý khi thiết kế 1 Unbound Form tôi đã trình bày tại #18, trong bài này chỉ tập trung vào việc thiết kế 1 Unbound Form nhưng lại có SubForm.
    Một ví dụ điển hình cho nhu cầu này là thiết kế Form nhập chứng từ nhập xuất (dưới đây gọi là Main Form), với 1 Subform trình bày chi tiết các mặt hàng phát sinh.

    1. Việc đầu tiên ta cần làm là làm sao để nạp thông tin của 1 chứng từ xác định xuống ô dữ liệu tương ứng trên Form khi cần (vì Unbound Form không duy trì thường trực 1 nguồn dữ liệu gắn kết với nó mà).
    Đây chính là trường hợp ta cần làm việc với thông tin của 1 chứng từ xác định đã lập trước.
    Trong file minh hoạ, công việc này được thực hiện thông qua các thủ tục:
    + LoadInvoiceInfoToForm: Nạp thông tin chứng từ lên Form

    Sub LoadInvoiceInfoToForm(SoCtuSt, Optional NoSetSourceRecForSubForm)
    Dim sqlSt As String, SQLrec As ADODB.Recordset
    Dim KHrec As ADODB.Recordset
    Dim tblName As String, MSKH, vIdKH As Long
    'Xác định nguồn dữ liệu chứa thông tin của chứng từ cần nạp
    tblName = "tblctunx"
    If IsNull(SoCtuSt) Then Exit Sub
    sqlSt = "SELECT * FROM " & GetSchemaTable(tblName) & "." & tblName
    sqlSt = sqlSt & " WHERE soctu ='" & SoCtuSt & "'"

    Set SQLrec = ProcessRecordset(sqlSt)
    'Nếu chứng từ hiện hữu, cho ghi thông tin chi tiết của chứng từ lên các ô dữ liệu tương ứng của Form
    If SQLrec.RecordCount > 0 Then
    Set objKhachHang = New clsDanhba
    With Me
    .txtId = SQLrec!id
    .txtNgay = SQLrec!Ngay
    .cmbNghiepvu = SQLrec!MSNV
    .txtTsuat = SQLrec!tsuatvat
    .txtNguoiGiaodich = SQLrec!NguoiGiaodich

    MSKH = SQLrec!MSKH

    sqlSt = "SELECT * FROM " & GetSchemaTable("tblDanhsach") & ".tblDanhsach"
    sqlSt = sqlSt & " WHERE MSKH = '" & MSKH & "'"
    Set KHrec = ProcessRecordset(sqlSt)
    objKhachHang.PopulatePropertiesFromRecordset KHrec

    .cmbKhachhang.RowSourceType = "Value List"
    .cmbKhachhang.RowSource = objKhachHang.HoChulot & ";" & objKhachHang.MaKhachHang
    .cmbKhachhang = objKhachHang.MaKhachHang

    .txtDiachi = objKhachHang.Diachi
    .txtPhone = objKhachHang.Dtvp
    .txtMasoThue = objKhachHang.Msthue

    KHrec.Close
    Set KHrec = Nothing
    ' Và nạp nguồn dữ liệu chi tiết các mặt hàng phát sinh cho Subform
    If IsMissing(NoSetSourceRecForSubForm) Then SetSourceRecForSubForm Me, "frmCtuNXCT"

    End With
    End If
    '
    SQLrec.Close
    Set SQLrec = Nothing
    End Sub

    + Để nạp nguồn dữ liệu chi tiết các mặt hàng phát sinh cho Subform ta dùng thủ tục SetSourceRecForSubForm

    Sub SetSourceRecForSubForm(mForm As Form, sForm As String)
    Dim sqlSt As String
    Dim SQLrec As ADODB.Recordset
    Dim tblName As String
    Dim vSoCtu, stChema As String
    '
    vSoCtu = mForm!cmbSoCtu
    'Xác định nguồn dữ liệu chứa thông tin chi tiết hàng hoá phát sinh của chứng từ cần nạp
    If Not IsNull(vSoCtu) Then
    tblName = "tblctunxct"
    stChema = GetSchemaTable(tblName)
    sqlSt = "SELECT " & stChema & ".tbldmhanghoa.tenhanghoa, " & stChema & ".tblctunxct.*"
    sqlSt = sqlSt & " FROM " & stChema & ".tbldmhanghoa INNER JOIN " & stChema & ".tblctunxct"
    sqlSt = sqlSt & " ON " & stChema & ".tbldmhanghoa.mahang=" & stChema & ".tblctunxct.mahang"
    sqlSt = sqlSt & " WHERE " & stChema & ".tblctunxct.soctu = '" & vSoCtu & "'"

    Set SQLrec = ProcessRecordset(sqlSt)

    Set mForm(sForm).Form.Recordset = SQLrec
    'Nạp thông tin chi tiết lên các ô dữ liệu tương ứng trên SubForm
    With mForm(sForm).Form
    .Requery
    !txtId.ControlSource = "id"
    !txtMahang.ControlSource = "mahang"
    !txtTenHanghoa.ControlSource = "tenhanghoa"
    !txtCapDvt.ControlSource = "dvt"
    !txtDvt.ControlSource = "=IIF(not isnull(dvt),flookup('kihieu','tbldonvitinh','cap=' & ),'')"
    !txtSoluong.ControlSource = "soluong"
    !txtDongia.ControlSource = "dongia"
    !chkCKTL.ControlSource = "lacktyle"
    !txtMucCK.ControlSource = "mucck"
    End With
    SQLrec.Close
    Set SQLrec = Nothing
    End If
    End Sub

    Như vậy, khi ta chọn 1 số chứng từ xác định, ứng dụng sẽ cho chạy các thủ tục nêu trên để nạp nguồn dữ liệu tương ứng cho Main Form và SubForm.
    Ta gán các thủ tục cần thực hiện với sự kiện ngay sau khi số chứng từ được cập nhật (cmbSoCtu_AfterUpdate)

    Private Sub cmbSoCtu_AfterUpdate()
    Dim vSoCtu
    ClearInputCTHH 'Xoá trống các ô nhập chi tiết hàng hoá
    vSoCtu = Trim(Me.cmbSoCtu.Text)
    LoadInvoiceInfoToForm vSoCtu 'Nạp thông tin chứng từ đã chọn lên MainForm và SubForm
    End Sub

    Vậy khi cần hiệu chỉnh chi tiết chứng từ đã lập và đang hiển thị trên Form thì làm sao?
    Thật đơn giản các Bạn ạ:
    + Đối với thông tin là chi tiết hàng hoá phát sinh: ta chỉ cần chuyển con trỏ đến dòng ghi mặt hàng cần hiệu chỉnh là ứng dụng sẽ copy các thông tin đó lên các ô có nền sẩm màu sẵn sàng cho ta hiệu chỉnh (hoặc xoá). Hiệu chỉnh xong ta bấm nút lệnh ghi bên phải (có hình chiếc đĩa mềm) để cho ghi lại nội dung vừa ddiiefu chỉnh.
    Việc này được thực hiện thông qua thủ tục SaveToInvoiceDetailFromForm

    Sub SaveToInvoiceDetailFromForm(Optional InVoiceDetailId)
    'Luu thong tin tren form vao tblctunxCT
    'UpdateInvoiceDetail
    On Error GoTo HandleError

    Dim sqlSt As String, tblName As String
    Dim vId
    Dim MucCK As Double, CKTL As Byte

    Call OpenMyConnection

    tblName = "tblctunxct"
    With Me
    vId = Me.txtDetailId
    MucCK = Nz(.txtMucCK)
    If IsNull(.chkCKTL) Then
    CKTL = 0
    Else
    If .chkCKTL.Value = True Then
    CKTL = 1
    Else
    CKTL = 0
    End If
    End If
    'Phân biệt là trường hợp nhập mới hay hiệu chỉnh lại dữ liệu đã có.
    If Not IsNull(vId) Then
    sqlSt = "UPDATE " & GetSchemaTable(tblName) & "." & tblName & " SET "
    sqlSt = sqlSt & " soctu ='" & Trim(.cmbSoCtu) & "',"
    sqlSt = sqlSt & " mahang ='" & .cmbMSHH & "',"
    sqlSt = sqlSt & " dvt =" & .cmbDvt & ","
    sqlSt = sqlSt & " soluong =" & .txtSoluong & ","
    sqlSt = sqlSt & " dongia =" & .txtDongia & ","
    sqlSt = sqlSt & " lacktyle =" & CKTL & ","
    sqlSt = sqlSt & " mucck =" & MucCK
    sqlSt = sqlSt & " WHERE ("
    sqlSt = sqlSt & " soctu='" & Trim(Me.cmbSoCtu) & "'"
    sqlSt = sqlSt & " AND id=" & InVoiceDetailId
    sqlSt = sqlSt & ")"
    Else
    sqlSt = "INSERT INTO " & GetSchemaTable(tblName) & "." & tblName
    sqlSt = sqlSt & "(soctu, mahang, dvt, soluong, dongia, lacktyle, mucck)"
    sqlSt = sqlSt & " VALUES ("
    sqlSt = sqlSt & " '" & Trim(.cmbSoCtu) & "',"
    sqlSt = sqlSt & " '" & .cmbMSHH & "',"
    sqlSt = sqlSt & " " & .cmbDvt & ","
    sqlSt = sqlSt & " " & .txtSoluong & ","
    sqlSt = sqlSt & " " & .txtDongia & ","
    sqlSt = sqlSt & " " & CKTL & ","
    sqlSt = sqlSt & " " & Nz(MucCK)
    sqlSt = sqlSt & ")"
    End If
    End With

    MyConn.Execute sqlSt

    Call CloseMyConnection 'Dùng xong rồi thì đóng lại cho đỡ tốn tài nguyên và khỏi gặp xung đột dữ liệu đó các Bạn.

    HandleError:
    If Err > 0 Then
    GeneralErrorHandler Err.Number, Err.Description, NhapXuat_FORM, "SaveToInvoiceDetailFromForm"
    Exit Sub
    End If
    End Sub

    Ở đây chúng ta chú ý: có 2 trường hợp cần phân biệt là nhập mới và hiệu chỉnh thông tin đang có.
    Xem trong thủ tục trên chúng ta thấy thủ tục có phân biệt 2 trường hợp này bằng cách xet giá trị của ô txtDetailId, đây là ô chứa giá trị Id của chi tiết hàng hoá phát sinh. Trong thiết kế, ta cho ô này ẩn đi (bằng cách khai báo thuộc tính Visible = False). Nếu ô này có chứa nội dung xác định thì là trường hợp hiệu chỉnh dữ liệu đang có, ngược lại nếu nó rổng không (IsNull) là trường hợp nhập mới.
    Trường hợp muốn xoá dòng ghi chi tiết hàng phát sinh xác định: ta cho nạp dòng ghi chi tiết hàng hoá đó lên các ô sẩm màu rồi bấm nút lệnh Xoá (có hình gạch chéo) nằm bên trái dòng của các ô sẩm màu này.
    Thủ tục tương ứng như sau:

    Dim vHoi As Long, sqlSt As String, tblName As String
    Dim DetailId
    DetailId = Me.txtDetailId
    If Not IsNull(DetailId) Then
    vHoi = Eval("msgbox('" & "Ban vua ra lenh cho xoa dong ghi mat hang nay" & vbCrLf & "Co phai Ban chac chan muon Xoa hay khong?" & "@" & "Bam YES de xoa, bam NO de huy bo lenh nay" & "@" & "',36,'Xoa chi tiet hang hoa')")
    If vHoi = vbYes Then
    tblName = "tblctunxct"
    sqlSt = "DELETE " & GetSchemaTable(tblName) & "." & tblName
    sqlSt = sqlSt & " WHERE Id =" & DetailId
    '
    Call OpenDbConnection
    ExecuteSQLCommand sqlSt
    Call CloseDbConnection
    'Xoá xong thì cho cập nhật lại nội dung hiển thị trên SubForm
    SetSourceRecForSubForm Me, "frmCtuNXCT"
    End If
    End If

    + Đối với thông tin chung của chứng từ: cũng tương tự như trên, ta hiệu chỉnh thông tin tại các ô tương ứng; và cũng phân biệt 2 trường hợp: nhập mới và hiệu chỉnh thông tin đang có. Việc ghi lại các thông tin đã cập nhật vào bảng dữ liệu ghi chứng từ phát sinh được thực hiện bằng thủ tục SaveToInvoiceFromForm

    Sub SaveToInvoiceFromForm(Optional InvoiceId, Optional NoLoadInfo)
    'Luu thong tin tren form vao tblctunx

    'UpdateOrInsert:
    '+ True: Luu thong tin thay doi vao mau tin dang hien huu
    '+ Flase: Them mau tin moi

    'InvoiceId: so chung tu
    '
    On Error GoTo HandleError

    Dim sqlSt As String, tblName As String
    Dim vId

    Call OpenMyConnection

    tblName = "tblctunx"

    With Me
    vId = Me.txtId
    If Not IsNull(vId) Then
    If IsNull(InvoiceId) Then Exit Sub
    sqlSt = "UPDATE " & GetSchemaTable(tblName) & "." & tblName & " SET "
    sqlSt = sqlSt & " soctu ='" & .cmbSoCtu & "',"
    sqlSt = sqlSt & " ngay ='" & Format$(.txtNgay, "dd-mmm-yy") & "',"
    sqlSt = sqlSt & " msnv ='" & .cmbNghiepvu & "',"
    sqlSt = sqlSt & " mskh ='" & .cmbKhachhang & "'"
    If Not IsNull(.txtNguoiGiaodich) Then sqlSt = sqlSt & ", nguoigiaodich ='" & .txtNguoiGiaodich & "'"
    If Not IsNull(.txtTsuat) Then sqlSt = sqlSt & ", tsuatvat =" & .txtTsuat
    sqlSt = sqlSt & " WHERE ("
    sqlSt = sqlSt & " soctu='" & InvoiceId & "'"
    sqlSt = sqlSt & ")"

    Else
    If IsNull(.cmbSoCtu) Then Exit Sub
    sqlSt = "INSERT INTO " & GetSchemaTable(tblName) & "." & tblName
    sqlSt = sqlSt & "(soctu, ngay, msnv, mskh"
    If Not IsNull(.txtNguoiGiaodich) Then sqlSt = sqlSt & ", nguoigiaodich"
    If Not IsNull(.txtTsuat) Then sqlSt = sqlSt & ", tsuatvat"
    sqlSt = sqlSt & ")"
    sqlSt = sqlSt & " VALUES ("
    sqlSt = sqlSt & " '" & .cmbSoCtu & "',"
    sqlSt = sqlSt & " '" & Format$(.txtNgay, "dd-mmm-yy") & "',"
    sqlSt = sqlSt & " '" & .cmbNghiepvu & "',"
    sqlSt = sqlSt & " '" & .cmbKhachhang & "'"
    If Not IsNull(.txtNguoiGiaodich) Then sqlSt = sqlSt & ", '" & .txtNguoiGiaodich & "'"
    If Not IsNull(.txtTsuat) Then sqlSt = sqlSt & ", " & .txtTsuat
    sqlSt = sqlSt & ")"
    End If
    End With

    MyConn.Execute sqlSt

    'Lưu xong thì cho cập nhật lại các thông tin đã lưu lên Form.
    If IsMissing(NoLoadInfo) Then
    LoadInvoiceInfoToForm Me.cmbSoCtu
    Else
    LoadInvoiceInfoToForm Me.cmbSoCtu, True
    End If

    Call CloseMyConnection 'Mở ra xài xong thì đóng lại

    HandleError:
    If Err > 0 Then
    GeneralErrorHandler Err.Number, Err.Description, NhapXuat_FORM, "SaveToInvoiceFromForm"
    Exit Sub
    End If
    End Sub

    Trong thủ tục trên ta chú ý đoạn

    If IsMissing(NoLoadInfo) Then
    LoadInvoiceInfoToForm Me.cmbSoCtu
    Else
    LoadInvoiceInfoToForm Me.cmbSoCtu, True
    End If

    Cho Lưu xong thì cho cập nhật lại các thông tin đã lưu lên Form. Cái này cần để ghi bổ sung những thông tin chỉ phát sinh khi dữ liệu được ghi vào bảng dữ liệu, chẳng hạn như chỉ số Id tự động của bản ghi, hoặc các giá trị tính toán cần thiết khác.
    Khi ghi dữ liệu vào bảng dữ liệu, chúng ta cần chú ý đến 1 thực tế là có những thông tin chi tiết của chứng từ không nhất thiết lúc nào cũng có. Do vậy khi ta viết các thủ tục cập nhật phải chú ý đến các trường hợp này. Các Bạn có thể thấy điều này được thể hiện ở những dòng sau đây trong thủ tục nêu trên:

    If Not IsNull(.txtNguoiGiaodich) Then sqlSt = sqlSt & ", '" & .txtNguoiGiaodich & "'"
    If Not IsNull(.txtTsuat) Then sqlSt = sqlSt & ", " & .txtTsuat

    Ở đây tôi xác định các chi tiết: Người trực tiếp giao dịch, thuế suất VAT là những chi tiết thông tin không phải lúc nào cũng bắt buộc phải có khi lập chứng từ nên đã dự liệu bằng các statement IF… THEN …

    Bài đã dài. Xin hẹn các Bạn trong bài sau.

  18. hands says:

    Chào các Bạn,
    Có Bạn vừa gửi email cho tôi góp ý rằng sao ta không phát triển thủ tục SetSourceRecForSubForm lên để áp dụng cho việc nạp RecordSource cho subform trong mọi trường hợp, chứ không phải chỉ riêng cho 1 trường hợp như tôi đã làm.
    Đây là một góp ý rất chí lý. Vậy xin mời các Bạn tham gia viết lại thủ tục SetSourceRecForSubForm theo hướng phát triển được đề nghị nêu trên.

    Chào ban lehongduc.
    Tôi là người mới tiếp cận access nên kiến thức chưa có nhiều mà chỉ đọc và ngẩm chưa thể thực hiện đc như các bạn. Tôi xin hỏi bạn một vấn đề sau:
    Tôi đang có 1 file mdb dùng cho nhiều máy tính khác nhau, trong file mdb này có rất nhiều table. Trong số các table đó có một số table phải nhập dữ liệu trực tiếp vào để các table khác thực thi dữ liệu. Vậy tôi muốn tạo 1 file database mdb dữ liệu do mình nạp dữ liệu vào các table cần thiết mỗi khi có dữ liệu mới sau đó sẽ truyền dữ liệu từ file nạp vào đến các file thực thi trên các máy tính khác với mục đích để cho đồng nhất dữ liệu.
    Ý tôi là chỉ cần 1click thì dữ liệu sẽ đc nạp vào file thực trên các máy tính khác thông qua internet hoặc mạng nội bộ.
    Kiến thức của tôi giới hạn nếu đc bạn hãy cho ví dụ minh họa, cảm ơn ban nhiều.

    Xin chào Bạn,
    Trong Microsoft Access không thể có table mà lại thực thi được dữ liệu như Bạn đã ghi

    … Trong số các table đó có một số table phải nhập dữ liệu trực tiếp vào để các table khác thực thi dữ liệu.

    Bản thân các tables chỉ là tập hợp các mẫu tin đơn thuần, không có khả năng "thực thi" được việc xử lý dữ liệu.
    Theo yêu cầu của nhiều Bạn, tôi xin giới thiệu các tài liệu tôi đã tham khảo làm cơ sở cho bài viết này, đó là các tài liệu "Beginning Access … VBA" và "Professional Access … Programming"
    Các Bạn có thể dùng Google để tìm thấy các tài liệu trên.

    Chúc sức khoẻ các Bạn.

Leave a Reply

Your email address will not be published. Required fields are marked *

Quảng cáo

Cũ vẫn chất

Xem thêm