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

Liên hệ QC
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: 146
  • Picture2.jpg
    Picture2.jpg
    10.4 KB · Đọc: 138
  • Picture3.jpg
    Picture3.jpg
    49.3 KB · Đọc: 135
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.
 
Web KT
Back
Top Bottom