Giáo trình C# và ứng dụng (Phần 2) - Nguyễn hoàng hà

pdf 138 trang ngocly 1740
Bạn đang xem 20 trang mẫu của tài liệu "Giáo trình C# và ứng dụng (Phần 2) - Nguyễn hoàng hà", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên

Tài liệu đính kèm:

  • pdfgiao_trinh_c_va_ung_dung_phan_2_nguyen_hoang_ha.pdf

Nội dung text: Giáo trình C# và ứng dụng (Phần 2) - Nguyễn hoàng hà

  1. CHƯƠNG 3 LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG VỚI C# Trong phần trước chúng ta đã thấy cách sử dụng của lớp đôi tượng. Ở đây, chúng ta sẽ nhắc lại một số khái niệm và tính chất của lớp đối tượng trước khi nói về tính hướng đối tượng. Lớp đối tượng được định nghĩa theo cú pháp: class MyClass { private int someField; public string SomeMethod(bool parameter) { } } Lớp đối tượng chứa các thành viên – thành viên là thuật ngữ được sử dụng để nói đến dữ liệu hoặc hàm được định nghĩa trong lớp. Thuật ngữ hàm (funciton) được dùng để nói đến bất kỳ thành viên nào có chứa mã lệnh, bao gồm phương thức (method), thuộc tính (property), hàm khởi dựng (constructor), hàm nạp chồng toán tử (operator overload). Lớp đối tượng trong C# là kiểu dữ liệu tham chiếu. Điều này có nghĩa là khi bạn khai báo một biến có kiểu dữ liệu lớp thì xem như bạn có một biến có thể chứa tham chiếu đến một thể hiện của lớp đối tượng đó. Bạn cũng cần phải khởi tạo ra đối tượng bằng cách dùng toán tử new. MyClass myObject; myObject = new MyClass(); Cả hai thao tác khai báo và khởi tạo đối tượng có thể được làm một lần như thế này: MyClass myObject = new MyClass(); Do là dữ liệu kiểu tham chiếu, nên phép gán hai biến tham chiếu có ý nghĩa là cho hai biến dữ liệu tham chiếu đến cùng một đối tượng. MyClass myObjectRef = myObject; Tức là myObjectRef sẽ cùng tham chiếu đến cùng đối tượng mà myObject đang tham chiếu đến. Các phương thức của đối tượng được tham chiếu đến có thể được triệu gọi từ Giáo trình Visual Studio .NET 52
  2. cả myObjectRef và myObject. 3.1 Đơn kế thừa trong C# C# hỗ trợ đơn kế thừa cho tất cả các lớp đối tượng, tức là một lớp chỉ có thể dẫn xuất trực tiếp nhiều nhất là từ một lớp đối tượng khác. Lớp cơ sở nhất trong C# là lớp System.Object class MyDerivedClass : MyBaseClass { // functions and data members here } Cũng như một số ngôn ngữ lập trình hướng đối tượng, C# có một số bổ từ truy cập để quy định phạm vi mã lệnh được phép truy xuất một thành viên trong lớp đối tượng. Mức truy cập Mô tả public Biến hoặc phương thức có thể được truy xuất từ bất cứ nơi nào internal Biến hoặc phương thức chỉ có thể truy xuất trong phạm vi cùng assembly protected Biến hoặc phương thức chỉ có thể truy xuất từ bên trong kiểu dữ liệu mà nó thuộc về, hoặc các kiểu dữ liệu dẫn xuất protected Biến hoặc phương thức có thể được truy xuất trong phạm vi assembly internal hiện tại, hoặc từ các kiểu dữ liệu dẫn xuất từ kiểu dữ liệu chứa nó private Biến hoặc phương thức chỉ có thể được truy xuất từ bên trong kiểu dữ liệu mà nó thuộc về 3.2 Nạp chồng phương thức (Method Overloading) C# hỗ trợ nạp chồng phương thức, cho phép có nhiều phiên bản cho một phương thức có các chữ ký khác nhau. Khái niệm chữ ký của phương thức ở đây được hiểu là tên phương Giáo trình Visual Studio .NET 53
  3. thức, số lượng đối số, kiểu đối số được sử dụng trong phương thức. Chẳng hạn, lớp đối tượng Student dưới đây có hai phương thức nạp chồng Display(): class Student { // void Display(string stMessage) { // implementation } void Display() { // implementation } } 3.3 Ghi đè phương thức và che dấu phương thức Bằng cách khai báo một hàm ở lớp cơ sở là virtual, chúng ta có thể ghi đè hàm đó ở lớp dẫn xuất của lớp này. class MyBaseClass { public virtual string VirtualMethod() { return "Phuong thuc nay la virtual trong MyBaseClass"; } } Điều này có nghĩa là chúng ta có thể cài đặt lại phương thức VirtualMethod() (với cùng chữ ký phương thức) trong lớp dẫn xuất của MyBaseClass. Khi chúng ta gọi phương thức này từ một thể hiện của lớp dẫn xuất thì phương thức của lớp dẫn xuất sẽ được triệu gọi chứ không phải là phương thức của lớp cơ sở. class MyDerivedClass: MyBaseClass { public override string VirtualMethod() { return "Phuong thuc nay duoc dinh nghia de` trong MyDerivedClass"; } } Đoạn mã lệnh dưới đây minh họa hiệu ứng của việc ghi đè phương thức: MyBaseClass obj; Giáo trình Visual Studio .NET 54
  4. obj = new MyBaseClass(); obj.VirtualMethod(); // in ra Phuong thuc nay là virtual trong MyBaseClass obj = new MyDerivedClass(); obj.VirtualMethod(); // in ra Phuong thuc nay duoc dinh nghia de` trong // MyDerivedClass Ở đoạn mã lệnh trên, chúng ta thấy rằng, việc quyết định phiên bản nào của phương thức VirtualMethod (ở lớp MyBaseClass hay MyDerivedClass) được sử dụng là tùy thuộc vào nội dung hiện tại của đối tượng mà obj tham chiếu đến. Nói cách khác, việc quyết định phiên bản phương thức để triệu gọi được quyết định trong thời gian thực thi chương trình chứ không phải là trong lúc biên dịch chương trình! Đặc tính này còn được gọi là tính gắn kết muộn (late-binding) trong kỹ thuật lập trình. Trong lớp đối tượng, các trường dữ liệu hoặc các hàm tĩnh không được khai báo là virtual. Nếu một phương thức với chữ ký được khai báo trong cả lớp cơ sở và lớp dẫn xuất, nhưng các phương thức không được khai báo tương ứng là virtual và override, thì phiên bản phương thức ở lớp dẫn xuất được gọi là đã che dấu phiên bản ở lớp cơ sở. Trong tình huống này, phiên bản của phương thức được sử dụng để hoạt động sẽ tùy thuộc vào kiểu dữ liệu của biến được sử dụng để tham chiếu đến đối tượng thể hiện chứ không phải là chính đối tượng thể hiện. Điều này được thể hiện trong cách thức hoạt động của đoạn mã lệnh dưới đây: class MyBaseClass { public string VirtualMethod() // KHONG DUOC KHAI BAO LA virtual NUA!!! { return "Phuong thuc nay la virtual trong MyBaseClass"; } } class MyDerivedClass: MyBaseClass { public string VirtualMethod() // KHONG DUOC KHAI BAO LA override NUA!!! { return "Phuong thuc nay duoc dinh nghia de` trong MyDerivedClass"; } } MyBaseClass obj; // obj la bien kieu MyBaseClass phien ban VirtualMethod() duoc // su dung DUOC QUYET DINH NGAY LUC BIEN DICH la cua MyBaseClass obj = new MyBaseClass(); obj.VirtualMethod(); // in ra Phuong thuc nay là virtual trong MyBaseClass Giáo trình Visual Studio .NET 55
  5. obj = new MyDerivedClass(); obj.VirtualMethod(); // van in ra Phuong thuc nay là virtual trong MyBaseClass!!! Trên thực tế, khi biên dịch đoạn mã lệnh tương tự như trên, trình biên dịch sẽ đưa ra cảnh báo về việc phương thức bị che giấu. Để tránh khỏi cảnh báo như vậy, bạn khai báo phương thức được định nghĩa lại trong lớp dẫn xuất thêm với từ khóa new. 3.4 Gọi phương thức với phiên bản của lớp cơ sở C# có một cú pháp đặc biệt để cho phép trong lớp dẫn xuất có thể triệu gọi phương thức với phiên bản được cài đặt ở lớp cơ sở: base. (). Ví dụ: class Student { public virtual void Dispaly() { Console.WriteLine(“Thong tin chung cua sinh vien ”); } } class ITStudent: Student { public override void Display() { base.Display(); Console.WriteLine(“Thong tin rieng doi voi sinh vien CNTT ”); } } Lưu ý rằng cách gọi phương thức base. () để gọi mọi phương thức của lớp cơ sở là có thể được sử dụng cho bất kỳ phương thức nào trong lớp dẫn xuất, chứ không nhất thiết là trong cùng phương thức được ghi đè. 3.5 Lớp trừu tượng và hàm trừu tượng C# cho phép cả lớp đối tượng và hàm được khai báo là abstract (trừu tượng). Một lớp trừu tượng thì không thể được tạo thể hiện, trong khi đó, một hàm trừu tượng thì không thể có phần cài đặt, và phải được ghi đè bởi một hàm không trừu tượng trong lớp dẫn xuất (hàm trừu tượng mặc nhiên được xem là virtual trong lớp cơ sở). Trong một lớp trừu tượng, chỉ có thể được khai báo trường dữ liệu thành phần và các chữ ký của phương thức, không có phần cài đặt của phương thức. abstract class SinhVien { Giáo trình Visual Studio .NET 56
  6. private bool damaged = false; // field public abstract decimal DiemTrungBinh(); // abstract method } 3.6 Lớp bị niêm phong và phương thức bị niêm phong C# cho phép cả lớp đối tượng và phương thức được khai báo là niêm phong. Lớp bị niêm phong (sealed class) là lớp không được dẫn xuất thêm. Phương thức bị niêm phong (sealed method) là phương thức không thể được ghi đè ở lớp dẫn xuất. Ví dụ về lớp bị niêm phong: sealed class FinalClass { // cai dat cho lop FinalClas } class DerivedClass : FinalClass // LOI KHI BIEN DICH!!! { //// } Ví dụ về phương thức bị niêm phong: class MyClass { public sealed override void FinalMethod() { // cai dat cho phuong thuc bi niem phong } } class DerivedClass : MyClass { public override void FinalMethod() // LOI KHI BIEN DICH { } } 3.7 Nạp chồng toán tử C# cung cấp cơ chế nạp chồng toán tử, cho phép cài đặt mã lệnh để quyết định cách thức một lớp đối tượng làm việc với toán tử thông thường. Cú pháp để nạp chồng một toán tử là như sau: public static operator (parameter list) { cai dat ma lenh o day} Giáo trình Visual Studio .NET 57
  7. Các quy tắc cần tuân thủ khi cài đặt và sử dụng phương thức nạp chồng toán tử: Bắt buộc phải có bổ từ truy cập public và static. Kiểu dữ liệu trả về là kiểu lớp đối tượng khi làm việc với các lớp đối tượng. Kiểu dữ liệu trả về không được là void. op là toán tử hai ngôi, một ngôi (unary), hoặc toán tử quan hệ. Cả hai toán tử == và != phải được cài đặt theo cặp. Các toán tử hai ngôi yêu cầu hai đối số, toán tử một ngôi chỉ yêu cầu một đối số Ví dụ dưới đây xây dựng một lớp mô phỏng kiểu dữ liệu số phức với cách sử dụng các phép toán +, - đơn giản: public class ComplexNumber { private int real; private int imaginary; public ComplexNumber() : this(0, 0) // constructor { } public ComplexNumber(int r, int i) // constructor { real = r; imaginary = i; } // Ghi de phuong thuc ToString() de hien thi so ao theo dang thong thuong: public override string ToString() { return(System.String.Format("{0} + {1}i", real, imaginary)); } // Nap chong toan tu '+': public static ComplexNumber operator+(ComplexNumber a, ComplexNumber b) { return new ComplexNumber(a.real + b.real, a.imaginary + b.imaginary); } // Nap chong toan tu '-': public static ComplexNumber operator-(ComplexNumber a, ComplexNumber b) { return new ComplexNumber(a.real - b.real, a.imaginary - b.imaginary); } } class TestComplexNumber { Giáo trình Visual Studio .NET 58
  8. static void Main() { ComplexNumber a = new ComplexNumber(10, 12); ComplexNumber b = new ComplexNumber(8, 9); System.Console.WriteLine("a = {0}", a.ToString()); System.Console.WriteLine("b = {0}", b.ToString()); ComplexNumber c = a + b; System.Console.WriteLine("c = a + b = {0}", c.ToString()); ComplexNumber d = a - b; System.Console.WriteLine("d = a - b = {0}", d.ToString()); } } Như chương trình minh họa, sau khi nạp chồng toán tử + và -, bạn có thể sử dụng hai phép toán + và – đối với dữ liệu ComplexNumber một cách trực tiếp. Ở đây, kết quả nhận được sẽ là: a = 10 + 12i b = 8 + 9i c = a + b = 18 + 21i d = a - b = 2 + 3i 3.8 Bài thực hành Bài thực hành 3.1: Quản lý sinh viên Tóm tắt Viết chương trình quản lý sinh viên của một trường. Sinh viên có thể học các chuyên ngành Công nghệ Thông tin, Vật lý, Ngữ văn. Mỗi chuyên ngành tương ứng có các môn học khác nhau. Sinh viên khoa Công nghệ Thông tin phải học 3 môn Pascal, C# và SQL. Sinh viên khoa Vật lý phải học 4 môn: Cơ học, Điện học, Quang học, Vật lý hạt nhân. Sinh viên khoa Văn phải học 2 môn Văn học cổ điển và Văn học Hiện đại Chương trình cho phép nhập danh sách sinh viên, sau đó in danh sách sinh viên cùng với điểm trung bình của họ ra màn hình. In ra danh sách những sinh viên có điểm trung bình cao trên 5.0 ra màn hình. Thông tin hiển thị có dạng Họ tên, Chuyên ngành đào tạo, Điểm trung bình. Giáo trình Visual Studio .NET 59
  9. Kỹ thuật được trình bày - Truy xuất tập tin có định dạng cho trước - Sử dụng một phương thức của lớp String - Các kỹ thuật hướng đối tượng được sử dụng trong bài toán thực tế Trình tự thực hiện 1. Trước khi tiến hành cài đặt, ta khảo sát qua sơ đồ lớp được sử dụng. Với những mô tả khá rõ ràng trong yêu cầu bài toán, ta có được cái nhìn tổng quan về các lớp như sau: Lưu ý rằng, phương thức dtb() được cài đặt là virtual để chúng ta có thể override một cách cụ thể, chi tiết hơn trong các lớp kế thừa từ class SinhVien. Phương thức ToString() được cài đặt override từ lớp object để sử dụng trong việc in “nội dung” của đối tượng. 2. Tạo mới một project kiểu Console Application với tên là studentManager 3. Tại cây phân cấp Solution Explorer nhắp phải chuột và chọn Add New Item Trong hộp thoại hiện ra, chọn tạo mới class SinhVien.cs Giáo trình Visual Studio .NET 60
  10. 4. Cài đặt các thành phần cơ bản cho lớp SinhVien Giáo trình Visual Studio .NET 61
  11. 5. Bổ sung thêm các class SinhVienCNTT, SinhVienVan, SinhVienVL theo phân tích thiết kế lớp từ trước. Dưới đây là phần mô tả cài đặt cho lớp SinhVienVan. Hai lớp còn lại SinhVienCNTT, SinhVienVL được cài đặt một cách tương tự. Giáo trình Visual Studio .NET 62
  12. 6. Trong phần chương trình (tập tin Program.cs) chúng ta thực hiện yêu cầu bài toán như sau: Giáo trình Visual Studio .NET 63
  13. Yêu cầu thêm - In ra 3 sinh viên có điểm trung bình cao nhất trường. - Chỉnh sửa để người sử dụng có thể nhập danh sách mà không biết trước số lượng sinh viên (sử dụng vòng lặp while, do, ) - Chỉnh sửa để có thể nhập dữ liệu các sinh viên từ file. Giáo trình Visual Studio .NET 64
  14. 3.2 Bài thực hành trên Winform Bài thực hành 3.2.1 helloWinForms Kỹ thuật được trình bày - Cấu trúc của và cơ chế hoạt động của một project Windows Form Application. - Cơ chế xử lý sự kiện của các Control trong một Windows Form - Một số phương thức, thuộc tính, sự kiện quan trọng của các điều khiển trong một Windows Form. Trình tự thực hiện 1. Tạo mới một ứng dụng kiểu Windows Form Application với tên là 01-helloWindowsForm như hình vẽ 2. Theo mặc định, một solution với một project được tạo ra. Project này có một lớp Form1. Khảo sát nội dung của project trong Windows Explorer, chúng ta sẽ thấy cấu trúc Giáo trình Visual Studio .NET 65
  15. của thư mục và các tập tin tương tự như hình dưới: Có thể thấy, mỗi Form được tạo ra tương ứng với 3 tập tin có tiếp đàu ngữ là giống nhau, lấy ví dụ là Form1 Form1.Designer.cs: chứa các mã lệnh do Form Designer tự sinh ra tương ứng với các thao tác do người sử dụng kéo thả các Control từ ToolBox vào bề mặt Form hay thực hiện các thiết lập đối với các Control. Form1.cs: chứa phần mã lệnh và khai báo thêm do người sử dụng cài đặt. Form1.resx: chứa các mô tả, khai báo về các tài nguyên được sử dụng trong Form. 3. Chúng ta cũng có thể quan sát cấu trúc của solution hay project bằng cách khảo sát cửa sổ Solution Explorer: Giáo trình Visual Studio .NET 66
  16. 4. Từ cửa sổ Solution Explorer, đổi tên tập tin Form1.cs thành FormMain.cs. Để ý rằng, cả ba tập tin liên quan đến Form1 đều được thay đổi theo một cách đồng bộ. 5. Thiết kế giao diện cho FormMain như hình vẽ Giáo trình Visual Studio .NET 67
  17. 6. Bước tiếp theo, chúng ta sẽ thực hiện cài đặt phương thức xử lý sự kiện Click của nút bấm btnCurrentTime: a. Chọn điều khiển nút bấm btnCurrentTime trong cửa số thiết kế Form. b. Ở trang Event trong cửa sổ Properties Windows, nhắp đúp chuột vào sự kiện Click (xem hình vẽ dưới). Form Designer sẽ sinh ra phương thức xử lý sự kiện có tên mặc định là btnCurrentTime_Click( ). (Phương thức xử lý sự kiện được mặc định đặt tên là _ ) Giáo trình Visual Studio .NET 68
  18. Soạn thảo phần mã lệnh cho phương thức này như sau: 7. Thực hiện chạy chương trình, khi nhấn vào nút bấm btnCurrentTime, một hộp thông báo được hiển thị ra như hình vẽ 8. Thực ra chúng ta có thể tự đặt tên cho phương thức xử lý sự kiện. Chẳng hạn, để cài đặt phương thức xử lý sự kiện MouseEnter cho nút bấm btnCurrentTime, trong cửa sổ Properties ở trang Events, tìm đến mục MouseEnter và: Giáo trình Visual Studio .NET 69
  19. a. Nhập vào tên phương thức xử lý sự kiện: btn_MouseEnter b. Nhấn Enter c. FormDesigner sẽ tạo ra phương thức với tên tương ứng d. Tiến hành cài đặt mã lệnh cho phương thức xử lý sự kiện trên như sau: private void btn_MouseEnter(object sender, EventArgs e) { btnCurrentTime.ForeColor = Color.Red; } 9. Tương tự, chúng ta cài đặt tiếp phương thức xử lý sự kiện MouseLeave cho nút bấm btnCurrentTime như sau private void btn_MouseLeave(object sender, EventArgs e) { btnCurrentTime.ForeColor = SystemColors.ControlText; } 10. Chạy chương trình và quan sát kết quả: Điều khiển nút bấm btnCurrentTime sẽ có hiệu ứng mouse hover khá ấn tượng: khi rê con trỏ chuột vào nút bấm btnCurrentTime, màu chữ của nó sẽ đổi sang màu đỏ; màu chữ của nút bấm trở Giáo trình Visual Studio .NET 70
  20. thành bình thường (màu ControlText) khi con trỏ chuột rê ra khỏi nút bấm. 11. Để tìm hiểu kỹ hơn bản chất của việc gắn kết phương thức xử lý sự kiện, chúng ta nhắp đúp chuột vào FormMain.Designer.cs trong cửa sổ Solution Explorer để xem phần nội dung được sinh ra bởi Form Designer: Giáo trình Visual Studio .NET 71
  21. Giáo trình Visual Studio .NET 72
  22. Chú ý những phần được tô sáng trong hình vẽ nói trên; từ đó suy ra được bản chất của việc gắn kết phương thức xử lý sự kiện trong khi thiết kế. 12. Đóng file nội dung FormMain.Designer.cs lại. Các bước tiếp theo sẽ minh họa cách thức dùng chung một phương thức xử lý sự kiện cho nhiều đối tượng khác nhau. 13. Trong cửa sổ thiết kế của FormMain, thực hiện a. Chọn cả hai đối tượng btnClose và btnAbout b. Trong trang Events của cửa sổ Properties, gõ tên phương thức xử lý sự kiện Click cho cả hai điều khiển nút bấm này là btnTask_Click rồi nhấn Enter (xem hình vẽ) 14. Thực hiện cài đặt mã lệnh cho phương thức này như sau: Giáo trình Visual Studio .NET 73
  23. private void btnTask_Click(object sender, EventArgs e) { if (sender == btnClose) this.Close(); else if (sender == btnAbout)1 MessageBox.Show("Day la chuong trinh minh hoa", "Thong bao"); } Trong phương thức trên, chúng ta sử dụng đối số sender để nhận biết điều khiển nào phát sinh sự kiện. Chúng ta cũng có thể thực hiện như thế này: private void btnTask_Click(object sender, EventArgs e) { string stTask = (sender as Button).Text; 2 if (stTask == "Close") this.Close(); else if (stTask == "About") MessageBox.Show("Day la chuong trinh minh hoa", "Thong bao"); } 15. Bây giờ, chúng ta tinh chỉnh thêm để chương trình hỗ trợ hiệu ứng mouse hover cho tất cả các điều khiển trong form: a. Sửa lại phần mã nguồn cho 2 phương thức xử lý sự kiện btn_MouseEnter và btn_MouseLeave như sau: private void btn_MouseEnter(object sender, EventArgs e) { (sender as Control).ForeColor = Color.Red; } private void btn_MouseLeave(object sender, EventArgs e) { (sender as Control).ForeColor = SystemColors.ControlText; 1 Thực ra không nhất thiết phải có nhánh else if, chỉ cần else là đủ, bởi vì ở đây chúng ta chỉ áp dụng phương thức này cho hai điều khiển btnClose và btnAbout!. 2 Phép chuyển kiểu (sender as Button) trong câu lệnh này là thành công vì cả btnClose và btnAbout đều là các điều khiển kiểu Button Giáo trình Visual Studio .NET 74
  24. } b. Trong phần FormDesigner, chọn tất cả các đối tượng trên bề mặt Form. c. Trong cửa sổ Properties, chọn phương thức xử lý sự kiện MouseLeave cho tất cả các đối tượng đang chọn là btn_MouseLeave (xem hình vẽ) d. Làm tương tự để gán phương thức xử lý sự kiện MouseEnter cho tất cả các điều khiển nói trên là btn_Enter. e. Chạy chương trình để xem hiệu ứng: khi rê con trỏ chuột qua các điều khiển, font chữ của chúng sẽ được đổi thành màu đỏ. 16. Trong bước 11, chúng ta đã biết được cách thức đưa một thành phần điều khiển vào giao diện của một Windows Form thông qua mã lệnh (bằng cách tìm hiểu phần mã sinh ra bởi Form Designer). Bây giờ, chúng ta sẽ áp dụng để thực hiện thêm các điều khiển vào Form và gán phương thức xử lý sự kiện cho chúng trong thời gian thực thi chương trình a. Bổ sung vào Form một nút bấm btnCreateButton Giáo trình Visual Studio .NET 75
  25. b. Cài đặt phương thức xử lý sự kiện Click cho nút bấm này như sau: Giáo trình Visual Studio .NET 76
  26. c. Chạy chương trình và quan sát kết quả. Giáo trình Visual Studio .NET 77
  27. Mở rộng - Hãy tìm hiểu ý nghĩa của việc cài đặt mã lệnh ở bước 15.a: (sender as Control). Có thể sử dụng phép ép kiểu nào khác không? Tại sao? - Điều chỉnh trong giao diện chương trình, trong đó có một số điều khiển (Label, TextBox, RadioButton, CheckBox hoặc Button) sử dụng màu khác với màu mặc định (là SystemColors.ControlText). Khi đó, hiệu ứng mouse hover hoạt động không đúng nữa. Hãy chỉnh sửa chương trình để khắc phục phát sinh này. Giáo trình Visual Studio .NET 78
  28. Bài thực hành 3.2.2 usingControls Tóm tắt Xây dựng chương trình điền thông tin cá nhân như minh họa Kỹ thuật được trình bày - Giới thiệu một ứng dụng WinForms cơ bản - Cách thức lưu file với nội dung tiếng Việt - Các thành phần điều khiển cơ bản: Button, Label, TextBox, PictureBox, Timer, - Nạp một ảnh từ file Trình tự thực hiện 1. Tạo mới một project loại Windows Application, đặt tên là usingControls 2. Theo mặc định, một lớp Form1 được sinh ra. Chỉnh sửa các thuộc tính của Form1 với các giá trị như bảng dưới: Thuộc tính Giá trị Ghi chú Name FormMain Giáo trình Visual Studio .NET 79
  29. Text Hello WinForms Tiêu để của cửa sổ FormBorderStyle FixedSingle Kích thước của cửa sỗ sẽ không được thay đổi khi chạy chương trình MaximizeBox False Vô hiệu hóa nút Maximize của cửa sổ Chú ý rằng, những thuộc tính có thay đổi giá trị so với mặc định sẽ được hiển thị trong cửa sổ Properties dưới dạng chữ in đậm 3. Thiết kế giao diện của form như minh họa. Lưu ý, với mỗi điều khiển bạn đưa vào form, nếu dự định truy xuất nó trong phần mã nguồn khi lập trình thì hãy đặt tên nó thay vì để như tên mặc định. Chỉnh sửa thuộc tính của một số đối tượng như sau: Điều khiển Thuộc tính Giá trị Giáo trình Visual Studio .NET 80
  30. dtpDOB Format Custom CustomFormat dd/MM/yyyy txtOther Enable False lblInfo Font Chọn font thích hợp, in đậm picImage SizeMode StretchImage lblName BackColor Transparent (Web) tmrScroll Interval 120 4. Nhấn Ctrl + S để lưu nội dung project. Do chúng ta có sử dụng ký tự tiếng Việt trong Form nên Visual Studio có hiển thị hộp thoại để yêu cầu chỉ định bảng mã lưu ký tự: Nhấn nút “Save With Other Encoding” để chọn bảng mã thích hợp – sau đó bạn có thể chọn cách lưu theo UTF8 như hình dưới (cũng có thể chọn tùy chọn Unicode – Codepage 1200): Giáo trình Visual Studio .NET 81
  31. 5. Cài đặt phần mã lệnh cho sự kiện Click của nút bấm btnSelectImage như sau: Khi người sử dụng nhấn vào nút này, một hộp thoại sẽ hiện ra cho phép chọn ảnh. Chỉ các tập tin có phần mở rộng là BMP, JPG, GIF mới được hiển thị để lựa chọn. Điều này được thiết lập thông qua thuộc tính Filter của đối tượng dlgOpen (thuộc lớp OpenFileDialog). 6. Khi người sử dụng gõ tên của họ vào txtName thì nội dung của lblName cũng thay đổi theo. Muốn vậy, ta cài đặt mã lệnh cho sự kiện TextChanged của txtName như (1) – xem minh họa code ở dưới 7. Đối tượng txtOther chỉ được sử dụng (Enabled) khi mà chkOther được check vào, do đó ta cũng cài đặt mã lệnh cho sự kiện CheckChanged của chkOther như (2) 8. Khi nhấn nút “Cập nhật” thì nội dung của lblInfo được cập nhật theo như phần mã lệnh cài đặt cho sự kiện Click của btnUpdate (3) 9. Người sử dụng có thể bật tắt chế độ cuộn nội dung dòng chữ lblInfo bằng cách nhấn chuột vào nó. Cài đặt mã lệnh cho sự kiện Click của lblInfo như (5) Giáo trình Visual Studio .NET 82
  32. 10. Để cuộn nội dung dòng chữ, cài đặt mã lệnh cho sự kiện Tick của tmrScroll như (4) Giáo trình Visual Studio .NET 83
  33. CHƯƠNG 4. XỬ LÝ DỮ LIỆU VỚI ADO.NET Xử lý dữ liệu là nhiệm vụ phổ biến và quan trọng của nhiều chương trình ứng dụng. Dữ liệu được truy xuất, xử lý của một chương trình ứng dụng có thể là một tập tin văn bản, tập tin các bản ghi, hay là một nguồn dữ liệu từ CSDL nào đó. .NET Framework cung cấp một lượng lớn các thành phần giao diện (Win Forms, Web Forms) hỗ trợ cho việc trình bày, kết buộc (bind) dữ liệu. Cùng với đó là nền tảng xử lý dữ liệu ADO.NET cung cấp cách thức làm việc với nhiều loại nguồn dữ liệu khác nhau một cách linh động. Do tính chất quan trọng của việc xử lý dữ liệu trong một ứng dụng cùng với sự phức tạp của ADO.NET, trước khi bắt tay vào thực hiện các bài tập thực hành, chúng ta khảo sát qua một số điểm lý thuyết cơ bản. 4.1 Kiến trúc tổng quan của ADO.NET Kiến trúc của ADO.NET được mô tả như hình dưới, bao gồm hai thành phần chính: Thành phần truy cập nguồn dữ liệu và thành phần lưu trữ xử lý dữ liệu. Thành phần thứ nhất:.NET Framework Data Provider được thiết kế để thực hiện các thao tác kết nối, gửi các lệnh xử lý đến CSDL (thành phần này còn được gọi với một tên khác là lớp kết nối – Connectectivity Layer). Trong ADO.NET, có 4 đối tượng chính với các chức năng cơ bản như sau: Giáo trình Visual Studio .NET 84
  34. Connection: giúp thực hiện kết nối đến các CSDL Command: giúp truy cập đến CSDL và thực hiện các phát biểu SQL hay thủ tục lưu trữ sẵn (stored procedure) của CSDL DataReader: dùng để đọc nhanh nguồn dữ liệu, chỉ được duyệt tuần tự theo chiều tiến của các record DataAdapter: dùng để chuyển dữ liệu truy vấn được cho các đối tượng lưu trữ và xử lý (DataSet, DataTable). DataAdapter chủ yếu thực hiện các thao tác như SELECT, INSERT, UPDATE, DELETE Về mặt thực chất, thành phần .NET Framework Data Provider cung cấp giao diện lập trình chung để làm việc với các nguồn dữ liệu. Mỗi nhà cung cấp 3 đặc thù sẽ đưa ra một loại data provider riêng. Dưới đây là bảng mô tả giao diện lập trình cùng với các lớp cơ bản của các data provider mà ADO.NET cung cấp sẵn: Interface SQL Server Oracle Provider OLEDB Provider ODBC Provider Provider IDbConnection SqlConnection OracleConnection OledbConnection OdbcConnection IDbDataAdapter SqlDataAdapter OracleDataAdapter OledbDataAdapter OdbcDataAdapter IDbCommand SqlCommand OracleCommand OledbCommand OdbcCommand IDbDataReader SqlDataReader OracleDataReader OledbDataReader OdbcDataReader Để sử dụng data provider nào, chúng ta phải tiến hành khái báo using namspace tương ứng. Chẳng hạn, using System.Data.SqlClient; Ngoài những data provider mô tả ở bảng trên, chúng ta có thể reference đến các data provider khác không được tích hợp sẵn bởi ADO.NET trong Visual Studio .NET, chẳng hạn như data provider dùng cho MySQL, Postgre, Thành phần thứ hai trong kiến trúc ADO.NET – DataSet – được xem như là container dùng để lưu trữ đối tượng liên quan đến dữ liệu như DataTable, DataRelation, DataView. Thành phần này còn được gọi là lớp không kết nối (disconected layer). 3 Nhà cung cấp ở đây được hiểu theo nghĩa cả về loại nguồn dữ liệu lẫn cách thức truy xuất nguồn dữ liệu. Ví dụ, ngoài data provider SqlClient do Microsoft cung cấp, cũng có thể có một tổ chức khác phát triển một provider khác để truy xuất loại nguồn dữ liệu này. Giáo trình Visual Studio .NET 85
  35. DataSet như là một CSDL thu nhỏ tại máy client, có thể chứa các đối tượng table, view, constaint, ralationship giữa các table, Tất cả dữ liệu từ nguồn dữ liệu thực sẽ được nạp vào DataSet dưới dạng các DataTable, đó là một snapshot của nguồn dữ liệu thực. Khối dữ liệu này sẽ được chỉnh sửa độc lập, sau đó nếu cần sẽ được cập nhật trở lại nguồn dữ liệu thực. Theo nguyên tắc này, chúng ta không cần duy trì kết nối liên tục một cách không cần thiết với nguồn dữ liệu thực trong suốt quá trình thao tác với nó. 4.2 Tổng quan về các mô hình xử lý dữ liệu trong ADO.NET 4.2.1 Mô hình Kết nối Trong mô hình kết nối của ADO.NET, có một connection hoạt động được duy trì giữa đối tượng DataReader của ứng dụng và một data source (nguồn dữ liệu). Một dòng dữ liệu (data row) được trả về từ data source mỗi khi phương thức Read của đối tượng DataReader được thực thi. Điểm quan trọng nhất của mô hình kết nối đó là dữ liệu được lấy từ tập dữ liệu (các record được trả về bởi một lệnh SQL nào đó) theo kiểu từng record một cho một lần đọc, chỉ đọc (read-only), và chỉ theo một hướng tiến (forward-only). Hình dưới đây mô tả cách sử dụng DataReader trong chế độ kết nối. Các bước điển hình để làm việc với đối tượng DataReader là như sau: 1. Tạo đối tượng Connection bằng cách truyền một chuỗi Connection string cho hàm khởi dựng của nó. 2. Khởi tạo một biến chuỗi và gán cho câu lệnh SQL dựa theo dữ liệu muốn nạp về. 3. Khởi tạo một đối tượng Command từ với nội dung câu lệnh SQL đã xác định ở trên. 4. Tạo đối tượng DataReader bằng cách thực thi phương thức Command.ExecuteReader(). Đối tượng này sau đó sẽ được dùng để đọc kết quả của câu truy vấn mỗi dòng một lần. Đoạn code sau minh họa các bước trên với Data Provider SqlClient. Đoạn code sẽ đọc Giáo trình Visual Studio .NET 86
  36. danh sách họ tên các sinh viên trong một bảng SinhVien của cơ sở dữ liệu và hiển thị lên một điều khiển ListBox. Chi tiết về các đối tượng DataReader, Command, Connection sẽ được đề cập chi tiết sau. using System.Data.SqlClient; // (1) Tao Connection SqlConnection cn = new SqlConnection(chuoiKetNoi); cn.Open(); // (2) Chuoi SQL thuc hien lay danh sach ten cac sinh vien xep tang dan theo NgaySinh string sql = "SELECT HoTen FROM SinhVien ORDER BY NgaySinh"; // (3) Tao doi tuong Command SqlCommand cmd = new SqlCommand(sql, conn); DbDataReader rdr; // (4) Tao doi tuong DataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection); while (rdr.Read()) listBox1.Items.Add(rdr["HoTen"]); // Fill ListBox rdr.Close(); // Dong datareader sau khi da su dung xong Tham số được sử dụng trong phương thức ExecuteReader xác định đối tượng Connection sẽ được đóng sau khi DataReader được đóng. 4.2.2 Mô hình Ngắt Kết nối Triết lý của mô hình Ngắt kết nối đó là: Dữ liệu được nạp – sử dụng một lệnh SQL – từ nguồn dữ liệu bên ngoài vào bộ nhớ đệm tại máy client; tập kết quả được xử lý tại máy cục bộ; mọi cập nhật sau đó sẽ được truyền từ dữ liệu trong bộ nhớ ngược trở lại nguồn dữ liệu. Mô hình được gọi là “ngắt kết nối” bởi vì đối tượng kết nối chỉ được mở đủ lâu để đọc dữ liệu từ nguồn dữ liệu và tiến hành các thao tác cập nhật. Bằng cách đưa dữ liệu về phía máy client, tài nguyên của server – chẳng hạn như thông tin dữ liệu Connection, bộ nhớ, thời gian xử lý – sẽ được giải phóng bớt. Tuy vậy, mô hình này cũng có nhược điểm về thời gian cần để nạp tập dữ liệu và bộ nhớ dùng để chứa dữ liệu tại máy client. Như hình dưới đây minh họa, các thành phần chính của mô hình ngắt kết nối đó là DataApdapter và DataSet. DataAdapter làm nhiệm vụ như là cầu nối giữa nguồn dữ liệu và DataSet, nạp dữ liệu vào các bảng của DataSet và đẩy các thay đối ngược trở lại nguồn Giáo trình Visual Studio .NET 87
  37. dữ liệu. Một DataSet đóng vai trò như là một cơ sở dữ liệu quan hệ nằm trong bộ nhớ, chứa một hay nhiều DataTables, giữa các DataTable này cũng có thể có các mối quan hệ với nhau như trong một cơ sở dữ liệu quan hệ thực. Một DataTable chứa các dòng và các cột dữ liệu thường được lấy từ cơ sở dữ liệu nguồn. Trong số các phương thức và thuộc tính của DataAdapter thì Fill() và Update() là hai phương thức quan trọng nhất. Fill() chuyển một query đến cơ sở dữ liệu và lưu tập kết quả trả về trong một DataTable nào đó; phương thức Update() thực hiện một thao tác thêm, xóa, cập nhật dựa trên những thay đối của đối tượng DataSet. Các lệnh cập nhật thực sự được chứa trong các thuộc tính của DataAdapter. Chi tiết về DataAdapter sẽ được đề cập ở phần sau. Để minh họa cách thức làm việc với DataAdapter và DataSet, đoạn code dưới đây giới thiệu cách tạo ra một đối tượng DataTable, nạp dữ liệu từ một cơ sở dữ liệu, và đưa nó vào một DataSet. string sql = "SELECT MaSinhVien, HoTen, NgaySinh FROM SinhVien"; string connStr = "Data Source=MYSERVER;Initial Catalog=qlsinhvien; User Id=k28;Password=k28;"; // (1) Tao doi tuong data adapter SqlDataAdapter da = new SqlDataAdapter(sql, connStr); // (2) Tao doi tuong dataset DataSet ds = new DataSet(); // (3) Tao mot Table co ten “SinhVien” trong dataset va nap du lieu cho no da.Fill(ds, "SinhVien"); // (4) Hien thi danh sach ten sinh vien ra list box DataTable dt = ds.Tables["SinhVien"]; for (int i=0; i< dt.Rows.Count;i++) { DataRow row = dt.Rows[i]; listBox1.Items.Add(row["HoTen"]); } Giáo trình Visual Studio .NET 88
  38. Bước đầu tiên là tạo ra một thể hiện của SqlDataAdapter bằng cách truyền một câu lệnh SELECT và chuỗi kết nối cho phương thức khởi dựng của lớp này. DataAdapter sẽ lo đến việc tạo ra đối tượng Connection cũng như việc mở, đóng Connection khi cần thiết. Sau khi một DataSet rỗng sẽ được tạo ra, phương thức Fill() của DataAdapter sẽ tạo ra một DataTable có tên là “SinhVien” trong DataSet và nạp các dòng dữ liệu vào DataTable này (bằng câu lện SQL dạng SELECT của DataAdapter). Mỗi column của DataTable sẽ tương ứng với một column trong bảng của cơ sở dữ liệu nguồn. Dữ liệu trong bảng dữ liệu sau đó được đưa vào một ListBox bằng cách duyệt qua danh sách các dòng của DataTable. 4.3 Làm việc với mô hình Kết nối trong ADO.NET Như đã mô tả tổng quan trong phần trước, mô hình Kết nối được dựa trên việc thiết lập một Connection đến CSDL và sau đó sử dụng các Command để thực hiện việc thêm, xóa, sửa, hay đọc dữ liệu từ data source (nguồn dữ liệu) được kết nối. Đặc điểm phân biệt của mô hình này đó là các Command được phát sinh, làm việc với data source thông qua một Connection đang hoạt động – Connection này sẽ mở cho đến khi các thao tác được hoàn tất. Cho dù là làm việc với mô hình Kết nối hay Ngắt kết nối, bước đầu tiên trong quá trình truy xuất một data source đó là tạo ra một đối tượng Connection để làm đường truyền giữa ứng dụng với data source. 4.3.1 Lớp Connection Có nhiều lớp Connection trong ADO.NET – mỗi lớp tương ứng với một Data Provider – bao gồm SqlConnection, OracleConnection, OleDbConnection, OdbcConnection. Mặc dù mỗi lớp có thể gồm những đặc tính riêng, nhưng các lớp này đều phải implement interface IDbConnection. Bảng dưới đây tóm tắt các thành phần được định nghĩa bởi interface này. Loại Tên Mô tả Property ConnectionString Get/Sets chuỗi kết nối đến data source. Property ConnectionTimeout Khoảng thời gian tối đa tính bằng giây để chờ thực hiện việc kết nối đến data source Property Database Tên CSDL ứng với Connection hiện tại Property State Trạng thái hiện tại của Connection. Trả về một giá trị kiểu liệt kê (enumeration): Broken, Closed, Connecting, Executing, Fetching, hoặc Open Giáo trình Visual Studio .NET 89
  39. Loại Tên Mô tả Method Open Mở một Connection. Roll back mọi thao tác đang làm Close dở. Đóng Connection – trả Connection cho Connection Pool nếu như có sử dụng Connection Pool Method BeginTransaction Khởi tạo một database transaction Method ChangeDatabase Thay đối CSDL hiện tại cho Connection đang mở. Chuỗi mô tả tên CSDL mới được truyền cho phương thức này Method CreateCommand Tạo ra một đối tượng Command ứng với Connection 4.3.1.1 Connection string Thuộc tính ConnectionString xác định data source và các thông tin cần thiết để truy xuất data source, chẳng hạn như User ID và Password, Ngoài những thông tin cơ bản này, Connection string còn có thể chứa các giá trị cho các trường dữ liệu đặc trưng cho data provider. Ví dụ, Connection string cho Ms Sql Server có thể chứa các giá trị để quy định Connection Timeout và Packet Size. Dưới đây là các ví dụ về cách thành lập chuỗi kết nối cho các data provider thường gặp. Danh sách đầy đủ về cách thành lập các chuỗi kết nối được cho ở Error! Reference source not found SqlConnection sử dụng cơ chế xác thực kiểu SQL Server: “server=(1);database=(2);uid=(3);pwd=(4)” hoặc “Data Source=(1);Initial Catalog=(2);User ID=(3);Password=(4)” SqlConnection sử dụng cơ chế xác thực kiểu Windows: “Server=(1);Database=(2);Trusted_Connection=yes” Ở đây, (1) là tên/máy chủ chứa CSDL, (2) là tên CSDL, (3) là tên đăng nhập, (4) là mật khẩu tương ứng. Ví dụ: “server=192.168.0.1;database=qlnhanvien;uid=k28;pwd=spider ” Giáo trình Visual Studio .NET 90
  40. hoặc “Server=192.168.0.1;Database=qlnhanvien;Trusted_Connection =yes” OleDbConnection sử dụng để kết nối CSDL Access phiên bản trước 2003: “Provider=Microsoft.Jet.OLEDB.4.0;DataSource=” Ví dụ: o string stConnection = string.Format(“Provider=Microsoft.Jet.OLEDB.4.0;DataSou rce={0}”, @”c:\program files\qlnhanvien.mdb”); o Sử dụng trong ứng dụng Internet: string stConnection = string.Format(“Provider=Microsoft.Jet.OLEDB.4.0;DataSou rce={0}”, Server.MapPath(“/data/qlnhanvien.mdb”); ODBC: “DSN=” với  là Data Source Name (DSN), ví dụ “DSN=qlnhanvien” Các Connection string được dùng để tạo ra đối tượng Connection. Cách thực hiện thông thường là truyền chuỗi này cho hàm khởi dựng như ví dụ dưới đây: string stConnection = "Data Source=192.168.0.1;” + “Initial Catalog=films;” + “User Id=k28;”+ “Password=spider"; SqlConnection cn = new SqlConnection(stConnection); cn.Open(); //Open connection 4.3.1.2 Connection Pooling Tạo một Connection là một quá trình tốn nhiều thời gian – trong một số trường hợp, việc này thậm chí còn tốn thời gian hơn việc thực thi các Command. Để loại bỏ điều này, ADO.NET cung cấp một khái niệm gọi là connection pool. Connection pool quản lý các Connection có trùng Connection string để tối ưu hóa số lần thiết lập, hợp lệ hóa thông tin kết nối. Giáo trình Visual Studio .NET 91
  41. Các quy tắc quy định connection pool cần biết: - Cơ chế Connection pooling được kích hoạt theo mặc định. Cơ chế này được tắt bằng cách thêm vào Connection string “Pooling=false” đối với SqlConnection hoặc “OLE DB Services=-4” đối với OleDbConnection. - Mỗi connection pool được ứng với một connection string duy nhất. Khi có một Connection được yêu cầu, pool handler (bộ quản lý pool) sẽ so sánh connection string với những connection string trong các pools đang tồn tại. Nếu có một Connection trùng khớp thì Connection tương ứng sẽ được xác định trong pool. - Nếu tất cả các connection trong pool đang được sử dụng khi lại có yêu cầu về connection thì yêu cầu đó sẽ được xếp vào hàng đợi cho đến khi có một connection rảnh. Các connection sẽ được giải phóng khi phương thức Close hay Dispose của đối tượng connection được gọi. - Connection pool được đóng khi tất cả các connection trong nó được giải phóng và hết thời gian (time out). Đối với SQL Server, bạn có thể điều khiển hành vi của conneciton pooling bằng cách gộp các cặp key-value vào connection string. Các key này có thể được sử dụng để thiết lập số lượng nhỏ nhất và lớn nhất các connection trong pool cũng như xác định xem một connection có cần phải reset khi nó được lấy từ pool ra hay không. Một key đặc biệt chú ý đó là key có tên Lifetime, xác định thời gian mà connection có thể tồn tại trước khi nó bị hủy bỏ. Giá trị này được kiểm tra khi một connection được trả về cho pool. Nếu connection đã được mở trước đó, và lâu hơn giá trị Lifetime thì nó sẽ bị hủy. Đoạn code dưới đây minh họa các dùng các key này cho SqlClient: string stConnection = "Server=192.168.0.1;” + “Trusted_Connection=yes;” + “database=qlnhanvien;" + "connection reset=false;" + "connection Lifetime=60;" + // Seconds "min pool size=1;" + "max pool size=50"; // Default=100 SqlConnection cn = new SqlConnection(cnString); Giáo trình Visual Studio .NET 92
  42. 4.3.2 Đối tượng Command Sau khi một đối tượng connection được tạo ra, bước tiếp theo trong quá trình truy xuất CSDL – đối với mô hình Kết nối – đó là tạo ra một đối tượng Command để gửi một query (select) hay một action command (thêm, xóa, sửa) đến data source. Có nhiều loại lớp Command ứng với các data provider; các lớp này đều implement interface IDbCommand. 4.3.2.1 Tạo đối tượng Command Bạn có thể dùng một trong nhiều hàm khởi dựng để tạo đối tượng Command một cách trực tiếp hoặc sử dụng cách tiếp cận ProviderFactory. Đoạn code dưới đây minh họa các tạo ra một đối tượng Command và thiết lập các thuộc tính của nó. SqlConnection conn = new SqlConnection(connstr); conn.open(); string sql = "INSERT INTO SinhVien (MaSinhVien, HoTen) VALUES (@pMaSinhVien, @pHoTen)"; SqlCommand cmd = new SqlCommand(); cmd.Connection = conn; cmd.commandText = sql; cmd.Parameters.AddWithValue ("@pMaSinhVien", 12); cmd.Parameters.AddWithValue ("@pHoTen", "tnv spider"); Trong trường hợp ứng dụng có thể phải sử dụng nhiều data provider, bạn nên sử dụng cách tiếp cận provider factory. Factory được tạo ra bằng cách truyền chuỗi data provider cho hàm khởi dựng của nó. Tiếp đến, phương thức CreateCommand được gọi để trả về một đối tượng command. string provider = "System.Data.SqlClient"; DBProviderFactory factory = DbProviderFactories.GetFactory(provider); DbCommand cmd = factory.CreateCommand(); // DbCommand là một lớp trừu tượng cmd.CommandText = sql; // sql là một chuỗi query hay command cmd.Connection = conn; // conn là một Connection 4.3.2.2 Thực thi một Command Lệnh SQL được gán trong thuộc tính CommandText của đối tượng Command sẽ được thực thi bằng một trong các phương thức được chỉ ra ở bảng dưới đây Phương thức Mô tả Giáo trình Visual Studio .NET 93
  43. ExecuteNonQuery Thực thi truy vấn hành động (action query) và trả về số lượng dòng dữ liệu bị ảnh hưởng bởi truy vấn đó: cmd.CommandText = "DELETE SinhVien WHERE MaSinhVien=12"; int soLuong = cmd.ExecuteNonQuery(); ExecuteReader Thực thi một query và trả về đối tượng DataReader để có thể truy cập tập kết quả của query đó. Phương thức này nhận một tham số tùy chọn kiểu CommandBehavior để có thể tăng hiệu năng thực thi query. cmd.CommandText = "SELECT * FROM SinhVien” + “WHERE YEAR(NgaySinh) > 1981”; SqlDataReader rdr= cmd.ExecuteReader(); ExecuteScalar Thực thi một query và trả về giá trị của cột đầu tiên trong dòng đầu tiên của tập kết quả. cmd.CommandText="SELECT COUNT(MaSinhVien) FROM SinhVien"; int soSinhVien = (int)cmd.ExecuteScalar(); ExecuteXmlReader Chỉ có cho data provider SQL Server. Trả về một đối tượng XmlReader dùng để truy xuất tập dữ liệu. Tham khảo thông tin về XmlReader trong MSDN ExecuteReader là phương thức quan trọng nhất trong các phương thức kể trên. Phương thức này trả về một đối tượng DataReader giúp truy xuất đến các dòng dữ liệu trả về bởi query. Xem ví dụ dưới đây: dr = cmd.ExecuteReader(sql, ); Ở đây,  là một giá trị kiểu CommandBehavior để chỉ định behavior (hành vi) thực thi của query. Một số data providers sử dụng  để tối ưu quá trình thực thi query. Danh sách Giáo trình Visual Studio .NET 94
  44. các giá trị và tác dụng của tham số  được mô tả chi tiết như dưới đây: SingleRow. Chỉ định rằng query chỉ trả về 1 dòng dữ liệu. Behavior mặc định là trả về nhiều tập kết quả. SingleResult. Query trả về một giá trị tuyến tính đơn nhất (single scalar value). KeyInfo. Trả về thông tin về column và primary key. Behavior này được sử dụng với phương thức GetSchema của DataReader để lấy thông tin về các column trong lược đồ (schema). SchemaOnly. Dùng để lấy về tên của các cột trong tập dữ liệu trả về: Ví dụ dr = cmd.ExecuteReader(CommandBehavior.SchemaOnly); string col1 = dr.GetName(0); // tên cột đầu tiên SequentialAccess. Cho phép dữ liệu trong dòng trả về có thể được truy xuất tuần tự theo column. Behavior này được dùng cho các trường dữ liệu BLOB hay TEXT. CloseConnection. Đóng connection khi DataReader được đóng. 3.3.2.3 Thực thi Stored Procedure (thủ tục lưu trữ sẵn) với đối tượng Command Một stored procedure là một đoạn code SQL được lưu sẵn trong CSDL và có thể được thực thi như là một script. ADO.NET hỗ trợ việc thực thi các stored procedure cho các data provider OleDb , SqlClient, ODBC, và OracleClient. Các bước để thực thi một stored procedure: - Thiết lập thuộc tính SqlCommand.CommandText thành tên của procedure; - Thiết lập thuộc tính CommandType là CommandType.StoredProcedure; - Thiết lập các Parameter (nếu có) cho procedure - Thực thi phương thức ExecuteNonQuery. Thủ tục dưới đây cho phép các mẫu tin lấy về từ bảng SinhVien được phân thành từng nhóm các trang, mỗi trang 10 record. Đầu vào của của procedure là @pTrang (số hiệu trang cần lấy); đầu ra của procedure là số trang tổng cộng của tập dữ liệu. Đoạn code minh họa phía dưới thực hiện việc thiết lập để lấy về trang dữ liệu đầu tiên. SqlCommand cmd = new SqlCommand(); cmd.CommandText = "spListSinhVien"; // tên procedure Giáo trình Visual Studio .NET 95
  45. cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add(“@pTrang", SqlDbType.Int); cmd.Parameters.Add(“@pTongSoTrang", SqlDbType.Int); cmd.Parameters[0].Direction= ParameterDirection.Input; cmd.Parameters[0].Value= 1; // thiết lập để lấy về trang đầu tiên cmd.Parameters[1].Direction=ParameterDirection.Output; cmd.CommandTimeout=10; // Cho command tối đa 10s để thực thi SqlDataReader dr = cmd.ExecuteReader(); while (dr.Read()) { // xử lý tập dữ liệu ở đây } dr.Close(); // DataReader phải được đóng trước khi đọc tham số đầu ra int tongSoTrang = cmd.Parameters[1].Value; Ví dụ này sử dụng data provider SqlClient. Có thể chỉnh sửa một phần nhỏ thì nó cũng có thể hoạt động với OleDb. Điểm khác biệt mấu chốt giữa SqlClient và OleDb đó là cách quản lý các parameter. SqlClient yêu cầu tên parameter phải đúng với tên parameter của stored procedure; trong khi đó OleDb lại truyền các parameter cho stored procedure dựa vào vị trí, vì vậy tên parameter là không quan trọng. Nếu procedure trả về giá trị kết quả, OleDb phải thiết kế để parameter đầu tiên trong danh sách làm nhiệm vụ này. Với SqlClient, chúng ta chỉ cần thêm một parameter với một tên nào đó và xác định hướng trả về (direction) của parameter này là Return Value. Phần code của stored procedure là như sau: CREATE PROCEDURE spListSinhVien @pTrang int, @pTongSoTrang int output AS /* Thủ tục trả về một tập kết quả gồm các SinhVien xếp theo HoTen. Tập kết quả được phân thành các trang, mỗi trang 10 SinhVien. */ SET NOCOUNT ON SELECT @pSoTrang = CEILING(COUNT(*)/10) FROM SinhVien if @pTrang = 1 or @pTrang <1 begin SELECT TOP MaSinhVien, HoTen FROM SinhVien ORDER BY HoTen set @pTrang = 1 Giáo trình Visual Studio .NET 96
  46. return 0 end if @pTrang > @pTongSoTrang begin SET @pTrang = @pTongSoTrang declare @RowCount int set @RowCount = (@pTrang * 10) exec ( 'SELECT * FROM ( SELECT TOP 10 a.* FROM ( SELECT TOP ' + @RowCount + ' * FROM SinhVien ORDER BY HoTen ) a ORDER BY HoTen desc ) b ORDER BY HoTen' ) return 0 end 4.3.2.4 Sử dụng Parameter trong các Command không phải là Stored Procedures Trong các query được thành lập trực tiếp (chứ không phải là stored procedure như ở trên), chúng ta cũng có thể sử dụng các Parameter. Ví dụ dưới đây minh họa cách thức bổ sung một record vào bảng SinhVien: string sql = "INSERT INTO SinhVien (MaSinhVien, HoTen) VALUES (@pMaSinhVien, @pHoTen)"; SqlCommand cmd = new SqlCommand(); cmd.Connection = conn; cmd.commandText = sql; Giáo trình Visual Studio .NET 97
  47. cmd.Parameters.AddWithValue("@pMaSinhVien", 12); cmd.Parameters.AddWithValue("@pHoTen", "tnv spider"); Một cách khác để thực hiện việc bổ sung record như trên là sử dụng phép nối chuỗi4 như thế này: int iMaSinhVien = 12; string stHoTen = "tnv spider"; sql = string.Format(“INSERT INTO SinhVien (MaSinhVien, HoTen) VALUES ({0}, ‘{1}’)”, iMaSinhVien, stHoTen); SqlCommand cmd = new SqlCommand(sql, conn); 4.3.3 Đối tượng DataReader Như đã thấy trong các ví dụ trước, một DataReader cho phép lấy các dòng và cột dữ liệu của dữ liệu trả về khi thực thi một query. Việc truy xuất dòng được định nghĩa bởi interface IDataRecord. Dưới đây là các member quan trọng của interface này. 4.3.3.1 Truy xuất các dòng dữ liệu với DataReader DataReader lấy về từng dòng đơn (single row) từ một tập dữ liệu trả về mỗi khi phương thức Read của nó được thực thi. Nếu không có dòng dữ liệu nào thì phương thức này trả về giá trị false. DataReader phải được đóng sau khi các thao tác xử lý các dòng được hoàn tất để giải phóng tài nguyên hệ thống. Bạn có thể sử dụng thuộc tính DataReader.IsClosed để biết được DataReader đã được đóng hay chưa. Mặc dù DataReader là ứng với một Command đơn, nhưng Command này lại có thể chứa nhiều query trong đó, do đó có thể trả về nhiều tập dữ liệu kết quả. Đoạn code dưới đây minh họa cách xử lý các dòng dữ liệu trả về bởi 2 query trong một Command. string q1 = "SELECT * FROM SinhVien WHERE YEAR(NgaySinh) 1990"; 4 Trong thực tế, giải pháp nối chuỗi ít khi được sử dụng vì lý do an toàn dữ liệu. Hãy hình dung trong đoạn code này, nếu stHoTen được gán giá trị là “tnv spider’); DELETE * FROM SinhVien; ”. Khi đó query được thực thi sẽ là “INSERT INTO SinhVien (MaSinhVien, HoTen) VALUES (12, ‘tnv spider’); DELETE * FROM SinhVien; )” Lỗ hổng kiểu này thường được gọi với tên SQL Injection. Giáo trình Visual Studio .NET 98
  48. cmd.CommandText = q1 + ";" + q2; // hai query được ngăn cách nhau bởi dấu ; DbDataReader rdr = cmd.ExecuteReader(); bool readNext = true; while (readNext) { while (rdr.Read()) MessageBox.Show(rdr.GetString(1)); readNext = rdr.NextResult(); // kiem tra xem con tap du lieu nao nua khong } rdr.Close(); conn.Close(); DataReader không có thuộc tính hay phương thức nào cho biết số lượng dòng dữ liệu trả về trong tập dữ liệu của nó (do đặc tính forward-only của DataReader), tuy nhiên, chúng ta có thể sử dụng thuộc tính HasRows (kiểu Boolean) của DataReader để xác định xem nó có 1 hay nhiều dòng để đọc hay không. 4.3.3.2 Truy xuất giá trị của column Có nhiều cách để truy xuất dữ liệu chứa trong các columns của dòng hiện tại của DataReader: - Truy xuất như là một array dùng số thứ tự column (bắt đầu từ 0) hoặc dùng tên column - Sử dụng phương thức GetValue bằng cách truyền cho phương thức này số thứ tự của column - Sử dụng một trong các phương thức định kiểu GetXXX, bao gồm GetString, GetInt32, GetDateTime, GetDouble, Đoạn code dưới đây minh họa các thức truy xuất giá trị dữ liệu của các column. cmd.CommandText = "SELECT MaSinhVien, Hoten, GioiTinh, NgaySinh FROM SinhVien ” + “WHERE YEAR(NgaySinh) = 1981"; dr = cmd.ExecuteReader(); dr.Read(); // Các cách để lấy dữ liệu kiểu string ở cột thứ 2 (HoTen) string stHoTen; stHoTen = dr.GetString(1); stHoTen = (string)dr.GetSqlString(1); // SqlClient provider stHoTen = (string)dr.GetValue(1); Giáo trình Visual Studio .NET 99
  49. stHoTen = (string)dr["HoTen"]; stHoTen = (string)dr[1]; // Lấy dữ liệu kiểu DateTime ở cột thứ 4 (NgaySinh) có kiểm tra giá trị NULL if (!dr.IsDbNull(3)) DateTime dtNgaySinh = dr.GetDateTime(3); Phương thức GetString có điểm thuận lợi trong việc ánh xạ nội dung dữ liệu từ CSDL sang kiểu dữ liệu của .NET. Các cách tiếp cận khác đều trả về các kiểu đối tượng có yêu cầu phép chuyển kiểu. Vì lý do này, bạn nên sử dụng các phương thức GetXXX cho các kiểu dữ liệu xác định. Cũng lưu ý rằng, phương thức GetString không yêu cầu phép chuyển kiểu, nhưng bản thân nó không thực hiện bất cứ phép chuyển đổi nào; chính vì thế, nếu dữ liệu là không đúng như kiểu dữ liệu trông đợi sẽ có Exception được trả ra. Nhiều ứng dụng phụ thuộc vào tầng xử lý dữ liệu để cung cấp DataReader. Với những trường hợp như thế, ứng dụng có thể sử dụng metadata (siêu dữ liệu) để xác định tên column, kiểu dữ liệu của column, và các thông tin khác về column. Đoạn code sau đây minh họa việc in ra danh sách các tên và kiểu dữ liệu của các column mà đối tượng DataReader đang quản lý: // In ra danh sách các tên column của một đối tượng DataReader có tên dr for (int i = 0; i < dr.FieldCount; i++) Console.WriteLine(“Column {0} co kieu du lieu {1}”, dr.GetName(i), dr.GetDataTypeName(i)); // Column name Có một cách khác toàn diện hơn để quản lý toàn bộ thông tin về lược đồ (schema) của tập dữ liệu kết quả trả về, đó là sử dụng phương thức GetSchemaTable. Phương thức này trả về một đối tượng DataTable mà mỗi dòng trong DataTable này sẽ biểu diễn một column trong tập dữ liệu kết quả. Đoạn code dưới đây minh họa cách truy xuất tất cả các thông tin về các column của một tập dữ liệu trả về. DataTable schemaTable = dr.GetSchemaTable(); int stt = 0; foreach (DataRow r in schemaTable.Rows) { foreach (DataColumn c in schemaTable.Columns) { Console.WriteLine(stt.ToString() + " " + c.ColumnName + ": " + r[c]); stt++; } Giáo trình Visual Studio .NET 100
  50. } Kết quả hiển thị: 0 ColumnName: movie_ID 1 ColumnOrdinal: 0 //không liệt kê 12 DataType: System.Int32 //không liệt kê 4.3.4 Bài thực hành Giả sử ta đã có cơ sở dữ liệu quanlythuvien trong SQL Server có quan hệ như sau: Bài thực hành về đối tượng Connection, Command và DataReader Thiết kế Form để tạo mới 1 tài khoản như sau (làm việc trên bảng nhanvien): Giáo trình Visual Studio .NET 101
  51. ListView1 Frmtaomoitk sử dụng các trường, phương thức và sự kiện sau: Giáo trình Visual Studio .NET 102
  52. Các điều khiển Tên điều khiển Thuộc tính Form Name: Frmtaomoitk Text:Tạo mới một tài khoản sử dụng chương trình listView Name:listView1 Columns: Add thêm 4 cột: Họ tên, Địa chỉ, Tên đăng nhập và Quyền hạn View: Details GridLines:True groupBox Name:groupBox1 Text: Thông tin cơ bản Label Tạo ra 5 label để hiển thị: Mã nhân viên, Họ tên, Địa chỉ, Tên đăng nhập và quyền hạn. TextBox Tạo ra 4 TextBox lần lượt với các tên: txtmanv, txthoten, txtdiachi, txttendangnhap Button Tạo 8 button lần lượt với các tên butdau, butlui, buttien, butcuoi, buttaomoi, buttimkiem, butxoabo,butthoat Các trường: Tên trường Ý nghĩa Cn Dùng để kết nối đến cơ sở dữ liệu quanlythuvien cmdSelect sqlCommand sử dụng câu lệnh select để hiển thị và tìm kiếm cmdInsert sqlCommand sử dụng câu lệnh Insert để tạo thêm 1 tài khoản cmdXoa sqlCommand sử dụng câu lệnh Delete để xóa 1 tài khoản I Tài khoản thứ i Các phương thức + Hàm dựng Frmtaomoitk để tạo giao diện public Frmtaomoitk() { InitializeComponent(); } + Phương thức Moketnoi(): Kiểm tra đường kết nối, nếu đang mở thì đóng lại, sau đó mở lại đường kết nối private void Moketnoi() { if (cn.State == ConnectionState.Open) Giáo trình Visual Studio .NET 103
  53. cn.Close(); cn.Open(); } + Phương thức LoadListView: Lấy dữ liệu của bảng nhanvien nạp dữ liệu lên listView1. Phương thức này được gọi khi thay đổi dữ liệu trong bảng nhận viên như nhâp thêm hoặc xóa đi 1 nhân viên. Sử dụng 2 đối tượng SqlCommand, SqlDataReader private void LoadListView() { Moketnoi(); cmdSelect = new SqlCommand("select * from nhanvien", cn); SqlDataReader r = cmdSelect.ExecuteReader(); listView1.Items.Clear(); // Xóa tất cả dữ liệu trong listView1 while (r.Read()) { string[] st = new string[5]; st[0] = r[0].ToString(); st[1] = r[1].ToString(); st[2] = r[2].ToString();// Không hiển thị mật khẩu, nên không có r[3] st[3] = r[4].ToString(); st[4] = r[5].ToString(); ListViewItem lv = new ListViewItem(st); listView1.Items.Add(lv); } cmdSelect.Dispose(); } + Phương thức LoadItem: Lấy dữ liệu từ dòng thứ i của listView1 đưa vào txtmanv, txthoten, txtdiachi, txttendangnhap và comboBox1. Phương thức này được gọi khi di chuyển qua lại thông tin của các nhân viên. private void LoadItem(int i) { txtmanv.Text = listView1.Items[i].Text; txthoten.Text = listView1.Items[i].SubItems[1].Text; txtdiachi.Text = listView1.Items[i].SubItems[2].Text; txttendangnhap.Text = listView1.Items[i].SubItems[3].Text; comboBox1.Text = listView1.Items[i].SubItems[4].Text; } + Phương thức LoadCombox: Đưa dữ liệu vào cho comboBox1. Giả sử chỉ có 3 quyền hạn: admin, sinhvien và Thuthu. Phương thức này được gọi khi vừa nạp Form lên Giáo trình Visual Studio .NET 104
  54. private void LoadCombox() { comboBox1.Items.Add("Admin"); comboBox1.Items.Add("Sinhvien"); comboBox1.Items.Add("ThuThu"); comboBox1.Text = "Admin"; } + Phương thức XoaTextBox: Xóa hết dữ liệu trong các textBox, phương thức này được goi khi nhập thêm 1 tài khoản. private void XoaTextBox() { txtmanv.Clear(); txthoten.Clear(); txtdiachi.Clear(); txttendangnhap.Clear(); txtmanv.Focus(); } + Phương thức KiemTraMa: Kiểm tra xem có mã nhân viên nào bằng với ma hay không. Phương thức này được gọi khi nhập thêm 1 tài khoản private int KiemTraMa(string ma) { Moketnoi(); cmdSelect = new SqlCommand("select count(*) from nhanvien where manhanvien='"+ma.Trim()+"'"); cmdSelect.Connection = cn; return (int)cmdSelect.ExecuteScalar(); } + Sự kiện Frmtaomoitk_Load: Tạo và mở ra đường kết nối đến cơ sở dữ liệu quanlythuvien, tên máy chủ nhha, sử dụng cơ chế xác thực kiểu Windows, nạo dữ liệu vào cho các điều khiển. private void Frmtaomoitk_Load(object sender, EventArgs e) { try { cn = new SqlConnection("Data Source=nhha;Initial Catalog=quanlythuvien; Trusted_Connection=yes"); cn.Open(); } Giáo trình Visual Studio .NET 105
  55. catch (Exception loi) { MessageBox.Show("Không thể kết nối được"); } LoadListView(); //Nạp dữ liệu vào cho listView1 i = 0; LoadItem(i);// Nạp dữ liệu vào cho các textBox và comboBox1 LoadCombox(); } + Sự kiện butdau_Click: Nạp dữ liệu của dòng đầu tiên từ listView1 vào cho các textBox và comboBox private void butdau_Click(object sender, EventArgs e) { i = 0; LoadItem(i); } + Sự kiện buttien_Click: Nạp dữ liệu của dòng tiếp theo từ listView1 vào cho các textBox và comboBox private void buttien_Click(object sender, EventArgs e) { i++; if (i == listView1.Items.Count) i = listView1.Items.Count - 1; LoadItem(i); } + Sự kiện butlui_Click: private void butlui_Click(object sender, EventArgs e) { i ; if (i < 0) i = 0; LoadItem(i); } + Sự kiện butcuoi_Click: private void butcuoi_Click(object sender, EventArgs e) { i = listView1.Items.Count - 1; LoadItem(i); } + Sự kiện butTaomoi_Click: Được sử dụng để thêm 1 tài khoản (1 nhân viên), nút butTaomoi có 2 trạng thái tạo mới và lưu. Nếu người sử dụng kích vào nút Tạo mới sẽ Giáo trình Visual Studio .NET 106
  56. chuyển sang trạng thái là lưu và ngược lại. private void butTaomoi_Click(object sender, EventArgs e) { if (butTaomoi.Text.Equals("Tạo mới")) { XoaTextBox(); butTaomoi.Text = "Luu"; } else // Kiểm tra xem mã nhân viên này có hay chưa ? if (KiemTraMa(txtmanv.Text)==1) { MessageBox.Show("Mã này đã có"); txtmanv.Clear(); txtmanv.Focus(); } else { string ma = txtmanv.Text; string hoten = txthoten.Text; string diachi = txtdiachi.Text; string tendangnhap = txttendangnhap.Text; string matkhau = "";// Khi tạo 1 tài khoản thì mật khẩu ban đầu là rỗng string quyenhan = comboBox1.Text; Moketnoi(); string sql="insert into nhanvien values("+"'"+ma+"','"+hoten+"','"+diachi +"','" +tendangnhap+"','"+matkhau+"','"+quyenhan +"')"; cmdInsert = new SqlCommand(sql,cn); cmdInsert.ExecuteNonQuery(); MessageBox.Show("Đã lưu thành công"); LoadListView(); //Nạp lại dữ liệu mới vào listView1 butTaomoi.Text = "Tạo mới"; cmdInsert.Dispose(); } } + Sự kiện buttimkiem_Click: Khi người sử dụng gõ 1 mã nhân viên vào txtmanv và kích vào nút buttimkiem, nếu tìm thấy mã nhân viên này sẽ hiển thị kết quả lên các textBox và comboBox private void buttimkiem_Click(object sender, EventArgs e) { Giáo trình Visual Studio .NET 107
  57. Moketnoi(); string sql = "select * from nhanvien where manhanvien='" + txtmanv.Text + "'"; cmdSelect = new SqlCommand(sql,cn); SqlDataReader dr = cmdSelect.ExecuteReader(); if (dr.Read())// Đã tìm thấy { txtmanv.Text = dr[0].ToString(); txthoten.Text = dr[1].ToString(); txtdiachi.Text = dr[2].ToString(); txttendangnhap.Text = dr[4].ToString(); comboBox1.Text = dr[5].ToString(); } else MessageBox.Show("Không tìm thấy"); } + Sự kiện butXoabo_Click: Xóa nhân viên có mã nhân viên ở txtmanv private void butXoabo_Click(object sender, EventArgs e) { DialogResult dr = MessageBox.Show("Chắc chắn xóa hay không ?", "Thông báo", MessageBoxButtons.YesNo); if (dr == DialogResult.Yes) // Nếu người sử dụng chọn nút yes { Moketnoi(); string Sql = "delete from nhanvien where manhanvien='" + txtmanv.Text + "'"; cmdXoa = new SqlCommand(Sql,cn); if (cmdXoa.ExecuteNonQuery() == 1) { MessageBox.Show("Xóa thành công"); LoadListView(); LoadItem(0); } else MessageBox.Show("Không tồn tại mã nhân viên " + txtmanv.Text); cmdXoa.Dispose(); } } 4.4 Làm việc với mô hình Ngắt kết nối: DataSet và DataTable Mô hình Ngắt Kết nối của ADO.NET dựa trên cơ sở sử dụng đối tượng DataSet như là một vùng nhớ đệm. Một đối tượng DataAdapter làm nhiệm vụ trung gian giữa DataSet và Giáo trình Visual Studio .NET 108
  58. data source (nguồn dữ liệu) để nạp dữ liệu vào vùng nhớ đệm. Sau khi DataAdapter hoàn thành nhiệm vụ nạp dữ liệu, nó sẽ trả đối tượng Connection về pool, vì thế nó ngắt kết nối khỏi nguồn dữ liệu. 4.4.1 Lớp DataSet DataSet đóng vai trò của một CSDL in-memory (CSDL nằm trong bộ nhớ). Thuộc tính Tables của DataSet là một tập hợp các DataTable chứa dữ liệu và lược đồ dữ liệu (data schema) mô tả dữ liệu trong DataTable. Thuộc tính Relations chứa tập hợp các đối tượng DataRelation xác định cách thức liên kết các đối tượng DataTable của DataSet. Lớp DataSet cũng hỗ trợ việc sao chép, trộn, và xóa DataSet thông qua các phương thức tương ứng là Copy, Merge, và Clear. DataSet và DataTable là phần lõi của ADO.NET và chúng không là đặc trưng của một data provider nào (giống như ở các lớp Connection, DataReader, DataAdapter). Một ứng dụng có thể định nghĩa và nạp dữ liệu từ nguồn bất kỳ (chứ không nhất thiết là từ một CSDL) vào DataSet. Bên cạnh các DataTable và các DataRelation, một DataSet còn có thể chứa các thông tin tùy biến khác được định nghĩa bởi ứng dụng. Hình dưới đây mô tả cả lớp chính trong DataSet. Trong số các thuộc tính này, chú ý thuộc tính PropertyCollection; đó là các thuộc tính được lưu trữ dưới dạng một hash table (bảng băm), thường chứa một giá trị time stamp hay các thông tin đặc tả như các yêu cầu hợp lệ hóa (validation requirements) cho column trong các DataTable trong DataSet. Giáo trình Visual Studio .NET 109
  59. 4.4.1.1 DataTable Thuộc tính DataSet.Tables chứa các đối tượng DataTable. Mỗi đối tượng trong tập hợp này có thể được truy xuất bằng chỉ số hoặc bằng tên. Các DataTable trong tập hợp DataSet.DataTables mô phỏng các Table trong CSDL quan hệ (các row, column, ). Các thuộc tính quan trọng nhất của lớp DataTable là Columns và Rows định nghĩa cấu trúc và nội dung bảng dữ liệu. 4.4.1.2 DataColumn Thuộc tính DataTable.Columns chứa một tập các đối tượng DataColumn biểu diễn các trường dữ liệu trong DataTable. Bảng dưới đây tóm tắt các thuộc tính quan trọng của lớp DataColumn. Phương thức Mô tả ColumnName Tên column DataType Kiểu của dữ liệu chứa trong column này Ví dụ: col1.DataType = System.Type.GetType("System.String") MaxLength Độ dài tối đa của một text column. -1 nếu không xác định độ dài tối đa ReadOnly Cho biết giá trị của column có được chỉnh sửa hay không Giáo trình Visual Studio .NET 110
  60. Phương thức Mô tả AllowDBNull Giá trị Boolean cho biết column này có được chứa giá trị NULL hay không Unique Giá trị Boolean cho biết column này có được chứa các giá trị trùng nhau hay không Expression Biểu thức định nghĩa cách tính giá trị của một column Ví dụ: colTax.Expression = "colSales * .085"; Caption Tiêu đề hiển thị trong thành phần điều khiển giao diện đồ họa DataTable Tên của đối tượng DataTable chứa column này Các column của DataTable được tạo ra một cách tự động khi table được nạp dữ liệu từ kết quả của một database query hoặc từ kết quả đọc được ở một file XML. Tuy nhiên, chúng ta cũng có thể viết code để tạo động các column. Đoạn code dưới đây sẽ tạo ra một đối tượng DataTable, sau đó tạo thêm các đối tượng DataColumn, gán giá trị cho các thuộc tính của column, và bổ sung các DataColumn này vào DataTable. DataTable tb = new DataTable("DonHang"); DataColumn dCol = new DataColumn("MaSo", Type.GetType("System.Int16")); dCol.Unique = true; // Dữ liệu của các dòng ở column này không được trùng nhau dCol.AllowDBNull = false; tb.Columns.Add(dCol); dCol = new DataColumn("DonGia", Type.GetType("System.Decimal")); tb.Columns.Add(dCol); dCol = new DataColumn("SoLuong",Type.GetType("System.Int16")); tb.Columns.Add(dCol); dCol= new DataColumn("ThanhTien",Type.GetType("System.Decimal")); dCol.Expression= "SoLuong*DonGia"; tb.Columns.Add(dCol); // Liệt kê danh sách các Column trong DataTable foreach (DataColumn dc in tb.Columns) { Console.WriteLine(dc.ColumnName); Console.WriteLine(dc.DataType.ToString()); } Để ý rằng column MaSo được định nghĩa để chứa các giá trị duy nhất. Ràng buộc này giúp cho column này có thể được dùng như là trường khóa để thiết lập relationship kiểu parent-child với một bảng khác trong DataSet. Để mô tả, khóa phải là duy nhất – như Giáo trình Visual Studio .NET 111
  61. trong trường hợp này – hoặc được định nghĩa như là một primary key của bảng. Ví dụ dưới đây mô tả cách xác định primary key của bảng: DataColumn[] col = {tb.Columns["MaSo"]}; tb.PrimaryKey = col; Nếu một primary key chứa nhiều hơn 1 column – chẳng hạn như HoDem và Ten – bạn có thể tạo ra một ràng buộc unique constraint trên các như ví dụ dưới đây: DataColumn[] cols = {tb.Columns["HoDem"], tb.Columns["Ten"]}; tb.Constraints.Add(new UniqueConstraint("keyHoVaTen", cols)); Chúng ta sẽ xem xét cách thức tạo relationship cho các bảng và trộn dữ liệu ở phần tiếp theo. 4.4.1.3 DataRows Dữ liệu được đưa vào table bằng cách tạo mới một đối tượng DataRow, gán giá trị cho các column của nó, sau đó bổ sung đối tượng DataRow này vào tập hợp Rows gồm các DataRow của table. DataRow row; row = tb.NewRow(); // Tạo mới DataRow row["DonGia"] = 22.95; row["SoLuong"] = 2; row["MaSo"] = 12001; tb.Rows.Add(row); // Bổ sung row vào tập Rows Console.WriteLine(tb.Rows[0]["ThanhTien"].ToString()); // 45.90 Một DataTable có các phương thức cho phép nó có thể commit hay roll back các thay đổi được tạo ra đối với table tương ứng. Để thực hiện được điều này, nó phải nắm giữ trạng thái của mỗi dòng dữ liệu bằng thuộc tính DataRow.RowState. Thuộc tính này được thiết lập bằng một trong 5 giá trị kiểu enumeration DataRowState sau: Added, Deleted, Detached, Modifed, hoặc Unchanged. Xem xét ví dụ sau: tb.Rows.Add(row); // Added tb.AcceptChanges(); // Commit changes Console.Write(row.RowState); // Unchanged tb.Rows[0].Delete(); // Deleted // Undo deletion tb.RejectChanges(); // Roll back Giáo trình Visual Studio .NET 112
  62. Console.Write(tb.Rows[0].RowState); // Unchanged DataRow myRow; MyRow = tb.NewRow(); // Detached Hai phương thức AcceptChanges và RejectChanges của DataTable là tương đương với các thao tác commit và rollback trong một CSDL. Các phương thức này sẽ cập nhất mọi thay đổi xảy ra kể từ khi table được nạp, hoặc từ khi phương thức AcceptChanges được triệu gọi trước đó. Ở ví dụ trên, chúng ta có thể khôi phục lại dòng bị xóa do thao tác xóa là chưa được commit trước khi phương thức RejectChanges được gọi. Điều đáng lưu ý nhất đó là, những thay đổi được thực hiện là ở trên table chứ không phải là ở data source. ADO.NET quản lý 2 giá trị - ứng với 2 phiên bản hiện tại và nguyên gốc - cho mỗi column trong một dòng dữ liệu. Khi phương thức RejectChanges được gọi, các giá trị hiện tại sẽ được đặt khôi phục lại từ giá trị nguyên gốc. Điều ngược lại được thực hiện khi gọi phương thức AcceptChanges. Hai tập giá trị này có thể được truy xuất đồng thời thông qua các giá trị liệt kê DataRowVersion là: Current và Original: DataRow r = tb.Rows[0]; r["DonGia"]= 14.95; r.AcceptChanges(); r["DonGia"]= 16.95; Console.WriteLine("Current: {0} Original: {1} ", r["Price", DataRowVersion.Current], r["Price", DataRowVersion.Original]); Kết quả in ra: Current: 16.95 Original: 14.95 4.4.1.4 DataView. DataView đóng vai trò như tầng hiển thị dữ liệu lưu trữ trong DataTable. Nó cho phép người sử dụng sắp xếp, lọc và tìm kiếm dữ liệu. //Giả sử đã có 1 dataset có tên là ds chứa dữ liệu của bảng DonHang DataView dv = new DataView(ds.Tables["DonHang”]; // Lọc ra tất cả các hàng có giá từ 10 đến 100 dv.RowFilter = "soluong>=10 and soluong<=100"; //Sắp xếp tăng dần theo số lượng nếu số lượng bằng nhau thì sắp xếp giảm dần thêm đơn giá dv.Sort = "soluong, dongia DESC"; Giáo trình Visual Studio .NET 113
  63. 4.4.2 Nạp dữ liệu vào DataSet Chúng ta đã biết cách thành lập một DataTable và xử lý dữ liệu theo kiểu từng dòng một. Phần này sẽ trình bày phương pháp để dữ liệu và lược đồ dữ liệu được nạp tự động từ CSDL quan hệ vào các table trong DataSet. 4.4.2.1 Dùng DataReader để nạp dữ liệu vào DataSet Đối tượng DataReader có thể được sử dụng để liên hợp đối tượng DataSet hay DataTable trong việc nạp các dòng dữ liệu kết quả (của query trong DataReader). cmd.CommandText = "SELECT * FROM nhanvien"; DBDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection); DataTable dt = new DataTable("nhanvien"); dt.Load(rdr); // Nạp dữ liệu và lược đồ vào table Console.WriteLine(rdr.IsClosed); // True Đối tượng DataReader được tự động đóng sau khi tất cả các dòng dữ liệu được nạp vào table. Do đã sử dụng tham số CommandBehavior.CloseConnection trong phương thức ExecuteReader nên connection được đóng sau khi DataReader được đóng. Nếu table đã có dữ liệu, phương thức Load sẽ trộn dữ liệu mới với các dòng dữ liệu đang có trong nó. Việc trộn này xảy ra chỉ khi các dòng dữ liệu có chung primary key. Nếu không có primary key được định nghĩa, các dòng dữ liệu sẽ được nối vào sau tập dữ liệu hiện tại. Chúng ta có thể sử dụng phương thức nạp chồng khác của phương thức Load để quy định cách thức làm việc. Phương thức Load với tham số kiểu enumeration LoadOption gồm 1 trong 3 giá trị OverwriteRow, PreserveCurrentValues, hoặc UpdateCurrentValues tương ứng với tùy chọn ghi đè nguyên dòng, giữ lại các giá trị hiện tại, hoặc cập nhật các giá trị hiện tại. Đoạn code dưới đây minh họa cách trộn dữ liệu vào các dòng hiện tại theo kiểu ghi đè các giá trị hiện tại: cmd.CommandText = "SELECT * FROM nhanvien WHERE diachi=’a’"; DBDataReader rdr = cmd.ExecuteReader(); DataTable dt = new DataTable("nhanvien"); dt.Load(rdr); Console.Write(dt.Rows[0]["HoTen"]); // giả sử giá trị nhận được là “tnv spider” // Gán khóa chính DataColumn[] col = new DataColumn[1]; col[0] = dt.Columns["Manv"]; Giáo trình Visual Studio .NET 114
  64. dt.PrimaryKey = col; DataRow r = dt.Rows[0]; // lấy dòng đầu tiên r["HoTen"] = "ten moi"; // thay đổi giá trị của cột HoTen // Do reader đã bị đóng sau khi nạp vào data table nên phải giờ phải fill lại rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection); // Trộn dữ liệu với các dòng hiện tại. Ghi đè các giá trị hiện tại dt.Load(rdr, LoadOption.UpdateCurrentValues); // Giá trị cập nhật đã bị ghi đè!!! Console.Write(dt.Rows[0]["HoTen"]); // “tnv spider” 4.4.2.2 Nạp dữ liệu vào DataSet bằng DataAdapter Đối tượng DataAdapter có thể được dùng để nạp một table hiện có vào một table khác, hoặc tạo mới và nạp dữ liệu cho table từ kết quả của một query. Bước đầu tiên là tạo ra một đối tượng DataAdapter tương ứng với data provider cụ thể. Dưới đây là các ví dụ để tạo ra đối tượng DataAdapter: (1) Tạo từ Connection string và câu truy vấn SELECT: String sql = "SELECT * FROM nhanvien"; SqlDataAdapter da = new SqlDataAdapter(sql, connStr); (2) Tạo từ đối tượng Connection và câu truy vấn SELECT: SqlConnection conn = new SqlConnection(connStr); SqlDataAdapter da = new SqlDataAdapter(sql, conn); (3) Gán đối tượng Command cho thuộc tính SelectCommand SqlDataAdapter da = new SqlDataAdapter(); SqlConnection conn = new SqlConnection(connStr); da.SelectCommand = new SqlCommand(sql, conn); Sau khi đối tượng DataAdapter đã được tạo ra, phương thức Fill của nó được thực thi để nạp dữ liệu vào table (đang tồn tại hoặc tạo mới). Ở ví dụ dưới đây, một table mới được tạo ra với tên mặc định là “Table”: DataSet ds = new DataSet(); // Tạo ra một DataTable, nạp dữ liệu vào DataTable, và đưa DataTable vào DataSet int nRecs = da.Fill(ds); // trả về số lượng record được nạp vào DataTable // Nếu muốn đặt tên cho DataTable trong DataSet thay vì lấy tên mặc định // thì sử dụng code như thế này Giáo trình Visual Studio .NET 115
  65. int nRecs = da.Fill(ds, "nhanvien ") Với một table đang tồn tại, tác dụng của lệnh Fill tùy thuộc vào table có primary hay không. Nếu có, những dòng dữ liệu có khóa trùng với dòng dữ liệu mới sẽ được thay thế. Các dòng dữ liệu mới không trùng với dữ liệu hiện có sẽ được nối vào sau DataTable. 4.4.3 Bài thực hành Bài thực hành về DataAdapter và DataSet Ví dụ này sử dụng cơ sở dữ liệu quanlythuvien như trong bài thực hành 4.3.4 Thiết kế form frmtimkiemsach để tìm theo tên sách hoặc tên tác giả như sau: DataGridView1 frmtimkiemsach sử dụng các trường, phương thức và sự kiện sau: Giáo trình Visual Studio .NET 116
  66. Các điều khiển Tên điều khiển Thuộc tính Form Name: frmtimkiemsach Text:Tìm kiếm theo nhan đề hoặc tên tác giả Label Text: Nhập tên sách hoặc tên tác giả cần tìm TextBox Name: txttimkiem dataGridView Name: dataGridView1 statusStrip Name: thanhtrangthai Items: Add thêm 2 statusLabel: với tên ketquatim và tóngoluong Các trường: Tên trường Ý nghĩa Cn Dùng để kết nối đến cơ sở dữ liệu quanlythuvien cmd sqlCommand sử dụng câu lệnh select để hiển thị và tìm kiếm sách da SqlDataAdapter chứa cmd và cn ds DataSet chứa dữ liệu của bảng sách hoặc chứa kết quả tìm kiếm Các phương thức + Hàm dựng frmtimkiemsach để tạo giao diện Giáo trình Visual Studio .NET 117
  67. public frmtimkiemsach() { InitializeComponent(); } + Phương thức Tongsoluong: được sử dụng để tính tổng số lượng sách của các sách lưu trong Dataset ds. private int Tongsoluong() { int s=0; foreach (DataRow r in ds.Tables["sach"].Rows) { s += (int)r["soluong"]; } return s; } + Phương thức Tongsoluongtk tính tổng số lượng sách trong DataView dv, dv chứa thông tin các sách tìm kiếm được. private int Tongsoluongtk(DataView dv) { int s = 0; foreach (DataRow r in dv.ToTable("sach").Rows ) { s += (int)r["soluong"]; } return s; } + Sự kiện frmtimkiemsach_Load:Nạp thông tin của 4 quyển sách đầu tiên theo thứ tự giảm dần của ngaynhap vào DataSet ds với tên bảng trong DataSet là sach, sau đó hiển thị thông tin của bảng sach trong DataSet vào dataGridView1, đưa tổng số sách và tổng số lượng sách trong DataSet vào thanh trạng thái private void frmtimkiemsach_Load(object sender, EventArgs e) { cn.Open(); // Mở kết nối cmd.CommandText = "select top 4 * from sach order by ngaynhap desc" ; cmd.Connection = cn; da.SelectCommand = cmd; da.Fill(ds, "sach"); // Nạp dữ liệu vào DataSet dataGridView1.DataSource = ds.Tables["sach"]; // Nạp dữ liệu vào dataGridView1 // Nạp dữ liệu vào thanh trạng thái thanhtrangthai.Items[0].Text = "Tổng số sách:" + ds.Tables["sach"].Rows.Count.ToString(); Giáo trình Visual Studio .NET 118
  68. thanhtrangthai.Items[1].Text = "Tổng số lượng:" + Tongsoluong().ToString(); } + Sự kiện txttimkiem_KeyPress: Khi người sử dụng nhấn Enter trên txttimkiem thì việc tìm kiếm tương đối bắt đầu: Tạo ra 1 DataView dv chứa dữ liệu của bảng sách trong DataSet ds, lọc trong DataView dv ra thông tin của các quyển sách gần giống với dữ liệu nhập trên txttimkiem, sau đó đưa kết quả lọc ra trên dataGridView1 và thanh trạng thái. private void txttimkiem_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == 13) { DataView dv = new DataView(ds.Tables["sach"]); //Nạp dữ liệu vào DataView //bắt đầu lọc dữ liệu dv.RowFilter = "nhande like '%" + txttimkiem.Text + "%' or tacgia like '%" + txttimkiem.Text + "%'"; dataGridView1.DataSource = dv; //Nạp kết quả lọc trong dv vào dataGridView1 // Đưa số quyển sách và tổng số lượng sách lọc được vào thanh trang thái thanhtrangthai.Items[0].Text = "Số kết quả tìm thấy được: " + dv.Count.ToString() + "/" + ds.Tables["sach"].Rows.Count.ToString(); thanhtrangthai.Items[1].Text = "Tổng số lượng tìm thấy được:" + Tongsoluongtk(dv).ToString() + "/" + Tongsoluong().ToString(); } } 4.4.4 Cập nhật CSDL bằng DataAdapter Sau khi DataAdapter đã nạp dữ liệu vào table, connection sẽ được đóng, và các thay đổi sau đó đối sau đó tạo ra cho dữ liệu sẽ chỉ có ảnh hưởng trong DataSet chứ không phải là ở dữ liệu nguồn! Để thực sự cập nhật các thay đổi này lên nguồn dữ liệu, DataAdapter phải được sử dụng để khôi phục connection và gửi các dòng dữ liệu đã được thay đổi lên CSDL. Ngoài SelectCommand, DataAdapter có thêm 3 thuộc tính Command nữa, gồm InsertCommand, DeleteCommand và UpdateCommand, làm nhiệm vụ thực hiện các thao tác tương ứng với tên thuộc tính của chúng (chèn, xóa, cập nhật). Các Command này được thực thi khi phương thức Update của DataAdapter được triệu gọi. Khó khăn nằm ở chỗ tạo ra các query command phức tạp này (cú pháp của câu lệnh SQL tương ứng càng dài dòng và phức tạp khi số lượng column nhiều lên). Rất may là các data provider đều có cài đặt một lớp gọi là CommandBuilder dùng để quản lý việc tạo các Command nói trên Giáo trình Visual Studio .NET 119
  69. một cách tự động. 4.4.4.1 CommandBuilder Một đối tượng CommandBuilder sẽ sinh ra các Command cần thiết để thực hiện việc cập nhật nguồn dữ liệu tạo ra bởi DataSet. Cách tạo đối tượng CommandBuilder là truyền đối tượng DataAdapter cho phương thức khởi dựng của nó; sau đó, khi phương thức DataAdapter.Update được gọi, các lệnh SQL sẽ được sinh ra và thực thi. Đoạn code dưới đây minh họa cách thức thay đổi dữ liệu ở một DataTable và cập nhật lên CSDL tương ứng bằng DataAdapter: //Giả sử đã có 1 DataSet ds chứa dữ liệu của bảng khoa DataTable dt= ds.Tables["khoa"]; // (1) Dùng commandBuilder để sinh ra các Command cần thiết để update SqlCommandBuilder sb = new SqlCommandBuilder(da); // (2) Thực hiện thay đổi dữ liệu: thêm 1 khoa mới DataRow drow = dt.NewRow(); drow["Makhoa"] = 12; drow["tenkhoa"] = "abc"; dt.Rows.Add(drow); // (3) Thực hiện thay đổi dữ liệu: xóa 1 khoa dt.Rows[4].Delete(); // (4) Thực hiện thay đổi dữ liệu: thay đổi giá trị 1 dòng dữ liệu dt.Rows[5]["tenkhoa"] = "this must be changed"; // (5) Tiến hành cập nhật lên CSDL int nUpdate = da.Update(ds, "khoa"); MessageBox.Show("Số dòng được thay đổi: " + nUpdate.ToString()); // 3 Có một số hạn chế khi sử dụng CommandBuilder: Command Select ứng với DataAdapter chỉ được tham chiếu đến 1 table, và table nguồn trong CSDL phải bao gồm một primary key hoặc một column chứa các giá trị duy nhất. Column này (hay tổ hợp các columns) phải được bao gồm trong command Select ban đầu. 4.4.4.2 Đồng bộ hóa dữ liệu giữa DataSet và CSDL Như đã minh họa trong ví dụ này, việc sử dụng DataAdapter làm đơn giản hóa và tự động hóa quá trình cập nhật CSDL hoặc bất kỳ data source nào. Tuy nhiên, có một vấn đề ở đây: multi-user (nhiều người sử dụng). Mô hình Ngắt kết nối được dựa trên cơ chế Optimistic Concurrency, một cách tiếp cận mà trong đó các dòng dữ liệu ở data source không bị khóa (lock) giữa thời gian mà chúng được đọc và thời gian mà các cập nhật được áp dụng cho data source. Trong khoảng thời gian này, user khác có thể cũng cập Giáo trình Visual Studio .NET 120
  70. nhật data source. Nếu có thay đổi xảy ra kể từ lần đọc trước đó thì phương thức Update sẽ nhận biết được và không cho áp dụng thay đổi đối với các dòng dữ liệu đó. Có hai phương án cơ bản để giải quyết lỗi concurrency (tương tranh) khi có nhiều cập nhật được áp dụng: roll back tất cả các thay đổi nếu như xuất hiện xung đột (violation), hoặc áp dụng các cập nhật không gây ra lỗi và xác định những cập nhật có gây ra lỗi để có thể xử lý lại. 4.4.4.3 Sử dụng Transactions để Roll Back nhiều cập nhật Khi thuộc tính DataAdapter.ContinueUpdateOnErrors được thiết lập là false, một ngoại lệ sẽ được ném ra khi một thay đổi dòng dữ liệu không thể thực hiện được. Điều này sẽ ngăn các cập nhật tiếp theo được thực thi, nhưng lại không ảnh hưởng đến các cập nhật đã xuất hiện trước ngoại lệ đó. Do những cập nhật có thể có liên quan với nhau, ứng dụng thường là cần chiến lược hoặc là tất cả, hoặc là không (all-or-none). Cách dễ nhất để thực thi chiến lược này là tạo ra một transaction trong đó tất cả các command update sẽ được thực thi. Để thực hiện điều này, tạo ra một đối tượng SqlTransaction và gắn nó với SqlDataAdapter.SelectCommand bằng cách truyền nó cho hàm khởi dựng của nó. Nếu có ngoại lệ xảy ra, phương thức Rollback sẽ được thực thi để undo mọi thay đổi trước đó; nếu không có ngoại lệ nào xuất hiện, phương thức Commit được thực thi để áp dụng tất cả các command update. Dưới đây là một ví dụ: SqlDataAdapter da = new SqlDataAdapter(); SqlCommandBuilder sb = new SqlCommandBuilder(da); SqlTransaction tran; SqlConnection conn = new SqlConnection(connStr); conn.Open(); // Connection phải được dùng với Transaction // (1) Tạo ra một transaction SqlTransaction tran = conn.BeginTransaction(); // (2) Gắn SelectCommand với transaction da.SelectCommand = new SqlCommand(sql, conn, tran); DataSet ds = new DataSet(); da.Fill(ds, "docgia"); // // Code ở phần này thực hiện các cập nhật lên các dòng dữ liệu ở DataSet try { int updates = da.Update(ds, "docgia"); MessageBox.Show("Cập nhật: " + updates.ToString()); } Giáo trình Visual Studio .NET 121
  71. // (3) Nếu có ngoại lệ xuất hiện, roll back mọi cập nhật trong transaction catch (Exception ex) { MessageBox.Show(ex.Message); // Lỗi khi cập nhật if (tran != null) { tran.Rollback(); // Roll back mọi cập nhật tran = null; MessageBox.Show("Tất cả các cập nhật đã bị roll back."); } } finally { // (4) Nếu không có lỗi, commit tất cả các cập nhật if (tran != null) { tran.Commit(); MessageBox.Show("Tất cả đã được cập nhật thành công. "); tran = null; } } conn.Close(); 4.4.4.4 Xác định các dòng gây ra lỗi cập nhật Khi thuộc tính DataAdapter.ContinueUpdateOnErrors được thiết lập là True, các xử lý sẽ không ngưng nếu có một dòng dữ liệu không thể cập nhật được. Thay vào đó, DataAdapter sẽ cập nhật tất cả các dòng dữ liệu không gây ra lỗi. Sau đó, lập trình viên có thể xác định các dòng dữ liệu không cập nhật được và quyết định cách xử lý chúng. Các dòng dữ liệu không cập nhật được có thể dễ dàng được xác định qua thuộc tính DataRowState của chúng (đã được trình bày trong phần mô tả DataRow). Các dòng dữ liệu đã được cập nhật thành công sẽ có trạng thái Unchanged; trong khi đó các dòng dữ liệu không cập nhật thành công sẽ mang trạng thái Added, Deleted hoặc Modified. Đoạn code dưới đây minh họa cách lặp qua các dòng dữ liệu và xác định các dòng chưa được cập nhật. // SqlDataAdapter da nạp dữ liệu của bảng docgia da.ContinueUpdateOnError = true; DataSet ds = new DataSet(); try Giáo trình Visual Studio .NET 122
  72. { da.Fill(ds, "docgia"); DataTable dt = ds.Tables["docgia"]; SqlCommandBuilder sb = new SqlCommandBuilder(da); // Các thao tác cập nhật dt.Rows[29].Delete(); // Delete dt.Rows[30]["HoTen"] = "try to change"; // Update dt.Rows[30][Madocgia] = 1234; // Update dt.Rows[31]["HoTen"] = "XYZ"; // Update DataRow drow = dt.NewRow(); drow["HoTen"] = "tnv spider"; drow["Madocgia"] = 25; dt.Rows.Add(drow); // insert // Submit updates int updates = da.Update(ds, "docgia"); if (ds.HasChanges()) { // Load rows that failed into a DataSet DataSet failures = ds.GetChanges(); int rowsFailed = failures.Rows.Count; Console.WriteLine("Số dòng không thể cập nhật: " + rowsFailed); foreach (DataRow r in failures.Tables[0].Rows ) { string state = r.RowState.ToString()); // Phải hủy bỏ thay đổi để hiển thị dòng đã bị xóa if (r.RowState == DataRowState.Deleted) r.RejectChanges(); string iMadocgia= ((int)r["Madocgia"]).ToString(); string msg = state + " Madocgia: " + iMadocgia; Console.WriteLine(msg); } } Lưu ý rằng ngay cả khi thao tác xóa xuất hiện trước, nó cũng không có tác dụng đối với các thao tác khác. Câu lệnh SQL xóa hay cập nhật một dòng dữ liệu được dựa theo giá trị của primary key chứ không liên quan đến vị trí của nó. Ngoài ra các cập nhật trên cùng một dòng được kết hợp và đếm như là 1 cập nhật cho dòng đó bởi phương thức Update. Ở ví dụ trên, các cập nhật cho dòng 30 được tính như là 1 cập nhật. 4.4.5 Định nghĩa Relationships giữa các Table trong DataSet Giáo trình Visual Studio .NET 123
  73. Một DataRelation là một mối quan hệ parent-child giữa hai đối tượng DataTables. Nó được định nghĩa dựa trên việc so khớp các columns trong 2 DataTable. Cú pháp hàm khởi dựng là như sau: public DataRelation( string relationName, DataColumn parentColumn, DataColumn childColumn) Một DataSet có một thuộc tính Relations giúp quản lý tập hợp các DataRelation đã được định nghĩa trong DataSet. Sử dụng phương thức Relations.Add để thêm các DataRelation vào tập hợp Relations. Ví dụ dưới đây thiết lập mối quan hệ giữa hai bảng khoa và docgia để có thể liệt kê danh sách các docgia của mỗi khoa. string connStr="Data Source=tên máy chủ;Initial Catalog=quanlythuvien; Trusted_Connection=yes"; DataSet ds = new DataSet(); // (1) Fill bảng docgia string sql = "SELECT * FROM docgia”; SqlConnection conn = new SqlConnection(connStr); SqlCommand cmd = new SqlCommand(); SqlDataAdapter da = new SqlDataAdapter(sql, conn); da.Fill(ds, "docgia"); // (2) Fill bảng khoa sql = "SELECT * FROM khoa”; da.SelectCommand.CommandText = sql; da.Fill(ds, "khoa"); // (3) Định nghĩa relationship giữa 2 bảng khoa và docgia DataTable parent = ds.Tables["khoa"]; DataTable child = ds.Tables["docgia"]; DataRelation relation = new DataRelation("khoa_docgia", parent.Columns["makhoa"], child.Columns["makhoa"]); // (4) Đưa relationship vào DataSet ds.Relations.Add(relation); // (5) Liệt kê danh sách các đọc giả của từng khoa foreach (DataRow r in parent.Rows) { Console.WriteLine(r["tenkhoa"]); // Tên khoa foreach (DataRow rc in r.GetChildRows("khoa_docgia")) { Console.WriteLine(" " + rc["HoTen"]); Giáo trình Visual Studio .NET 124
  74. } } /* Ví dụ kết quả: Khoa Tin Nguyễn Văn Trung Ngô Anh Tuấn Lê Thanh Hoa Khoa Toán Nguyễn Thị Hoa Trần Văn Phúc */ Khi một relationship được định nghĩa giữa 2 tables, nó cũng sẽ thêm một ForeignKeyConstraint vào tập hợp Constraints của DataTable con. Constraint này quyết định cách mà DataTable con bị ảnh hưởng khi các dòng dữ liệu ở phía DataTable cha bị thay đổi hay bị xóa. Trong thực tế, điều này có nghĩa là khi bạn xóa 1 dòng trong DataTable cha, các dòng con có liên quan cũng bị xóa – hoặc cũng có thể là các key bị đặt lại giá trị thành NULL. Tương tự như thế, nếu một giá trị key bị thay đổi trong DataTable cha, các dòng dữ liệu có liên quan trong DataTable con cũng có thể bị thay đổi theo hoặc bị đổi giá trị thành NULL. Các luật như trên được xác định bởi các thuộc tính DeleteRule và UpdateRule của constraint. Các luật này được nhận các giá trị liệt kê sau đây: - Cascade. Xóa/Cập nhật các dòng dữ liệu có liên quan trong DataTable con. Đây là giá trị mặc định. - None. Không làm gì. - SetDefault. Thiết lập các giá trị khóa trong các dòng dữ liệu có liên quan của DataTable con thành giá trị mặc định của column tương ứng. - SetNull. Thiết lập các giá trị khóa trong các dòng dữ liệu có liên quan của DataTable con thành null. Xem xét ví dụ dưới đây: // (1) Thêm một dòng với khóa mới vào DataTable con DataRow row = child.NewRow(); row["Makhoa"] = 999; // giả sử trong bảng khoa không có record nào có Makhoa = 999 child.Rows.Add(row); // Không được do 999 không tồn tại trong DataTable cha Giáo trình Visual Studio .NET 125
  75. // (2) Xóa một dòng trong DataTable cha row = parent.Rows[0]; row.Delete(); // Xóa các dòng trong DataTable con có khóa này // (3) Tạm thời vô hiệu hóa constraints và thử thêm dòng mới ds.EnforceConstraints = false; row["Makhoa"] = 999; child.Rows.Add(row); // Được chấp nhận!!! ds.EnforceConstraints = true; // Kích hoạt constraint trở lại // (4) Thay đổi constraint để đặt các dòng dữ liệu thành null nếu DataTable thay đổi ((ForeignKeyConstraint)child.Constraints[0]).DeleteRule = Rule.SetNull; Lưu ý rằng thuộc tính EnforeceConstraint được đặt thành false sẽ làm vô hiệu hóa tất cả các constraint – điều này trong thuật ngữ CSDL gọi là bỏ qua tính toàn vẹn tham chiếu. Điều này cho phép một khoa được bổ sung vào thậm chí khi cả column Makhoa không tương ứng với dòng nào trong bảng khoa. Nó cũng cho phép một dòng khoa được xóa ngay cả khi có nhiều dòng tương ứng với nó trong bảng docgia. 4.5 Sử dụng Data Binding Chúng ta đã đề cập đến các điều khiển để thiết kế giao diện như TextBox, ListBox, RadioButton, ComBoBox và các các điều khiển của ADO.NET như DataSet, DataTable và DataView. Các điều khiển này làm việc một cách độc lập với nhau, tuy nhiên trong một số tình huống chúng cần kết hợp lại với nhau. Ví dụ ta cần hiển thị tên khoa từ cơ sở dữ liệu ra 1 TextBox, khi đó ta cần tạo ra 1 DataSet chứa dữ liệu của bảng khoa và 1 TextBox, sau đó liên kết dữ liệu trong DataSet vào TextBox. Sự kết hợp giữa hai điều khiển này có thể sử dụng DataBinding. 4.5.1 Các loại của Binding ADO.NET cung cấp 2 loại Binding: - DataBinding đơn giản (Simple DataBinding): Tại một thời điểm, một giá trị đơn trong DataSet có thể bị buộc vào bất kỳ một điều khiển. Ví dụ: giả sử đã có 1 DataSet ds chứa dữ liệu của bảng Khoa, cần buộc tên khoa vào TextBox txttenkhoa: txttenkhoa.DataBindings.Add("Text", ds, "khoa.tenkhoa"); Khi đó mọi thay đổi trên DataSet ds sẽ ảnh hưởng đến TextBox txtdocgia và ngược lại. - DataBinding phức tạp (Complex DataBinding): Các dữ liệu trong DataSet bị buộc vào một điều khiển thay vì chỉ một giá trị đơn. Chỉ có DataGidView và ComboBox hỗ trợ chức năng DataBinding phức tạp. Giáo trình Visual Studio .NET 126
  76. Ví dụ: giả sử đã có 1 DataSet ds chứa dữ liệu của bảng Khoa, cần buộc tên khoa vào ComboBox cmbkhoa và buộc toàn bộ dữ liệu của bảng khoa vào DataSet ds: //Buộc tenkhoa của bảng khoa trong DataSet ds vào cmbkhoa cmbkhoa.DataSource = ds; cmbkhoa.DisplayMember = "khoa.tenkhoa"; //Buộc toàn bộ dữ liệu của bảng khoa trong DataSet ds vào DataGridView dgvkhoa dgvKhoa.DataSource = ds; dgvKhoa.DataMember = "khoa"; 4.5.2 Các nguồn dữ liệu của DataBinding Nhiều thành phần có thể hoạt động như nguồn dữ liệu của DataBinding. Bất kỳ các thành phần được cài đặt từ giao diện Ilist có thể xem là như nguồn dữ liệu của DataBinding. Các ví dụ sau minh họa bằng cách nào để sử dụng DataTable, DataView, DataSet và Mảng như là nguồn dữ liệu để cài đặt DataBinding đơn giản và phức tạp. - DataTable: Loại dữ liệu này lưu trữ dữ liệu của một bảng trong cơ sở dữ liệu. DataTable t = ds.Tables["khoa"]; //DataBinding đơn giản txttenkhoa.DataBindings.Add("Text", t, "tenkhoa"); //DataBinding phức tạp cmbkhoa.DataSource = t; cmbkhoa.DisplayMember = "tenkhoa"; dgvKhoa.DataSource = t; - DataView: DataView dv =new DataView (ds.Tables["khoa"]); //DataBinding đơn giản txttenkhoa.DataBindings.Add("Text", dv, "tenkhoa"); //DataBinding phức tạp cmbkhoa.DataSource = dv; cmbkhoa.DisplayMember = "tenkhoa"; dgvKhoa.DataSource = dv; - DataSet: như ví dụ ở phần 3.5.1 - Mảng: int[] t = new int[4] { 12, 2, 3, 4 }; //DataBinding đơn giản txttenkhoa.DataBindings.Add("Text", t, ""); Giáo trình Visual Studio .NET 127
  77. //DataBinding phức tạp cmbkhoa.DataSource = t; 4.5.3 BindingContext Sơ đồ bên dưới chỉ ra cách buộc dữ liệu từ nguồn dữ liệu vào các điều khiển trên Form. Phần này chủ yếu thảo luận về các lớp BindingContext, CurrencyManager và chỉ ra bằng cách nào chúng tương tác khi dữ liệu bị buộc vào một hoặc nhiều điều khiển trên Form: BindingContext Mỗi Windows Form đều có một thuộc tính BindingContext. Một BindingContext có một tập hợp các BindingManagerBase, các đối tượng này được tạo ra khi dữ liệu buộc vào một điều khiển. Nếu nguồn dữ liệu bao gồm một danh sách các đối tượng như a DataTable, DataView hoặc bất kỳ đối tượng nào cài đặt trên giao diện Ilist khi đó CurrencyManager sẽ được sử dụng. Một CurrencyManager có thể duy trì một vị trí hiện thời (current position ) bên trong nguồn dữ liệu. Nếu nguồn dữ liệu chỉ trả về một giá trị đơn khi đó PropertyManager sẽ được lưu trữ bên trong BindingContext . Một CurrencyManager hoặc PropertyManager chỉ được tạo ra một lần cho một nguồn dữ liệu. Nếu hai TextBox bị buộc vào 1 dòng của DataTable khi đó chỉ có mộ Giáo trình Visual Studio .NET 128
  78. CurrencyManager được tạo ra trong BindingContext Khi tạo ra một điều khiển trên form, điều khiển này sẽ được liên kết với bộ quản lý buộc dữ liệu trên form ( form's binding manager). Khi tạo ra một điều khiển thì thuộc tính BindingContext của nó bằng null. Để buộc dữ liệu vào một điều khiển ta dùng thuộc tính DataBindings như ví dụ bên dưới buộc mã khoa và tên khoa trong DataSet ds vào TextBox1 và textBox2 . textBox1.DataBindings.Add("Text", ds, "khoa.makhoa"); textBox2.DataBindings.Add("Text", ds, "khoa.tenkhoa"); CurrencyManager and PropertyManager Khi buộc dữ liệu vào 1 điều khiển trên form khi đó một CurrencyManager hoặc một PropertyManager tương ứng sẽ được tạo ra. Mục đích của lớp này là xác định vị trí của mẫu tin hiện thời bên trong nguồn dữ liệu và khi vị trí này thay đổi thì dữ liệu trên các điều khiển bị buộc trên form sẽ tự động thay đổi theo. Các thuộc tính của BindingContext: Thuộc tính Mô tả Bindings Tập hợp các đối tượng Binding được quản lý bởi CurrencyManager Count Số dòng được quả lý trong CurrencyManager Current Giá trị của các đối tượng hiện thời trong nguồn dữ liệu Position Gets hoặc sets đối tượng hiện thời trong danh sách các đối tượng được quản lý trong CurrencyManager 4.5.4 Bài thực hành Bài thực hành về DataSet và DataBinding Ví dụ này sử dụng cơ sở dữ liệu quanlythuvien như trong bài thực hành 4.3.4 Thiết kế form frmkhoa để nhập, xóa, lưu và duyệt qua các mẫu tin trong bảng khoa như sau: Giáo trình Visual Studio .NET 129
  79. dataGridView1 frmkhoa sử dụng các trường, phương thức và sự kiện sau: Các điều khiển Giáo trình Visual Studio .NET 130
  80. Tên điều khiển Thuộc tính Form Name: frmKhoa Text: Thông tin về bảng Khoa Label Tạo ra 4 lable với các Text: Mã Khoa, Tên Khoa, Địa chỉ, Số điện thoại TextBox Tạo ra 4 TextBox với các Name: txtMakhoa, txtTenKhoa, txtDiachi, txtSodienthoai Button Tạo ra 7 Button với các Name: butBosung, butLuu, butXoa, butFirst, butPre, butNext, butLast dataGridView Name: dataGridView1 Các phương thức + Hàm dựng frmKhoa để tạo giao diện: public frmKhoa() { InitializeComponent(); } + Phương thức BuocCacDieuKhien(): Buộc dữ liệu vào dataGridView1 và các textBox private void BuocCacDieuKhien() { //Buộc dữ liệu vào dataGridView1 dataGridView1.DataSource = ds;dataGridView1.DataMember = "khoa"; // Buộc dữ liệu vào các textBox txtMaKhoa.DataBindings.Add("Text", ds, "khoa.makhoa"); txtTenKhoa.DataBindings.Add("Text", ds, "khoa.tenkhoa"); txtdiachi.DataBindings.Add("Text", ds, "khoa.diachi"); txtSodienthoai.DataBindings.Add("Text", ds, "khoa.sdt"); } + Sự kiện: frmKhoa_Load() được sử dụng để kết nối dữ liệu, tạo ra DataSet ds chứa toàn bộ dữ liệu của bảng khoa, buộc dữ liệu vào cho các điều khiển và tạo ra 1SqlCommandBuilder cb để quản lý việc nhập thêm, xóa và lưu dữ liệu của SqlDataAdapter dakhoa . private void frmKhoa_Load(object sender, EventArgs e) { cn.Open(); // Kết nối dữ liệu cmdkhoa = new SqlCommand("select * from khoa", cn); dakhoa = new SqlDataAdapter(cmdkhoa); dakhoa.Fill(ds, "khoa"); BuocCacDieuKhien(); cb = new SqlCommandBuilder(dakhoa); } + Sự kiện: butFirst_Click: Di chuyển con trỏ về mẫu tin đầu tiên private void butFirst_Click(object sender, EventArgs e) { this.BindingContext[ds, "khoa"].Position = 0; Giáo trình Visual Studio .NET 131
  81. } + Sự kiện: butPre_Click: Di chuyển con trỏ về mẫu tin trước mẫu tin hiện thời private void butPre_Click(object sender, EventArgs e) { this.BindingContext[ds, "khoa"].Position ; } + Sự kiện: butNext_Click: Di chuyển con trỏ đến mẫu tin kế tiếp private void butNext_Click(object sender, EventArgs e) { this.BindingContext[ds, "khoa"].Position++; } + Sự kiện: butLast_Click: Di chuyển con trỏ về mẫu tin cuối cùng private void butLast_Click(object sender, EventArgs e) { int ViTriMauTinCuoiCung = this.BindingContext[ds, "khoa"].Count - 1; this.BindingContext[ds, "khoa"].Position = ViTriMauTinCuoiCung; } + Sự kiện: butBosung_Click: Tạo mới một dòng private void butBosung_Click(object sender, EventArgs e) { this.BindingContext[ds, "khoa"].AddNew(); } + Sự kiện: butLuu_Click: Di chuyển con trỏ về mẫu tin cuối cùng, nếu có thay đổi trong DataSet ds thì cập nhật lại dữ liệu, việc cập nhật nhờ vào SqlCommandBuilder cb. Các thao tác bổ sung và xóa chỉ được cập nhật vào cơ sở dư liệu khi người sử dụng kích chuột vào nút Lưu private void butLuu_Click(object sender, EventArgs e) { this.BindingContext[ds, "khoa"].EndCurrentEdit(); if (ds.HasChanges() == true) { try { dakhoa.Update(ds, "khoa"); MessageBox.Show("Da cap nhat"); } catch (Exception ll) { MessageBox.Show(ll.Message); } } } + Sự kiện: butXoa_Click: Lấy vị trí của con trỏ hiện thời, sau đó xóa đi mẫu tin này. Giáo trình Visual Studio .NET 132
  82. private void butXoa_Click(object sender, EventArgs e) { int donghientai; donghientai = this.BindingContext[ds, "khoa"].Position; this.BindingContext[ds, "khoa"].RemoveAt(donghientai); } Bài thực hành về đặt quan hệ giữa các bảng, DataSet và DataBinding Ví dụ này sử dụng cơ sở dữ liệu quanlythuvien như trong bài thực hành 4.3.4. Ví dụ này liên quan đến hai bảng dữ liệu: Khoa và docgia Thiết kế form frmKhoa_Docgia để nhập, xóa, lưu, phục hồi và duyệt qua các mẫu tin trong bảng docgia cho từng khoa như sau: dataGridView1 frmkhoa sử dụng các trường, phương thức và sự kiện sau: Giáo trình Visual Studio .NET 133
  83. Các điều khiển Tên điều khiển Thuộc tính ListBox Name: lstKhoa Form Name: frmKhoa_Docgia Text: Khoa và độc giả Label Tạo ra 6 lable với các Text: Mã độc giả, Họ và tên, Ngày sinh, Địa chỉ, Ngày lập thẻ và Mã khoa TextBox Tạo ra 6 TextBox với các Name: txtMadocgia, txtHoten, txtNgaysinh, txtDiachi, txtNgaylapthe, txtMakhoa Button Tạo ra 8 Button với các Name: butBosung, butLuu, butXoa, butPhuchoi, butFirst (| >), butLast (>|) dataGridView Name: Các phương thức + Hàm dựng frmKhoa_Docgia() để tạo giao diện: public frmKhoa_Docgia() { InitializeComponent(); } + Phương thức Datquanhe bao gồm các tham số: bảng chính, bảng phụ, khóa chính, khóa Giáo trình Visual Studio .NET 134
  84. phụ và tên quan hệ. Phương thức này nạp dữ liệu của 2 bảng: bảng chính và bảng phụ vào DataSet DataSet ds, sau đó đặt quan hệ giữa 2 bảng trong DataSet DataSet ds. private void Datquanhe(string bangchinh, string bangphu, string khoachinh, string khoaphu,string tenquanhe) { cn.Open(); cmdkhoa = new SqlCommand ("select * from "+ bangchinh, cn); dakhoa = new SqlDataAdapter(cmdkhoa); cmddocgia = new SqlCommand("select * from " + bangphu, cn); dadocgia = new SqlDataAdapter(cmddocgia); ds = new DataSet(); dakhoa.Fill(ds, bangchinh); dadocgia.Fill(ds, bangphu); DataColumn chinh=ds.Tables[bangchinh].Columns[khoachinh]; DataColumn phu=ds.Tables[bangphu].Columns[khoaphu]; DataRelation r = new DataRelation(tenquanhe,chinh ,phu ); ds.Relations.Add(r); } + Phương thức BuocCacDieuKhien(): Buộc dữ liệu vào lstKhoa, dataGridView1 và các textBox private void BuocCacDieuKhien() { lstKhoa.DataSource = ds; lstKhoa.DisplayMember = "khoa.tenkhoa"; dataGridView1.DataSource = ds; dataGridView1.DataMember = "khoa.khoa_docgia"; //khoa_docgia là tên quan hệ của 2 bảng khoa và docgia trong DataSet ds txtMadocgia.DataBindings.Add("Text", ds, "khoa.khoa_docgia.madocgia"); txtHoten.DataBindings.Add("Text", ds, "khoa.khoa_docgia.hoten"); txtNgaysinh.DataBindings.Add("Text", ds, "khoa.khoa_docgia.ngaysinh"); txtdiachi.DataBindings.Add("Text", ds, "khoa.khoa_docgia.diachi"); txtNgaylapthe.DataBindings.Add("Text", ds, "khoa.khoa_docgia.ngaylapthe"); txtMakhoa.DataBindings.Add("Text", ds, "khoa.khoa_docgia.makhoa"); } + Sự kiện frmKhoa_Docgia_Load: Đặt quan hệ giữa 2 bảng khoa và docgia trong DataSet DataSet ds, tạo ra 1 SqlCommandBuilder để quản lý việc lưu dữ liệu vào cơ sở dữ liệu ,buộc dữ liệu vào các điều khiển trên form: private void frmKhoa_Docgia_Load(object sender, EventArgs e) { Datquanhe("khoa", "docgia", "makhoa", "makhoa", "khoa_docgia"); cb = new SqlCommandBuilder(dadocgia); Giáo trình Visual Studio .NET 135
  85. BuocCacDieuKhien(); } + Sự kiện: butFirst_Click: Di chuyển con trỏ về mẫu tin đầu tiên private void butFirst_Click(object sender, EventArgs e) { this.BindingContext[ds, "khoa.khoa_docgia"].Position = 0;; } + Sự kiện: butPre_Click: Di chuyển con trỏ về mẫu tin trước mẫu tin hiện thời private void butPre_Click(object sender, EventArgs e) { this.BindingContext[ds, "khoa.khoa_docgia"].Position ; } + Sự kiện: butNext_Click: Di chuyển con trỏ đến mẫu tin kế tiếp private void butNext_Click(object sender, EventArgs e) { this.BindingContext[ds, "khoa.khoa_docgia"].Position++; } + Sự kiện: butLast_Click: Di chuyển con trỏ về mẫu tin cuối cùng private void butLast_Click(object sender, EventArgs e) { int ViTri = this.BindingContext[ds"khoa.khoa_docgia"].Count - 1; this.BindingContext[ds, "khoa"].Position = ViTri; } + Sự kiện: butBosung_Click: Tạo mới một dòng private void butBosung_Click(object sender, EventArgs e) { this.BindingContext[ds, "khoa.khoa_docgia"].].AddNew(); } + Sự kiện: butLuu_Click: Di chuyển con trỏ về mẫu tin cuối cùng, nếu có thay đổi trong DataSet ds thì cập nhật lại dữ liệu, việc cập nhật nhờ vào SqlCommandBuilder cb. Các thao tác bổ sung và xóa chỉ được cập nhật vào cơ sở dư liệu khi người sử dụng kích chuột vào nút Lưu private void butLuu_Click(object sender, EventArgs e) { this.BindingContext[ds, "khoa.khoa_docgia"].EndCurrentEdit(); if (ds.HasChanges() == true) { try { dakhoa.Update(ds, "docgia"); Giáo trình Visual Studio .NET 136