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

Liên hệ QC

Người dùng đang xem chủ đề này

lehongduc

Thành viên chính thức
Tham gia
14/9/06
Bài viết
88
Được thích
149
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:
http://www.mediafire.com/download/j5v854t46ozo5ck/qldanhba_150714.rar
 

File đính kèm

Lần chỉnh sửa cuối:
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?
 
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:
DanhbaObject.png

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: http://msdn.microsoft.com/en-us/library/aa140954(v=office.10)
 
Lần chỉnh sửa cuối:
Xin các Bạn đừng bỏ tôi một mình độc thoại nhé.
Bàn ra hoặc tán vào nào các Bạn.
 
Xin các Bạn đừng bỏ tôi một mình độc thoại nhé.
Bàn ra hoặc tán vào nào các Bạn.

Cái này là Access nên ít người biết, ít người "bàn ra tán vào" là phải rồi
Sao bác không viết cái gì đó liên quan đến Excel?
 
Chào các Bạn,

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: http://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.
 
Lần chỉnh sửa cuố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:
Mã:
[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:
Mã:
[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é: http://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õ.
 
Chỉnh sửa lần cuối bởi điều hành viên:
Xin các Bạn đừng bỏ tôi một mình độc thoại nhé.
Bàn ra hoặc tán vào nào các Bạn.

Bạn thấy việc làm của mình có ý nghĩa, tôi cũng thấy vậy bạn cứ làm đi, tôi rất ủng hộ bạn. Bài của bạn mới mở nhưng thời điểm này có khoảng 240 người đọc - Chính là lượng người quan tâm tới bài của bạn.
 
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":
Mã:
[COLOR=#006400]Sub OpenDbConnection()[/COLOR]
'
[COLOR=#0000cd]'Co the tham khao chuoi ket noi den cac nguon du lieu khac nhau[/COLOR]
[COLOR=#0000cd]'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:
Mã:
    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 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:
Mã:
[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:
Mã:
[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]


Mã:
[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:
Mã:
[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:
Mã:
...
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é.
 
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:
Mã:
...
    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é:

Mã:
[SIZE=5]strSQLInsert = strSQLInsert & "[COLOR=#FF0000]N[/COLOR][B][COLOR=#0000CD]'[/COLOR][/B]" & objDanhba.Ten & "[B][COLOR=#0000CD]'[/COLOR][/B], "[/SIZE]
 
Lần chỉnh sửa cuối:
Chào các Bạn,

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:
Mã:
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:
Mã:
...
    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.

Mã:
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
 
Lần chỉnh sửa cuối:
Chào các Bạn,

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:
Mã:
[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 đó:
MeKeyword.jpg

Như vậy, trong class module, từ khóa "Me" không phải chỉ form đang mở các Bạn nhé.
 
Lần chỉnh sửa cuối:
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:
Mã:
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:
Mã:
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. Nội dung bài viết, đúng, sai?
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.


 
Chào các Bạn,

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:
Mã:
[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:
Mã:
...
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é:
http://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).
 
Lần chỉnh sửa cuối:
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ề: http://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.
 
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.
 
Lần chỉnh sửa cuối:
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:
Mã:
[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:
Mã:
[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:
Mã:
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:
Mã:
[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:
http://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é.
 
Lần chỉnh sửa cuối:
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
 
Chào các Bạn,


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”.


Mã:
[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:

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


Có Bạn nào có giải pháp khác xin trao đổi thêm nhé.
 
Lần chỉnh sửa cuối:
Chào các Bạn,
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

Mã:
[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]

Các Bạn nào có giải pháp khác xin trao đổi thêm nhé.
 
Lần chỉnh sửa cuối:
Chào các Bạn,

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.
 
Sau đây là Link tải xuống file ứng dụng đã cập nhật theo bài này:
http://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é.

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:

attachment.php


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

attachment.php


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

attachment.php



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.
 

File đính kèm

  • Picture1.jpg
    Picture1.jpg
    46 KB · Đọc: 147
  • Picture2.jpg
    Picture2.jpg
    10.4 KB · Đọc: 139
  • Picture3.jpg
    Picture3.jpg
    49.3 KB · Đọc: 136
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":

Mã:
[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:

Mã:
[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
 
Lần chỉnh sửa cuối:
Chào Bạn,
Muốn sử dụng chức năng "Lấy Danh sách" trước hết bạn phải tạo 1 bảng dữ liệu ngay bên trong file ứng dụng, tên bảng là "tblDs" với 2 cột:
+ Cột Id, kiểu dữ liệu là Number (Long)
+ Cột Ten, kiểu dữ liệu là Text

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.
 
Chào Bạn,
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.
 
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.
Chào Bạ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.
 
Lần chỉnh sửa cuối:
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.
 
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:
http://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”:
Mã:
[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:
Mã:
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:
Mã:
[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:
Mã:
[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):
Mã:
[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:
Mã:
[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:
Mã:
[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é.

Rất mong các Bạn tham gia trao đổi thêm.
 
Lần chỉnh sửa cuối:
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
 
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:
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:
Optimizing Microsoft Office Access Applications Linked to SQL Server
 
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":
Mã:
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":
Mã:
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)[/B]
[B]        .RowSourceType = "Table/Query"[/B]
[B]    End With[/B]

    Set Me(ComboName).Recordset = SourceRec
    
    SourceRec.Close
    Set SourceRec = Nothing


End Sub
 
Chào các Bạn,

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:
Mã:
Set mForm(sForm).Form.Recordset = SQLrec

mà không phải là:
Mã:
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
 
Lần chỉnh sửa cuối:
Chào các Bạn,
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.
 
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:
http://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:
Mã:
[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]
 
Chào các Bạn,

Để 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")

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.
 
Chào các Bạn,

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.
 
cảm on bạn rất nhiều bài viết rất hữu ích.
 
Cám ơn bác lehongduc ! Tiện xin bác hướng dẫn thêm 1 demo nữa về Multi-User, nhiều User cùng xem - sửa - xóa 1 chứng từ thì giải quyết như thế nào ? Vấn đề này cũng đã thấy nói nhiều ở các trang khác nhưng chưa có 1 ví dụ cụ thể.
 
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?
Chào Bác,
Thật sự ra topic của Bác không phải ai cũng có thể tham gia thảo luận. Đơn giản là bởi vì người đọc cần có "kiến thức tương đối" về lập trình. Mặc dù vậy, vẫn có bạn vẫn đang tham gia bằng việc gởi email, hoặc điện thoại cho Bác đấy thôi.

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.
Theo thiển ý của em, mỗi một ngôn ngữ, ứng dụng nó có điểm mạnh/yếu của nó. Việc so sánh Access và các ứng dụng được viết bởi các ngôn ngữ lập trình trên nền .NET là khập khiển. Nhưng chắc có lẻ Bác cũng đồng ý với em rằng "Có rất nhiều ứng dụng trên nền .NET mà chắc chắn rằng Access không thể nào làm được."

Hy vọng Bác vẫn cứ tiếp tục loạt bài viết rất hay này của Bác.

Lê Văn Duyệt
 
Lần chỉnh sửa cuối:
Cám ơn bác lehongduc ! Tiện xin bác hướng dẫn thêm 1 demo nữa về Multi-User, nhiều User cùng xem - sửa - xóa 1 chứng từ thì giải quyết như thế nào ? Vấn đề này cũng đã thấy nói nhiều ở các trang khác nhưng chưa có 1 ví dụ cụ thể.

Chào Bạn,
Những ví dụ mẫu (có file ứng dụng và database MS SQL SERVER cho phép truy xuất từ xa qua internet) tôi đã upload lên trong loạt bài này đều cho phép nhiều người truy xuất cùng lúc. Bạn chịu khó xem lại các bài từ đầu nhé.
Chúc sức khoẻ.
 
Chào Anh Duyệt và các Bạn,
Do thời gian gần đây tôi quá bận bịu công việc nên chưa thể viết tiếp được.
Trong loạt bài vừa qua cũng chỉ mới dừng lại việc xử lý dữ liệu do file ứng dụng MS Access thực hiện, còn SQL SERVER mới chỉ được sử dụng để lưu dữ liệu phát sinh.
Tôi sẽ sắp xếp viết tiếp trong thời gian tới, tập trung vào việc chuyển toàn bộ việc xử lý dữ liệu cho SQL SERVER còn file ứng dụng MS Access chỉ tập trung cho việc xây dựng giao diện.

Xin cảm ơn các Bạn đã quan tâm. Chúc Anh Duyệt và các Bạn nhiều sức khoẻ.
 
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.
 
Lần chỉnh sửa cuối:
Chào các Bạn,

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.

cám ơn bầu nhiệt quyết của bạn, cũng nhờ bạn mà mõi người học hỏi được rất nhiều, nhưng do kiến thức của nhiều người còn hạn chế trong lĩnh vực này, nên chưa mấy ai bàn ra tán vào, nhưng mõi người cũng đang theo dõi bài viết của bạn, rất mong sớm được theo dõi thêm những bài tiếp theo của bạn, thanks

để minh họa Tính năng của access mà bạn đã chia sẽ, mình xin kèm đường link sau :
http://office.microsoft.com/zh-tw/access-help/HA010356866.aspx?CTT=3
 
Lần chỉnh sửa cuối:
cám ơn bầu nhiệt quyết của bạn, cũng nhờ bạn mà mõi người học hỏi được rất nhiều, nhưng do kiến thức của nhiều người còn hạn chế trong lĩnh vực này, nên chưa mấy ai bàn ra tán vào, nhưng mõi người cũng đang theo dõi bài viết của bạn, rất mong sớm được theo dõi thêm những bài tiếp theo của bạn, thanks

để minh họa Tính năng của access mà bạn đã chia sẽ, mình xin kèm đường link sau :
http://office.microsoft.com/zh-tw/access-help/HA010356866.aspx?CTT=3

Đường link trên sử dụng ngôn ngữ khác EngLish. Các Bạn có thể xem tiếng Anh tại link sau:
http://office.microsoft.com/en-001/...se-to-share-on-the-web-HA010356866.aspx?CTT=1
 
Nếu có thời gian xin anh hướng dẫn thêm về cách cài đặt SQL Server và cấu hình router để truy vấn dữ liệu qua Internet. Có thể có nhiều người chưa biết cái bước quan trọng này thì làm sao mà thực hiện và áp dụng được bước thứ 2 của anh đang hướng dẫn.
 
Lần chỉnh sửa cuối:
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
Chào các 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.
 
Lần chỉnh sửa cuối:
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.
Chào Bạ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.
 

File đính kèm

Lần chỉnh sửa cuối:
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]https://skydrive.live.com/redir?resid=6AA5E28C7FECD5C4!642&authkey=!AOJBLcFPTaZmE2s[/CODE]
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.
Chúc các Bạn nhiều sức khoẻ và thành công.
 
Chào các Bạn,
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.
 
Lần chỉnh sửa cuối:
Nếu có thời gian xin anh hướng dẫn thêm về cách cài đặt SQL Server và cấu hình router để truy vấn dữ liệu qua Internet. Có thể có nhiều người chưa biết cái bước quan trọng này thì làm sao mà thực hiện và áp dụng được bước thứ 2 của anh đang hướng dẫn.
Xin chào các Bạ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 http://bis.net.vn

Hoặc tải xuống bản in tài liệu này từ link sau: http://www.mediafire.com/view/bcsk4nwfiwub17j/HuongdanCauhinhSQLServerDeTruyxuatQuaInternet.doc
 
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=' & [dvt]),'')"
!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.
 
Lần chỉnh sửa cuối:
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.
 
Chào các Bạn,

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.
 
Mến chào anh lehongduc!
Em tên, leminhdoan (l.m.d84st@gmail.com)
Em có 1 file ứng dụng đã chế từ lâu nhằm giúp a/e thực hiện một số nhiệm vụ cho nhanh mà quản lý được dữ liệu.
Nhưng tham khảo qua mấy topic của anh, em thấy cái u/d của em cùi quá.
U/d của em chỉ mình em xửa lý: nhập, xuất,... k có SQL thật bất tiện.
Nhờ anh giúp em, hướng dẫn làm class trong vb và kết nối SQL như thế nào. để em làm lại ứng dụng mới hay hơn, đồng thời a/e máy trạm có thể cùng làm trên data có sẵn nhằm giúp công việc nhanh hơn nhiều.
Mong sớm được sự trợ giúp từ anh lehongduc, em thành thật biết ơn anh nhiều vì đó là điều mong ước của em từ khi vào làm đến giờ.
Chúc anh sức khỏe tốt!
Thân!
/-*+/
 
Tôi cũng quan tâm tới nội dung này, nhưng chủ yếu là xuất dữ liệu từ Access sang Excel qua DAO, ADO. Nội dung sớm được update trong sách lập trình phần 2_
 
Chào bạn, mình cũng muốn mày mò làm 1 cái như bạn để dùng trong công việc của mình, nhưng mình không biết access. Có bạn nào có bài mẫu nào liên quan đến kết nối dữ liệu giữa EXcel VBA và SQL Server không mình xin với.

Cảm ơn các bạn rất nhiều.
 

Bài viết mới nhất

Back
Top Bottom