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

Thảo luận trong 'Access và VBA' bắt đầu bởi lehongduc, 20/6/12.

  1. lehongduc

    lehongduc Member Hội viên mới

    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 15/7/2014:
    qldanhba_150714
     

    Các file đính kèm:

    Chỉnh sửa cuối: 15/7/14

  2. lehongduc

    lehongduc Member Hội viên mới

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

    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?
     
    Thanhtandofico thích bài này.
  3. lehongduc

    lehongduc Member Hội viên mới

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

    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

    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/libr...4(v=office.10)
     
  4. lehongduc

    lehongduc Member Hội viên mới

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

    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: qldanhba_210612.zip

    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.
     
  5. lehongduc

    lehongduc Member Hội viên mới

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

    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, gồm: 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ị là 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ị 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="blue"]'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="blue"]'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 cuối: 23/6/12
  6. lehongduc

    lehongduc Member Hội viên mới

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

    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="green"]Sub OpenDbConnection()[/COLOR]
    '
    'Co the tham khao chuoi ket noi den cac nguon du lieu khac nhau
    'tai dia chi sau: www.connectstring.com
    
    
        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 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="green"]Function FixNull[/COLOR]([COLOR="blue"]varIn As Variant[/COLOR]) [COLOR="green"]As String[/COLOR]
    
    
        If IsNull(varIn) Then
            FixNull = ""
        Else
            FixNull = varIn
        End If
        
    [COLOR="green"]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="green"]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="green"]End Sub[/COLOR]
    

    Mã:
    [COLOR="green"]Sub PopulatePropertiesFromRecordset[/COLOR]([COLOR="blue"]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="green"]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="green"]Public Sub GeneralErrorHandler[/COLOR]([COLOR="blue"]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="green"]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, <Tên module>, <Tên procedure>
        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é.
     
  7. lehongduc

    lehongduc Member Hội viên mới

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

    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="red"]N[/COLOR]'" & objDanhba.Ten & "', "
        strSQLInsert = strSQLInsert & "[COLOR="red"]N[/COLOR]'" & objDanhba.HoChulot & "', "
        strSQLInsert = strSQLInsert & "[COLOR="red"]N[/COLOR]'" & objDanhba.Diachi & "', "
        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.

    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="4"]strSQLInsert = strSQLInsert & "N'" & objDanhba.Ten & "', "[/SIZE]
    
     
  8. lehongduc

    lehongduc Member Hội viên mới

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

    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="red"]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="red"]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
     
  9. lehongduc

    lehongduc Member Hội viên mới

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

    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ã:
    Sub PopulatePropertiesFromRecordset(rsCont As ADODB.Recordset)
     '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
    
    End Sub
    
    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ở)

    Như vậy, trong class module, từ khóa "Me" không phải chỉ form đang mở các Bạn nhé.
     
    Chỉnh sửa cuối: 26/6/12
  10. lehongduc

    lehongduc Member Hội viên mới

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

    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, ...
     
  11. lehongduc

    lehongduc Member Hội viên mới

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

    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
    Và những vấn đề có liên quan khác.
    Thú thật, một mình độc thoại thấy cũng hơi vắng thiếu.

    Mong các Bạn nhiệt tình tham gia trao đổi.
     
  12. lehongduc

    lehongduc Member Hội viên mới

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

    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="green"]Sub GetDataFromCollection([COLOR="blue"]strSQL As String[/COLOR])[/COLOR]
    '
    Dim sqlSt As String, sCri As String, vCount As Long
    Dim MyRec As ADODB.Recordset
    '
    [COLOR="blue"]Dim MyObj As clsDanhba[/COLOR]
    Dim TestCol As Collection
    Set TestCol = New Collection
    '
    Set MyRec = ProcessRecordset(strSQL)
    [COLOR="blue"]'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="blue"]'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="blue"]'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.RunSQL "DELETE * FROM tblDs"
    [COLOR="blue"]'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="red"]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="red"]Next[/COLOR]
    Set TestCol = Nothing
     
    [COLOR="blue"]'Open Table tblDs[/COLOR]
    MsgBox "Danh sach nay co tat ca la: " & vCount & " nguoi"
    DoCmd.OpenTable "tblDs", , acReadOnly
     
    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é:
    qldanhba_030712.zip

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

    ---------- Post added at 10:06 ---------- Previous post was at 09:23 ----------

    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ề: CustomCollection.zip

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

    lehongduc Member Hội viên mới

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

    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.
     
    Chỉnh sửa cuối: 5/7/12
  14. lehongduc

    lehongduc Member Hội viên mới

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

    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 độ 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ã:
    Function BuildSQLSelectLimitDanhba(FromId, ToId) As String
     
        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
     
    End Function
    
    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ã:
    Function RetrieveDanhba(WithLimit As Boolean, Optional FromId, Optional ToId) As ADODB.Recordset
     
    '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
     
        ‘Dòng ngay bên dưới là dòng để nạp chuỗi SELECT toàn bộ danh bạ
        'strSQLStatement = BuildSQLSelectDanhba
     
        ‘==== Nay ta REM nó lại để nạp đoạn code thay thế sau đây:======
        If WithLimit = True Then
            strSQLStatement = BuildSQLSelectLimitDanhba(FromId, ToId)
        Else
            strSQLStatement = BuildSQLSelectDanhba
        End If
        ‘============== HẾT ĐOẠN CODE MỚI ===============
     
        Set rsCont = ProcessRecordset(strSQLStatement)
       
        Set RetrieveDanhba = rsCont
      
        Exit Function
     
    HandleError:
        GeneralErrorHandler Err.Number, Err.Description, CLS_DANHBA, "RetrieveDanhba"
        Exit Function
     
    End Function
    
    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ã:
    Sub GetTotalRecCount()
    Dim strSQLStatement As String
    Dim rsSourceRec As ADODB.Recordset
    strSQLStatement = BuildSQLSelectDanhba
    Set rsSourceRec = ProcessRecordset(strSQLStatement)
    lngRecCount = rsSourceRec.RecordCount
    Set rsSourceRec = Nothing
    End Sub
    
    - 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:
    qldanhba_070712.zip

    Đế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é.
     
    Chỉnh sửa cuối: 7/7/12
  15. lehongduc

    lehongduc Member Hội viên mới

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

    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
     
  16. lehongduc

    lehongduc Member Hội viên mới

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

    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 & " WHERE ten LIKE N'%" & stFilter & "%'"
    sqlSt = sqlSt & " ORDER BY danhbaid"
    Debug.Print sqlSt
    Set r = ProcessRecordset(sqlSt)
    Set Me.Combo0.Recordset = r
    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é.
     
    Chỉnh sửa cuối: 8/7/12
  17. lehongduc

    lehongduc Member Hội viên mới

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

    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 ở #17. 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 (#17)
    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="blue"][B]KeyCode [/B]As Integer, Shift As Integer[/COLOR])[/COLOR]
    Dim stFilter
    If [B]KeyCode [/B]= [B]vbKeyF4 [/B]Or ([B]KeyCode = vbKeyDown And Shift = acAltMask[/B]) Then [COLOR="red"]'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é.
     
    Chỉnh sửa cuối: 9/7/12
  18. lehongduc

    lehongduc Member Hội viên mới

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

    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.
     
  19. lehongduc

    lehongduc Member Hội viên mới

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

    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.
     
  20. lehongduc

    lehongduc Member Hội viên mới

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

    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:
    qldanhba_160712.zip

    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ã:
     
    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
        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
    End Sub
    
    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ã:
     
    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
            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
    End Sub
    
    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ã:
    Private Sub SetComboRowSource(ComboName As String, RecSourceSt As String, stFilter As String)
        '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
    End Sub
    
    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ã:
     
    Private Sub cmbKhachhang_KeyDown(KeyCode As Integer, Shift As Integer)
        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
        '
    End Sub
    
    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ã:
     
    Sub SaveToInvoiceFromForm(Optional InvoiceId)
        '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
    End Sub
    
    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ã:
     
    Sub LoadInvoiceInfoToForm(SoCtuSt)
        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
    End Sub
    
    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.

    ---------- Post added at 05:42 ---------- Previous post was at 05:37 ----------

    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ỉnh sửa cuối: 16/7/12

Chia sẻ trang này

XenForo Add-ons by Brivium ™ © 2012-2013 Brivium LLC.