C# abc

Màu nền
Font chữ
Font size
Chiều cao dòng

Mục lục 

Lời cám ơn.......................................................................................................................3

Mục lục ............................................................................................................................4

Tóm tắt.............................................................................................................................1

Phần 1 Tìm hiểu ngôn ngữC#.........................................................................................1

Chương 1 C# và .Net Framework................................................................................2

1.1 Nền tảng của .NET.............................................................................................2

1.2 .NET Framework ...............................................................................................3

1.3 Biên dịch và ngôn ngữtrung gian (MSIL).........................................................4

1.4 Ngôn ngữC# ......................................................................................................5

Chương 2 Khởi đầu......................................................................................................6

2.1 Lớp, đối tượng và kiểu .......................................................................................6

2.2 Phát triển “Hello World”....................................................................................8

Chương 3 Những cơsởcủa ngôn ngữC# .................................................................12

3.1 Các kiểu............................................................................................................12

3.2 Biến và hằng.....................................................................................................14

3.3 Biểu thức ..........................................................................................................16

3.4 Khoảng trắng....................................................................................................16

3.5 Câu lệnh ...........................................................................................................16

3.6 Toán tử.............................................................................................................19

3.7 Tạo vùng tên.....................................................................................................21

3.8 Chỉthịtiền xửlý ..............................................................................................22

Chương 4 Lớp và đối tượng.......................................................................................24

4.1 Định nghĩa lớp..................................................................................................24

4.2 Tạo đối tượng ...................................................................................................25

4.3 Sửdụng các thành viên tĩnh .............................................................................27

4.4 Hủy đối tượng ..................................................................................................29

4.5 Truyền tham số.................................................................................................30

4.6 Nạp chồng phương thức và hàm dựng .............................................................32

4.7 Đóng gói dữliệu với property..........................................................................33

Chương 5 Thừa kếvà Đa hình...................................................................................35

5.1 Đặc biệt hoá và tổng quát hoá..........................................................................35 

5.2 Sựkếthừa.........................................................................................................35

5.3 Đa hình .............................................................................................................37

5.4 Lớp trừu tượng .................................................................................................38

5.5 Lớp gốc của tất cảcác lớp: Object...................................................................39

5.6 Kiểu Boxing và Unboxing ...............................................................................40

5.7 Lớp lồng ...........................................................................................................42

Chương 6 Nạp chồng toán tử.....................................................................................44

6.1 Cách dùng từkhoá operator .............................................................................44

6.2 Cách hổtrợcác ngôn ngữ.Net khác ................................................................44

6.3 Sựhữu ích của các toán tử...............................................................................44

6.4 Các toán tửlogic hai ngôi ................................................................................45

6.5 Toán tửso sánh bằng........................................................................................45

6.6 Toán tửchuyển đổi kiểu (ép kiểu) ...................................................................45

Chương 7 Cấu trúc.....................................................................................................48

7.1 Định nghĩa cấu trúc ..........................................................................................48

7.2 Cách tạo cấu trúc..............................................................................................49

Chương 8 Giao diện...................................................................................................50

8.1 Cài đặt một giao diện .......................................................................................50

8.2 Truy xuất phương thức của giao diện ..............................................................52

8.3 Nạp chồng phần cài đặt giao diện ....................................................................54

8.4 Thực hiện giao diện một cách tường minh ......................................................55

Chương 9 Array, Indexer, and Collection .................................................................58

9.1 Mảng (Array) ...................................................................................................58

9.2 Câu lệnh foreach ..............................................................................................59

9.3 Indexers ............................................................................................................62

9.4 Các giao diện túi chứa......................................................................................65

9.5 Array Lists........................................................................................................65

9.6 Hàng đợi...........................................................................................................65

9.7 Stacks ...............................................................................................................66

9.8 Dictionary.........................................................................................................66

Chương 10 Chuỗi.......................................................................................................67

10.1 Tạo chuỗi mới ................................................................................................67

10.2 Phương thức ToString() .................................................................................67

10.3 Thao tác chuỗi................................................................................................68

10.4 Thao tác chuỗi động.......................................................................................70

Chương 11 Quản lý lỗi...............................................................................................72

11.1 Ném và bắt biệt lệ..........................................................................................73

11.2 Đối tượng Exception ......................................................................................80

11.3 Các biệt lệtựtạo ............................................................................................82

11.4 Ném biệt lệlần nữa. .......................................................................................83 

Chương 12 Delegate và Event ...................................................................................87

12.1 Delegate (ủy thác, ủy quyền) .........................................................................87

12.2 Event (Sựkiện) ............................................................................................101

Chương 13 Lập trình với C#....................................................................................109

13.1 Ứng dụng Windows với Windows Form.....................................................109

Chương 14 Truy cập dữliệu với ADO.NET ...........................................................144

14.1 Cơsởdữliệu và ngôn ngữtruy vấn SQL ....................................................144

14.2 Một sốloại kết nối hiện đang sửdụng .........................................................144

14.3 Kiến trúc ADO.NET ....................................................................................145

14.4 Mô hình đối tượng ADO.NET .....................................................................146

14.5 Trình cung cấp dữliệu (.NET Data Providers)............................................148

14.6 Khởi sựvới ADO.NET ................................................................................148

14.7 Sửdụng trình cung cấp dữliệu được quản lý ..............................................151

14.8 Làm việc với các điều khiển kết buộc dữliệu .............................................152

14.9 Thay đổi các bản ghi của cơsởdữliệu........................................................161

Chương 15 Ứng dụng Web với Web Forms............................................................173

1.1  Tìm hiểu vềWeb Forms............................................................................173

15.1 Các sựkiện của Web Forms ........................................................................174

15.2 Hiển thịchuỗi lên trang................................................................................175

15.3 Điều khiển xác nhận hợp..............................................................................178

15.4 Một sốví dụmẫu minh họa .........................................................................179

Chương 16 Các dịch vụWeb...................................................................................192

Chương 17 Assemblies và Versioning....................................................................196

17.1 Tập tin PE.....................................................................................................196

17.2 Metadata.......................................................................................................196

17.3 Ranh giới an ninh .........................................................................................196

17.4 Sốhiệu phiên bản (Versioning) ...................................................................196

17.5 Manifest........................................................................................................196

17.6 Đa Module Assembly...................................................................................197

17.7 Assembly nội bộ(private assembly) ............................................................198

17.8 Assembly chia sẻ(shared assembly)............................................................198

Chương 18 Attributes và Reflection ........................................................................200

18.1 Attributes......................................................................................................200

18.2 Attribute mặc định (intrinsic attributes).......................................................200

18.3 Attribute do lập trình viên tạo ra..................................................................201

18.4 Reflection .....................................................................................................203

Chương 19 Marshaling và Remoting.......................................................................204

19.1 Miền Ứng Dụng (Application Domains).....................................................204

19.2 Context .........................................................................................................206

19.3 Remoting ......................................................................................................208 

Chương 20 Thread và Sự Đồng Bộ.........................................................................215

20.1 Thread ..........................................................................................................215

20.2 Đồng bộhóa (Synchronization) ...................................................................216

20.3 Race condition và DeadLock .......................................................................221

Chương 21 Luồng dữliệu........................................................................................223

21.1 Tập tin và thưmục .......................................................................................223

21.2 Đọc và ghi dữliệu ........................................................................................230

21.3 Bất đồng bộnhập xuất .................................................................................235

21.4 Serialization..................................................................................................238

21.5 Isolate Storage..............................................................................................244

Chương 22 Lập trình .NET và COM.......................................................................246

22.1 P/Invoke .......................................................................................................246

22.2 Con trỏ..........................................................................................................248

Phần 2 Xây dựng một ứng dụng minh họa..................................................................250

Chương 23 Website dạy học ngôn ngữC# ..............................................................251

23.1 Hiện trạng và yêu cầu...................................................................................251

23.2 Phân tích hướng đối tượng...........................................................................258

23.3 Thiết kếhướng đối tượng.............................................................................262 

Tóm tắt 

Đềtài này tập trung tìm hiểu toàn bộcác khái niệm liên quan đến ngôn ngữC#. 

Bởi vì C# được Microsoft phát triển nhưlà một thành phần của khung ứng dụng 

.NET Framework và hướng Internet nên đềtài này bao gồm hai phần sau: 

Phần 1: Tìm hiểu vềngôn ngữC# 

Việc tìm hiểu bao gồm cảcác kiến thức nền tảng vềcông nghệ.NET Framework, 

chuẩn bịcho các khái niệm liên quan giữa C# và .NET Framework. Sau đó tìm 

hiểu vềbộcú pháp của ngôn ngữnày, bao gồm toàn bộtập lệnh, từkhóa, khái 

niệm vềlập trình hướng đối tượng theo C#, các hỗtrợlập trình hướng 

component ... Sau cùng là cách lập trình C# với ứng dụng Window cho máy để

bàn và C# với các công nghệhiện đại nhưASP.NET. ADO.NET, XML cho lập 

trình Web. 

Phần 2: Xây dựng một ứng dụng 

Phần này là báo cáo về ứng dụng minh họa cho việc tìm hiểu ởtrên. Tên ứng 

dụng là Xây dựng một Website dạy học C#. Đây là ứng dụng Web cài đặt bằng 

ngôn ngữC# và ASP.NET. Trong đó ASP.NET được dùng đểxây dựng giao 

diện tương tác với người dùng; còn C#là ngôn ngữlập trình bên dưới. Ứng dụng 

có thao tác cơsởdữliệu (Microsoft SQL Server) thông quan mô hình 

ADO.NET. 

Phần 1 

Tìm hiểu ngôn ngữC# 

C# và .Net Framework   Gvhd: Nguyễn Tấn Trần Minh Khang

Chương 1 C# và .Net Framework 

Mục tiêu của C# là cung cấp một ngôn ngữlập trình đơn giản, an toàn, hiện đại, 

hướng đối tượng, đặt trọng tâm vào Internet, có khảnăng thực thi cao cho môi 

trường .NET. C# là một ngôn ngữmới, nhưng tích hợp trong nó những tinh hoa của 

ba thập kỷphát triển của ngôn ngữlập trình. Ta có thểdểdàng thầy trong C# có 

những đặc trưng quen thuộc của Java, C++, Visual Basic, … 

Đềtài này đặt trọng tâm giới thiệu ngôn ngữC# và cách dùng nó nhưlà một công 

cụlập trình trên nền tảng .NET. Với ngôn ngữC++, khi học nó ta không cần quan 

tâm đến môi trường thực thi. Với ngôn ngữC#, ta học đểtạo một ứng dụng .NET, 

nếu lơlà ý này có thểbỏlỡquan điểm chính của ngôn ngữnày. Do đó, trong đềtài 

này xét C# tập trung trong ngữcảnh cụthểlà nền tảng .NET của Microsoft và trong 

các ứng dụng máy tính đểbàn và ứng dụng Internet. 

Chương này trình bày chung vềhai phần là ngôn ngữC# và nền tảng .NET, bao 

gồm cảkhung ứng dụng .NET (.NET Framework) 

1.1 Nền tảng của .NET 

Khi Microsoft công bốC# vào tháng 7 năm 2000, việc khánh thành nó chỉlà một 

phần trong sốrất nhiều sựkiện mà nền tảng .Net được công công bố. Nền tảng .Net 

là bô khung phát triển ứng dụng mới, nó cung cấp một giao diện lập trình ứng dụng 

(Application Programming Interface - API) mới mẽcho các dịch vụvà hệ điều hành 

Windows, cụthểlà Windows 2000, nó cũng mang lại nhiều kỹthuật khác nổi bật 

của Microsoft suốt từnhững năm 90. Trong số đó có các dịch vụCOM+, công nghệ

ASP, XML và thiết kếhướng đối tượng, hỗtrợcác giao thức dịch vụweb mới như

SOAP, WSDL và UDDL với trọng tâm là Internet, tất cả được tích hợp trong kiến 

trúc DNA. 

Nền tảng .NET bao gồm bốn nhóm sau: 

1. Một tập các ngôn ngữ, bao gồm C# và Visual Basic .Net; một tập các công cụ

phát triển bao gồm Visual Studio .Net; một tập đầy đủcác thưviện phục vụ

cho việc xây dựng các ứng dụng web, các dịch vụweb và các ứng dụng 

Windows; còn có CLR - Common Language Runtime: (ngôn ngữthực thi 

dùng chung) đểthực thi các đối tượng được xây dựng trên bô khung này. 

2. Một tập các Server Xí nghiệp .Net nhưSQL Server 2000. Exchange 2000, 

BizTalk 2000, … chúng cung cấp các chức năng cho việc lưu trữdữliệu quan 

hệ, thư điện tử, thương mại điện tửB2B, … 

C# và .Net Framework   Gvhd: Nguyễn Tấn Trần Minh Khang

3. Các dịch vụweb thương mại miễn phí, vừa được công bốgần đậy nhưlà dự

án Hailstorm; nhà phát triển có thểdùng các dịch vụnày đểxây dựng các ứng 

dụng đòi hỏi tri thức về định danh người dùng… 

4. .NET cho các thiết bịkhông phải PC như điện thoại (cell phone), thiết bịgame 

1.2 .NET Framework 

.Net hỗtrợtích hợp ngôn ngữ, tức là ta có thểkếthừa các lớp, bắt các biệt lệ, đa 

hình thông qua nhiều ngôn ngữ. .NET Framework thực hiện được việc này nhờvào 

đặc tảCommon Type System - CTS (hệthống kiểu chung) mà tất cảcác thành phần 

.Net đều tuân theo. Ví dụ, mọi thứtrong .Net đều là đối tượng, thừa kếtừlớp gốc 

System.Object. 

Ngoài ra .Net còn bao gồm Common Language Specification - CLS (đặc tảngôn 

ngữchung). Nó cung cấp các qui tắc cơbản mà ngôn ngữmuốn tích hợp phải thỏa 

mãn. CLS chỉra các yêu cầu tối thiểu của ngôn ngữhỗtrợ.Net. Trình biên dịch 

tuân theo CLS sẽtạo các đối tượng có thểtương hợp với các đối tượng khác. Bộthư

viện lớp của khung ứng dụng (Framework Class Library - FCL) có thể được dùng 

bởi bất kỳngôn ngữnào tuân theo CLS. 

.NET Framework nằm ởtầng trên của hệ điều hành (bất kỳhệ điều hành nào không 

chỉlà Windows). .NET Framework bao bao gồm: 

• Bốn ngôn ngữchính thức: C#, VB.Net, C++, và Jscript.NET 

• Common Language Runtime - CLR, nền tảng hướng đối tượng cho phát triển 

ứng dụng Windows và web mà các ngôn ngữcó thểchia sẻsửdụng. 

• Bộthưviện Framework Class Library - FCL. 

Hình 1-1 Kiến trúc khung ứng dụng .Net 

C# và .Net Framework   Gvhd: Nguyễn Tấn Trần Minh Khang

Thành phần quan trọng nhất của .NET Framework là CLR, nó cung cấp môi trường 

cho ứng dụng thực thi, CLR là một máy ảo, tương tựmáy ảo Java. CLR kích hoạt 

đối tượng, thực hiện kiểm tra bảo mật, cấp phát bộnhớ, thực thi và thu dọn chúng. 

Trong Hình 1-1 tầng trên của CLR bao gồm: 

• Các lớp cơsở

• Các lớp dữliệu và XML 

• Các lớp cho dịch vụweb, web form, và Windows form. 

Các lớp này được gọi chung là FCL, Framework Class Library, cung cấp API 

hướng đối tượng cho tất cảcác chức năng của .NET Framework (hơn 5000 lớp). 

Các lớp cơsởtương tựvới các lớp trong Java. Các lớp này hỗtrợcác thao tác nhập 

xuất, thao tác chuổi, văn bản, quản lý bảo mật, truyền thông mạng, quản lý tiểu trình 

và các chức năng tổng hợp khác … 

Trên mức này là lớp dữliệu và XML. Lớp dữliệu hỗtrợviệc thao tác các dữliệu 

trên cơsởdữliệu. Các lớp này bao gồm các lớp SQL (Structure Query Language: 

ngôn ngữtruy vấn có cấu trúc) cho phép ta thao tác dữliệu thông qua một giao tiếp 

SQL chuẩn. Ngoài ra còn một tập các lớp gọi là ADO.Net cũng cho phép thao tác 

dữliệu. Lớp XML hỗtrợthao tác dữliệu XML, tìm kiếm và diễn dịch XML. 

Trên lớp dữliệu và XML là lớp hỗtrợxây dựng các ứng dụng Windows (Windows 

forms), ứng dụng Web (Web forms) và dịch vụWeb (Web services). 

1.3 Biên dịch và ngôn ngữtrung gian (MSIL) 

Với .NET chương trình không biên dịch thành tập tin thực thi, mà biên dịch thành 

ngôn ngữtrung gian (MSIL - Microsoft Intermediate Language, viết tắt là IL), sau 

đó chúng được CLR thực thi. Các tập tin IL biên dịch từC# đồng nhất với các tập 

tin IL biên dịch từngôn ngữ.Net khác. 

Khi biên dịch dựán, mã nguồn C# được chuyển thành tập tin IL lưu trên đĩa. Khi 

chạy chương trình thì IL được biên dịch (hay thông dịch) một lần nữa bằng trình 

Just In Time- JIT, khi này kết quảlà mã máy và bộxửlý sẽthực thi. 

Trình biên dịch JIT chỉchạy khi có yêu cầu. Khi một phương thức được gọi, JIT 

phân tích IL và sinh ra mã máy tối ưu cho từng loại máy. JIT có thểnhận biết mã 

nguồn đã được biên dịch chưa, đểcó thểchạy ngay ứng dụng hay phải biên dịch lại. 

CLS có nghĩa là các ngôn ngữ.Net cùng sinh ra mã IL. Các đối tượng được tạo theo 

một ngôn ngữnào đó sẽ được truy cập và thừa kếbởi các đối tượng của ngôn ngữ

khác. Vì vậy ta có thểtạo được một lớp cơsởtrong VB.Net và thừa kếnó từC#. 

C# và .Net Framework   Gvhd: Nguyễn Tấn Trần Minh Khang

1.4 Ngôn ngữC# 

C# là một ngôn ngữrất đơn giản, với khoảng 80 từkhoá và hơn mười kiểu dữliệu 

dựng sẵn, nhưng C# có tính diễn đạt cao. C# hỗtrợlập trình có cấu trúc, hướng đối 

tượng, hướng thành phần (component oriented). 

Trọng tâm của ngôn ngữhướng đối tượng là lớp. Lớp định nghĩa kiểu dữliệu mới, 

cho phép mởrộng ngôn ngữtheo hướng cần giải quyết. C# có những từkhoá dành 

cho việc khai báo lớp, phương thức, thuộc tính (property) mới. C# hỗtrợ đầy đủ

khái niệm trụcột trong lập trình hướng đối tượng: đóng gói, thừa kế, đa hình. 

Định nghĩa lớp trong C# không đòi hỏi tách rời tập tin tiêu đềvới tập tin cài đặt như

C++. Hơn thế, C# hỗtrợkiểu sưu liệu mới, cho phép sưu liệu trực tiếp trong tập tin 

mã nguồn. Đến khi biên dịch sẽtạo tập tin sưu liệu theo định dạng XML. 

C# hỗtrợkhái niệm giao diện, interfaces(tương tựJava). Một lớp chỉcó thểkế

thừa duy nhất một lớp cha nhưng có thếcài đặt nhiều giao diện. 

C# có kiểu cấu trúc,struct (không giống C++). Cấu trúc là kiểu hạng nhẹvà bịgiới 

hạn.Cấu trúc không thểthừa kếlớp hay được kếthừa nhưng có thểcài đặt giao diện. 

C# cung cấp những đặc trưng lập trình hướng thành phần nhưproperty, sựkiện và 

dẫn hướng khai báo (được gọi là attribute). Lập trình hướng component được hỗtrợ

bởi CLR thông qua siêu dữliệu (metadata). Siêu dữliệu mô tảcác lớp bao gồm các 

phương thức và thuộc tính, các thông tin bảo mật …. 

Assembly là một tập hợp các tập tin mà theo cách nhìn của lập trình viên là các thư

viện liên kết động (DLL) hay tập tin thực thi (EXE). Trong .NET một assembly là 

một đon vịcủa việc tái sửdụng, xác định phiên bản, bảo mật, và phân phối. CLR 

cung cấp một sốcác lớp đểthao tác với assembly. 

C# cũng cho truy cập trực tiếp bộnhớdùng con trỏkiểu C++, nhưng vùng mã đó 

được xem nhưkhông an toàn. CLR sẽkhông thực thi việc thu dọn rác tự động các 

đối tượng được tham chiếu bởi con trỏcho đến khi lập trình viên tựgiải phóng. 

Khởi đầu Gvhd: Nguyễn Tấn Trần Minh Khang

Chương 2 Khởi đầu 

Chương này ta sẽtạo, biên dịch và chạy chương trình “Hello World” bằng ngôn ngữ

C#. Phân tích ngắn gọn chương trình đểgiới thiệu các đặc trưng chính yếu trong 

ngôn ngữC#. 

Ví dụ2-1 Chương trình Hello World 

classHelloWorld 

static voidMain( ) 

// sửdụng đối tượng console của hệthống 

System.Console.WriteLine("Hello World"); 

Sau khi biên dịch và chạy HelloWorld, kết quảlà dòng chữ“Hello World” hiển thị

trên màn hình. 

2.1 Lớp, đối tượng và kiểu 

Bản chất của lập trình hướng đối tượng là tạo ra các kiểu mới. Một kiểubiểu diễn 

một vật gì đó. Giống với các ngôn ngữlập trình hướng đối tượng khác, một kiểu 

trong C# cũng định nghĩa bằng từkhoá class(và được gọi là lớp) còn thểhiện của 

lớp được gọi là đối tượng. 

Xem Ví dụ2-1 ta thấy cách khai báo một lớp HelloWorld. Ta thấy ngay là cách 

khai báo và nội dung của một lớp hoàn toàn giống với ngôn ngữJava và C++, chỉ

có khác là cuối khai báo lớp không cần dấu “;” 

2.1.1 Phương thức 

Các hành vi của một lớp được gọi là các phương thức thành viên (gọi tắt là phương 

thức) của lớp đó. Một phương thứclà một hàm(phương thức thành viên còn gọi là 

hàm thành viên). Các phương thức định nghĩa những gì mà một lớp có thểlàm. 

Cách khai báo, nội dung và cách sửdụng các phương thức giống hoàn toàn với Java 

và C++. Trong ví dụtrên có một phương thức đặc biệt là phương thức Main()(như

hàm main()trong C++) là phương thức bắt đầu của một ứng dụng C#, có thểtrảvề

kiểu voidhay int. Mỗi một chương trình (assembly) có thểcó nhiều phương thức 

Main nhưng khi đó phải chỉ định phương thức Main() nào sẽbắt đầu chương trình. 

Khởi đầu Gvhd: Nguyễn Tấn Trần Minh Khang

2.1.2 Các ghi chú 

C# có ba kiểu ghi chú trong đó có hai kiểu rất quen thuộc của C++ là dùng: "//"và 

"/* … */". Ngoài ra còn một kiểu ghi chú nữa sẽtrình bày ởcác chương kế. 

Ví dụ2-2 Hai hình thức ghi chú trong C# 

classHelloWorld 

static voidMain( ) // Đây là ghi trên một dòng 

/* Bắt đầu ghi chú nhiều dòng 

Vẫn còn trong ghi chú    

Kết thúc ghi chú bằng */ 

System.Console.WriteLine("Hello World"); 

2.1.3 Ứng dụng dạng console 

“Hello World” là một ứng dụng console. Các ứng dụng dạng này thường không có 

giao diện người dùng đồhọa Các nhập xuất đều thông qua các console chuẩn (dạng 

dòng lệnh nhưDOS). 

Trong ví dụtrên, phương thức Main()viết ra màn hình dòng “Hello World”. Do 

màn hình quản lý một  đối tượng Console,  đối tượng này có phương thức 

WriteLine()cho phép đặt một dòng chữlên màn hình. Đểgọi phương thức này ta 

dùng toán tử“.”, nhưsau: Console.WriteLine(…). 

2.1.4 Namespaces - Vùng tên 

Console là một trong rất nhiều (cảngàn) lớp trong bộthưviện .NET. Mỗi lớp đều 

có tên và nhưvậy có hàng ngàn tên mà lập trình viên phải nhớhoặc phải tra cứu 

mỗi khi sửdụng. Vấn đềlà phải làm sao giảm bớt lượng tên phải nhớ. 

Ngoài vấn đềphải nhớquá nhiều tên ra, còn một nhận xét sau: một sốlớp có mối 

liên hệnào đó vềmặt ngữnghĩa, ví dụnhưlớp Stack, Queue, Hashtable … là các 

lớp cài đặt cấu trúc dữliệu túi chứa. Nhưvậy có thểnhóm những lớp này thành một 

nhóm và thay vì phải nhớtên các lớp thì lập trình viên chỉcần nhớtên nhóm, sau đó 

có thểthực hiện việc tra cứu tên lớp trong nhóm nhanh chóng hơn. Nhóm là một 

vùng têntrong C#. 

Một vùng tên có thểcó nhiều lớp và vùng tên khác. Nếu vùng tên A nằm trong vùng 

tên B, ta nói vùng tên A là vùng tên con của vùng tên B. Khi đó các lớp trong vùng 

tên A được ghi nhưsau: B.A.Tên_lớp_trong_vùng_tên_A

System là vùng tên chứa nhiều lớp hữu ích cho việc giao tiếp với hệthống hoặc các 

lớp công dụng chung nhưlớp Console, Math, Exception….Trong ví dụHelloWorld 

trên, đối tượng Console được dùng nhưsau: 

System.Console.WriteLine("Hello World"); 

Khởi đầu Gvhd: Nguyễn Tấn Trần Minh Khang

2.1.5 Toán tửchấm “.” 

Nhưtrong Ví dụ2-1 toán tửchấm được dùng đểtruy suất dữliệu và phương thức 

một lớp (nhưConsole.WriteLine()), đồng thời cũng dùng đểchỉ định tên lóp trong 

một vùng tên (nhưSystem.Console). 

Toán tửdấu chấm cũng được dùng đểtruy xuất các vùng tên con của một vùng tên 

Vùng_tên.Vùng_tên_con.Vùng_tên_con_con 

2.1.6 Từkhoá using 

Nếu chương trình sửdụng nhiều lần phương thức Console.WriteLine, từSystem sẽ

phải viết nhiều lần. Điều này có thểkhiến lập trình viên nhàm chán. Ta sẽkhai báo 

rằng chương trình có sửdụng vùng tên System, sau đó ta dùng các lớp trong vùng 

tên System mà không cần phải có từSystem đi trước. 

Ví dụ2-3 Từkhóa using 

// Khai báo chương trình có sửdụng vùng tên System 

usingSystem; 

classHelloWorld 

static voidMain( ) 

// Console thuộc vùng tên System 

Console.WriteLine("Hello World"); 

2.1.7 Phân biệt hoa thường 

Ngôn ngữC# cũng phân biệt chữhoa thường giống nhưJava hay C++ (không như

VB). Ví dụnhư  WriteLinekhác với  writeLinevà cảhai cùng khác với 

WRITELINE. Tên biến, hàm, hằng … đều phân biệt chữhoa chữthường. 

2.1.8 Từkhoá static 

Trong Ví dụ2-1 phương thức Main() được khai báo kiểu trảvềlà void và dùng từ

khoá static. Từkhoá static cho biết là ta có thểgọi phương thức Main()mà không 

cần tạo một đối tượng kiểu HelloWorld. 

2.2 Phát triển “Hello World” 

Có hai cách đểviết, biên dịch và chạy chương trình HelloWorld là dùng môi trưởng 

phát triển tích hợp (IDE) Visual Studio .Net hay viết bằng trình soạn thảo văn bản 

và biên dịch bằng dòng lệnh. IDE Vs.Net dễdùng hơn. Do đó, trong đềtài này chỉ

trình bày theo hướng làm việc trên IDE Visual Studio .Net. 

Khởi đầu Gvhd: Nguyễn Tấn Trần Minh Khang

2.2.1 Soạn thảo “Hello World” 

Đểtạo chương trình “Hello World” trong IDE, ta chọn Visual Studio .Net từthanh 

thực đơn. Tiếp theo trên màn hình của IDE chọn File > New > Projecttừthanh 

thực đơn, theo đó xuất hiện một cửa sổnhưsau: 

Hình 2-1 Tạo một ứng dụng console trong VS.Net 

Đểtạo chương trình “Hello World” ta chọn Visual C# Project > Console 

Application, điền HelloWorldtrong ô Name, chọn đường dẫn và nhấn OK. Một cửa 

sổsoạn thảo xuất hiện. 

Khởi đầu Gvhd: Nguyễn Tấn Trần Minh Khang

10 

Hình 2-2 Cửa sổsoạn thảo nội dung mã nguồn 

Vs.Net tựtạo một sốmã, ta cần chỉnh sửa cho phù hợp với chương trình của mình. 

2.2.2 Biên dịch và chạy “Hello World” 

Sau khi đã đầy đủmã nguồn ta tiến hành biên dịch chương trình: nhấn “Ctrl–Shift–

B” hay chọn Build > Build Solution.Kiểm tra xem chương trình có lỗi không ởcủa 

sổ Outputcuối màn hình. Khi biên dịch chương trình nó sẽlưu lại thành tập tin .cs. 

Chạy chương trình bằng “Ctrl–F5” hay chọn Debug > Start Without Debugging. 

2.2.3 Trình gởrối của Visual Studio .Net 

Trình gỡrối của VS.Net rất mạnh hữu ích. Ba kỹnăng chính yếu đểsửdụng của 

trình gởrối là: 

• Cách đặt điểm ngắt (breakpoint) và làm sao chạy cho đến điểm ngắt 

• Làm thếnào chạy từng bước và chạy vượt qua một phương thức. 

• Làm sao đểquan sát và hiệu chỉnh giá trịcủa biến, dữliệu thành viên, … 

Cách đơn giản nhất để đặt điểm ngắt là bấm chuột trái vào phía lềtrái, tại đó sẽhiện 

lên một chấm đỏ. 

Khởi đầu Gvhd: Nguyễn Tấn Trần Minh Khang

11 

Hình 2-3 Minh họa một điểm ngắt 

Cách dùng trình gởrối hoàn toàn giống với trình gởrối trong VS 6.0. Nó cho phép 

ta dừng lại ởmột vịtrí bất kỳ, cho ta kiểm tra giá trịtức thời bằng cách di chuyển 

chuột đến vịtrịbiến. Ngoài ra, khi gởrối ta cũng có thểxem giá trịcác biến thông 

qua cửa sổ Watchvà Local. 

Đểchạy trong chế độgởrối ta chọn Debug Æ Starthay nhấn F5, muốn chạy từng 

bước ta bấm F11và chạy vượt qua một phương thức ta bấm F10. 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

12 

Chương 3 Những cơsởcủa ngôn ngữC# 

Trong chương này sẽtrình bày vềhệthống kiểu trong C#; phân biệt kiểu dựng sẵn 

(int, long, bool, …) với các kiểu do người dùng định nghĩa. Ngoài ra, chương này 

cũng sẽtrình bày cách tạo và dùng biến, hằng; giới thiệu kiểu liệt kê, chuỗi, kiểu 

định danh, biểu thức, và câu lệnh. Phần hai của chương trình bày vềcác cấu trúc 

điều kiện và các toán tửlogic, quan hệ, toán học, … 

3.1 Các kiểu 

C# buộc phải khai báo kiểu của đối tượng được tạo. Khi kiểu được khai báo rõ ràng, 

trình biên dịch sẽgiúp ngăn ngừa lỗi bằng cách kiểm tra dữliệu được gán cho đối 

tượng có hợp lệkhông, đồng thời cấp phát đúngkích thước bộnhớcho đối tượng. 

C# phân thành hai loại: loai dữliệu dựng sẵn và loại do người dùng định nghĩa. 

C# cũng chia tập dữliệu thành hai kiểu: giá trịvà tham chiếu. Biến kiểu giá trị được 

lưu trong vùng nhớstack, còn biến kiểu tham chiếu được lưu trong vùng nhớheap. 

C# cũng hỗtrợkiểu con trỏcủa C++, nhưng ít khi được sửdụng. Thông thường con 

trỏchỉ được sửdụng khi làm việc trực tiếp với Win API hay các đối tượng COM. 

3.1.1 Loại dữliệu định sẳn 

C# có nhiểu kiểu dữliệu định sẳn, mỗi kiểu ánh xạ đến một kiểu được hổtrợbởi 

CLS (Commom Language Specification), ánh xạ để đảm bảo rằng đối tượng được 

tạo trong C# không khác gì đối tượng được tạo trong các ngôn ngữ.NET khác Mỗi 

kiểu có một kích thước cố định được liệt kê trong bảng sau 

Bảng 3-1 Các kiểu dựng sẵn 

Kiểu

Kích thước 

(byte) 

Kiểu .Net  Mô tả- giá trị

byte  1  Byte  Không dấu (0..255) 

char 1 Char Mã ký thựUnicode 

bool 1 Boolean true hay false 

sbyte 1 Sbyte Có dấu (-128 .. 127) 

short 2 Int16 Có dấu (-32768 .. 32767) 

ushort 2 Uint16 Không dấu (0 .. 65535) 

int 4 Int32 Có dấu (-2147483647 .. 2147483647) 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

13 

uint 4 Uint32 Không dấu (0 .. 4294967295) 

float 4 Single Sốthực (≈±1.5*10-45 .. ≈±3.4*1038) 

double 8 Double Sốthực (≈±5.0*10-324 .. ≈±1.7*10308) 

decimal 8 Decimal sốcó dấu chấm tĩnh với 28 ký sốvà dấu chấm 

long 

Int64 Sốnguyên có dấu (- 9223372036854775808 .. 

9223372036854775807) 

ulong 8 Uint64 Sốnguyên không dấu (0 .. 0xffffffffffffffff.) 

3.1.1.1 Chọn một kiểu định sẵn 

Tuỳvào từng giá trịmuốn lưu trữmà ta chọn kiểu cho phù hợp. Nếu chọn kiểu quá 

lớn so với các giá trịcần lưu sẽlàm cho chương trình đòi hỏi nhiều bộnhớvà chạy 

chậm. Trong khi nếu giá trịcần lưu lớn hơn kiểu thực lưu sẽlàm cho giá trịcác biến 

bịsai và chương trình cho kết quảsai. 

Kiểu charbiểu diễn một ký tựUnicode. Ví dụ“\u0041” là ký tự“A” trên bảng 

Unicode. Một sốký tự đặc biệt được biểu diễn bằng dấu “\” trước một ký tựkhác. 

Bảng 3-2 Các ký tự đặc biệt thông dụng 

Ký tự  Nghĩa 

\’  dầu nháy đơn 

\”  dấu nháy đôi 

\\  dấu chéo ngược “\” 

\0  Null 

\a  Alert 

\b  lùi vềsau 

\f  Form feed 

 xuống dòng 

\r  về đầu dòng 

\t  Tab ngang 

\v  Tab dọc

3.1.1.2 Chuyển đổi kiểu định sẳn 

Một đối tượng có thểchuyển từkiểu này sang kiểu kia theo hai hình thức: ngầm 

hoặc tường minh. Hình thức ngầm được chuyển tự động còn hình thức tường minh 

cần sựcan thiệp trực tiếp của người lập trình (giống với C++ và Java). 

shortx = 5; 

inty ; 

y = x; // chuyển kiểu ngầm định - tự động 

x = y; // lỗi, không biên dịch được 

x = (short) y; // OK 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

14 

3.2 Biến và hằng 

Biến dùng đểlưu trữdữliệu. Mỗi biến thuộc vềmột kiểu dữliệu nào đó. 

3.2.1 Khởi tạo trước khi dùng 

Trong C#, trước khi dùng một biến thì biến đó phải được khởi tạo nếu không trình 

biên dịch sẽbáo lỗi khi biên dịch. Ta có thểkhai báo biến trước, sau đó khởi tạo và 

sửdụng; hay khai báo biến và khởi gán trong lúc khai báo. 

intx; // khai báo biến trước

x = 5; // sau đó khởi gán giá trịvà sửdụng

inty = x; // khai báo và khởi gán cùng lúc 

3.2.2 Hằng 

Hằng là một biến nhưng giá trịkhông thay đổi theo thời gian. Khi cần thao tác trên 

một giá trịxác định ta dùng hằng. Khai báo hằng tương tựkhai báo biến và có thêm 

từkhóa const ởtrước. Hằng một khi khởi động xong không thểthay đổi được nữa. 

constint HANG_SO = 100; 

3.2.3 Kiểu liệt kê 

Enum là một cách thức để đặt tên cho các trịnguyên (các trịkiểu sốnguyên, theo 

nghĩa nào đó tương tựnhưtập các hằng), làm cho chương trình rõ ràng, dễhiểu 

hơn. Enum không có hàm thành viên. Ví dụtạo một enum tên là Ngay nhưsau: 

enum Ngay {Hai, Ba, Tu, Nam, Sau, Bay, ChuNhat}; 

Theo cách khai báo này enum ngày có bảy giá trịnguyên đi từ0 = Hai, 1 = Ba, 2 = 

Tư… 7 = ChuNhat. 

Ví dụ3-1 Sửdụng enum Ngay 

using System; 

public class EnumTest 

enum Ngay {Hai, Ba, Tu, Nam, Sau, Bay, ChuNhat }; 

public static void Main() 

int x = (int) Ngay.Hai; 

int y = (int) Ngay.Bay; 

Console.WriteLine("Thu Hai = {0}", x); 

Console.WriteLine("Thu Bay = {0}", y); 

Kết quả

Thu Hai = 0 

Thu Bay = 5 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

15 

Mặc định enum gán giá trị đầu tiên là 0 các trịsau lớn hơn giá trịtrước một đơn vị, 

và các trịnày thuộc kiểu int. Nếu muốn thay đổi trịmặc định này ta phải gán trị

mong muốn. 

Ví dụ3-2 Sửdụng enum Ngay (2) 

usingSystem; 

namespaceConsoleApplication 

enumNgay: byte { Hai=2,Ba,Tu,Nam,Sau,Bay,ChuNhat=10 }; 

classEnumTest 

static voidMain(string[] args) 

bytex = (byte)Ngay.Ba; 

bytey = (byte)Ngay.ChuNhat; 

   Console.WriteLine("Thu Ba = {0}", x); 

   Console.WriteLine("Chu Nhat = {0}", y); 

Console.Read(); 

Kết quả: 

Thu Ba = 3 

Chu Nhat = 10 

Kiểu enum ngày được viết lại với một sốthay đổi, giá trịcho Hai là 2, giá trịcho Ba 

là 3 (Hai + 1) …, giá trịcho ChuNhat là 10, và các giá trịnày sẽlà kiểu byte. 

Cú pháp chung cho khai báo một kiểu enum nhưsau 

[attributes] [modifiers] enum identifier [:base-type] 

enumerator-list 

}; 

attributes(tùy chọn): các thông tin thêm (đềcập sau) 

modifiers(tùy chọn): public, protected, internal, private 

(các bổtừxác định phạm vi truy xuất) 

identifer: tên của enum 

base_type(tùy chọn): kiểu số, ngoại trừchar 

enumerator-list: danh sách các thành viên. 

3.2.4 Chuỗi 

Chuỗi là kiểu dựng sẵn trong C#, nó là một chuổi các ký tự đơn lẻ. Khi khai báo 

một biến chuỗi ta dùng từkhoá string. Ví dụkhai báo một biến string lưu chuỗi 

"Hello World" 

stringmyString = "Hello World"; 

3.2.5 Định danh 

Định danh là tên mà người lập trình chọn đại diện một kiểu, phương thức, biến, 

hằng, đối tượng… của họ. Định danh phảibắt đầu bằng một ký tựhay dấu “_”. 

Định danh không được trùng với từkhoá C# và phân biệt hoa thường. 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

16 

3.3 Biểu thức 

Bất kỳcâu lệnh định lượng giá trị được gọi là một biểu thức (expression). Phép gán 

sau cũng được gọi là một biểu thức vì nó định lượng giá trị được gán (là 32) 

x = 32; 

vì vậy phép gán trên có thể được gán một lần nữa nhưsau 

y = x = 32; 

Sau lệnh này y có giá trịcủa biểu thức x = 32 và vì vậy y = 32. 

3.4 Khoảng trắng 

Trong C#, khoảng trống, dấu tab, dấu xuống dòng đều được xem là khoảng trắng 

(whitespace). Do đó, dấu cách dù lớn hay nhỏ đều nhưnhau nên ta có: 

x = 32; 

cũng như

x = 32; 

Ngoại trừkhoảng trắng trong chuỗi ký tựthì có ý nghĩa riêng của nó. 

3.5 Câu lệnh 

Cũng nhưtrong C++ và Java một chỉthịhoàn chỉnh thì được gọi là một câu lệnh 

(statement). Chương trình gồm nhiều câu lệnh, mỗi câu lệnh kết thúc bằng dấu “;”. 

Ví dụ: 

intx; // là một câu lệnh 

x = 23; // một câu lệnh khác 

Ngoài các câu lệnh bình thường nhưtrên, có các câu lệnh khác là: lệnh rẽnhánh 

không điều kiện, rẽnhánh có điều kiện và lệnh lặp. 

3.5.1 Các lệnh rẽnhánh không điều kiện 

Có hai loại câu lệnh rẽnhánh không điều kiện. Một là lệnh gọi phương thức: khi 

trình biên dịch thấy có lời gọi phương thức nó sẽtạm dừng phương thức hiện hành 

và nhảy đến phương thức được gọi cho đến hết phương thức này sẽtrởvềphương 

thức cũ. 

Ví dụ3-3 Gọi một phương thức 

usingSystem; 

classFunctions 

static voidMain( ) 

  Console.WriteLine("In Main! Calling SomeMethod( )..."); 

SomeMethod( ); 

  Console.WriteLine("Back in Main( )."); 

static voidSomeMethod( ) 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

17 

Console.WriteLine("Greetings from SomeMethod!"); 

Kết quả: 

In Main! Calling SomeMethod( )... 

Greetings from SomeMethod! 

Back in Main( ). 

Cách thứhai đểtạo các câu lệnh rẽnhánh không điều kiện là dùng từkhoá: goto, 

break, continue, return,hay throw. Cách từkhóa này sẽ được giới thiệu trong các 

phần sau. 

3.5.2 Lệnh rẽnhánh có điều kiện 

Các từkhóa if-else, while, do-while, for, switch-case, dùng để điều khiển dòng chảy 

chương trình. C# giữlại tất cảcác cú pháp của C++, ngoại trừswitch có vài cải tiến. 

3.5.2.1 Lệnh If .. else … 

Cú pháp: 

if( biểu thức logic ) 

khối lệnh; 

hoặc 

if( biểu thức logic ) 

khối lệnh 1; 

else

khối lệnh 2; 

Ghi chú: Khối lệnhlà một tập các câu lện trong cặp dấu “{…}”. Bất kỳ

nơi đâu có câu lệnh thì ở đó có thểviết bằng một khối lệnh. 

Biểu thức logic là biểu thức cho giá trịdúng hoặc sai (true hoặc false). Nếu “biểu 

thức logic” cho giá trị đúng thì “khối lệnh” hay “khối lệnh 1” sẽ được thực thi, 

ngược lại “khối lệnh 2” sẽthực thi. Một điểm khác biệt với C++ là biểu thức trong 

câu lệnh if phải là biểu thức logic, không thểlà biểu thức số. 

3.5.2.2 Lệnh switch 

Cú pháp: 

switch( biểu_thức_lựa_chọn ) 

casebiểu_thức_hằng : 

 khối lệnh; 

 lệnh nhảy; 

[ default: 

 khối lệnh; 

 lệnh nhảy; ] 

Biểu thức lựa chọn là biểu thức sinh ra trịnguyên hay chuỗi. Switch sẽso sánh 

biểu_thức_lựa_chọn với các biểu_thức_hằng đểbiết phải thực hiện với khối lệnh 

nào. Lệnh nhảy nhưbreak, goto…đểthoát khỏi câu switch và bắt buộc phải có. 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

18 

intnQuyen = 0; 

switch( sQuyenTruyCap ) 

case“Administrator”: 

 nQuyen = 1; 

break; 

case“Admin”: 

 goto case “Administrator”; 

default: 

 nQuyen = 2; 

break; 

3.5.3 Lệnh lặp 

C# cung cấp các lệnh lặp giống C++ như for, while, do-whilevà lệnh lặp mới 

foreach. Nó cũng hổtrợcác câu lệnh nhảy như: goto, break, continuevà return. 

3.5.3.1 Lệnh goto 

Lệnh gotocó thểdùng đểtạo lệnh nhảy nhưng nhiều nhà lập trình chuyên nghiệp 

khuyên không nên dùng câu lệnh này vì nó phá vỡtính cấu trúc của chương trình. 

Cách dùng câu lệnh này nhưsau: (giống nhưtrong C++) 

1. Tạo một nhãn 

2. goto đến nhãn đó. 

3.5.3.2 Vòng lặp while 

Cú pháp: 

while( biểu_thức_logic ) 

khối_lệnh; 

Khối_lệnh sẽ được thực hiện cho đến khi nào biểu thức còn đúng. Nếu ngay từ đầu 

biểu thức sai, khối lệnh sẽkhông được thực thi. 

3.5.3.3 Vòng lặp do … while 

Cú pháp: 

do

khối_lệnh 

while( biếu_thức_logic ) 

Khác với while khối lệnh sẽ được thực hiện trước, sau đó biệu thức được kiểm tra. 

Nếu biểu thức đúng khối lệnh lại được thực hiện. 

3.5.3.4 Vòng lặp for 

Cú pháp: 

for( [khởi_tạo_biến_đếm]; [biểu_thức]; [gia_tăng_biến_đếm] ) 

khối lệnh; 

Ví dụ3-4 Tính tổng các sốnguyên từa đến b 

inta = 10; int b = 100; int nTong = 0; 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

19 

for( int i = a; i <= b; i++ ) 

 nTong += i; 

Câu lệnh lặp foreachsẽ được trình bày ởcác chương sau. 

3.5.3.5 Câu lệnh break, continue, và return 

Cảba câu lệnh break, continue, vàreturnrất quen thuộc trong C++ và Java, trong 

C#, ý nghĩa và cách sửdụng chúng hoàn toàn giống với hai ngôn ngữnày. 

3.6 Toán tử

Các phép toán +, -, *, / là một ví dụvềtoán tử. Áp dụng các toán tửnày lên các biến 

kiểu sốta có kết quảnhưviệc thực hiện các phép toán thông thường. 

inta = 10; 

intb = 20; 

intc = a + b; // c = 10 + 20 = 30 

C# cung cấp cấp nhiều loại toán tửkhác nhau đểthao tác trên các kiểu biến dữliệu, 

được liệt kê trong bảng sau theo từng nhóm ngữnghĩa. 

Bảng 3-3 Các nhóm toán tửtrong C# 

Nhóm toán tử  Toán tử  Ý nghĩa 

Toán học  + - * / %  cộng , trừ, nhân chia, lấy phần dư

Logic  & | ^ ! ~ && || true 

false 

phép toán logic và thao tác trên bit 

Ghép chuỗi  +  ghép nối 2 chuỗi 

Tăng, giảm  ++, --  tăng / giảm toán hạng lên / xuống 1. Đứng 

trước hoặc sau toán hạng. 

Dịch bit  << >>  dịch trái, dịch phải 

Quan hệ  == != < > <= >=  bằng, khác, nhỏ/lớn hơn, nhỏ/lớn hơn 

hoặc bằng 

Gán  = += -= *= /= %= &= 

|= ^= <<= >>= 

phép gán 

Chỉsố  []  cách truy xuất phần tửcủa mảng 

Ép kiểu  () 

Indirection và 

Address 

* -> [] &  dùng cho con trỏ

3.6.1 Toán tửgán (=) 

Toán tửnày cho phép thay đổi các giá trịcủa biến bên phải toán tửbằng giá trịbên 

trái toán tử. 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

20 

3.6.2 Nhóm toán tửtoán học 

C# dùng các toàn tửsốhọc với ý nghĩa theo đúng tên của chúng như: + (cộng), – 

(trừ) , * (nhân) và / (chia). Tùy theo kiểu của hai toán hạng mà toán tửtrảvềkiểu 

tương ứng. Ngoài ra, còn có toán tử %(lấy phần dư) được sửdụng trong các kiểu số

nguyên. 

3.6.3 Các toán tửtăng và giảm 

C# cũng kếthừa từC++ và Java các toán tử: +=,-=, *=, /= , %= nhằm làm đơn 

giản hoá. Nó còn kếthừa các toán tử tiền tốvà hậu tố(như biến++, hay ++biến) để

giảm bớt sựcồng kềnh trong các toán tửcổ điển. 

3.6.4 Các toán tửquan hệ

Các toán tửquan hệ được dùng đểso sánh hai giá trịvới nhau và kết quảtrảvềcó 

kiểu Boolean. Toán tửquan hệgồm có: == (so sánh bằng), != (so sánh khác), > (so 

sánh lớn hơn), >= (lớn hơn hay bằng), < (so sánh nhỏhơn), <= (nhỏhơn hay bằng). 

3.6.5 Các toán tửlogic 

Các toán tửlogic gồm có: && (và), || (hoặc), ! (phủ định). Các toán tửnày được 

dùng trong các biểu thức điều kiện đểkết hợp các toán tửquan hệtheo một ý nghĩa 

nhất định. 

3.6.6 Thứtựcác toán tử

Đối với các biểu thức toán, thứtự ưu tiên là thứtự được qui định trong toán học. 

Còn thứtự ưu tiên thực hiện của các nhóm toán tử được liệt kê theo bảng dưới đây 

Bảng 3-4 Thứtự ưu tiên của các nhóm toán tử(chiều ưu tiên từtrên xuống) 

Nhóm toán tử  Toán tử  Ý nghĩa 

Primary (chính)  {x} x.y f(x) a[x] x++ 

x-- 

Unary  + - ! ~ ++x –x (T)x 

Nhân  * / %  Nhân, chia, lấy phần dư

Cộng  + -  cộng, trù 

Dịch bít  << >>  Dịch trái, dịch phải 

Quan hệ  < > <= >= is  nhỏhơn, lớn hơn, nhỏhơn hay bằng, 

lớn hơn hay bằng và là

Bằng  == !=  bằng, khác 

Logic trên bit AND  &  Vàtrên bit. 

XOR  ^  Xor trên bit 

OR  |  hoặctrên bit 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

21 

Điều kiện AND  &&  Vàtrên biểu thức điều kiện 

Điều kiện OR  ||  Hoặctrên biểu thức điều kiện 

Điều kiện  ?:  điều kiệntương tự if

Assignment  = *= /= %= += -= <<= 

=>> &= ^= |= 

3.6.7 Toán tửtam phân 

Cú pháp: 

<biểu thức điều kiện>? <biểu thức 1>: <biểu thức 2>; 

Ý nghĩa: 

Nếu biểu thức điều kiện đúng thì thực hiện biểu thức 1. 

Nếu sai thì thực hiện biểu thức 2. 

3.7 Tạo vùng tên 

Như đã có giải thích trong phân tích ví dụHelloWorld, vùng tên là một cách tổchức 

mã nguồn thành các nhóm có ngữnghĩa liên quan. Ví dụ: 

Trong mô hình kiến trúc 3 lớp (3 tầng, tiếng Anh là 3 – tier Architecture) chia một 

ứng dụng ra thành 3 tầng: tầng giao diện, tầng nghiệp vụvà tầng dữliệu 

(Presentation, Bussiness và Data). Ta có thểchia dựán thành 3 vùng tên tương ứng: 

Presentation, Bussiness và Data. Các vùng tên này chứa các lớp thuộc vềtầng của 

mình. 

Một vùng tên chứa các lớp và các vùng tên con khác. Vậy trong ví dụtrên ta sẽtạo 

một vùng tên chung cho ứng dụng là MyApplication vàba vùng tên kia sẽlà ba 

vùng tên con của vùng tên MyApplication. Cách này giải quyết được trường hợp 

nếu ta có nhiều dựán mà chỉcó 3 vùng tên và dẫn đến việc không biết một lớp 

thuộc vùng tên Data nhưng không biết thuộc dựán nào. 

Vùng tên con được truy xuất thông qua tên vùng tên cha cách nhau bằng dấu chấm. 

Đểkhai báo vùng tên ta sửdụng từkhóa namespace. Ví dụdưới đây là 2 cách khai 

báo các vùng tên trong ví dụ ởtrên. 

Sô ñoà caây vuøng teân

vuøng teân con Caùc lôùp

Presentation

vuøng teân con Caùc lôùp

Bussiness

vuøng teân con Caùc lôùp

Data

MyApplication

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

22 

Cách 1 

namespace MyApplication 

namespace Presentation 

// khai báo lớp 

// khai báo vùng tên con 

namespace Bussiness 

// khai báo lớp 

// khai báo vùng tên con 

namespace Data 

// khai báo lớp 

// khai báo vùng tên con 

Cách 2 

namespace MyApplication.Presentation 

// khai báo lớp 

// khai báo vùng tên con 

namespace MyApplication.Bussiness 

// khai báo lớp 

// khai báo vùng tên con 

namespace MyApplication.Data 

// khai báo lớp 

// khai báo vùng tên con 

Cách khai báo vùng tên thứnhất chỉtiện nếu các vùng tên nằm trên cùng một tập 

tin. Cách thứhai tiện lợi hơn khi các vùng tên nằm trên nhiều tập tin khác nhau. 

3.8 Chỉthịtiền xửlý 

Không phải mọi câu lệnh đều được biên dịch cùng lúc mà có một sốtrong chúng 

được biên dịch trước một sốkhác. Các câu lệnh nhưthếnày gọi là các chỉthịtiền 

xửlý. Các chỉthịtiền xửlý được đặt sau dấu #. 

3.8.1 Định nghĩa các định danh 

#defineDEBUG định nghĩa một định danh tiền xửlý(preprocessor identifier) 

DEBUG. Mặc dù các chỉthịtiền xửlý có thể định nghĩa ở đâu tuỳthích nhưng định 

danh tiền xửlý bắt buộc phải định nghĩa ở đầu của chương trình, trước cảtừkhóa 

using. Do đó, ta cần trình bày nhưsau: 

#defineDEBUG 

//... mã nguồn bình thường - không ảnh hưởng bởi bộtiền xửlý 

Những cơsởcủa ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

23 

#ifDEBUG 

// mã nguồn được bao gồm trong chương trình 

// khi chạy dưới chế độdebug 

#else 

// mã nguồn được bao gồm trong chương trình 

// khi chạy dưới chế độkhông debug 

#endif 

//... các đoạn mã nguồn không ảnh hưởng tiền xửlý

Trình biên dịch nhảy đến các đoạn thoả điều kiện tiền biên dịch đểbiên dịch trước. 

3.8.2 Hủy một định danh 

Ta hủy một định danh bằng cách dùng #undef.Bộtiền xửlý duyệt mã nguồn từ

trên xuống dưới, nên định danh được định nghĩa từ #define, hủy khi gặp #undef

hay đến hết chương trình. Ta sẽviết là: 

#defineDEBUG 

#ifDEBUG 

 // mã nguồn được biên dịch 

#endif 

#undefDEBUG 

#ifDEBUG 

 // mã nguồn sẽkhông được biên dịch 

#endif 

3.8.3 #if, #elif, #else và #endif 

Đây là các chỉthị đểchọn lựa xem có tiền biên dịch hay không. Các chỉthịtrên có ý 

nghĩa tương tựnhưcâu lệnh điều kiện if - else. Quan sát ví dụsau: 

#ifDEBUG 

// biên dịch đoạn mã này nếu DEBUG được định nghĩa 

#elifTEST 

// biên dịch đoạn mã này nếu DEBUG không được định nghĩa 

// nhưng TEST được định nghĩa 

#else 

// biên dịch đoạn mã này nếu DEBUG lẫn TEST 

// không được định nghĩa 

#endif

3.8.4 Chỉthị#region và #endregion 

Chỉthịphục vụcho các công cụIDE nhưVS.NET cho phép mở/đóng các ghi chú. 

#region Đóng mởmột đoạn mã 

// mã nguồn 

#endregion 

khi này VS.NET cho phép đóng hoặc mởvùng mã này. Ví dụtrên đang ởtrạng thái 

mở. Khi ởtrạng thái đóng nó vhưsau 

Đóng mởmột đoạn mã 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

24 

Chương 4 Lớp và đối tượng 

Đối tượng là một trịcó thể được tạo ra, lưu giữvà sửdụng. Trong C# tất cảcác biến 

đều là đối tượng. Các biến kiểu số, kiểu chuỗi … đều là đối tượng. Mỗi một đối 

tượng đều có các biến thành viên đểlưu giữdữliệu và có các phương thức (hàm) để

tác động lên biến thành viên. Mỗi đối tượng thuộc vềmột lớp đối tương nào đó. Các 

đối tượng có cùng lớp thì có cùng các biến thành viên và phương thức. 

4.1 Định nghĩa lớp 

Định nghĩa một lớp mới với cú pháp nhưsau: 

[attribute][bổtừtruy xuất] class định danh[:lớp cơsở] 

thân lớp 

Ví dụ4-1 Khai báo một lớp 

public classTester 

public static intMain( ) 

... 

Khi khai báo một lớp ta định nghĩa các đặc tính chung của tất cảcác đối tượng của 

lớp và các hành vi của chúng. 

Ví dụ4-2 Khai báo, tạo và sửdựng một lớp 

usingSystem; 

public classTime 

// phương thức public 

public voidDisplayCurrentTime( ) 

  Console.WriteLine( "stub for DisplayCurrentTime" ); 

// các biến private 

intYear; intMonth; intDate; 

intHour; intMinute; intSecond; 

public classTester 

static voidMain( ) 

Time t = newTime( ); 

t.DisplayCurrentTime( ); 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

25 

4.1.1 Bổtừtruy xuất 

Bổtừtruy xuất xác định thành viên (nói tắt của biến thành viên và phương thức 

thành viên) nào của lớp được truy xuất từlớp khác. Có các loại kiểu truy xuất sau: 

Bảng 4-1 Các bổtừtruy xuất 

Từkhóa  Giải thích 

public Truy xuất mọi nơi 

protected Truy xuất trong nội bộlớp hoặc trong các lớp con 

internal  Truy xuất nội trong chương trình (assembly) 

protected internal  Truy xuất nội trong chương trình (assembly) và trong 

các lớp con 

private (mặc định) Chỉ được truy xuất trong nội bộlớp 

4.1.2 Các tham sốcủa phương thức 

Mỗi phương thức có thểkhông có tham sốmà cũng có thểcó nhiều tham số. Các 

tham sốtheo sau tên phương thức và đặt trong cặp ngoặc đơn. Ví dụnhưphương 

thức SomeMethodsau: 

Ví dụ4-3 Các tham sốvà cách dùng chúng trong phương thức 

usingSystem; 

public classMyClass 

public voidSomeMethod(intfirstParam, floatsecondParam) 

  Console.WriteLine("Here are the parameters received: {0}, {1}", 

firstParam, secondParam); 

public classTester 

static voidMain( ) 

inthowManyPeople = 5; 

floatpi = 3.14f; 

MyClass mc = newMyClass( ); 

mc.SomeMethod(howManyPeople, pi); 

4.2 Tạo đối tượng 

Tạo một đối tượng bẳng cách khai báo kiểu và sau đó dùng từkhoá new đểtạo như

trong Java và C++. 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

26 

4.2.1 Hàm dựng - Constructor 

Hàm dựng là phương thức đầu tiên được triệu gọi và chỉgọi một lần khi khởi tạo 

đối tượng, nó nhằm thiết lập các tham số đầu tiên cho đối tượng. Tên hàm dựng 

trùng tên lớp; còn các mặt khác nhưphương thức bình thường. 

Nếu lớp không định nghĩa hàm dựng, trình biên dịch tự động tạo một hàm dựng 

mặc định. Khi đó các biến thành viên sẽ được khởi tạo theo các giá trịmặc định: 

Bảng 4-2 Kiểu cơsởvà giá trịmặc định 

Kiểu  Giá trịmặc định 

số(int, long, …)  0 

bool false 

char ‘\0’ (null) 

enum 0 

Tham chiếu null 

Ví dụ4-4 Cách tạo hàm dựng 

public classTime 

// public accessor methods 

public voidDisplayCurrentTime( ) 

System.Console.WriteLine("{0}/{1}/{2} {3}:{4}:{5}", 

   Month, Date, Year, Hour, Minute, Second); 

// constructor

publicTime(System.DateTime dt) 

Year = dt.Year; 

Month = dt.Month; 

Date = dt.Day; 

Hour = dt.Hour; 

Minute = dt.Minute; 

Second = dt.Second; 

// private member variables 

intYear; 

intMonth; 

intDate; 

intHour; 

intMinute; 

intSecond; 

public classTester 

static voidMain( ) 

System.DateTime currentTime = System.DateTime.Now; 

Time t = newTime(currentTime); 

t.DisplayCurrentTime( ); 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

27 

Kết quả: 

11/16/2000 16:21:40 

4.2.2 Khởi tạo 

Ta có thểkhởi tạo giá tri các biến thành viên theo ý muốn bằng cách khởi tạo nó 

trong constructor của lớp hay có thểgán vào trực tiếp lúc khai báo. Với giá trịkhởi 

tạo này thì khi một đối tượng khai báo kiểu của lớp này thì giá trịban đầu là các giá 

trịkhởi tạo chứkhông phải là giá trịmặc định. 

4.2.3 Hàm dựng sao chép 

Hàm dựng sao chép (copy constructor) là sao chép toàn bộnội dung các biến từ đối 

tượng đã tồn tại sang đối tượng mới khởi tạo. 

Ví dụ4-5 Một hàm dựng sao chép 

publicTime(Time existingTimeObject) 

Year = existingTimeObject.Year; 

 Month = existingTimeObject.Month; 

Date = existingTimeObject.Date; 

Hour = existingTimeObject.Hour; 

 Minute = existingTimeObject.Minute; 

 Second = existingTimeObject.Second; 

4.2.4 Từkhoá this 

Từkhoá this được dùng đểtham chiếu đến chính bản thân của đối tượng đó. Ví dụ: 

public voidSomeMethod (inthour) 

this.hour = hour; 

4.3 Sửdụng các thành viên tĩnh 

Các đặc tính và phương thức của một lớp có thểlà thành viên thểhiện (instance 

member)hay thành viên tĩnh. Thành viên thểhiện thì kết hợp với thểhiện của một 

kiểu, trong khi các thành viên của static nó lại là một phần của lớp. Ta có thểtruy 

cập các thành viên static thông qua tên của lớp mà không cần tạo một thểhiện lớp. 

4.3.1 Cách gọi một thành viên tĩnh 

Phương thức tĩnh (static) được nói là hoạt động trong lớp. Do đó, nó không thể

được tham chiếu thischỉtới. Phương thức static cũng không truy cập trực tiếp vào 

các phương thức không static được mà phải dùng qua thểhiện của đối tượng. 

Ví dụ4-6 Cách sửdụng phương thức tĩnh 

usingSystem; 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

28 

public classMyClass 

public voidSomeMethod(intfirstParam, floatsecondParam) 

Console.WriteLine( 

   "Here are the parameters received: {0}, {1}", 

firstParam, secondParam); 

public classTester 

static voidMain( ) 

inthowManyPeople = 5; 

floatpi = 3.14f; 

MyClass mc = newMyClass( ); 

mc.SomeMethod(howManyPeople, pi); 

Trong ví dụtrên phương thức Main()là tĩnh và phương thức SomeMethod()không 

là tĩnh. 

4.3.2 Sửdụng hàm dựng tĩnh 

Hàm dựng tĩnh (static constructor) sẽ được chạy trước khi bất kỳ đối tượng nào tạo 

ra.Ví dụ: 

staticTime( ) 

Name = "Time"; 

Khi dùng hàm dựng tĩnh phải khá thận trọng vì nó có thểcó kết quảkhó lường. 

4.3.3 Hàm dựng private 

Khi muốn tạo một lớp mà không cho phép tạo bất kỷmột thểhiện nào của lớp thì ta 

dùng hàm dựng private.

4.3.4 Sửdụng các trường tĩnh 

Cách dùng chung các biến thành viên tĩnh là giữvết của một sốcác thểhiện mà 

hiện tại nó đang tồn tại trong lớp đó. 

Ví dụ4-7 Cách dùng trường tĩnh 

usingSystem; 

public classCat 

publicCat( ) 

instances++; 

public static voidHowManyCats( ) 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

29 

Console.WriteLine("{0} cats adopted", 

instances); 

private static intinstances = 0; 

public classTester 

static voidMain( ) 

Cat.HowManyCats( ); 

Cat frisky = newCat( ); 

Cat.HowManyCats( ); 

Cat whiskers = newCat( ); 

Cat.HowManyCats( ); 

Kết quả: 

0 cats adopted 

1 cats adopted 

2 cats adopted 

Ta có thểthấy được rằng phương thức static có thểtruy cập vào biến static. 

4.4 Hủy đối tượng 

Giống với Java, C# cũng cung cấp bộthu dọn rác tự động nó sẽngầm hủy các biến 

khi không dùng. Tuy nhiên trong một sốtrường hợp ta cũng cần hủy tường minh, 

khi đó chỉviệc cài đặt phương thức Finalize(),phương thức này sẽ được gọi bởi bộ

thu dọn rác. Ta không cần phải gọi phương thức này. 

4.4.1 Hủy tửcủa C# 

Hủy tửcủa C# cũng giống nhưhủy tửtrong C++. Khai báo một hủy tửtheo cú 

pháp: 

~<định danh>() {} 

trong đó, định danh của hủy tửtrùng với dịnh danh của lớp. Đểhủy tường minh ta 

gọi phương thức Finalize()của lớp cơsởtrong nội dung của hủy tửnày. 

4.4.2 Finalize hay Dispose 

Finalize không được pháp gọi tường minh; tuy nhiên trong trường hợp ta đang giữ

môt tài nguyên hệthống và hàm gọi có khảnăng giải phóng tài nguyên này, ta sẽ

cài đặt giao diện IDisposable (chí có một phương thức Dispose). Giao diện sẽ được 

đềcậpp ởchương sau. 

4.4.3 Câu lệnh using

Bởi vì ta không thểchắc rằng Dispose()sẽ được gọi và vì việc giải phóng tài 

nguyên không thểxác định được, C# cung cấp cho ta lệnh using để đảm bảo rằng 

Dispose()sẽ được gọi trong thời gian sớm nhất. Ví dụsau minh hoạvấn đềnày: 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

30 

Ví dụ4-8 Sửdụng using 

usingSystem.Drawing; 

classTester 

public static voidMain( ) 

using(Font theFont = newFont("Arial", 10.0f)) 

// sửdụng theFont 

} // phương thức Dispose của theFont được gọi 

Font anotherFont = newFont("Courier",12.0f); 

using(anotherFont) 

// sửdụng anotherFont 

} // phương thức Dispose của anotherFont được gọi 

4.5 Truyền tham số

C# cung cấp các tham số ref đểh iệu chỉnh giá trịcủa những đối tượng bằng các 

tham chiếu. 

4.5.1 Truyền bằng tham chiếu 

Một hàm chỉcó thểtrảvềmột giá trị. Trong trường hợp muốn nhận vềnhiều kết 

quả, ta sửdụng chính các tham sốtruyền cho hàm nhưcác tham sốcó đầu ra (chứa 

trịtrảvề). Ta gọi tham sốtruyền theo kiểu này là tham chiếu. 

Trong C#, tất cảcác biến có kiểu tham chiếu sẽmặc định là tham chiếu khi các biến 

này được truyền cho hàm. Các biến kiểu giá trị đểkhai báo tham chiếu, sửdụng từ

khóa ref. 

Ví dụ4-9 Trịtrảvềtrong tham số

public classTime 

// một phương thức public 

public voidDisplayCurrentTime( ) 

System.Console.WriteLine("{0}/{1}/{2} {3}:{4}:{5}", 

   Month, Date, Year, Hour, Minute, Second); 

public intGetHour( ) 

returnHour; 

public voidGetTime(ref inth, ref intm, ref ints) 

h = Hour; 

m = Minute; 

s = Second; 

// hàm dựng 

publicTime(System.DateTime dt) 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

31 

Year = dt.Year; 

Month = dt.Month; 

Date = dt.Day; 

Hour = dt.Hour; 

Minute = dt.Minute; 

Second = dt.Second; 

// biến thành viên private 

private intYear; 

private intMonth; 

private intDate; 

private intHour; 

private intMinute; 

private intSecond; 

public classTester 

static voidMain( ) 

System.DateTime currentTime = System.DateTime.Now; 

Time t = newTime(currentTime); 

t.DisplayCurrentTime( ); 

inttheHour = 0; 

inttheMinute = 0; 

inttheSecond = 0; 

t.GetTime(ref theHour, ref theMinute, ref theSecond); 

System.Console.WriteLine("Current time: {0}:{1}:{2}", 

theHour, theMinute, theSecond); 

Kết quả: 

11/17/2000 13:41:18 

Current time: 13:41:18 

4.5.2 Truyền tham số đầu ra (out parameter) 

Như đã có đề ập ởcác chương trước, dểsửdụng được, một biến phải được khai báo 

và khởi tạo giá trịban đầu. Nhưtrong Ví dụ4-9 các biến theHour, theMinute, 

theSecond phải được khởi tạo giá trị0 trước khi truyền cho hàm GetTime. Sau lời 

gọi hàm thì giá trịcác biến sẽthay đổi ngay, vì vậy C# cung cấp từkhóa out để

không cần phải kho8\73i tạo tham sốtrước khi dùng. Ta sửa khai báo hàm GetTime 

trong ví dụtrên nhưsau: 

public voidGetTime(out inth, out intm, out ints) 

Hàm Main() không cần khởi tạo trước tham số

inttheHour, theMinute, theSecond; 

t.GetTime(out theHour, out theMinute, out theSecond); 

Vì các tham sốkhông được khời gán trước nên trong thân hàm (nhưtrường hợp này 

là GetTime) không thểsửdung các tham số(thực hiện phép lấy giá trịtham số) này 

trước khi khởi gán lại trong thân hàm. Ví dụ

public voidGetTime(out inth, out intm, out ints) 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

32 

intnKhong_y_nghia = h; // lỗi, h chưa khởi gán

4.6 Nạp chồng phương thức và hàm dựng 

Ta muốn có nhiều phương thức cùng tên mà mỗi phương thức lại có các tham số

khác nhau, sốlượng tham sốcũng có thểkhác nhau. Nhưvậy ý nghĩa của các 

phương thức được trong sáng hơn và các phương thức linh động hơn trong nhiều 

trường hợp. Nạp chồng cho phép ta làm được việc này. 

Ví dụ4-10 Nạp chồng hàm dựng 

public classTime 

// public accessor methods 

public voidDisplayCurrentTime( ) 

System.Console.WriteLine("{0}/{1}/{2} {3}:{4}:{5}", 

   Month, Date, Year, Hour, Minute, Second); 

// constructors 

publicTime(System.DateTime dt) 

Year = dt.Year; 

Month = dt.Month; 

Date = dt.Day; 

Hour = dt.Hour; 

Minute = dt.Minute; 

Second = dt.Second; 

publicTime(intYear, intMonth, intDate, 

intHour, intMinute, intSecond) 

this.Year = Year; 

this.Month = Month; 

this.Date = Date; 

this.Hour = Hour; 

this.Minute = Minute; 

this.Second = Second; 

// private member variables 

private intYear; 

private intMonth; 

private intDate; 

private intHour; 

private intMinute; 

private intSecond; 

public classTester 

static voidMain( ) 

System.DateTime currentTime = System.DateTime.Now; 

Time t = newTime(currentTime); 

t.DisplayCurrentTime( ); 

Time t2 = newTime(2000,11,18,11,03,30); 

t2.DisplayCurrentTime( ); 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

33 

4.7 Đóng gói dữliệu với property 

Trong lập trình C++, thông thường để đọc hoặc gán giá trịcho biến thành viên, lập 

trình viên thường viết hai hàm get và set tương ứng cho biến. C# cung cấp khai báo 

hàm chung gọi là property cho hàm get và set. 

Ví dụ: trong lớp DocGia có biến thành viên m_sHoTen, cài đặt Property cho biến 

thành viên này nhưsau: 

public stringHoTen 

get{  returnm_sHoTen; } 

set{ m_sHoTen = value; } 

Property có một vài khác biệt so với hàm thành viên. Thứnhất khai báo Property 

không có tham sốvà cặp ngoặc. Trong thân property dùng hai từkhóa get/set tương 

ứng cho hai hành động lấy/thiết đặt giá trịthuộc tính. Trong thân set, có biến mặc 

dịnh là value, biến này sẽmang kiểu đã được khai báo property, nhưtrong trường 

hợp trên là string. Biến value sẽnhận giá trị được gán cho Property. Cách sửdụng 

một Property nhưsau: 

1 // trong thân của một hàm 

2 DocGia dgMoi = new DocGia(); 

4 // sửdung property set 

5 dgMoi.HoTen = "Nguyễn Văn A"; 

7 // sửdụng property get 

8 string ten = dgMoi.HoTen; //ten có giá trị"Nguyễn Văn A" 

Ởdòng mã thứ5, khối set trong property HoTen sẽ được gọi, biến value sẽcó giá 

trịcủa biến nằm sau phép gán (trong trường hợp này là "Nguyễn Van A"). 

Nếu trong thân hàm không cài đặt hàm set, property sẽcó tính chỉ đọc, phép gán sẽ

bịcấm. Ngược lại nếu không cài đặt hàm get, property sẽcó tính chỉghi. 

Ví dụ4-11 Minh họa dùng một property 

public classTime 

// public accessor methods 

public voidDisplayCurrentTime( ) 

System.Console.WriteLine("Time\t: {0}/{1}/{2} {3}:{4}:{5}", 

   month, date, year, hour, minute, second); 

// constructors 

publicTime(System.DateTime dt) 

year = dt.Year; 

month = dt.Month; 

date = dt.Day; 

Lớp và đối tượng Gvhd: Nguyễn Tấn Trần Minh Khang

34 

hour = dt.Hour; 

minute = dt.Minute; 

second = dt.Second; 

// tạo một đặc tính 

public intHour 

get { returnhour; } 

set { hour = value; } 

// các biến thành viên kiểu private 

private intyear; 

private intmonth; 

private intdate; 

private inthour; 

private intminute; 

private intsecond; 

public classTester 

static voidMain( ) 

System.DateTime currentTime = System.DateTime.Now; 

Time t = newTime(currentTime); 

t.DisplayCurrentTime( ); 

inttheHour = t.Hour; 

  System.Console.WriteLine("

Retrieved the hour: {0}

", 

  theHour);  theHour++; 

t.Hour = theHour; 

  System.Console.WriteLine("Updated the hour: {0}

", 

  theHour);  } 

4.7.1 Phương thức get 

Thân của phương thức truy cập getcũng giống nhưcác phương thức khác nhưng 

phương thức này trảvểmột đối tượng kiểu là một đặc tính của lớp. Ví dụmuốn lấy 

Hournhưsau: 

get { returnhour; } 

4.7.2 Phương thức set 

Phương thức setthiết lập giá trịmột property của đối tượng và có trịtrảvềlà void. 

Phương thức setcó thểghi vào cơsởdữliệu hay cập nhật biến thành viên khi cần. 

Ví dụ: 

set { hour = value; } 

4.7.3 Các trường chỉ đọc 

C# cung cấp từkhoá readonly đểkhai báo các biến thành viên. Các biến khai báo 

kiểu này chỉcho phép gán giá trịcho biến một lần vào lúc khởi tạo qua constructor. 

Thừa kếvà Đa hình   Gvhd: Nguyễn Tấn Trần Minh Khang

35 

Chương 5 Thừa kếvà Đa hình 

Thừa kếlà cách tạo mới một lớp từnhững lớp có sẵn. Tức là nó cho phép tái sử

dụng lại mã nguồn đã viết trong lớp có sẵn. Thừa kếnói đơn giản là việc tạo một 

đối tượng khác B thừa hưởng tất cảcác đặc tính của lớp A. Cách này gọi là đơn 

thừa kế. Nếu lớp B muốn có đặc tính của nhiều lớp A1, A2 … thì gọi là đa thừa kế. 

Đa thừa kếlà khái niệm rất khó cài đặt cho các trình biên dịch. C# cũng nhưnhiều 

ngôn ngữkhác tìm cách tránh né khái niệm này. 

Đa hình là việc lớp B thừa kếcác đặc tính từlớp A nhưng có thêm một sốcài đặt 

riêng. 

5.1 Đặc biệt hoá và tổng quát hoá 

Sự đặc biệt và tổng quát hoá có mối quan hệtương hổvà phân cấp. Khi ta nói 

ListBoxvà Buttonlà những cửa sổ(Window), có nghĩa rằng ta tìm thấy được đầy đủ

các đặc tính và hành vi của Window đều tồn tại trong hai loại trên. Ta nói rằng 

Window là tổng quát hoá của ListBox và Button; ngược lại ListBox và Button là hai 

đặc biệt hoá của Window 

5.2 Sựkếthừa 

Trong C#, mối quan hệchi tiết hoá là một kiểu kếthừa. Sựkếthừa không cho mang 

ý nghĩa chi tiết hoá mà còn mang ý nghĩa chung của tựnhiên vềmối quan hệnày. 

Khi ta nói rằng ListBoxkếthửa từ Windowcó nghĩa là nó chi tiết hoá Window. 

Window được xem nhưlà lớp cơsở(base class) vàListBox được xem là lớp kếthừa

(derived class). Lớp ListBox này nhận tất cảcác đặc tính và hành vi của Windowvà 

chi tiết hoá nó bằng một sốthuộc tính và phương thức của nó cần. 

5.2.1 Thực hiện kếthừa 

Trong C#, khi ta tạo một lớp kếthừa bằng cách công một thêm dấu “:” và sau tên 

của lớp kếthừa và theo sau đó là lớp cơsởnhưsau: 

public classListBox : Window 

có nghĩa là ta khai báo một lớp mới ListBoxkếthừa từlớp Window. 

Lớp kếthừa sẽthừa hưởng được tất các phương thức và biến thành viên của lớp cơ

sở, thậm chí còn thừa hưởng cảcác thành viên mà cơsở đã thừa hưởng. 

Ví dụ5-1 Minh hoạcách dùng lớp kếthừa 

public classWindow 

Thừa kếvà Đa hình   Gvhd: Nguyễn Tấn Trần Minh Khang

36 

// constructor takes two integers to 

// fix location on the console 

publicWindow(inttop, intleft) 

this.top = top; 

this.left = left; 

// simulates drawing the window 

public voidDrawWindow( ) 

  System.Console.WriteLine("Drawing Window at {0}, {1}", 

top, left); 

// these members are private and thus invisible 

// to derived class methods; we'll examine this 

// later in the chapter 

private inttop; 

private intleft; 

// ListBox kếthừa từWindow 

public classListBox : Window 

// thêm tham sốvào constructor 

publicListBox( 

inttop, 

intleft, 

stringtheContents): 

base(top, left) // gọi constructor cơsở

mListBoxContents = theContents; 

// tạo một phương thức mới bởi vì trong 

// phương thức kếthừa có sựthay đổi hành vi 

public new voidDrawWindow( ) 

base.DrawWindow( ); // gọi phương thức cơsở

  System.Console.WriteLine ("Writing string to the listbox: 

   {0}", mListBoxContents);  } 

private stringmListBoxContents; // biến thành viên mới 

public classTester 

public static voidMain( ) 

// tạo một thểhiện cơsở

Window w = newWindow(5,10); 

w.DrawWindow( ); 

// tạo một thềhiện kếthừa 

ListBox lb = newListBox(20,30,"Hello world"); 

lb.DrawWindow( ); 

Kết quả: 

Drawing Window at 5, 10 

Drawing Window at 20, 30 

Writing string to the listbox: Hello world 

Thừa kếvà Đa hình   Gvhd: Nguyễn Tấn Trần Minh Khang

37 

5.2.2 Gọi hàm dựng lớp cơsở

Trong Ví dụ5-1 lớp ListBoxthừa kếtừ Windowvà có hàm dựng ba tham số. Trong 

hàm dựng của ListBoxcó lời gọi đến hàm dựng của Window thông qua từkhoá

basenhưsau: 

publicListBox( inttop, intleft, stringtheContents): 

base(top, left) // gọi constructor cơsở

Bởi vì các hàm dựng không được thừa kếnên lớp kếthừa phải thực hiện hàm dựng 

của riêng nó và chỉcó thểdùng hàm dựng cơsởthông qua lời gọi tường minh. Nếu 

lớp cơsởcó hàm dựng mặc định thì hàm dựng lớp kếthừa không cần thiết phải gọi 

hàm dựng cơsởmột cách tường minh (mặc định được gọi ngầm). 

5.2.3 Gọi các phương thức của lớp cơsở

Đểgọi các phương thức của lớp cơsởC# cho phép ta dùng từkhoábase đểgọi đến 

các phương thức của lớp cơsởhiện hành. 

base.DrawWindow( ); // gọi phương thức cơsở

5.2.4 Cách điều khiển truy cập 

Cách truy cập vào các thành viên của lớp được giới hạn thông qua cáchdùng các từ

khoá khai báo kiểu truy cập và hiệu chỉnh (nhưtrong chương 4.1). Xem Bảng 4-1 

Các bổtừtruy xuất 

5.3 Đa hình 

Đa hình là việc lớp B thừa kếcác đặc tính từlớp A nhưng có thêm một sốcài đặt 

riêng. Đa hình cũng là cách có thểdùng nhiều dạng của một kiểu mà không quan 

tâm đến chi tiết. 

5.3.1 Tạo kiểu đa hình 

ListBoxvà Button đều là một Window, ta muốn có một form đểgiữtập hợp tất cả

các thểhiện của Window đểkhi một thểhiện nào được mởthì nó có thểbắt 

Windowcủa nó vẽlên. Ngắn gọn, form này muốn quản lý mọi cưxửcủa tất cà các 

đối tượng đa hình của Window. 

5.3.2 Tạo phương thức đa hình 

Tạo phương thức đa hình, ta cần đặt từkhoá virtualtrong phương thức của lớp cơ

sở. Ví dụnhư: 

public virtual voidDrawWindow( ) 

Trong lớp kếthừa đểnạp chồng lại mã nguồn của lớp cơsởta dùng từkhoá 

overridekhi khai báo phương thức và nội dung bên trong viết bình thường. Ví dụvề

nạp chồng phương thức DrawWindow: 

public override voidDrawWindow( ) 

Thừa kếvà Đa hình   Gvhd: Nguyễn Tấn Trần Minh Khang

38 

base.DrawWindow( ); // gọi phương thức của lớp co sở

  Console.WriteLine ("Writing string to the listbox: {0}", 

listBoxContents); 

Dùng hình thức đa hình phương thức này thì tuỳkiểu khai báo của đối tượng nào thì 

nó dùng phương thức của lớp đó. 

5.3.3 Tạo phiên bản với từkhoá new và override 

Khi cần viết lại một phương thức trong lớp kếthừa mà đã có trong lớp cơsởnhưng 

ta không muốn nạp chồng lại phương thức virtualtrong lớp cơsởta dùng từkhoá 

new đánh dấu trước khi từkhoá virtualtrong lớp kếthừa. 

public classListBox : Window 

public new virtual voidSort( ) {...} 

5.4 Lớp trừu tượng 

Phương thức trừu tượng là phương thức chỉcó tên thôi và nó phải được cài đặt lại ở

tất các các lớp kếthừa. Lớp trừu tượng chỉthiết lập một cơsởcho các lớp kếthừa 

mà nó không thểcó bất kỳmột thểhiện nào tồn tại. 

Ví dụ5-2 Minh hoạphương thức và lớp trừu tượng 

usingSystem; 

abstract public classWindow 

// constructor takes two integers to 

// fix location on the console 

publicWindow(inttop, intleft) 

this.top = top; 

this.left = left; 

// simulates drawing the window 

// notice: no implementation 

abstract public voidDrawWindow( ); 

// these members are private and thus invisible 

// to derived class methods. We'll examine this 

// later in the chapter 

protected inttop; 

protected intleft; 

// ListBox derives from Window 

public classListBox : Window 

// constructor adds a parameter 

publicListBox(inttop, intleft, stringcontents): 

base(top, left) // call base constructor 

listBoxContents = contents; 

// an overridden version implementing the 

// abstract method 

Thừa kếvà Đa hình   Gvhd: Nguyễn Tấn Trần Minh Khang

39 

public override voidDrawWindow( ) 

   Console.WriteLine("Writing string to the listbox: {0}", 

listBoxContents); 

private stringlistBoxContents; // new member variable 

public classButton : Window 

publicButton( inttop, intleft): base(top, left) 

// implement the abstract method 

public override voidDrawWindow( ) 

  Console.WriteLine("Drawing a button at {0}, {1}

", top, left); 

public classTester 

static voidMain( ) 

Window[] winArray = newWindow[3]; 

winArray[0] = newListBox(1,2,"First List Box"); 

winArray[1] = newListBox(3,4,"Second List Box"); 

winArray[2] = newButton(5,6); 

for(inti = 0;i < 3; i++) 

winArray[i].DrawWindow( ); 

5.4.1 Giới hạn của lớp trừu tượng 

Ví dụtrên, phương thức trừu tượng DrawWindow()của lớp trừu tượng Window

được lớp ListBoxkếthừa. Nhưvậy, các lớp sau này kếthừa từlớp ListBox đều phải 

thực hiện lại phương thức DrawWindow(), đây là điểm giới hạn của lớp trừu tượng. 

Hơn nữa, nhưthếsau này không bao giờta tạo được lớp Window đúng nghĩa. Do 

vậy, nên chuyển lớp trừu tượng thành giao diện trừu tượng. 

5.4.2 Lớp niêm phong 

Lớp niêm phong với ý nghĩa trái ngược hẳn với lớp trừu tượng. Lớp niêm phong 

không cho bất kỳlớp nào khác kếthừa nó. Ta dùng từkhoá sealed đểthay cho từ

khoá abstract để được lớp này. 

5.5 Lớp gốc của tất cảcác lớp: Object 

Trong C#, các lớp kếthừa tạo thành cây phân cấp và lớp cao nhất (hay lớp cơbản 

nhất) chính là lớp Object. Các phương thức của lớp Objectnhưsau: 

Thừa kếvà Đa hình   Gvhd: Nguyễn Tấn Trần Minh Khang

40 

Bảng 5-1 Các phương thức của lớp đối tượng Object 

Phương thức  Ý nghĩa sửdụng 

Equals So sánh giá trịcủa hai đối tượng 

GetHashCode 

GetType Cung cấp kiểu truy cập của đối tượng 

ToString Cung cấp một biểu diễn chuổi của đối tượng 

Finalize() Xoá sạch bộnhớtài nguyên 

MemberwiswClone Tạo sao chép đối tượng; nhưng không thực thi kiểu 

Ví dụ5-3 Minh hoạviệc kếthừa lớp Object 

usingSystem; 

public classSomeClass 

publicSomeClass(intval) 

value= val; 

public virtual stringToString( ) 

return value.ToString( ); 

private int value; 

public classTester 

static voidMain( ) 

inti = 5; 

  Console.WriteLine("The value of i is: {0}", i.ToString( )); 

SomeClass s = newSomeClass(7); 

  Console.WriteLine("The value of s is {0}", s.ToString( )); 

Kết quả: 

The value of i is: 5 

The value of s is 7 

5.6 Kiểu Boxing và Unboxing 

Boxing và unboxing là tiến trình cho phép kiểu giá trị(value type) được đối xửnhư

kiểu tham chiếu (reference type). Biến kiểu giá trị được "gói (boxed)" vào đối tượng 

Object, sau đó ngươc lại được "tháo (unboxed)" vềkiểu giá trịnhưcũ. 

5.6.1 Boxing là ngầm định 

Boxing là tiến trình chuyển đổi một kiểu giá trịthành kiểu Object. Boxing là một 

giá trị được định vịtrong một thểhiện của Object. 

Thừa kếvà Đa hình   Gvhd: Nguyễn Tấn Trần Minh Khang

41 

Hình 5-1 Kiểu tham chiếu Boxing 

Boxing là ngầm định khi ta cung cấp một giá trị ở đó một tham chiếu đến giá trịnày 

và giá trị được chuyển đổi ngầm định. 

Ví dụ5-4 Minh họa boxing 

usingSystem; 

classBoxing 

public static voidMain( ) 

inti = 123; 

  Console.WriteLine("The object value = {0}", i); 

Console.WriteLine()mong chờmột đối tượng, không phải là sốnguyên. Đểphù 

hợp với phương thức, kiểu interger được tự động chuyển bởi CLR và ToString() 

được gọi đểlấy kết quả đối tượng. Đặc trưng này cho phép ta tạo các phương thức 

lấy một đối tượng nhưlà một tham chiếu hay giá trịtham số, phương thức sẽlàm 

việc với nó. 

5.6.2 Unboxing phải tường minh 

Trảkết quảcủa một đối tượng vềmột kiểu giá trị, ta phải thực hiện mởtường minh 

nó. Ta nên thiết lập theo hai bước sau: 

1. Chắc chắn rằng đối tượng là thểhiện của một trị đã được box. 

2. Sao chép giá trịtừthểhiện này thành giá trịcủa biến. 

Thừa kếvà Đa hình   Gvhd: Nguyễn Tấn Trần Minh Khang

42 

Hình 5-2 Boxing và sau đó unboxing 

Ví dụ5-5 Minh họa boxing và unboxing 

usingSystem; 

public classUnboxingTest 

public static voidMain( ) 

inti = 123; 

//Boxing 

objecto = i; 

// unboxing (must be explict) 

intj = (int) o; 

Console.WriteLine("j: {0}", j); 

5.7 Lớp lồng 

Lớp được khai báo trong thân của một lớp được gọi là lớp nội (inner class) hay lớp 

lồng (nested class), lớp kia gọi là lớp ngoại (outer class). Lớp nội có thuận lợi là 

truy cập được trực tiếp tất cảcác thành viên của lớp ngoài. Một phương thức của 

lớp nội cũng có thểtruy cập đến các thành viên kiểu privatecủa các lớp ngoài. Hơn 

nữa, lớp nội nó ẩn trong lớp ngoài so với các lớp khác, nó có thểlà thành viên kiểu 

privatecủa lớp ngoài. Khi lớp nội (vd: Inner) được khai báo public, nó sẽ được truy 

xuất thông qua tên của lớp ngoài (vd: Outer) như: Outer.Inner. 

Ví dụ5-6 Cách dùng lớp nội 

usingSystem; 

usingSystem.Text; 

public classFraction 

publicFraction(intnumerator, intdenominator) 

this.numerator=numerator; 

this.denominator=denominator; 

Thừa kếvà Đa hình   Gvhd: Nguyễn Tấn Trần Minh Khang

43 

// Methods elided... 

public override stringToString( ) 

StringBuilder s = newStringBuilder( ); 

s.AppendFormat("{0}/{1}", 

numerator, denominator); 

returns.ToString( ); 

internal classFractionArtist 

public voidDraw(Fraction f) 

Console.WriteLine("Drawing the numerator: {0}", 

f.numerator); 

Console.WriteLine("Drawing the denominator: {0}", 

f.denominator); 

private intnumerator; 

private intdenominator; 

public classTester 

static voidMain( ) 

Fraction f1 = newFraction(3,4); 

  Console.WriteLine("f1: {0}", f1.ToString( )); 

Fraction.FractionArtist fa = newFraction.FractionArtist(); 

fa.Draw(f1); 

Nạp chồng toán tửGvhd: Nguyễn Tấn Trần Minh Khang

44 

Chương 6 Nạp chồng toán tử

Mục tiêu thiết kếcủa C# là kiểu người dùng định nghĩa (lớp) phải được đối xửnhư

các kiểu định sẵn. Ví dụ, chúng ta muốn định nghĩa lớp phân số(Fraction) thì các 

chức năng nhưcộng, trừ, nhân, … phân sốlà điều tất yếu phải có. Đểlàm được việc 

đó ta định nghĩa các phương thức: cộng, nhân, … khi đó, ta phải viết là: 

Phân_số tổng= số_thứ_nhất.cộng(số_thứ_hai); 

Cách này hơi gượng ép và không thểhiện hết ý nghĩa. Điểu ta muốn là viết thành: 

Phân_số tổng= số_thứ_nhất+ số_thứ_hai; 

đểlàm được điều này ta dùng từkhoá operator đểthểhiện. 

6.1 Cách dùng từkhoá operator 

Trong C#, các toán tửlà các phương thức tĩnh, kết quảtrảvềcủa nó là giá trịbiểu 

diễn kết quảcủa một phép toán và các tham sốlà các toán hạng. Khi ta tạo một toán 

tửcho một lớp ta nói là ta nạp chồng toán tử, nạp chồng toán tửcũng giống nhưbất 

kỳviệc nạp chồng các phương thức nào khác. Ví dụnạp chồng toán tửcộng (+) ta 

viết nhưsau: 

public staticFraction operator+ (Fraction lhs, Fraction rhs) 

Nó chuyển tham số lhsvềphía trái toán tửvà rhsvềphía phải của toán tử. 

Cú pháp C# cho phép nạp chồng toán tửthông qua việc dùng từkhoá operator. 

6.2 Cách hổtrợcác ngôn ngữ.Net khác 

C# cung cấp khảnăng nạp chồng toán tửcho lớp của ta, nói đúng ra là trong 

Common Language Specification (CLS). Những ngôn ngữkhác nhưVB.Net có thể

không hổtrợnạp chồng toán tử, do đó, điều quan trọng là ta cũng cung cấp các 

phương thức hổtrợkèm theo các toán tử đểcó thểthực hiện được ởcác môi trường 

khác. Do đó, khi ta nạp chồng toán tửcộng (+) thì ta cũng nên cung cấp thêm 

phương thức add()với cùng ý nghĩa. 

6.3 Sựhữu ích của các toán tử

Các toán tử được nạp chồng có thểgiúp cho đoạn mã nguồn của ta dễnhìn hơn, dễ

quản lý và trong sáng hơn. Tuy nhiên nếu ta quá lạm dụng đưa vào các toán tửquá 

mới hay quá riêng sẽlàm cho chương trình khó sửdụng các toán tửnày mà đôi khi 

còn có các nhầm lẩn vô vịnữa. 

Nạp chồng toán tửGvhd: Nguyễn Tấn Trần Minh Khang

45 

6.4 Các toán tửlogic hai ngôi 

Các toán tửkhá phổbiến là toán tử(==) so sánh bằng giữhai đối tượng, (!=) so 

sánh không bằng, (<) so sánh nhỏhơn, (>) so sánh lớn hơn, (<=, >=) tương ứng nhỏ

hơn hay bằng và lớn hơn hay bằng là các toán tửphải có cặp toán hạng hay gọi là 

các toán tửhai ngôi. 

6.5 Toán tửso sánh bằng 

Nếu ta nạp chồng toán tửso sánh bằng (==), ta cũng nên cung cấp phương thức ảo 

Equals()bởi objectvà hướng chức năng này đến toán tửbằng. Điều này cho phép 

lớp của ta đa hình và cung cấp khảnăng hữu ích cho các ngôn ngữ.Net khác. 

Phương thức Equals() được khai báo nhưsau: 

public override boolEquals(objecto) 

Bằng cách nạp chồng phương thức này, ta cho phép lớp Fraction đa hình với tất cả

các đối tượng khác. Nội dung của Equals()ta cần phải đảm bảo rằng có sựso sánh 

với đối tượng Fractionkhác. Ta viết nhưsau: 

public override boolEquals(objecto) 

if(! (o isFraction) ) 

return false; 

return this== (Fraction) o; 

Toán tử is được dùng đểkiểm tra kiểu đang chạy có phù hợp với toán hạng yêu cầu 

không. Do đó, o isFraction là đúng nếu ocó kiểu là Fraction. 

6.6 Toán tửchuyển đổi kiểu (ép kiểu) 

Trong C# cũng nhưC++ hay Java, khi ta chuyển từkiểu thấp hơn (kích thước nhỏ) 

lên kiểu cao hơn (kích thước lớn) thì việc chuyển đổi này luôn thành công nhưng 

khi chuyển từkiểu cao xuống kiểu thấp có thểta sẽmất thông tin. Ví dụta chuyển 

từ intthành longluôn luôn thành công nhưng khi chuyển ngược lại từ longthành int

thì có thểtràn sốkhông nhưý của ta. Do đó khi chuyển từkiểu cao xuống thấp ta 

phải chuyển tường minh. 

Cũng vậy muốn chuyển từ intthành kiểu Fractionluôn thành công, ta dùng từkhoá 

implicit đểbiểu thịtoán tửkiểu này. Nhưng khi chuyển từkiểu Fractioncó thểsẽ

mất thông tin do vậy ta dùng từkhoá explicit đểbiểu thịtoán tửchuyển đổi tưởng 

minh. 

Ví dụ6-1 Minh hoạchuyển đổi ngầm định và tường minh 

usingSystem; 

public classFraction 

publicFraction(intnumerator, intdenominator) 

Nạp chồng toán tửGvhd: Nguyễn Tấn Trần Minh Khang

46 

  Console.WriteLine("In Fraction Constructor(int, int)"); 

this.numerator=numerator; 

this.denominator=denominator; 

publicFraction(intwholeNumber) 

Console.WriteLine("In Fraction Constructor(int)"); 

numerator = wholeNumber; 

denominator = 1; 

public static implicit operatorFraction(inttheInt) 

  System.Console.WriteLine("In implicit conversion to Fraction"); 

return newFraction(theInt); 

public static explicit operator int(Fraction theFraction) 

  System.Console.WriteLine("In explicit conversion to int"); 

returntheFraction.numerator / theFraction.denominator; 

public static bool operator==(Fraction lhs, Fraction rhs) 

Console.WriteLine("In operator =="); 

if(lhs.denominator == rhs.denominator && 

lhs.numerator == rhs.numerator) 

return true; 

// code here to handle unlike fractions 

return false; 

public static bool operator!=(Fraction lhs, Fraction rhs) 

Console.WriteLine("In operator !="); 

return!(lhs==rhs); 

public override boolEquals(objecto) 

Console.WriteLine("In method Equals"); 

if(! (o isFraction) ) 

return false; 

return this== (Fraction) o; 

public staticFraction operator+(Fraction lhs, Fraction rhs) 

Console.WriteLine("In operator+"); 

if(lhs.denominator == rhs.denominator) 

return newFraction(lhs.numerator+rhs.numerator, 

lhs.denominator); 

// simplistic solution for unlike fractions 

// 1/2 + 3/4 == (1*4) + (3*2) / (2*4) == 10/8 

intfirstProduct = lhs.numerator * rhs.denominator; 

intsecondProduct = rhs.numerator * lhs.denominator; 

Nạp chồng toán tửGvhd: Nguyễn Tấn Trần Minh Khang

47 

return newFraction( 

firstProduct + secondProduct, 

lhs.denominator * rhs.denominator 

); 

public override stringToString( ) 

  String s = numerator.ToString( ) + "/" + 

denominator.ToString( ); 

returns; 

private intnumerator; 

private intdenominator; 

public classTester 

static voidMain( ) 

//implicit conversion to Fraction

Fraction f1 = newFraction(3); 

  Console.WriteLine("f1: {0}", f1.ToString( )); 

Fraction f2 = newFraction(2,4); 

  Console.WriteLine("f2: {0}", f2.ToString( )); 

  Fraction f3 = f1 + f2; 

  Console.WriteLine("f1 + f2 = f3: {0}", f3.ToString( )); 

  Fraction f4 = f3 + 5; 

  Console.WriteLine("f3 + 5 = f4: {0}", f4.ToString( )); 

Fraction f5 = newFraction(2,4); 

if(f5 == f2) 

   Console.WriteLine("F5: {0} == F2: {1}", f5.ToString( ), 

     f2.ToString( ));  } 

intk = (int)f4; //explicit conversion to int 

  Console.WriteLine("int: F5 = {0}", k.ToString()); 

Cấu trúc   Gvhd: Nguyễn Tấn Trần Minh Khang

48 

Chương 7 Cấu trúc 

Một cấu trúc(struct) là một kiểu do người dùng định nghĩa, nó tương tựnhưlớp 

nhưnhẹhơn lớp. 

7.1 Định nghĩa cấu trúc 

Cú pháp 

[thuộc tính] [kiểu truy cập] struct <định danh> [: ] 

 // Các thành viên của cấu trúc 

Ví dụ7-1 Minh họa cách khai báo và dùng một cấu trúc 

usingSystem; 

public structLocation 

publicLocation(intxCoordinate, intyCoordinate) 

xVal = xCoordinate; 

yVal = yCoordinate; 

public intx 

get{ returnxVal; } 

set{ xVal = value; } 

public inty 

get{ returnyVal; } 

set{ yVal  = value; } 

public override stringToString( ) 

return(String.Format("{0}, {1}", xVal,yVal)); 

private intxVal; 

private intyVal; 

public classTester 

public voidmyFunc(Location loc) 

loc.x = 50; 

loc.y = 100; 

Console.WriteLine("Loc1 location: {0}", loc); 

static voidMain( ) 

Location loc1 = newLocation(200,300); 

Cấu trúc   Gvhd: Nguyễn Tấn Trần Minh Khang

49 

Console.WriteLine("Loc1 location: {0}", loc1); 

Tester t = newTester( ); 

t.myFunc(loc1); 

Console.WriteLine("Loc1 location: {0}", loc1); 

Kết quả: 

Loc1 location: 200, 300 

In MyFunc loc: 50, 100 

Loc1 location: 200, 300 

Không giống nhưlớp, cấu trúc không hỗtrợkếthừa. Tất cảcác cấu trúc thừa kế

ngầm định objectnhưng nó không thểthừa kếtừbất kỳlớp hay cấu trúc nào khác. 

Các cấu trúc cũng ngầm định là đã niêm phong. Tuy nhiên, nó có điểm giống với 

lớp là cho phép cài đặt đa giao diện. 

Cấu trúc không có hủy tửcũng nhưkhông thể đặt các tham sốtuỳý cho hàm dựng. 

Nếu ta không cài đặt bất kỳhàm dựng nào thì cấu trúc được cung cấp hàm dựng 

mặc định, đặt giá trị0 cho tất cảcác biến thành viên. 

Do cấu trúc được thiết kếcho nhẹnhàng nên các biến thành viên đều là kiểu private 

và được gói gọn lại hết. Tuỳtừng tình huống và mục đích sửdụng mà ta cần cân 

nhắc chọn lựa dùng lớp hay cấu trúc. 

7.2 Cách tạo cấu trúc 

Muốn tạo một thểhiện của cấu trúc ta dùng từkhoá new. Ví dụnhư: 

Location loc1 = newLocation(200,300); 

7.2.1 Cấu trúc nhưcác kiểu giá trị

Khi ta khai báo và tạo mới một cấu trúc nhưtrên là ta đã gọi đến constructor của 

cấu trúc. Trong Ví dụ7-1 trình biên dịch tự động đóng gói cấu trúc và nó được 

đóng gói kiểu object thông qua WriteLine(). ToString()được gọi theo kỉểu của 

object, bởi vì các cấu trúc thừa kếngầm từ object, nên nó có khảnăng đa hình, nạp 

chồng phương thức nhưbất kỳ đối tượng nào khác. 

Cấu trúc là object giá trịvà khi nó qua một hàm, nó được thông qua nhưgiá trị. 

7.2.2 Gọi hàm dựng mặc định 

Theo trên đã trình bày khi ta không tạo bất kỳnày thì khi tạo một thểhiện của cấu 

trúc thông qua từkhoá newnó sẽgọi đến constructor mặc định của cấu trúc. Nội 

dung của constructor sẽ đặt giá trịcác biến về0. 

7.2.3 Tạo cấu trúc không dùng new 

Bởi vì cấu trúc không phải là lớp, do đó, thểhiện của lớp được tạo trên stack. Cấu 

trúc cũng cho phép tạo mà không cần dùng từkhoá new, nhưng trong trường hợp 

này constructor không được gọi (cảmặc định lẫn người dùng định nghĩa). 

Giao diện Gvhd: Nguyễn Tấn Trần Minh Khang

50 

Chương 8 Giao diện 

Giao diện định nghĩa các hợp đồng (constract). Các lớp hay cấu trúc cài đặt giao 

diện này phải tôn trọng hợp đồng này. Điều này có nghĩa là khẳng định với client 

(người dùng lớp hay cấu trúc) rằng “Tôi bảo đảm rằng tôi sẽhỗtrợ đầy đầy đủcác 

phương thức, property, event, delegate, indexer đã được ghi trong giao diện” 

Một giao diện có thểthừa kếmột hay nhiều giao diện khác, và một lớp hay cấu trúc 

có thểcài đặt một hay nhiều giao diện. 

Quan sát vềphía lập trình thì giao diện là tập các hàm được khai báo sẵn mà không 

cài đặt. Các lớp hay cấu trúc cài đặt có nhiệm vụphải cài tất cảcác hàm này. 

8.1 Cài đặt một giao diện 

Cú pháp của việc định nghĩa một giao diện: 

[attributes] [access-modifier] interface interface-name [:base-list]

interface-body

Ý nghĩa của từng thành phần nhưsau 

attributes:sẽ đềcập ởphần sau. 

modifiers: bổtừphạm vi truy xuất của giao diện 

identifier: tên giao diện muốn tạo 

base-list: danh sách các giao diện mà giao diện này thừa kế, 

(nói rõ trong phần thừa kế) 

interface-body: thân giao diện luôn nằm giữa cặp dấu {}

Trong thưviện .NET Framework các giao diện thường bắt đầu bởi chữI (i hoa), 

điều này không bắt buộc. Giảsửrằng chúng ta tạo một giao diện cho các lớp muốn 

lưu trữxuống/đọc ra từcơsởdữliệu hay các hệlưu trữkhác. Đặt tên giao diện này 

là IStorable, chứa hai phương thức Read( ) và Write( ). 

interface IStorable 

void Read( ); 

void Write(object); 

Giao diện như đúng tên của nó: không dữliệu, không cài đặt. Một giao diện chỉ

trưng ra các khảnăng, và khải năng này sẽ được hiện thực hoá trong các lớp cài đặt 

nó. Ví dụnhưta tạo lớp Document, do muốn các đối tượng Document sẽ được lưu 

trữvào cơsởdữliệu, nên ta cho Document kếthừa (cài đặt) giao diện IStorable. 

// lớp Document thừa kếIStorable, 

// phải cài đặt tất cảcác phương thức của IStorable 

public class Document : IStorable 

Giao diện Gvhd: Nguyễn Tấn Trần Minh Khang

51 

 public void Read( ) { // phải cài đặt...} 

 public void Write(object obj) { // phải cài đặt...} 

// ... 

8.1.1 Cài đặt nhiều giao diện 

Lớp có thểcài đặt một hoặc nhiều giao diện. Chẳng hạn như ởlớp Document ngoài 

lưu trữra nó còn có thể được nén lại. Ta cho lớp Document cài đặt thêm một giao 

diện thứhai là ICompressible 

public class Document : IStorable, ICompressible 

Tương tự, Document phải cài đặt tất cảphương thức của ICompressible: 

public void Compress( ) 

 Console.WriteLine("Implementing the Compress Method"); 

public void Decompress( ) 

 Console.WriteLine("Implementing the Decompress Method"); 

8.1.2 Mởrộng giao diện 

Chúng ta có thểmởrộng (thừa kế) một giao diện đã tồn tại bằng cách thêm vào đó 

những phương thức hoặc thành viên mới. Chẳng hạn nhưta có thểmởrộng 

ICompressable thành ILoggedCompressable với phương thức theo dõi những byte 

đã được lưu: 

interface ILoggedCompressible : ICompressible 

void LogSavedBytes( ); 

Lớp cài  đặt phải cân nhắc chọn lựa giữa 2 lớp ICompressable hay 

ILoggedCompressable, điều này phụthuộc vào nhu cầu của lớp đó. Nếu một lớp có 

sửdụng giao diện ILoggedCompressable thì nó phải thực hiện toàn bộcác phương 

thức của ILoggedCompressable (bao gồm ICompressable và phương thức mởrộng). 

8.1.3 Kết hợp các giao diện khác nhau 

Tương tự, chúng ta có thểtạo một giao diện mới bằng việc kết hợp nhiều giao diện 

và ta có thểtùy chọn việc có thêm những phương thức hoặc những thuộc tính mới. 

Ví dụnhưta tạo ra giao diện IStorableCompressable từgiao diện IStorable và 

ILoggedCompressable và thêm vào một phương thức mới dùng đểlưu trữkích 

thước tập tin trước khi nén. 

interface IStorableCompressible: IStoreable,ILoggedCompressible 

 void LogOriginalSize( ); 

Giao diện Gvhd: Nguyễn Tấn Trần Minh Khang

52 

8.2 Truy xuất phương thức của giao diện 

Chúng ta có thểtruy xuất thành viên của giao diện IStorable nhưchúng là thành 

viên của lớp Document: 

Document doc = new Document("Test Document"); 

doc.status = -1; 

doc.Read( ); 

hoặc ta có thểtạo một thểdiện của giao diện bằng việc phân phối tài liệu vềkiểu 

của giao diện và sau đó sửdụng giao diện đểtruy cập những phương thức: 

IStorable isDoc = (IStorable) doc; 

isDoc.status = 0; 

isDoc.Read( ); 

In this case, in Main( ) you know that Document is in fact an IStorable, so you can take 

advantage of that knowledge. As stated earlier, you cannot instantiate an interface 

directly. That is, you cannot say: 

IStorable isDoc = new IStorable( ); 

Mặc dù vậy, chúng ta có thểtạo một thểhiện của lớp thi công nhưsau: 

Document doc = new Document("Test Document"); 

Sau đấy ta có thểtạo một thểhiện của giao diện bằng việc phân bổnhững đối tượng 

thi công đến những kiểu giao diện, trong trường hợp này là IStorable: 

IStorable isDoc = (IStorable) doc; 

Chúng ta kết hợp những bước đã mô tảtrên bằng đoạn mã dưới đây: 

IStorable isDoc = (IStorable) new Document("Test Document"); 

8.2.1 Ép kiểu thành giao diện 

Trong nhiều trường hợp, chúng ta không biết đối tượng ấy hỗtrợnhững giao diện 

loại gì. Giảsửnhưchúng ta có một tập các giao diện của Documents, một sốtrong 

chúng có thểlưu trữcòn một sốkhác thì không thể, chúng ta sẽthêm vào một giao 

diện thứhai ICompressable cho những đối tượng thuộc loại này đểchúng có thểnén 

lại cho công việc chuyển đổi có liên quan đến email nhanh hơn. 

interface ICompressible 

void Compress( ); 

void Decompress( ); 

Với kiểu của Document, chúng ta có thểkhông biết rằng chúng được hỗtrợbởi giao 

diện IStorable hoặc giao diện ICompressable hoặc cảhai. Chúng ta có thểgiải quyết 

điều này bằng cách phân bổnhững giao diện lại: 

Document doc = new Document("Test Document"); 

IStorable isDoc = (IStorable) doc; 

isDoc.Read( ); 

ICompressible icDoc = (ICompressible) doc; 

icDoc.Compress( ); 

Nếu Document chỉhỗtrợbởi giao diện IStorable thì giá trịtrảvềlà: 

Giao diện Gvhd: Nguyễn Tấn Trần Minh Khang

53 

public class Document : IStorable 

Việc phân bổICompressable phải  đến khi biên dịch mới biết  được bởi vì 

ICompressable là một giao diện hợp lệ. Mặc dù vậy, nếu sựphân bổtồi thì có thểsẽ

xảy ra lỗi, và lúc ấy thì một exception sẽ được quăng ra đểcảnh báo: 

An exception of type System.InvalidCastException was thrown. 

Chi tiết vềexception sẽ được đềcập trong những chương sau: 

8.2.2 Toán tử“is “ 

Khi chúng ta muốn một đối tượng có khảnăng hỗtrợgiao diện, theo nguyên tắc là 

chúng ta phải gọi phương thức tương ứng lên. Trong C# có 2 phương thức hỗtrợ

công việc này. 

Cú pháp nhưsau: 

expression is type 

hay 

if (doc is IStorable) 

Chắc lớp giao diện IStorable chắc bạn vẫn còn nhớ, ở đây câu lệnh if sẽkiểm tra 

xem đối tượng doc có hỗtrợgiao diện IStorable không mà thôi. 

Thật không may mắn cho chúng ta, tuy rát dễhiểu với cách viết nhưthếnhưng 

chúng lại không hiệu quảcho lắm. Đểhiểu vấn đềlà tại sao lại nhưthếthì chúng ta 

cần phải nhúng chúng vào trong mã MSIL và sau đó phát sinh. Và sau đây là một 

sốkết quả(thểhiện bằng sốHexa) 

IL_0023: isinst ICompressible 

IL_0028: brfalse.s IL_0039 

IL_002a: ldloc.0 

IL_002b: castclass ICompressible 

IL_0030: stloc.2 

IL_0031: ldloc.2 

IL_0032: callvirt instance void ICompressible::Compress( ) 

IL_0037: br.s IL_0043 

IL_0039: ldstr "Compressible not supported" 

Có một sốvấn đềlà chúng ta phải chú ý là trong phần kiểm tra ICompressable trong 

dòng 23. Từkhóa isinst là mã MSIL của tác tửis. Nhưta thấy trong phần kiểm tra 

đối tượng doc ởphía bên phải và ởdòng 2b thì việc kiểm tra thành công khi 

castclass được gọi. 

8.2.3 Toán tử“as” 

Toán tửas kết hợp tác tửis và sựphân bổcác thao tác bằng việc kiểm tra sựphân 

bổcó hợp lệhay không (giá trịsẽtrảvềlà true) và sau đấy sẽhoàn tất công việc. 

Nếu sựphân bổkhông hợp lệ(tác tửis sẽtrảvềgiá trịfalse), tác tửas sẽtrảvềgiá 

trịnull. Cú pháp của việc khai báo: 

expression as type 

Đoạn mã sau đây sửdụng tác tửas: 

static void Main( ) 

Giao diện Gvhd: Nguyễn Tấn Trần Minh Khang

54 

 Document doc = new Document("Test Document"); 

 IStorable isDoc = doc as IStorable; 

 if (isDoc != null) 

isDoc.Read( ); 

else 

Console.WriteLine("IStorable not supported"); 

 ICompressible icDoc = doc as ICompressible; 

 if (icDoc != null) 

icDoc.Compress( ); 

else 

Console.WriteLine("Compressible not supported"); 

Hãy xem qua đoạn mã MSIL, chúng ta thấy có một số điểm thuận tiện: 

IL_0023: isinst ICompressible 

IL_0028: stloc.2 

IL_0029: ldloc.2 

IL_002a: brfalse.s IL_0034 

IL_002c: ldloc.2 

IL_002d: callvirt instance void ICompressible::Compress( ) 

8.2.4 Toán tửis hay toán tửas 

Các giao diện xem ra có vẻlà những lớp trừu tượng. Thật ra thì chúng ta có thểthay 

đổi phần khai báo của giao diện IStorable thành lớp trừu tượng: 

abstract class Storable 

 abstract public void Read( ); 

 abstract public void Write( ); 

Lớp Document kếthừa từlớp Storable, giảsửnhưchúng ta vừa mua một lớp List từ

một hãng thứba với mong muốn là có sựkết hợp của List với Storable. Trong C++ 

ta có thểtạo một lớp StorableList bằng cách kếthừa từList và Storable nhưng trong 

C# thì ta không thểvì C# không hỗtrợ đa thừa kế. 

Mặc dù vậy, C# cho phép chúng ta chỉrõ ra sốgiao diện và kết xuất từlớp cơsở. 

Bằng vệc tạo một giao diện Storable, ta có thểkếthừa từlớp List và giao diện 

IStorable nhưtrong ví dụsau: 

public class StorableList : List, IStorable 

 // List methods here ... 

 public void Read( ) {...} 

 public void Write(object obj) {...} 

// ... 

8.3 Nạp chồng phần cài đặt giao diện 

Một lớp thi công thật sựtựdo thì phải đánh dấu một vài hoặc toàn bộcác phương 

thức có thểthực hiện được giao diện nhưlà phương thức ảo. Lớp dẫn xuất từchúng 

có thểnạp chồng. Chẳng hạn nhưlớp Document có thểthực hiện giao diện 

Giao diện Gvhd: Nguyễn Tấn Trần Minh Khang

55 

IStorable và xem các phương thức Read( ) và Write( ) nhưlà phương thức ảo. 

Người phát triển có thểkết xuất từnhững kiểu của Document, nhưlà kiểu Note hay 

EmailMessage và anh ta có là quyết định Note với tính năng là sẽ được đọc và viết 

vào cơsởdữliệu hơn là việc thểhiện bằng một tập tin. 

8.4 Thực hiện giao diện một cách tường minh 

Bởi vì một lớp có thểcài đặt nhiều giao diện nên có thểxảy ra trường hợp đụng độ

vềtên khi khi hai giao diện có cùng một tên hàm. Đểgiải quyết xung đột này ta 

khai báo cài đặt một cách tường minh hơn. Ví dụnhưnếu ta có hai giao diện 

IStorable và ITalk đều cùng có phương thức Read(), lớp Document sẽcài đặt hai 

giao diện này. Khi đó ta ta phải thêm tên giao diện vào trước tên phương thức 

using System; 

interface IStorable 

void Read( ); 

void Write( ); 

interface ITalk 

void Talk( ); 

void Read( ); 

public class Document : IStorable, ITalk 

 // document constructor 

 public Document(string s) 

  Console.WriteLine("Creating document with: {0}", s); 

// tạo read của IStorable 

 public virtual void Read( ) 

Console.WriteLine("Implementing IStorable.Read"); 

 public void Write( ) 

Console.WriteLine("Implementing IStorable.Write"); 

// cài đặt phương htức Read của ITalk 

 void ITalk.Read( ) 

Console.WriteLine("Implementing ITalk.Read"); 

 public void Talk( ) 

Console.WriteLine("Implementing ITalk.Talk"); 

public class Tester 

 static void Main( ) 

Giao diện Gvhd: Nguyễn Tấn Trần Minh Khang

56 

  // create a document object 

  Document theDoc = new Document("Test Document"); 

// Ép kiểu đểcó thểgọi IStorable.Read() 

  IStorable isDoc = theDoc as IStorable; 

  if (isDoc != null) 

isDoc.Read( ); 

// Ép kiểu đểcó thểgọi ITalk.Read() 

  ITalk itDoc = theDoc as ITalk; 

  if (itDoc != null) 

itDoc.Read( ); 

theDoc.Read( ); 

theDoc.Talk( ); 

Kết quả: 

Creating document with: Test Document 

Implementing IStorable.Read 

Implementing ITalk.Read 

Implementing IStorable.Read 

Implementing ITalk.Talk

8.4.1 Chọn lựa phơi bày các phương thức của giao diện 

Người thiết kếlớp có thêm một thận lợi là khi một giao diện được thi công thì trong 

suốt quá trình ây sựthi công tường minh ấy không được thểhiện bên phía client 

ngoại trừviệc phân bổ. Giảsửnhư đối tượng Document thi công giao diện 

IStorable nhưng chúng ta không muốn các phương thức Read( ) và Write( ) được 

xem nhưlà public trong lớp Document. Chúng ta có thểsửdụng phần thực hiện 

tường minh đểchắc rằng chúng không sẵn có trong suốt quá trình phân bổ. Điều 

này cho phép chúng giữgìn ngữnghĩa của lớp Document trong khi ta thực hiện 

IStorable. Nếu Client muốn một object có thểthi công trên giao diện IStorable, thì 

chúng phải có sựphân bổmột cách tường minh nhưng khi sửdụng tài liệu của 

chúng ta nhưlà Document trong ngữnghĩa là sẽkhông có các phương thức Read( ) 

và Write ( ). 

8.4.2 Thành viên ẩn 

Với một khảnăng mới là một thành viên của giao diện có thể được ẩn đi. Ví dụnhư

chúng ta tạo một giao diện IBase với property P: 

interface IBase 

 int P { get; set; } 

Giao diện Gvhd: Nguyễn Tấn Trần Minh Khang

57 

Và khi những giao diện được kếthừa từnó, chẳng hạn IDerived thì property P đựoc 

ẩn đi với một phương thức mới P( ) 

interface IDerived : IBase 

 new int P( ); 

Việc làm ẩn thành viên nhưlà làm trên đối với IBase có thểxem nhưlà một ý tưởng 

tốt, bây giờthì chúng ta có thể ẩn property P trong giao diện cơsở. Và trong những 

giao diện được kếthừa từchúng sẽphải cần tối thiều là 1 giao diện thành viên 

tường minh. Do đó ta có thểsửdụng phần thi công tường minh này cho property cơ

sởhoặc cho những phương thức kếthừa hoặc sửdụng cảhai. Do đó mà ta có thể3 

phiên bản cài đặt khác nhau nhưng vận hợp lệ: 

class myClass : IDerived 

 // explicit implementation for the base property 

 int IBase.P { get {...} } 

 // implicit implementation of the derived method 

 public int P( ) {...} 

class myClass : IDerived 

 // implicit implementation for the base property 

 public int P { get {...} } 

 // explicit implementation of the derived method 

 int IDerived.P( ) {...} 

class myClass : IDerived 

 // explicit implementation for the base property 

 int IBase.P { get {...} } 

 // explicit implementation of the derived method 

 int IDerived.P( ) {...} 

Array, Indexer, and Collection   Gvhd: Nguyễn Tấn Trần Minh Khang

58 

Chương 9 Array, Indexer, and Collection 

.NET Framework cung cấp cho ta rất nhiều kiểu lớp tập hợp: Array, ArrayList, 

NameValueCollection, StringCollection, Queue, Stack, và BitArray. Array là lớp 

đơn giản nhất. Trong C# nó được ánh xạthành cú pháp dựng sẵn tương tựnhư

C/C++. 

Net Framework cũng cung nấp những giao diện chuẩn nhưIEnumerable, 

ICollection đểtương tác với các lớp tập hợp (túi chứa). 

9.1 Mảng (Array) 

Mảng là một tập hợp các phần tửcó cùng kiểu, được xác định vịtrí trong tập hợp 

bằng chỉmục. C# cung cấp những dạng cú pháp dạng đơn giản nhất cho việc khai 

báo một mảng, rất dễhọc và sửdụng. 

9.1.1 Khai báo mảng 

Chúng ta có thểkhai báo một mảng kiểu C# nhưsau: 

kiểu[] tên_mảng; 

Ví dụnhư: 

int[] myIntArray; 

Dấu ngoặc vuông [ ] biểu thịcho tên biến ởsau là một mảng Ví dụdưới đây khai 

báo một biến kiểu mảng nguyên myIntArray với sốphần tửban đầu là 5: 

myIntArray = new int[5]; 

9.1.2 Giá trịmặc định 

Giảsửcó đoạn mã sau: 

/*1*/ int[] myArray; 

/*2*/ maArray = new int[5]; 

/*3*/ Button[] myButtonArray; 

/*4*/ myButtonArray = newButton[5]; 

dòng /*1*/ khai báo biến myArray là một mảng kiểu int. Khi này biến myArray có 

giá trịlà null do chưa được khởi tạo. Dòng /*2*/ khởi tạo biến myArray, các phần 

tửtrong mảng được khởi tạo bằng giá trịmặc định là 0. Dòng /*3*/ tương tự/*1*/ 

nhưng Button thuộc kiểu tham chiếu (reference type). Dòng /*4*/ khởi tạo biến 

myButtonArray, các phần tửtrong mảng không được khởi tạo (giá trị"khởi tạo" là 

null). Sửdụng bất kỳphần tửnào của mảng cũng gây lỗi chưa khởi tạo biến. 

Array, Indexer, and Collection   Gvhd: Nguyễn Tấn Trần Minh Khang

59 

9.1.3 Truy cập đến những phần tửtrong mảng 

Đểtruy cập đến những phần tửtrong mảng, ta sửdụng toán tửlấy chỉmục []. Cũng 

giống nhưC/C++, chỉmục mảng được tính bắt đầu từphần tử0. Property Length 

của lớp Array cho biết được kích thước một mảng. Nhưvậy chỉmục của mảng đi từ

0 đến Length - 1. Trong mảng myArray ví dụtrên đểlấy phần tửthứ2 (có chỉsốlà 

1) trong mảng, ta viết nhưsau: 

int phan_tu_thu_hai = myArray[1]; 

9.2 Câu lệnh foreach 

foreach là một lệnh vòng lặp, dùng đểduyệt tất cảcác phần tửcủa một mảng, tập 

hợp (nói đúng hơn là những lớp có cài đặt giao diện IEnumerable). Cú pháp của 

foreach nhẹnhàng hơn vòng lặp for (ta có thểdùng for thay cho foreach) 

foreach (kiểu tên_biến in biến_mảng) 

khối lệnh 

Ví dụ9-1 Sửdụng foreach 

usingSystem; 

namespaceProgramming_CSharp 

// một lớp đơn giản đểchứa trong mảng 

public classEmployee 

publicEmployee(intempID) 

this.empID = empID; 

public override stringToString() 

returnempID.ToString(); 

private intempID; 

private intsize; 

public classTester 

static voidMain() 

int[] intArray; 

Employee[] empArray; 

intArray = new int[5]; 

empArray = newEmployee[3]; 

// populate the array 

for(inti = 0; i < empArray.Length; i++) 

empArray[i] = newEmployee(i+10); 

foreach(inti inintArray) 

Console.WriteLine(i.ToString()); 

foreach(Employee e inempArray) 

Console.WriteLine(e.ToString()); 

Array, Indexer, and Collection   Gvhd: Nguyễn Tấn Trần Minh Khang

60 

9.2.1 Khởi tạo các phần tửmảng 

Ta có thểkhởi tạo các phần tửmảng vào thời điểm khai báo mảng, bằng cách ta 

cung cấp một danh sách những giá trịcủa mảng được giới hạn trong hai dấu ngoặc 

nhọn { }. C# có thểcung cấp những cú phápngắn gọn nhưsau: 

int[] myIntArray = new int[5] { 2, 4, 6, 8, 10 } 

int[] myIntArray = { 2, 4, 6, 8, 10 } 

Hai cách trên cho cùng kết quảlà một mảng 5 phần tửcó giá trịlà 2, 4, 6, 8, 10. 

9.2.2 Từkhóa params 

Đôi lúc có những phương thức ta không biết trước sốlương tham số được truyền 

vào như: phương thức Main() không thểbiết trước sốlượng tham sốngười dùng sẽ

truyền vào. Ta có thểsửtham sốlà mảng. Tuy nhiên khi gọi hàm ta phải tạo một 

biến mảng đểlàm tham số. C# cung cấp cú pháp đểta không cần truyền trực tiếp 

các phần tửcủa mảng bằng cách thêm từkhóa params 

Ví dụ9-2 Sửdụng từkhóa params 

usingSystem; 

namespaceProgramming_CSharp 

public classTester 

static voidMain( ) 

Tester t = newTester( ); 

/** 

* cách truyền tham sốbằng các phần tử

* không cần phải khởi tạo mảng 

* (cú pháp rất tựdo) 

*/

t.DisplayVals(5,6,7,8); 

/** 

* Cách truyền tham sốbằng mảng 

* Mảng phải được tạo sẵn 

*/ 

int[] explicitArray = new int[5] {1,2,3,4,5}; 

t.DisplayVals(explicitArray); 

public voidDisplayVals(params int[] intVals) 

foreach(inti inintVals) 

Console.WriteLine("DisplayVals {0}",i); 

Array, Indexer, and Collection   Gvhd: Nguyễn Tấn Trần Minh Khang

61 

Kết quả: 

DisplayVals 5 

DisplayVals 6 

DisplayVals 7 

DisplayVals 8 

DisplayVals 1 

DisplayVals 2 

DisplayVals 3 

DisplayVals 4 

DisplayVals 5

9.2.3 Mảng nhiều chiều 

Ma trận là một ví dụvềmảng hai chiều. C# cho phép khai báo mảng n chiều, tuy 

nhiên thông dụng nhất vẫn là mảng một chiều (mảng) và mảng hai chiều. Ví dụ

trong phần này là mảng hai chiều, tuy nhiên đối với n chiều cú pháp vẫn tương tự. 

9.2.3.1 Mảng chữnhật 

Trong mảng chữnhật (Rectangular array) 2 chiều, chiều thứnhất là sốdòng và 

chiều thứhai là sốcột. Sốphần tửtrong các dòng là nhưnhau và bằng sốcột (tương 

tựsốphần tửtrong các cột là nhưnhau và bằng sốdòng) đểkhai báo ta sửdụng cú 

pháp sau: 

type [,] array-name 

ví dụnhư: 

int [,] myRectangularArray; 

9.2.3.2 Mảng Jagged 

Mảng jagged là loại mảng trong mảng. Loại mảng này thật sựthì chúng chỉlà mảng 

một chiều nhưng những phần tửcủa chúng có khảnăng quản lí được một mảng 

khác nữa, mà kích thước các mảng này thay đổi tùy theo nhu cầu của lập trình viên. 

Ta có thểkhai báo nhưsau: 

type [ ] [ ]... 

Ví dụnhưkhai báo một mảng hai chiều với tên là myJaggedArray: 

int [ ] [ ] myJaggedArray; 

Chúng ta có thểtruy cập phần tửthứ5 của mảng thứba bằng cú pháp 

myJaggedArray[2][4] 

9.2.4 Lớp System.Array 

Lớp Array có rất nhiều hàm hữu ích, nó làm cho mảng trong C# "thông minh" hơn 

nhiều ngôn ngữkhác. Chúng được hỗtrợnhưlà các phương thức được dựng sẵn 

nhưtrường hợp string. Hai phương thức quan trong nhất của lớp System.Array là 

Sort() và Reverse(). 

Array, Indexer, and Collection   Gvhd: Nguyễn Tấn Trần Minh Khang

62 

9.3 Indexers 

Indexer tương tựnhưProperty, tuy có khác nhau một chút vềý nghĩa. Xét một ví dụ

mô phỏng một quyển sách có nhiều chương 

Xây dựng 2 lớp Sách và Chương. Lớp Chương cài đặt bình thường. Với lớp Sách ta 

sẽcài đặt một biến thành viên có kiểu túi chứa. Để đơn giản biến này có kiểu là một 

mảng 

public class Chuong 

// Các biến thành viên 

string m_sTen; 

string m_sNoiDung; 

public class Sach 

// biến thành viên 

Chuong[] m_dsChuong; 

// Property 

public Chuong[] DsChuong 

get{ return m_dsChuong; } 

Cách làm này có vài bất lợi nhưsau: thứnhất đểlấy nội dung từng chương chúng ta 

dùng Property đểlấy danh sach chương sau đó duyệt qua mảng đểlấy chương mong 

muốn. Thứhai là mỗi chương được định danh bởi tên chương nên ta mong muốn có 

cách lấy một chương thông qua tên của nó. Ta có thểcài đặt một hàm đểduyệt qua 

mảng các chương, nhưng Indexer sẽgiúp làm việc này. 

Ví dụ9-3 Sửdụng indexer 

usingSystem; 

usingSystem.Collections; 

namespaceConsoleApplication 

// Cài đặt lớp Chuong 

public classChuong 

private stringm_sTen; 

private stringm_sNoiDung; 

publicChuong() 

m_sTen = ""; 

m_sNoiDung = ""; 

publicChuong(stringsTen, stringsNoiDung) 

m_sTen = sTen; 

Array, Indexer, and Collection   Gvhd: Nguyễn Tấn Trần Minh Khang

63 

m_sNoiDung = sNoiDung; 

public stringTen 

get{ returnm_sTen; } 

set{ m_sTen = value; } 

public stringNoiDung 

get{ returnm_sNoiDung; } 

set{ m_sNoiDung = value; } 

} // hết class Chuong 

// Cài đặt lớp Sach 

public classSach 

private stringm_sTen; 

privateArrayList m_dsChuong; 

publicSach() 

m_sTen = ""; 

m_dsChuong = newArrayList(); 

publicSach(stringsTen) 

m_sTen = sTen; 

m_dsChuong = newArrayList(); 

public stringTen 

get{ returnm_sTen; } 

set

if( value== null) 

throw newArgumentNullException(); 

m_sTen = value; 

// indexer thứnhất có một tham sốkiểu int 

publicChuong this[intindex] 

get 

if( index < 0 || index > m_dsChuong.Count - 1 ) 

return null; 

return(Chuong)m_dsChuong[index]; 

set 

if( index < 0 || index > m_dsChuong.Count - 1 ) 

throw newArgumentOutOfRangeException(); 

m_dsChuong[index] = value; 

Array, Indexer, and Collection   Gvhd: Nguyễn Tấn Trần Minh Khang

64 

// indexer thứhai có một tham sốkiểu string 

publicChuong this[stringtenChuong] 

get 

foreach(Chuong chuong inm_dsChuong) 

if( chuong.Ten == tenChuong ) 

returnchuong; 

return null; 

public intadd (Chuong chuong) 

if( chuong == null) 

throw newArgumentNullException(); 

returnm_dsChuong.Add(chuong); 

}// hết class Sach 

classClass 

static voidMain(string[] args) 

Sach s = newSach("tlv"); 

s.add(newChuong("CS", "Tac gia CS")); 

 s.add(newChuong("VB", "Tac gia VB")); 

Console.WriteLine(s.Ten); 

// dùng indexer thứnhất 

Console.WriteLine(s[0].Ten + ": "+ s[0].NoiDung); 

// dùng indexer thứhai 

Console.WriteLine("VB: " + s["VB"].NoiDung); 

Console.Read(); 

Trước hết quan sát lớp Sach đểxem khai báo một indexer 

// indexer thứnhất có một tham sốkiểu int 

publicChuong this[intindex] 

public: phạm vi truy xuất của indexer 

Chuong: kiếu trảvề

int index: kiểu và tên tham sốnhận vào 

this[...]: bắt buộc đểkhai báo indexer 

Thân hàm Indexer cũng chia thành 2 hàm get và set y hệt nhưProperty. Indexer 

cung cấp thêm một hoặc nhiều tham sốvà cho ta cách sửdụng nhưsửdụng một 

mảng: 

// dùng indexer thứnhất 

Console.WriteLine(s[0].Ten + ": "+ s[0].NoiDung); 

Array, Indexer, and Collection   Gvhd: Nguyễn Tấn Trần Minh Khang

65 

// dùng indexer thứhai 

Console.WriteLine("VB: " + s["VB"].NoiDung);

9.4 Các giao diện túi chứa 

.NET Framework cung cấp một sốcác giao diện chuẩn đểtương tác với các lớp túi 

chứa hay đểcài đặt các lớp túi chứa mới tương thích (có cùng giao diện) với các lớp 

chuẩn của .NET Framework. Các giao diện được liệt kê ởBảng 9-1 Các giao diện 

túi chứa 

Bảng 9-1 Các giao diện túi chứa 

Giao diện  Ý nghĩa 

IEnumerable Khi một lớp cài đặt giao diện này, đối tượng thuộc lớp có được 

dùng trong câu lệnh foreach 

ICollection  Được cài đặt bởi tất cảcác lớp túi chứa có thành viên CopyTo(), 

Count, IsReadOnly(), IsSyncronize(), SyncRoot() 

IComparer  So sánh hai đối tượng trong túi chứa 

IList Dùng bởi các lớp túi chứa truy xuất phần tửthông qua chỉmục 

(số) 

IDictionary Dùng bởi các lớp túi chứa truy xuất phần tửthông qua quan hệ

khóa/giá trịnhưHashtabe, StoredList. 

IDictionaryEnumerator Cho phép duyệt đối với các túi chứa cài đặt IDictionary 

9.5 Array Lists 

Một vấn đềcổ điển trong khi sửdụng lớp Array là kích thước: kích thước một mảng 

cố định. Nếu không thểbiết trước cần có bao nhiêu phần tử, ta có thểkhai báo quá 

nhiều (lãng phí) hay quá ích (chương trình có lỗi). ArrayList cài đãt cấu trúc dữliệu 

danh sach liệt kê cho phép cấp phát động các phần tử. Lớp này cài đặt giao diện 

IList, ICollection, IEnumerable và có rất nhiều hàm dùng đểthao tác lên danh sách. 

IComparable 

ArrayList có phương thức Sort( ) giúp chúng ta sắp xếp các phần tử. Điều bắt buộc 

là phần tửphải thuộc lớp có cài đặt giao diện IComparable (có duy nhất một 

phương thức CompareTo()). 

9.6 Hàng đợi 

Hàng đợi (queue) là một túi chứa hoạt động theo cơchếFIFO (First in first out - 

vào trước ra trước). Cũng giống nhưta đi xếp hàng mua vé xem phim, nếu ta vào 

trước mua vé thì ta sẽ được mua vé trước. 

Hàng đợi là một tập hợp tốt cho việc ta sửdụng đểquản lí nguồn tài nguyên có giới 

hạn. Ví dụnhưta gửi đi những thông điệp đến tài nguyên, mà tài nguyên thì chỉcó 

thểgiải quyết cho một thông điệp. Do đó chúng ta phải tạo một hàng đợi những 

Array, Indexer, and Collection   Gvhd: Nguyễn Tấn Trần Minh Khang

66 

thông điệp đểcho tài nguyên có thểdựa vào đó mà xửlí “từtừ”. Lớp Queue cài đặt 

túi chứa này. 

9.7 Stacks 

Stack là túi chứa hoạt động theo cơchếLIFO (Last in first out - vào sau ra trước). 

Hai phương thức cơbản trong việc thêm hoặc xóa Stack là:Push( ) và Pop( ). Ngoài 

ra Stack còn có phương thức Peek( ), tương tựnhưPop() nhưng không xóa bỏphần 

tửnày. 

Các lớp ArrayList, Queue và Stack đều có những phương thức giống nhau như

CopyTo( ) và ToArray( ) dùng đểsao chép những phần tửvào trong một mảng. 

Trong trường hợp Stack thì phương thức CopyTo( ) sẽsao chép những phần tửvào 

một mảng một chiều đã tồn tại, và nội dung chúng sẽghi đè lên vịtrí bắt đầu mà 

chúng ta đã chỉ định. Phương thức ToArray( ) trảvềmột mảng mới với nội dung là 

nhữngphần tửtrong stack. 

9.8 Dictionary 

Dictionary là tên gọi chung cho các túi chứa lưu trữcác phần tửtheo quan hệ

khóa/giá trị. Điều này có nghĩa là tương ứng với một "khóa", ta tìm được một và chỉ

duy nhất một "giá trị" tương ứng.Nói cách khác là một "giá trị" có một "khóa" duy 

nhất không trùng với bất kỳ"khóa" của giá trịkhác. 

Một lớp muốn là một Dictionary thì cài đặt giao diện IDictionary. Lớp Dictionary 

muốn  được sửdụng trong câu lệnh foreach thì cài  đặt giao diện 

IDictionaryEnumerator. 

Dictionary thường được dùng nhất là bảng băm (Hashtable). 

Bảng băm 

Hashtable là cấu trúc dữliệu có mục tiêu tối ưu hóa việc tìm kiếm. .et Framework 

cung cấp lớp Hashtable cài đặt cấu trúc dữliệu này. 

Một đối tượng được dùng như"khóa" phải cài đặt hay thừa kếphương thức 

Object.GetHashCode() và Object.Equals() (các lớp thưviện .NET Framework hiển 

nhiên thỏa điều kiện này). Một điều kiện nữa là đối tượng này phải immutable (dữ

liệu các trường thành viên không thay đổi) trong lúc đang là khóa. 

Chuỗi Gvhd: Nguyễn Tấn Trần Minh Khang

67 

Chương 10 Chuỗi 

Chuỗi (string) trong C# là một kiểu dựng sẵn nhưcác kiểu int, long…, có đầy đủ

tính chất mềm dẻo, mạnh mẽvà dễdùng. Một đối tượng chuỗi trong C# là một hay 

nhiều ký tựUnicode không thểthay đổi thứtự. Nói cách khác là các phương thức 

áp dụng lên chuỗi không làm thay đổi bản thân chuỗi, chúng chỉtạo một bản sao có 

sửa đổi, chuỗi gốc vẫn giữnguyên. 

Đểkhai báo một đối tượng chuỗi, sửdụng từkhóa string; đối tượng này thật sự

trùng với đối tượng System.Stringtrong thưviện lớp .NET Framework. Việc sử

dụng hai đối tượng này là nhưnhau trong mọi trường hợp. 

Khai báo lớp System.Stringnhưsau: 

public sealed classString:IComparable,ICloneable,Iconvertible 

Khai báo này có nghĩa nhưsau: 

seal - không thểthừa kếtừlớp này 

ICompareable - các đối tượng chuỗi có thể được sắp thứthự

IClonable - có thểtạo một đối tượng B mới y hệt đối tượng A. 

IConvertible - có thểchuyển thành các kiểu dựng sẵn khác như

ToInt32(), ToDouble() … 

10.1 Tạo chuỗi mới 

Cách đơn giản nhất đểtạo một biến kiểu chuỗi là khai báo và gán một chuỗi cho nó 

stringsChuoi = "Khai báo và gán một chuỗi"; 

Một sốký tự đặc biệt có qui tắc riêng như "

", "\\"hay "\t"… đại diện cho ký 

tựxuống dòng, dấu xuyệt (\), dấu tab…Ví dụkhai báo 

stringsDuongDan = "C:\\WinNT\\Temp"; 

biến sDuongDan sẽcó giá trịC:\WinNT\Temp. C# cung cấp một cách khai báo theo 

đúng nguyên gốc chuỗi bằng cách thêm ký tự@. Khai báo sDuongDansẽnhưsau 

stringsDuongDan = @"C:\WinNT\Temp"; 

10.2 Phương thức ToString() 

Đây là phương thức của đối tượng object(và của tất cảcác đối tượng khác) 

thường dùng đểchuyển một đối tượng bất kỳsang kiểu chuỗi. 

intmyInteger = 5; 

stringintegerString = myInteger.ToString(); 

Chuỗi intergerStringcó giá trịlà "5". Bằng cách này ta cũng có thểtạo một 

chuỗi mới. Chuỗi cũng có thể  được tạo thông qua hàm dựng của lớp 

System.String. Lớp này có hàm dựng nhận vào một mảng các ký tự. Nhưvậy ta 

cũng tạo được chuỗi từmảng ký tự. 

Chuỗi Gvhd: Nguyễn Tấn Trần Minh Khang

68 

10.3 Thao tác chuỗi 

Lớp chuỗi cung cấp nhiều phương thức cho việc so sánh, tìm kiếm… được liệt kê 

trong bảng sau: 

Bảng 10-1 Các thành viên lớp string 

Thành viên  Giải thích 

Empty Biến thành viên tĩnh đại diện cho một chuỗi rỗng 

Compare() Phương thức tĩnh so sánh hai chuỗi 

CompareOrdinal() Phương thức tĩnh, so sánh 2 chuỗi không quan tâm đến ngôn ngữ

Concat() Phương thức tĩnh, tạo một chuỗi mới từnhiều chuỗi 

Copy() Phương thức tĩnh, tạo một bản sao 

Equals() Phương thức tĩnh, so sánh hai chuỗi có giống nhau 

Format() Phương thức tĩnh, định dạng chuỗi bằng các định dạng đặc tả

Intern() Phương thức tĩnh, nhận vềmột tham chiếu đến chuỗi 

IsInterned() Phương thức tĩnh, nhận vềmột tham chiếu đến chuỗi đã tồn tại 

Join() Phương thức tĩnh, ghép nối nhiều chuỗi, mềm dẻo hơn Concat() 

Chars Indexer của chuỗi 

Length Chiều dài chuỗi (sốký tự) 

Clone() Trảvềmột chuỗi 

CompareTo() So sánh với chuỗi khác 

CopyTo() Sao chép một lượng ký tựtrong chuỗi sang mảng ký tự

EndsWith() Xác định chuỗi có kết thúc bằng chuỗi tham sốkhông 

Equals() Xác định hai chuỗi có cùng giá trị

Insert() Chèn một chuỗi khác vào chuỗi 

LastIndexOf() vịtrí xuất hiện cuối cùng của một chuỗi con trong chuỗi 

PadLeft() 

Canh phải các ký tựtrong chuỗi, chèn thêm các khoảng trắng bên 

trái khi cần 

PadRight() 

Canh trái các ký tựtrong chuỗi, chèn thêm các khoảng trắng bên 

phải khi cần 

Remove() Xóa một sốký tự

Split() Cắt một chuỗi thành nhiều chuỗi con 

StartsWith() Xác định chuỗi có bắt đầu bằng một chuỗi con tham số

SubString() Lấy một chuỗi con 

ToCharArray()  Sao chép các ký tựcủa chuỗi thành mảng các ký tự

ToLower() Tạo bản sao chuỗi chữthường 

ToUpper() Tạo bản sao chuỗi chữhoa 

Trim() Cắt bỏcác khoảng trắng hai đầu chuỗi 

Chuỗi Gvhd: Nguyễn Tấn Trần Minh Khang

69 

Thành viên  Giải thích 

TrimEnd() Cắt bỏkhoảng trắng cuối chuỗi 

TrimStart() Cắt bỏkhoảng trắng đầu chuỗi 

Đểbiết chi tiết các sửdụng của các hàm trên, có thểtham thảo tài liệu của 

Microsoft, đặc biệt là MSDN. Dưới đây chỉgiới thiệu vài phương thức thao dụng để

thao tác chuỗi. 

Ghép chuỗi 

Đểghép 2 chuỗi ta dùng toán tử +

stringa = "Xin"; 

stringb = "chào"; 

stringc = a + " " + b; // c = "Xin chào" 

Chú ý: việc ghép nối bằng toán tử+ tuy cho mã nguồn đẹp, tựnhiên 

nhưng sẽkhông cho hiệu quảtốt khi thực hiện nhiều lần vì C# sẽcấp 

phát vùng nhớlại sau mỗi phép ghép chuỗi. 

Lấy ký tự

Đểlấy một ký tựtại một ví trí trên chuỗi ta dùng toán tử[] 

strings = "Xin chào mọi người"; 

charc = s[5]; // c = 'h' 

Chú ý: vịtrí rên chuỗi bắt đầu từvịtrí số0 

Chiều dài chuỗi 

Đểbiết sốký tựcủa chuỗi, dùng thuộc tính Length 

strings = "Xin chào"; 

intl = s.Length; // l = 8 

Chú ý: không cần đóng ngoặc sau property 

Lấy chuỗi con 

Đểlấy chuỗi con của một chuỗi, sửdụng phương thức Substring(). 

strings; 

/* 1 */ s = "Lay chuoi con".Substring(4);// s = "chuoi con" 

/* 2 */ s = "Lay chuoi con".Substring(4, 5); // s = "chuoi" 

Trong /*1*/ slấy chuỗi con tính từvịtrí thứ4 trởvềsau, còn trong/*2*/ slấy 

chuỗi con từvịtrí thứ4 và lấy chuỗi con có chiều dài là 5. 

Thay thếchuỗi con 

Đểthay thếchuỗi con trong chuỗi bằng một chuỗi con khác, sửdụng phương thức 

Replace()

strings; 

/* 1 */ s = "thay the chuoi.".Replace('t', 'T'); 

   // s = "Thay The chuoi" 

/* 2 */ s = "thay the chuoi.".Replace("th", "TH"); 

Chuỗi Gvhd: Nguyễn Tấn Trần Minh Khang

70 

   // s = "THay THe chuoi" 

Trong /*1*/ slà chuỗi đã thay thếký tự't'thành'T', còn trong/*2*/là 

chuỗi đã thay thếchuỗi "th"thành "TH".

Định dạng chuỗi 

Chuỗi được sửdụng nhiều trong trường hợp kết xuất kết quảra cho người dùng. 

Trong nhiều trường hợp ta không thểcó được chính xác chuỗi cần thiết mà phải phụ

thuộc vào một sốbiến. Vì vậy hàm định dạng chuỗi giúp ta định dạng lại chuỗi 

trước khi kết xuất. 

double d = tinh_toan_phuc_tap_1(); 

double e = tinh_toan_phuc_tap_2(); 

// giảsửd = 2.5, e = 3.5 

string s; 

s = string.Format("Kết quảlà: {0:C} va {1:c} đôla", d, e); 

// s = "Kết quảlà: $2.5 và $3.5 đôla" 

Hàm định dạng chuỗi khá phức tạp vì có nhiều tùy chọn. Cú pháp củhàm định dạng 

tổng quát nhưsau 

string.Format(provider, format, arguments) 

provider: nguốn cung cấp định dạng 

format: chuỗi cần định dạng chứa thông tin định dạng 

arguments: các thông sốcho định dạng 

C# tạo sẵn các nguồn định đạng cho kiểu số, kiểu dùng nhiều nhất, vì vậy ta chỉ

quan tâm đến cú pháp rút gọn sau và các thông tin định dạng cho kiểu số. 

string.Format (format, arguments); 

Hình 10-1 Vài định dạng thông dụng 

Ký tự  Mô tả  Ví dụ  Kết quả

C hoặc c  Tiền tệ(Currency)  string.Format("{0:C}", 2.5); 

string.Format("{0:C}", -2.5); 

$2.50 

($2.50) 

D hoặc d  Decimal  string.Format("{0:D5}", 25);  00025

E hoặc e  Khoa hoc (Scientific)  string.Format("{0:E}", 250000);  2.500000E+005

F hoặc f  Cố định phần thập phân 

(Fixed-point)

string.Format("{0:F2}", 25); 

string.Format("{0:F0}", 25); 

25.00 

25 

G hoặc g  General  string.Format("{0:G}", 2.5);  2.5

N hoặc n  Số(Number)  string.Format("{0:N}", 2500000);  2,500,000.00

X hoặc x  Hệsố16 (Hexadecimal)  string.Format("{0:X}", 250); 

string.Format("{0:X}", 0xffff); 

FA 

FFFF 

10.4 Thao tác chuỗi động 

Sau mỗi thao tác lên chuỗi sẽtạo ra một bản sao chuỗi mới. Vì vậy sửdụng đối 

tượng stringcó thểlàm giảm hiệu năng hệthống. Khi đó ta nên sửdụng lớp 

StringBuilder(một loại chuỗi khác). Các thao tác lên chuỗi làm thay đổi trên 

chính chuỗi. Vài phương thức quan trọng của lớp được liệt kê dưới đây. 

Chuỗi Gvhd: Nguyễn Tấn Trần Minh Khang

71 

Phương thức  Giải thích 

Capacity Lấy/thiết đặt sốký tựtối đa chuỗi có thểlưu giữ

Chars Indexer 

Length Kích thước chuỗi 

MaxCapacity Lấy sốký tựtối đa lớp có thểlưu giữ

Append() Thêm một đối tượng vào cuối chuỗi 

AppendFormat()  Định dạng chuỗi tham số, sau đó thêm chuỗi này vào cuối 

EnsureCapacity() Xác định chuỗi có thểlưu giữtối thiểu một lượng ký tựkhông 

Insert() Chèn một đối tượng vào chuỗi tại vịtrí 

Remove() Xóa một sốký tựtrong chuỗi 

Replace() Thay một ký tự/chuỗi con bằng ký tự/chuỗi con mới 

Ví dụ10-1 Sửdụng StringBuilder 

usingSystem; 

usingSystem.Text; 

namespaceProgramming_CSharp 

public classStringTester 

static voidMain( ) 

// một chuỗi bất kỳ đểthao tác 

strings1 = "One,Two,Three Liberty Associates, Inc."; 

// hằng ký tự

const charSpace = ' '; 

const charComma = ','; 

// mảng các dấu cách 

char[] delimiters = new char[]{ Space, Comma }; 

// dùng StringBuilder đểtạo một chuỗi 

StringBuilder output = newStringBuilder( ); 

intctr = 1; 

// tách chuỗi, sau đó ghép lại theo dang mong muốn 

// tách chuỗi theo các dấu phân cách trong delimiter 

foreach(stringsubString ins1.Split(delimiters)) 

// chèn một chuỗi sau khi định dạng chuỗi xong 

output.AppendFormat("{0}: {1}

",ctr++,subString); 

Console.WriteLine(output); 

Kết quả: 

1: One 

2: Two 

3: Three 

4: Liberty 

5: Associates 

6: 

7: Inc. 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

72 

Chương 11 Quản lý lỗi 

C# quản lý lỗi và các trạng thái bất thường bằng biệt lệ(exception). Một biệt lệlà 

một đối tượng chứa các thông tin vềsựcốbất thường của chương trình. 

Điều quan trọng trước hết là phải phân biệt rõ sựkhác nhau giữa bug, error và biệt 

lệ. Bug là lỗi vềmặt lập trình do chính lập trình viên không kiểm soát được mã 

nguồn. Biệt lệkhông thểsửa các bug. Mặc dù bug sẽphát sinh (ném) một biệt lệ, 

chúng ta không nên dựa vào các biệt lệ đểsửa các bug, mà nên viết lại mã nguồn 

cho đúng. 

Error là lỗi gây ra bởi người dùng. Chẳng hạn nhưngười dùng nhập một con sốthay 

vì phải nhập các ký tựchữcái. Một error cũng ném ra một biệt lệ, nhưng ta có thể

ngăn chặn bằng cách bắt lấy lỗi này, yêu cầu người dùng chỉnh sửa cho đến khi hợp 

lệ. Bất cứkhi nào có thể, error nên được tiên đoán trước và ngăn chặn. 

Ngay cảkhi các bug đã được sửa, các error đã được tiên đoán hết thì vẫn còn nhiều 

tình huống không thểlường trước như: hệthống đã hết bộnhớhay chương trình 

đang truy cập một tập tin không tồn tại…. Chúng ta không thểngăn chặn được biệt 

lệnhưng có lại có thểquản lý được chúng đểchúng không làm gẫy đỗ ứng dụng. 

Khi chương trình gặp phải tình huống trên, chẳng hạn hết bộnhớ, nó sẽném (phát 

sinh) một biệt lệ. Khi một biệt lệ được ném ra, hàm đang thực thi sẽbịtạm dừng và 

vùng nhớstack sẽ được duyệt ngược cho đến khi gặp trình giải quyết biệt lệ. 

Điều này có nghĩa là nếu hàm hiện hành không có trình giải quyết biệt lệthì hàm sẽ

bịngắt và hàm gọi sẽcó cơhội đểgiải quyết lỗi. Nếu không có hàm gọi nào giải 

quyết biệt lệthì biệt lệsẽ được ném cho CLR giải quyết. Điều này đồng nghĩa với 

việc chương trình sẽbịdừng một cách bất thường. 

Trình quản lý lỗi (exception handler) là một đoạn mã được thiết kế đểgiải quyết các 

biệt lệ được ném ra. Trình giải quyết lỗi được cài đặt trong khối lệnh bắt đầu bởi từ

khóa catch{}. Một cách lý tưởng thì khi biệt lệ được bắt và giải quyết thì chương 

trình tiếp tục thực thi và vấn đề được giải quyết. Ngay cảtrong trường hợp chương 

trình không thểtiếp tục được thì bằng cách bắt biệt lệta vẫn còn một cơhội in (hoặc 

ghi lại thành tập tin) các thông báo lỗi và kết thúc chương trình một êm đẹp. 

Nếu trong hàm có những đoạn mã phải được thực thi bất chấp có hay không có xảy 

ra biệt lệ(như đoạn mã giải phóng các nguồn lực được cấp phát), đoạn mã này nên 

được bỏtrong khối lệnh finnally{}. 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

73 

11.1 Ném và bắt biệt lệ

Trong C# chúngta có thểném bất kỳmột đối tượng nào thuộc lớp hay lớp con của 

lớp System.Exception (viết tắt là Exception). Vùng tên Systemkhai báo sẵn 

nhiều lớp biệt lệhữu ích chẳng hạn như  ArgumentNullException, 

InValidCastException, OverflowException… 

11.1.1 Lệnh ném throw

Đểbáo hiệu một tình huống bất thường trong một lớp C#, ta ném ra một biệt lệ

bằng cách sửdụng từkhóa throw. Dòng lệnh sau tạo một thểhiện của lớp 

Exceptionvà sau đó ném nó ra 

throw new System.Exception(); 

Ném một biệt lệsẽlàm chương trình tạm dừng lập tức và CLR tìm kiếm một trình 

quản lý biệt lệ. Nếu hàm ném không có trình giải quyết biệt lệ, stacksẽ được 

duyệt ngược (unwind) bằng cách popra cho đến khi gặp được trình giải quyết biệt 

lệ. Nếu vẫn không tìm thấy cho đến tận hàm Main(), chương trình sẽbịdừng lại. 

Ví dụ11-1. Ném một biệt lệ

usingSystem; 

namespaceProgramming_CSharp 

public classTest 

public static voidMain( ) 

Console.WriteLine("Enter Main..."); 

Test t = newTest( ); 

t.Func1( ); 

Console.WriteLine("Exit Main..."); 

public voidFunc1( ) 

Console.WriteLine("Enter Func1..."); 

Func2( ); 

Console.WriteLine("Exit Func1..."); 

public voidFunc2( ) 

Console.WriteLine("Enter Func2..."); 

throw newSystem.Exception( ); 

Console.WriteLine("Exit Func2..."); 

Kết quả: 

Enter Main... 

Enter Func1... 

Enter Func2... 

Exception occurred: System.Exception: An exception of type 

System.Exception was thrown. 

at Programming_CSharp.Test.Func2( ) 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

74 

in ...exceptions01.cs:line 26 

at Programming_CSharp.Test.Func1( ) 

in ...exceptions01.cs:line 20 

at Programming_CSharp.Test.Main( ) 

in ...exceptions01.cs:line 12 

Ví dụtrên in thông báo ra màn hình consolekhi bắt đầu và kết thúc mỗi hàm. 

Hàm Main()tạo một đối tượng kiểu Testvà gọi hàm Func1().Sau khi in thông 

báo Enter Func1, hàm Func1()gọi hàm Func2(). Func2()in ra câu thông 

báo bắt đầu và ném ra một biệt lệ. 

Chương trình sẽtạm ngưng thực thi và CLR tìm trình giải quyết biệt lệtrong hàm 

Func2().Không có, vùng nhớ stack được unwindcho đến hàm Func1().Vẫn 

không có, vùng nhớ stacktiếp tục được unwindcho đến hàm Main().Vẫn 

không có, trình giải quyết biệt lệmặc định được gọi. Thông báo lỗi hiển thịtrên 

màn hình. 

11.1.2 Lệnh bắt catch

Trình giải quyết biệt lệ đặt trong khối lệnh catch, bắt đầu bằng từkhóa catch. 

Trong ví dụ11-2, lệnh ném throw được đặt trong khối lệnh try, lệnh bắt đặt trong 

khối catch. 

Ví dụ11-2.Bắt một biệt lệ. 

usingSystem; 

namespaceProgramming_CSharp 

public classTest 

public static voidMain( ) 

Console.WriteLine("Enter Main..."); 

Test t = newTest( ); 

t.Func1( ); 

Console.WriteLine("Exit Main..."); 

public voidFunc1( ) 

Console.WriteLine("Enter Func1..."); 

Func2( ); 

Console.WriteLine("Exit Func1..."); 

public voidFunc2( ) 

Console.WriteLine("Enter Func2..."); 

try 

Console.WriteLine("Entering try block..."); 

throw newSystem.Exception( ); 

Console.WriteLine("Exiting try block..."); 

catch 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

75 

Console.WriteLine( 

"Exception caught and handled."); 

}

Console.WriteLine("Exit Func2..."); 

Kết quả: 

Enter Main... 

Enter Func1... 

Enter Func2... 

Entering try block... 

Exception caught and handled. 

Exit Func2... 

Exit Func1... 

Exit Main... 

Ví dụnày y hệt nhưví dụ11-1 ngoại trừchương trình được đặt trong khối lệnh 

try/catch. Ta đặt các đoạn mã dễgây lỗi trong khối lệnh try, chẳng hạn như

đoạn mã truy cập tập tin, xin cấp phát vùng nhớ…. 

Theo sau khối lệnh trylà khối lệnh catch. Khối lệnh catchtrong ví dụlà khối 

lệnh catchchung vì ta không thể đoán trước được loại biệt lệnào sẽphát sinh. Nếu 

biết chính xác loại biệt lệnào phát sinh, ta sẽviết khối lệnh catchcho loại biệt lệ

đó (sẽ đềcập ởphần sau). 

11.1.2.1 Sửa chữa lỗi lầm 

Trong ví dụ11-2, lệnh bắt catchchỉ đơn giản thông báo rằng một biệt lệ đã được 

bắt và quản lý. Trong ứng dụng thực tế, chúng ta sẽviết các đoạn mã giải quyết lỗi 

ở đây. Ví dụnếu người dùng cốmởmột tập chỉ đọc, ta hẳn cho gọi một phương 

thức cho phép người dùng thay đổi thuộc tính tập tin. Nếu trường hợp hết bộnhớ, ta 

hẳn cho người dùng cơhội đóng các ứng dụng khác. Nếu tất cả đều thất bại, khối 

lệnh catchsẽcho in các thông báo mô tảchi tiết lỗi đểngười dùng biết rõ vấn đề. 

11.1.2.2 Duyệt lại (unwind) vùng nhớ stack

Nếu xem kết quảví dụ11-2 cẩn thận, ta sẽthấy các thông báo bắt đầu hàm 

Main(), Func1(), Func2()và khối lệnh try; tuy nhiên lại không thấy thông 

báo kết thúc khối trymặc dù nó đã thoát khỏi hàm Func2(), Func1()và hàm 

Main().

Khi một biệt lệxảy ra, khối tryngừng thực thi ngay lập tức và quyền được trao cho 

khối lệnh catch. Nó sẽkhông bao giờquay trởlại khối tryvà vì thếkhông thểin 

dòng lệnh thoát khối try. Sau khi hoàn tất khối lệnh catch, các dòng lệnh sau 

khối catch được thực thi tiếp tục. 

Không có khối catch, vùng nhớ stack được duyệt ngược, nhưng nếu có khối 

catchviệc này sẽkhông xảy ra. Biệt lệ đã được giải quyết, không còn lỗi nữa, 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

76 

chương trình tiếp tục thực thi. Điều này sẽrõ ràng hơn nếu đặt try/catchtrong 

hàm Func1()nhưtrong ví dụ11-3 

Ví dụ11-3. Bắt biệt lệtrong hàm gọi. 

usingSystem; 

namespaceProgramming_CSharp 

public classTest 

public static voidMain( ) 

Console.WriteLine("Enter Main..."); 

Test t = newTest( ); 

t.Func1( ); 

Console.WriteLine("Exit Main..."); 

public voidFunc1( ) 

Console.WriteLine("Enter Func1..."); 

try 

Console.WriteLine("Entering try block..."); 

Func2( ); 

Console.WriteLine("Exiting try block..."); 

catch 

    Console.WriteLine( "Exception caught and handled." ); 

}

Console.WriteLine("Exit Func1..."); 

public voidFunc2( ) 

Console.WriteLine("Enter Func2..."); 

throw newSystem.Exception( ); 

Console.WriteLine("Exit Func2..."); 

Kết quả: 

Enter Main... 

Enter Func1... 

Entering try block... 

Enter Func2... 

Exception caught and handled. 

Exit Func1... 

Exit Main... 

Bây giờbiệt lệkhông được giải quyết trong trong hàm Func2(),nó được giải 

quyết trong hàm Func1().Khi Func2() được gọi, nó in dòng Enter Func2và 

sau đó ném một biệt lệ. Chương trình tạm ngừng thực thi, CLR tìm kiếm trình giải 

quyết biệt lệtrong hàm Func2().Không có. Vùng nhớ stack được duyệt ngược 

và CLR tìm thấy trình giải quyết biệt lệtrong hàm Func1().Khối lệnh catch

được gọi, chương trình tiếp tục thực thi sau khối lệnh catchnày, in ra dòng Exit 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

77 

của Func1()và sau đó là của Main(). Dòng Exit Try Blockvà dòng Exit 

Func2không được in. 

11.1.2.3 Tạo một lệnh catch chuyên dụng 

Ta có thểtạo một lệnh catchchuyên dụng quản lý một loại biệt lệ. Ví dụ11-4 mô 

tảcách xác định loại biệt lệnào ta nên quản lý. 

Ví dụ11-4. Xác định biệt lệphải bắt 

usingSystem; 

namespaceProgramming_CSharp 

public classTest 

public static voidMain( ) 

Test t = newTest( ); 

t.TestFunc( ); 

// thửchia hai số

// và giải quyết các biệt lệ

public voidTestFunc( ) 

try 

doublea = 5; 

doubleb = 0; 

Console.WriteLine("{0}/{1}={2}", a, b, DoDivide(a,b)); 

// các biệt lệthuộc lớp con phải đứng trước 

catch(System.DivideByZeroException) 

Console.WriteLine("DivideByZeroException caught!"); 

catch(System.ArithmeticException) 

Console.WriteLine("ArithmeticException caught!"); 

// biệt lệtổng quát đứng sau cùng 

catch 

Console.WriteLine("Unknown exception caught"); 

// thực hiện phép chia hợp lệ

public doubleDoDivide(doublea, doubleb) 

if(b == 0) 

throw newSystem.DivideByZeroException( ); 

if(a == 0) 

throw newSystem.ArithmeticException( ); 

returna/b; 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

78 

Kết quả: 

DivideByZeroException caught! 

Trong ví dụnày, DoDivide()sẽkhông cho phép chia một sốcho 0 hay chia 0 cho 

sốkhác. Nó sẽném ra biệt lệ DivideByZeroExceptionnếu ta cốchia cho 

không. Nếu ta đem chia 0 cho sốkhác, sẽkhông có biệt lệthích hợp: vì chia không 

cho một sốlà một phép toán hợp lệvà không nên ném bất kỳbiệt lệnào. Tuy nhiên 

giảsửtrong ví dụnày ta không muốn đem 0 chia cho sốkhác và sẽném ra biệt lệ

ArithmeticException. 

Khi một biệt lệ được ném ra, CLR tìm kiếm trình giải quyết biệt lệtheo theo trình 

tự, và chọn trình giải quyết phù hợp với biệt lệ. Khi chạy chương trình với a = 5

và b = 7, kết quảlà: 

5 / 7 = 0.7142857142857143 

Không có biệt lệnào phát sinh. Tuy nhiên nếu thay a = 0, kết quảsẽlà: 

ArithmeticException caught! 

Một biệt lệ được ném ra, và CLR xác định trình giải quyết biệt lệ đầu tiên: 

DivideByZeroException. Không đúng, CLR sẽ đi đến trình giải quyết biệt lệ

kết tiếp, ArithmeticException. 

Cuối cùng, nếu a=7, và b=0biệt lệ DivideByZeroException được ném ra. 

Ghi chú: Bời vì DevideByZero thừa kếtừArithmeticException, nên trong 

ví dụtrên nếu hoán vịhai khối lệnh catch thì có thểkhối lệnh catch bắt 

biệt lệDivideByZeroException sẽkhông bao giờ được thực thi. Thay vào 

đó khối catch bắt biệt lệArithmeticException sẽbắt các biệt lệ

DivideByZeroException. Trình biên dịch sẽnhận ra điều này và báo lỗi. 

Thông thường hàm sẽbắt các biệt lệchuyên dụng cho riêng mục tiêu của hàm, còn 

các biệt lệtổng quát hơn sẽdo các hàm cấp cao hơn bắt. 

11.1.3 Lệnh finally 

Trong một sốtrường hợp, ném một biệt lệvà unwindvùng nhớ stackcó thểgây 

thêm vấn đề. Ví dụnhưnếu ta đang mởmột tập tin hoặc nói chung là đang giữmột 

tài nguyên nào khác, ta mong muốn có một cơhội để đóng tập tin hay giải phóng tài 

nguyên đó. 

Trong trường hợp đóng một tập tin đang mở, ta có thểgiải quyết bằng cách viết một 

lệnh đóng ởkhối trymột ởkhối catch(nhưvậy lệnh đóng sẽluôn được gọi). Tuy 

nhiên đoạn mã này lặp lại một cách vô lý. Mặc khác cách này có thểkhông giải 

quyết được nếu ta quyết định không viết khối catch ởhàm này mà giao cho hàm 

gọi xửlý. Khi đó không thểviết lệnh đóng tập tin. 

Cách viết đẹp nhất là trong khối finally. Khối lệnh này chắc chắn được gọi cho 

dù có hay không có xảy ra biệt lệ. Ví dụ11-5 chứng minh cho điều này 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

79 

Ví dụ11-5. Sửdụng khối lệnh finally 

usingSystem; 

namespaceProgramming_CSharp 

public classTest 

public static voidMain( ) 

Test t = newTest( ); 

t.TestFunc( ); 

// thửchia hai số

// và giải quyết các biệt lệ

public voidTestFunc( ) 

try 

Console.WriteLine("Open file here"); 

doublea = 5; 

doubleb = 0; 

    Console.WriteLine ("{0} / {1} = {2}", 

     a, b, DoDivide(a,b)); 

    Console.WriteLine ("This line may or may not print"); 

catch(System.DivideByZeroException) 

Console.WriteLine("DivideByZeroException caught!"); 

catch 

Console.WriteLine("Unknown exception caught"); 

finally 

Console.WriteLine ("Close file here."); 

}

// thực hiện phép chia hợp lệ

public doubleDoDivide(doublea, doubleb) 

if(b == 0) 

throw newSystem.DivideByZeroException( ); 

if(a == 0) 

throw newSystem.ArithmeticException( ); 

returna/b; 

Kết quả: 

Open file here 

DivideByZeroException caught! 

Close file here. 

Output when b = 12: 

Open file here 

5 / 12 = 0.41666666666666669 

This line may or may not print 

Close file here. 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

80 

Trong ví dụnày dòng thông báo Close file here luôn luôn xuất hiện, cho dù biệt 

lệcó xảy ra hay không. 

Ghi chú: khối lệnh finallycó thể được tạo mà không cần khối catch, 

nhưng bắt buộc phải có khối try. Không thểdùng các lệnh break, 

continue, returnvà gototrong khối finally.

11.2 Đối tượng Exception 

Đối tượng System.Exceptioncung cấp nhiều phương thức và property hữu ích 

cho việc bẫy lỗi. Chẳng hạn property Messagecung cấp thông tin tại sao nó được 

ném. Message là thuộc tính chỉ đọc, nó được thiết đặt vào lúc khởi tạo biệt lệ. 

Property HelpLinkcung cấp một kết nối đến tập tin giúp đỡ. Property này có thể

đọc và thiết đặt. Property StackTracechỉ đọc và được thiết lập vào lúc chạy. 

Trong ví dụ11-6, property Exception.HelpLink được thiết đặt và nhận về để

thông tin thêm cho người dùng vềbiệt lệ DivideByZeroException. Property 

StackTrace được dùng đểcung cấp các vết của vùng nhớ stack. Nó hiển thị

hàng loạt các phương thức đã gọi dẫn đến phương thức mà biệt lệ được ném ra. 

Ví dụ11-6. Làm việc với đối tượng Exception 

usingSystem; 

namespaceProgramming_CSharp 

public classTest 

public static voidMain( ) 

Test t = newTest( ); 

t.TestFunc( ); 

public voidTestFunc( ) 

try 

Console.WriteLine("Open file here"); 

doublea = 12; 

doubleb = 0; 

    Console.WriteLine ("{0} / {1} = {2}", 

     a, b, DoDivide(a,b));      Console.WriteLine ("This line may or may not print"); 

catch(System.DivideByZeroException e) 

Console.WriteLine( 

"

DivideByZeroException! Msg: {0}", e.Message); 

Console.WriteLine("

HelpLink: {0}", e.HelpLink); 

Console.WriteLine( 

     "

Here's a stack trace: {0}

", e.StackTrace); 

catch 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

81 

Console.WriteLine("Unknown exception caught"); 

finally 

Console.WriteLine ("Close file here."); 

public doubleDoDivide(doublea, doubleb) 

if(b == 0) 

DivideByZeroException e = newDivideByZeroException(); 

e.HelpLink = "http://www.libertyassociates.com"; 

throwe; 

if(a == 0) 

throw newArithmeticException( ); 

returna / b; 

Kết quả: 

Open file here 

DivideByZeroException! Msg: Attempted to divide by zero. 

HelpLink: http://www.libertyassociates.com 

Here's a stack trace: 

at Programming_CSharp.Test.DoDivide(Double a, Double b) 

in c:\...exception06.cs:line 56 

at Programming_CSharp.Test.TestFunc( ) 

in...exception06.cs:line 22 

Close file here. 

Kết quảliệt kê các phương thức theo trình tựngược với trình tựchúng được gọi. 

Đọc kết quảtrên nhưsau: Có một biệt lệxảy ra tại hàm DoDivide(), hàm 

DoDividenày được gọi bởi hàm TestFunc().

Trong ví dụnày ta tạo một thểhiện của DivideByZeroException

DivideByZeroException e = newDivideByZeroException(); 

Do không truyền tham số, thông báo mặc định được dùng: 

DivideByZeroException! Msg: Attempted to divide by zero. 

Ta có thểthay thông báo mặc định này bằng cách truyền tham sốkhi khởi tạo: 

newDivideByZeroException( 

  "You tried to divide by zero which is not meaningful"); 

Trong trường hợp này kết quảsẽlà: 

DivideByZeroException! Msg:You tried to divide by zero which is not 

meaningful 

Trước khi ném biệt lệnày, ta thiết đặt thuộc tính HelpLink

e.HelpLink = "http://www.libertyassociates.com"; 

Khi biệt lệ được bắt, chương trình in thông báo và cả đường dẫn đến kết nối giúp đỡ

catch(System.DivideByZeroException e) 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

82 

 Console.WriteLine("

DivideByZeroException! Msg: {0}", 

e.Message); 

 Console.WriteLine("

HelpLink: {0}", e.HelpLink); 

Nhờvậy ta có thểcung cấp các thông tin cần thiết cho người dùng. Sau đó là in 

StackTrace

Console.WriteLine("

Here's a stack trace:{0}", e.StackTrace); 

Ta có kết quảcuối cùng. 

11.3 Các biệt lệtựtạo 

Với các biệt lệcó thểtùy biến thông báo do CLR cung cấp, thường đủcho hầu hết 

các ứng dụng. Tuy nhiên sẽcó lúc ta muốn thêm nhiều dạng thông tin hơn cho đối 

tượng biệt lệ, khi đó ta phải tựtạo lấy các biệt lệmong muốn. Biệt lệtựtạo bắt buộc 

thừa kếtừlớp System.Exception. Ví dụ11-7 mô tảcách tạo một biệt lệmới. 

Ví dụ11-7. Tựtạo biệt lệ

usingSystem; 

namespaceProgramming_CSharp 

public classMyCustomException : System.ApplicationException 

publicMyCustomException(stringmessage) : base(message) 

public classTest 

public static voidMain( ) 

Test t = newTest( ); 

t.TestFunc( ); 

public voidTestFunc( ) 

try 

Console.WriteLine("Open file here"); 

doublea = 0; 

doubleb = 5; 

Console.WriteLine("{0}/{1}={2}", a, b, DoDivide(a,b)); 

    Console.WriteLine("This line may or may not print"); 

catch(System.DivideByZeroException e) 

Console.WriteLine("

DivideByZeroException! Msg: {0}", 

    e.Message);  Console.WriteLine("

HelpLink: {0}

", e.HelpLink); 

catch(MyCustomException e) 

Console.WriteLine("

MyCustomException! Msg: {0}", 

    e.Message);  Console.WriteLine("

HelpLink: {0}

", e.HelpLink); 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

83 

catch 

Console.WriteLine("Unknown exception caught"); 

finally 

Console.WriteLine ("Close file here."); 

// do the division if legal 

public doubleDoDivide(doublea, doubleb) 

if(b == 0) 

DivideByZeroException e = newDivideByZeroException(); 

e.HelpLink = "http://www.libertyassociates.com"; 

throwe; 

if(a == 0) 

MyCustomException e = newMyCustomException( 

         "Can't have zero divisor"); 

e.HelpLink = 

"http://www.libertyassociates.com/NoZeroDivisor.htm"; 

throwe; 

returna / b; 

MyCustomExceptionthừa kếtừ  System.ApplicationExceptionvà nó 

không có gì khác hơn là một hàm dựng nhận tham sốlà một thông báo. Câu thông 

báo này sẽ được chuyển tới lớp cha. Biệt lệ MyCustomException được thiết kế

cho chính lớp Test, không cho phép chia cho 0 và không chia 0 cho sốkhác. Sử

dụng ArithmeticExceptioncũng cho kết quảtương tựnhưng có thểgây nhầm 

lẫn cho lập trình viên khác do phép chia 0 cho một sốkhông phải là một lỗi toán 

học. 

11.4 Ném biệt lệlần nữa. 

Sẽcó trường hợp ta muốn rằng trong khối lệnh catchta sẽkhởi động một hành 

động sửa lỗi, và sau đó ném biệt lệcho khối trykhác (khối trycủa hàm gọi). Biệt 

lệnày có thểcùng loại hay khác loại với biệt lệkhối catchbắt được. Nếu là cùng 

loại, khối catchsẽném biệt lệnày một lần nữa; còn nếu khác loại, ta sẽnhúng biệt 

lệcũvào biệt lệmới đểkhối tryhàm gọi biết được lịch sửcủa biệt lệ. Property 

InnerException được dủng đểthực hiện việc này. Biệt lệ đem nhúng gọi là biệt 

lệnội. 

Bởi vì  InnerExceptioncũng chính là một biệt lệnên nó cũng có 

InnerExceptioncủa nó. Cứnhưvậy tạo nên một loạt các biệt lệ. 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

84 

Ví dụ11-8. Ném biệt lệlần nữa và biệt lệnội (inner exception) 

usingSystem; 

namespaceProgramming_CSharp 

public classMyCustomException : System.Exception 

publicMyCustomException(stringmessage,Exception inner): 

base(message,inner) 

public classTest 

public static voidMain() 

Test t = newTest(); 

t.TestFunc(); 

public voidTestFunc() 

try 

DangerousFunc1(); 

// khi bắt được biệt lệtựtạo 

// in lịch sửcác biệt lệ

catch(MyCustomException e) 

Console.WriteLine("

{0}", e.Message); 

Console.WriteLine("Retrieving exception history..."); 

Exception inner = e.InnerException; 

while(inner != null) 

Console.WriteLine("{0}",inner.Message); 

inner = inner.InnerException; 

public voidDangerousFunc1( ) 

try 

DangerousFunc2( ); 

// nếu bắt được một biệt lệ

// ném một biệt lệtựtạo 

catch(System.Exception e) 

MyCustomException ex = newMyCustomException( 

"E3 - Custom Exception Situation!",e); 

throwex; 

public voidDangerousFunc2( ) 

try 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

85 

DangerousFunc3( ); 

// nếu bắt được biệt lệDivideByZeroException thực hiện 

// vài công việc sữa lỗi và ném ra biệt lệtổng quát 

catch(System.DivideByZeroException e) 

Exception ex = newException( 

     "E2 - Func2 caught divide by zero",e); 

throwex; 

public voidDangerousFunc3( ) 

try 

DangerousFunc4( ); 

catch(System.ArithmeticException) 

throw; 

catch(System.Exception) 

Console.WriteLine("Exception handled here."); 

public voidDangerousFunc4( ) 

throw newDivideByZeroException( 

"E1 - DivideByZero Exception"); 

Kết quả: 

E3 - Custom Exception Situation! 

Retrieving exception history... 

E2 - Func2 caught divide by zero 

E1 - DivideByZeroException 

Ghi chú: Kết quảxuất hiện trên màn hình không đủ đểthểhiện hết ý, 

cách tốt nhất là nên chạy chương trình ởchế độtừng dòng lệnh đểhiểu 

rõ vấn đềhơn. 

Chúng ta bắt đầu bằng lời gọi hàm DangerousFunc1()trong khối try

try 

DangerousFunc1( ); 

DangerousFunc1()gọi  DangerousFunc2(),  DangerousFunc2()gọi 

DangerousFunc3(), DangerousFunc3()gọi DangerousFunc4().Tất cả

các lời gọi này đều có khối trycủa riêng nó. Cuối cùng DangerousFunc4()ném 

một biệt lệ DivideByZeroExceptionvới câu thông báo E1 - DivideByZero 

Exception. 

Quản lý lỗi Gvhd: Nguyễn Tấn Trần Minh Khang

86 

Khối lệnh catchtrong hàm DangerousFunc3()sẽbắt biệt lệnày. Theo logic, tất 

cảcác lỗi toán học đều được bắt bởi biệt lệ ArithmeticException(vì vậy cả

DivideByZeroException). Nó chẳng làm gì, chỉném biệt lệnày lần nữa. 

catch(System.ArithmeticException) 

throw; 

Cú pháp trên ném cùng một loại biệt lệcho khối trybên ngoài (chỉcần từkhóa 

throw)

DangerousFunc2()sẽbắt được biệt lệnày, nó sẽném ra một biệt lệmới thuộc 

kiểu Exception. Khi khởi tạo biệt lệnày, ta truyền cho nó hai tham số: thông báo 

E2 - Func2 caught divide by zero, và biệt lệcũ đểlàm biệt lệnội. 

DangerousFunc1()bắt biệt lệnày, làm vài công việc nào đó, sau đó tạo một biệt 

lệcó kiểu MyCustomException. Tương tựnhưtrên khi khởi tạo biệt lệta truyền 

cho nó hai tham số: thông báo E3 - Custom Exception Situation!, và biệt 

lệvừa bắt được làm biệt lệnội. Đến thời điểm này biệt lệ đã có hai mức biệt lệnội. 

Cuối cùng, khối catch sẽbắt biệt lệnày và in thông báo 

E3 - Custom Exception Situation! 

Sau đó sẽtiếp tục in các thông báo của các biệt lệnội 

while(inner != null) 

Console.WriteLine("{0}",inner.Message); 

 inner = inner.InnerException; 

Và ta có kết quả

Retrieving exception history... 

E2 - Func2 caught divide by zero 

E1 - DivideByZero Exception 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

87 

Chương 12 Delegate và Event 

Delegate có nghĩa là ủy quyền hay ủy thác. Trong lập trình đôi lúc ta gặp tình huống 

phải thực thi một hành động nào đó, nhưng lại không biết sẽgọi phương thức nào 

của đối tượng nào. Chằng hạn, một nút nhấn button khi được nhấn phải thông báo 

cho đối tượng nào đó biết, nhưng đối tượng này không thể được tiên đoán trong lúc 

cài đặt lớp button. Vì vậy ta sẽkết nối lớp button với một đối tượng ủy thác và ủy 

thác (hay thông báo) cho đối tượng này trách nhiệm thực thi khi button được nhấn. 

Đối tượng ủy thác sẽ được gán (đăng ký ủy thác) vào thời điểm khác thích hợp. 

Event có nghĩa là sựkiện. Ngày nay mô hình lập trình giao diện người dùng đồhọa 

(Graphical User Interface - GUI) đòi hỏi cách tiếp cận theo hướng sựkiện. Một ứng 

dụng ngày nay hiển thịgiao diện người dùng và chờngười dùng thao tác. Ứng với 

mỗi thao tác nhưchọn một trình đơn, nhấn một nút button, nhập liệu vào ô textbox 

… sẽmột sựkiện sẽphát sinh. Một sựkiện có nghĩa là có điều gì đó đã xảy ra và 

chương trình phải đáp trả. 

Delegate và event là hai khái niệm có liên quan chặt chẽvới nhau. Bởi vì đểquản lý 

các sựkiện một cách mềm dẻo đòi hỏi các đáp trảphải được phân phối đến các 

trình giải quyết sựkiện. Trình giải quyết sựkiện trong C# được cài đặt bằng 

delegate. 

Delegate còn được sửdụng nhưmột hàm callback. Hàm callback là hàm có thể

được tự động gọi bởi hàm khác. Công dụng thứhai này của delegate được đềcập 

trong chương 19. 

12.1 Delegate (ủy thác, ủy quyền) 

Trong C#, delegate được hỗtrợhoàn toàn bởi ngôn ngữ. Vềmặt kỹthuật, 

delegatethuộc kiểu tham chiếu được dùng để đóng gói phương thức đã xác định 

kiểu trảvềvà sốlượng, kiểu tham số. Chúng ta có thể đóng gói bất kỳphương thức 

thức nào phù hợp với phương thức của delegate. (Trong C++ có kỹthuật tương 

tựlà con trỏhàm, tuy nhiên delegatecó tính hướng đối tượng và an toàn vềkiểu) 

Một delegatecó thể được tạo bắng từkhóa delagate, sau đó là kiểu trảvề, tên 

delegatevà các tham sốcủa phương thức mà delegatechấp nhận: 

public delegate intWhichIsFirst(objectobj1, objectobj2) 

Dòng trên khai báo một delegatetên là WhichIsFirstcó thể đóng gói (nhận) 

bất kỳmột phương thức nào nhận vào hai tham sốkiểu objectvà trảvềkiểu int. 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

88 

Khi một delegate được định nghĩa, ta có thể đóng gói một phương thức với 

delegate đó bằng cách khởi tạo với tham sốlà phương thức cho delegate. 

12.1.1 Dùng delegate đểxác định phương thức vào lúc chạy 

Delegate được dùng đểxác định (specify) loại (hay kiểu) của các phương thức dùng 

đểquản lý các sựkiện; hoặc đểcài đặt các hàm callback trong ứng dụng. Chúng 

cũng được dùng đểxác định các phương thức tĩnh và không tĩnh (còn gọi là phương 

thức thềhiện - instance methods: là phương chỉgọi được thông qua một thểhiện 

của lớp) chưa biết trước vào lúc thiết kế(có nghĩa là chỉbiết vào lúc chạy). 

Ví dụ, giảsửchúng ta muốn tạo một lớp túi chứa đơn giản có tên là Pair(một 

cặp). Lớp này chứa 2 đối tượng được sắp xếp. Chúng ta không biết trước được đối 

tượng nào sẽ được truyền vào cho một thểhiện của lớp Pair, vì vậy không thểxây 

dựng hàm sắp xếp tốt cho tất cảcác trường hợp. Tuy nhiên ta sẽ đẩy trách nhiệm 

này cho đối tượng bằng cách tạo phương thức mà công việc sắp xếp có thể ủy thác. 

Nhờ đó ta có thểsắp thứthựcủa các đối tượng chưa biết bằng cách ủy thác trách 

nhiệm này chính phương thức của chúng. 

Ta định nghĩa một delegate có tên WhichIsFirst trong lớp Pair. Phương thức 

sortsẽnhận một tham sốkiểu WhichIsFirst. Khi lớp Paircần biết thứtựcủa 

đối tượng bên trong, nó sẽgọi delegate với hai đối tượng làm tham số. Trách nhiệm 

quyết định xem đối tượng nào trong 2 đối tượng có thứtựtrước được ủy thác cho 

phương thức được đóng gói trong delegate. 

Đểkiểm thử delegate, ta tạo ra hai lớp: Dogvà Student. Lớp Dogvà Student

không giống nhau ngoại trừcảhai cùng cài đặt phương thức có thể được đóng gói 

bới WhichIsFirst, vì vậy cả Doglẫn Student đều thích hợp được giữtrong đối 

tượng Pair.

Đểkiểm thửchương trình chúng ta tạo ra một cặp đối tượng Studentvà một cặp 

đối tương Dogvà lưu trữchúng trong hai đối tượng Pair. Ta sẽtạo một đối tượng 

delegate để đóng gói cho từng phương thức, sau đó ta yêu cầu đối tượng Pair

sắp xếp đối tượng Dogvà Student. Sau đây là các bước thực hiện: 

public classPair 

// cặp đối tượng 

publicPair(objectfirstObject, objectsecondObject) 

thePair[0] = firstObject; 

thePair[1] = secondObject; 

// biến lưu giữhai đối tượng 

private object[]thePair = new object[2]; 

Kếtiếp, ta override hàm ToString()

public override stringToString( ) 

returnthePair[0].ToString() + ", " + thePair[1].ToString(); 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

89 

Chúng ta đã có hai đối tượng trong lớp Pairvà có thểin chúng ra. Bây giờlà phần 

sắp xếp chúng và in kết quảsắp xếp. Chúng ta không thểbiết trước sẽcó loại đối 

tượng nào, và vì vậy chúng ta sẽ ủy thác quyền quyết định đối tượng nào có thứtự

trước cho chính các đối tượng. Nhưvậy ta sẽyêu cầu đối tượng được xếp thứtự

trong lớp Pairphải cài đặt phương thức cho biết trong hai đối tượng, đối tượng nào 

có thứtựtrước. Phương thức này sẽnhận vào hai đối tượng (thuộc bất kỳloại nào) 

và trảvềkiểu liệt kê: theFirstComeFirstnếu đối tượng đầu có thứtựtrước và 

theSecondComeFirstnếu đối tượng sau có thứtựtrước. 

Những phương thức này sẽ được đóng gói bởi delegate WhichIsFirst định 

nghĩa trong lớp Pair. 

public delegatecomparisn WhichIsFirst(object obj1,object obj2) 

Trịtrảvềthuộc kiểu kiểu liệt kê comparison. 

public enumcomparison 

 theFirstComesFirst = 1, 

 theSecondComesFirst = 2 

Bất kỳmột phương thức tĩnh nào nhận hai tham sốkiểu objectvà trảvềkiểu 

comparison đều có thể được đóng gói bởi delegatenày vào lúc chạy. 

Bây giờta định nghĩa phương thức Sortcủa lớp Pair

public voidSort(WhichIsFirst theDelegatedFunc) 

if( theDelegatedFunc(thePair[0],thePair[1]) == 

comparison.theSecondComesFirst) 

objecttemp = thePair[0]; 

thePair[0] = thePair[1]; 

thePair[1] = temp; 

Phương thức này nhận một tham số delegatetên WhichIsFirst. Phương thức 

Sort ủy thác quyền quyết định đối tượng nào có thứtựtrước cho phương thức 

được đóng gói trong delegate. Trong thân hàm Sort(), có lời gọi phương thức 

được ủy thác và xác định giá trịtrảvề. 

Nếu trịtrảvềlà theSecondComesFirst, hai đối tượng trong Pairsẽhoán 

chuyển vịtrí, ngược lại không có gì xảy ra. 

Chúng ta sẽxắp xếp các sinh viên theo thứtựtên. Chúng ta phải viết một phương 

thức trảvề theFirstComesFirstnếu tên của sinh viên đầu có thứtựtrước và 

ngược lại theSecondComesFirstnếu tên sinh viên sau có thứtựtrước. Nếu hàm 

trảvề theSecondComesFirstta sẽthực hiện việc đảo vịtrí của hai sinh viên 

trong Pair. 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

90 

Bây giờthêm phương thức ReverseSort, đểsắp các đối tượng theo thứtựngược. 

public voidReverseSort(WhichIsFirst theDelegatedFunc) 

if( theDelegatedFunc(thePair[0], thePair[1]) == 

  comparison.theFirstComesFirst ) 

objecttemp = thePair[0]; 

thePair[0] = thePair[1]; 

thePair[1] = temp; 

Bây giờchúng ta cần vài đối tượng đểsắp xếp. Ta sẽtạo hai lớp Studentvà Dog. 

Gán tên cho Studentlúc khởi tạo 

public classStudent 

publicStudent(stringname) 

this.name = name; 

Lớp Studentyêu cầu hai phương thức, một overridetừhàm ToString()và 

một để đóng gói nhưphương thức được ủy thác. 

Studentphải overridehàm ToString() đểphương thức ToString()trong 

lớp Pairgọi. Hàm chỉ đơn giản trảvềtên của sinh viên. 

public override stringToString() 

returnname; 

Cũng cần phải cài đặt phương thức để Pair.Sort()có thể ủy thác quyền quyết 

định thứtựhai đối tượng. 

return(String.Compare(s1.name, s2.name) < 0 ? 

  comparison.theFirstComesFirst : 

  comparison.theSecondComesFirst ); 

Hàm String.Comparelà phương thức của lớp Stringtrong thưviện .Net 

Framework. Hàm so sánh hai chuỗi và trảvềsốnhỏhơn 0 nếu chuỗi đầu nhỏhơn 

và trảvềsốlớn hơn 0 nếu ngược lại. Chú ý rằng hàm trảvề

theFirstComesFirstnếu chuỗi  đầu nhỏhơn, và trảvề

theSecondComesFirstnếu chuỗi sau nhỏhơn. 

Lớp thứhai là Dog. Các đối tượng Dogsẽ được sắp xếp theo trọng lượng, con nhẹsẽ

đứng trước con nặng. Đây là khai báo đầy đủlớp Dog: 

public classDog 

publicDog(intweight) 

this.weight=weight; 

// dogs are ordered by weight 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

91 

public staticcomparison WhichDogComesFirst(  Object o1, 

    Object o2 )  { 

  Dog d1 = (Dog) o1; 

  Dog d2 = (Dog) o2; 

returnd1.weight > d2.weight ? theSecondComesFirst : 

 theFirstComesFirst;  } 

public override stringToString( ) 

returnweight.ToString( ); 

private intweight; 

Chú ý rằng lớp Dogcũng override phương thức ToString()và cài đặt phương 

thức tĩnh với nguyên mẫu hàm được khai báo trong delegate. Cũng chú rằng hai 

phương thức chuẩn bị ủy thác của hai lớp Dogvà Studentkhông cần phải trùng 

tên. Ví dụ12 - 1 là chương tình hoàn chỉnh. Chương trình này giải thích cách các 

phương thức ủy thác được gọi. 

Ví dụ12 - 1. Làm việc với delegate 

usingSystem; 

namespaceProgramming_CSharp 

public enumcomparison 

theFirstComesFirst = 1, 

theSecondComesFirst = 2 

// túi chứa đơn giản chứa 2 đối yựơng 

public classPair 

// khai báo delegate 

public delegatecomparison WhichIsFirst( objectobj1,              objectobj2 ); 

// hàm khởi tạo nhận 2 đối tượng 

// ghi nhận theo đúng trình tựnhận vào 

publicPair( objectfirstObject, objectsecondObject) 

thePair[0] = firstObject; 

thePair[1] = secondObject; 

// phương thức sắp thứtự(tăng) hai đối tượng 

// theo thứtựdo chính chúng qui định. 

public voidSort(WhichIsFirst theDelegatedFunc) 

if( theDelegatedFunc(thePair[0],thePair[1]) ==  

comparison.theSecondComesFirst ) 

objecttemp = thePair[0]; 

thePair[0] = thePair[1]; 

thePair[1] = temp; 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

92 

// phương thức sắp thứtựngược (giảm) các đối tượng 

// theo thứtựdo chính chúng qui định. 

public voidReverseSort(  WhichIsFirst theDelegatedFunc) 

if(theDelegatedFunc(thePair[0],thePair[1]) == 

comparison.theFirstComesFirst ) 

objecttemp = thePair[0]; 

thePair[0] = thePair[1]; 

thePair[1] = temp; 

// kết hợp hai hàm ToString() của hai đối tượng 

public override stringToString( ) 

returnthePair[0].ToString( ) + ", " +   

thePair[1].ToString( ); 

// mảng giữhai đối tượng 

private object[] thePair = new object[2]; 

public classDog 

publicDog(intweight) 

this.weight=weight; 

// chó được sắp theo trọng lượng 

public staticcomparison WhichDogComesFirst( objecto1,     objecto2) 

Dog d1 = (Dog) o1; 

Dog d2 = (Dog) o2; 

returnd1.weight > d2.weight ? 

comparison.theSecondComesFirst : 

comparison.theFirstComesFirst; 

public override stringToString() 

returnweight.ToString(); 

private intweight; 

public classStudent 

publicStudent(stringname) 

this.name = name; 

// sinh viên sắp theo thứtựtên 

public staticcomparison WhichStudentComesFirst( objecto1, 

    objecto2 ) 

   Student s1 = (Student) o1; 

   Student s2 = (Student) o2; 

return(String.Compare(s1.name, s2.name) < 0 ? 

comparison.theFirstComesFirst : 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

93 

comparison.theSecondComesFirst); 

public override stringToString( ) 

returnname; 

private stringname; 

public classTest 

public static voidMain( ) 

// tạo hai đối tượng sinh viên và hai đối tượng chó 

// đẩy chúng vào 2 đối tượng Pair 

Student Jesse = newStudent("Jesse"); 

Student Stacey = newStudent ("Stacey"); 

Dog Milo = newDog(65); 

Dog Fred = newDog(12); 

Pair studentPair = newPair(Jesse,Stacey); 

Pair dogPair = newPair(Milo, Fred); 

Console.WriteLine("studentPair\t\t\t: {0}", 

studentPair.ToString( )); 

Console.WriteLine("dogPair\t\t\t\t: {0}", 

dogPair.ToString( )); 

// tạo thểhiện của delegate 

Pair.WhichIsFirst theStudentDelegate = 

newPair.WhichIsFirst(Student.WhichStudentComesFirst); 

Pair.WhichIsFirst theDogDelegate = 

newPair.WhichIsFirst(Dog.WhichDogComesFirst); 

// sắp xếp sửdụng delegate 

studentPair.Sort(theStudentDelegate); 

Console.WriteLine("After Sort studentPair\t\t: {0}", 

studentPair.ToString( )); 

studentPair.ReverseSort(theStudentDelegate); 

Console.WriteLine("After ReverseSort studentPair\t:{0}", 

studentPair.ToString( )); 

dogPair.Sort(theDogDelegate); 

Console.WriteLine("After Sort dogPair\t\t: {0}", 

dogPair.ToString( )); 

dogPair.ReverseSort(theDogDelegate); 

Console.WriteLine("After ReverseSort dogPair\t: {0}", 

dogPair.ToString( )); 

Kết quả: 

studentPair : Jesse, Stacey 

dogPair : 65, 12 

After Sort studentPair : Jesse, Stacey 

After ReverseSort studentPair : Stacey, Jesse 

After Sort dogPair : 12, 65 

After ReverseSort dogPair : 65, 12 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

94 

Chương trình testtạo ra hai đối tượng Studentvà hai đối tượng Dog, sau đó đưa 

chúng vào túi chứa Pair. Hàm khởi tạo của Studentnhận vào tên sinh viên cò 

hàm khởi tạo Dognhận vào trọng lượng của chó. 

Student Jesse = newStudent("Jesse"); 

Student Stacey = newStudent ("Stacey"); 

Dog Milo = newDog(65); 

Dog Fred = newDog(12); 

Pair studentPair = newPair(Jesse,Stacey); 

Pair dogPair = newPair(Milo, Fred); 

Console.WriteLine("studentPair\t\t\t:{0}",studentPair.ToString()); 

Console.WriteLine("dogPair\t\t\t\t: {0}", dogPair.ToString( )); 

Sau đó in nội dung của hai túi chứa Pair đểxem thứtựcủa chúng. Kết quảnhư

sau: 

studentPair : Jesse, Stacey 

dogPair : 65, 12 

Nhưmong đợi thứtựcủa các đối tượng là thứtựchúng được thêm vào túi chứa 

Pair. Kếtiếp chúng ta khởi tạo hai đối tượng delegate

Pair.WhichIsFirst theStudentDelegate = 

newPair.WhichIsFirst( Student.WhichStudentComesFirst ); 

Pair.WhichIsFirst theDogDelegate = 

newPair.WhichIsFirst( Dog.WhichDogComesFirst ); 

Ở  delegatethứnhất,  theStudentDelegate,  được tạo bằng cách truyền 

phương thức tĩnh thích hợp từlớp  Student.  Ở  delegatethứhai, 

theDogDelegate được truyền phương thức tĩnh của lớp Dog.

Các delegatebây giờcó thể  được truyền cho các phương thức. Ta truyền 

delegatethứnhất cho phương thức Sort()của đối tượng Pair, và sau đó là 

phương thức ReverseSort. Kết quả được in trên màn hình Consolenhưsau. 

After Sort studentPair : Jesse, Stacey 

After ReverseSort studentPair : Stacey, Jesse 

After Sort dogPair : 12, 65 

After ReverseSort dogPair : 65, 12 

12.1.2 Delegate tĩnh 

Điểm bất lợi của ví dụ12-1 là nó buộc lớp gọi, trong trường hợp này là lớp Test, 

phải khởi tạo các delegatenó cần đểsắp thứtựcác đối tượng trong một cặp. Sẽ

tốt hơn nếu nhưcó thểlấy các delegatetừlớp Dogvà Student. Chúng ta có thể

làm điều này bằng cách tạo cho trong mỗi lớp một delegatetĩnh. Đối với lớp 

Studentta thêm nhưsau: 

public static readonlyPair.WhichIsFirst OrderStudents = 

newPair.WhichIsFirst(Student.WhichStudentComesFirst); 

Dòng lệnh này tạo một delegatetĩnh, chỉ đọc có tên là OrderStudent

Ta có thểtạo tương tựcho lớp Dog

public static readonlyPair.WhichIsFirst OrderDogs = 

newPair.WhichIsFirst(Dog. WhichDogComesFirst); 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

95 

Nhưvậy mỗi lớp có một delegateriêng, khi cần ta lấy các delegatenày và 

truyền nhưtham số. 

studentPair.Sort(theStudentDelegate); 

Console.WriteLine("After Sort studentPair\t\t: {0}", 

  studentPair.ToString( ));  studentPair.ReverseSort(Student.OrderStudents); 

Console.WriteLine("After ReverseSort studentPair\t: {0}", 

  studentPair.ToString( ));  dogPair.Sort(Dog.OrderDogs); 

Console.WriteLine("After Sort dogPair\t\t: {0}", 

  dogPair.ToString( ));  dogPair.ReverseSort(Dog.OrderDogs); 

Console.WriteLine("After ReverseSort 

  dogPair.ToString( ));  Kết quảhoàn toàn nhưví dụtrên. 

12.1.3 Delegate nhưProperty 

Một vấn đềvới delagatetĩnh là nó phải được khởi tạo trước, cho dù có được 

dùng hay không. Ta có thểcải tiến bằng cách thay đổi biến thành viên tĩnh thành 

property 

Đối với lớp Student, ta bỏkhai báo sau: 

public static readonlyPair.WhichIsFirst OrderStudents = 

newPair.WhichIsFirst(Student.WhichStudentComesFirst); 

và thay thếbằng 

public staticPair.WhichIsFirst OrderStudents 

get{ return newPair.WhichIsFirst(WhichStudentComesFirst); } 

Tương tựthay thếcho lớp Dog

public staticPair.WhichIsFirst OrderDogs 

get{ return newPair.WhichIsFirst(WhichDogComesFirst);} 

Khi property OrderStudent được truy cập, delegatesẽ được tạo: 

return newPair.WhichIsFirst(WhichStudentComesFirst); 

Khác biệt chính ở đây là delegatesẽchỉ được khởi tạo khi có yêu cầu. 

12.1.4 Thứtựthực thi với mảng các các delegate 

Delegatecó thểgiúp ta xậy dựng một hệthống cho phép người dùng có thểquyết 

định một cách động trình tựthực thi các thao tác. Giảsửchúng ta có hệthống sửlý 

ảnh, hệthống này có thểthao tác ảnh theo nhiều cách như: làm mờ(blur) ảnh, làm 

sắc nét, quay, lọc v.v…ảnh. Cũng giảsửrằng trình tựáp dụng các hiệu ứng trên ảnh 

hưởng lớn đến đến chất lượng của ảnh. Người dùng sẽmong muốn chọn các hiệu 

ứng họlẫn trình tựcủa chúng từmột thực đơn, sau đó hệthống sẽthực hiện các 

hiệu ứng này theo trình tựhọ đã định. 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

96 

Ta có thểtạo một delegatecho mỗi thao tác (hiệu ứng) và đẩy chúng vào một túi 

chứa có thứtự, nhưmột mảng chẳng hạn, theo đúng trình tựnó sẽ được thực thi. 

Khi tất cảcác delegate được tạo và thêm vào túi chứa, ta chỉ đơn giản duyệt suốt 

mảng, gọi các delegatekhi tới lượt. 

Ta bắt đầu tạo lớp Image  để  đại diện cho một bức ảnh sẽ  được xửlý bởi 

ImageProcessor: 

public classImage 

publicImage( ) 

Console.WriteLine("An image created"); 

Lớp ImageProcessorkhai báo một delegatekhông tham sốvà trảvềkiểu 

void

public delegate voidDoEffect( ); 

Sau đó khai báo một sốphương thức đểthao tác ảnh có nguyên mẫu hàm như

delegate đã khai báo ởtrên. 

public static voidBlur( ) 

Console.WriteLine("Blurring image"); 

public static voidFilter( ) 

Console.WriteLine("Filtering image"); 

public static voidSharpen( ) 

Console.WriteLine("Sharpening image"); 

public static voidRotate( ) 

Console.WriteLine("Rotating image"); 

Lớp ImageProccessorcần một mảng đểgiữcác delegatengười dùng chọn; 

một biến đểgiữsốlượng hiệu ứng muốn xửlý và hiển nhiên một bức ảnh image

DoEffect[] arrayOfEffects; 

Image image; 

intnumEffectsRegistered = 0; 

ImageProccessorcũng cần một phương thức đểthêm delegatevào mảng 

public voidAddToEffects(DoEffect theEffect) 

if(numEffectsRegistered >= 10) 

throw newException("Too many members in array"); 

arrayOfEffects[numEffectsRegistered++] = theEffect; 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

97 

Một phương thức đểgọi thực thi các hiệu ứng 

public voidProcessImages( ) 

for(inti = 0;i < numEffectsRegistered;i++) 

arrayOfEffects[i]( ); 

Cuối cùng ta khai báo các delegatetĩnh để clientcó thểgọi. 

publicDoEffect BlurEffect = newDoEffect(Blur); 

publicDoEffect SharpenEffect = newDoEffect(Sharpen); 

publicDoEffect FilterEffect = newDoEffect(Filter); 

publicDoEffect RotateEffect = newDoEffect(Rotate); 

Clientsẽcó các đoạn mã đểtương tác với người dùng, nhưng chúng ta sẽlàm lơ

chuyện này, mặc định các hiệu ứng, thêm chúng vào mảng và sau đó gọi 

ProcessImage

Ví dụ12-2. Sửdụng mảng các deleage 

usingSystem; 

namespaceProgramming_CSharp 

// ảnh ta sẽthao tác 

public classImage 

publicImage( ) 

Console.WriteLine("An image created"); 

public classImageProcessor 

  // khai báo delegate 

public delegate voidDoEffect( ); 

// tạo các delegate tĩnh gắn với các phương thức thành viên 

publicDoEffect BlurEffect = newDoEffect(Blur); 

publicDoEffect SharpenEffect = newDoEffect(Sharpen); 

publicDoEffect FilterEffect = newDoEffect(Filter); 

publicDoEffect RotateEffect = newDoEffect(Rotate); 

// hàm dựng khởi tạo ảng và mảng 

publicImageProcessor(Image image) 

this.image = image; 

arrayOfEffects = newDoEffect[10]; 

public voidAddToEffects(DoEffect theEffect) 

if(numEffectsRegistered >= 10) 

throw newException( "Too many members in array" ); 

arrayOfEffects[numEffectsRegistered++] = theEffect; 

// các hiệu ứng ảnh 

public static voidBlur( ) 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

98 

Console.WriteLine("Blurring image"); 

public static voidFilter( ) 

Console.WriteLine("Filtering image"); 

public static voidSharpen( ) 

Console.WriteLine("Sharpening image"); 

public static voidRotate( ) 

Console.WriteLine("Rotating image"); 

public voidProcessImages( ) 

for(inti = 0;i < numEffectsRegistered;i++) 

arrayOfEffects[i]( ); 

// các biến thành viên 

privateDoEffect[] arrayOfEffects; 

privateImage image; 

private intnumEffectsRegistered = 0; 

// lớp kiểm thử

public classTest 

public static voidMain( ) 

Image theImage = newImage( ); 

// không giao diện đểlàm đơn giản vấn đề

ImageProcessor theProc = newImageProcessor(theImage); 

theProc.AddToEffects(theProc.BlurEffect); 

theProc.AddToEffects(theProc.FilterEffect); 

theProc.AddToEffects(theProc.RotateEffect); 

theProc.AddToEffects(theProc.SharpenEffect); 

theProc.ProcessImages( ); 

Kết quả: 

An image created 

Blurring image 

Filtering image 

Rotating image 

Sharpening image 

Trong lớp Test, ImageProcessor được khởi tạo và các hiệu ứng được thêm vào. 

Nếu người dùng chọn làm mờ ảnh (blur) trước khi lọc ảnh (filter), chỉcần đơn giản 

thay đổi thứtựcủa chúng trong mảng Tương tự, bất kỳmột hiệu ứng nào cũng có 

thể được lặp lại bằng cách thêm vào túi chứa delegatenhiều lần. 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

99 

12.1.5 Multicasting 

Multicastinglà cách đểgọi hai phương thức thông qua một delegate đơn. 

Điều này sẽtrởnên quan trọng khi quản lý các sựkiện. Mục tiêu chính là đểcó một 

delegate đơn có thểgọi nhiều phương thức cùng một lúc. Nó khác với mảng các 

delagte, trong mảng delegate mỗi delegatechỉgọi một phương thức. Ví dụ

trước dùng một mảng làm túi chứa nhiều delegatekhác nhau. 

Với multicastingta có thểtạo một delegate đơn đóng gói nhiều phương thức. 

Ví dụkhi một button được nhấn, ta hằn muốn thao tác nhiều hành động cùng một 

lúc. Ta có thểcài đặt  điều này bằng cách cho mỗi  buttonmột mảng các 

delegate, nhưng sẽdễhơn và rõ nghĩa hơn khi tạo một  multicasting

delegate. 

Bất kỳmột delegatenào trảvề void đều là multicast delegate, mặc dù ta 

có thể đối xửnó như single cast delegate(là delegate đềcập ởphần trên) nếu 

muốn. Hai multicast delegatecó thểkết nối với nhau bằng toán tửcộng (+). 

Kết quảlà một multicast delegatemới đóng gói tất cảcác phương thức của 

hai delegatetoán hạng. Ví dụ, giảsử Writervà Loggerlà các delegatetrảvề

kiểu void, dòng lệnh sau sẽkết nối chúng và tạo ra một multicast delegate

mới có tên là myMulticastDelegate

myMulticastDelegate = Writer + Logger; 

Ta cũng có thểthêm một delegatevào một multicast delegatebằng toán tử

cộng bằng (+=). Giảsửta có Transmittervà myMulticastDelegatelà các 

delegate, dòng lệnh sau: 

myMulticastDelegate += Transmitter; 

tương tựnhưdòng: 

myMulticastDelegate = myMulticastDelegate + Transmitter; 

Đểxem cách multicast delegate được tạo và sửdụng, xem qua toàn bộví dụ

12-3. Trong ví dụnày ta tạo một lớp tên là MyClassWithDelegate, lớp này định 

nghĩa một delegatenhận một tham sốkiểu chuỗi và trảvềkiểu void. 

public delegate voidStringDelegate(strings); 

Sau đó ta định nghĩa một lớp tên là MyImplementingClasscó ba phương thức, tấ

cả đều trảvề voidvà nhận một tham sốkiểu chuỗi: WriteString, LogString

và TransmitString. Phương thức đầu viết một chuỗi ra màn hình (đầu ra chuẩn), 

phương thức thứhai viết ra tập tin lỗi (log file) và phương thức thứba chuyển chuỗi 

lên Internet. Ta tạo các delegate đểgọi các phương thức thích hợp. 

Writer("String passed to Writer

"); 

Logger("String passed to Logger

"); 

Transmitter("String passed to Transmitter

"); 

Đểxem cách kết hợp các delegateta tạo ra một delegatekhác 

MyClassWithDelegate.StringDelegate myMulticastDelegate; 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

100 

và gán nó bằng kết quảcủa phép cộng hai delegate đã tồn tại 

myMulticastDelegate = Writer + Logger; 

Ta cũng có thểthêm bằng toán tửcộng bằng 

myMulticastDelegate += Transmitter; 

Cuối cùng ta có thểbỏmột delegatebằng toán tửtrừbằng (-=) 

DelegateCollector -= Logger; 

Ví dụ12-3. Kết hợp các delegate 

usingSystem; 

namespaceProgramming_CSharp 

public classMyClassWithDelegate 

// khai báo delegate 

public delegate voidStringDelegate(strings); 

public classMyImplementingClass 

public static voidWriteString(strings) 

Console.WriteLine("Writing string {0}", s); 

public static voidLogString(strings) 

Console.WriteLine("Logging string {0}", s); 

public static voidTransmitString(strings) 

Console.WriteLine("Transmitting string {0}", s); 

public classTest 

public static voidMain( ) 

// định nghĩa ba đối tượng StringDelegate 

MyClassWithDelegate.StringDelegate Writer,Logger,Transmitter; 

// định nghĩa một SringDelegate khác 

// hành động nhưmột multicast delegate 

MyClassWithDelegate.StringDelegate myMulticastDelegate; 

// khởi tạo 3 delegate đầu tiên, 

// truyền vào các phương thức định đóng gói 

Writer = newMyClassWithDelegate.StringDelegate( 

 MyImplementingClass.WriteString);  Logger = newMyClassWithDelegate.StringDelegate( 

MyImplementingClass.LogString); 

Transmitter = newMyClassWithDelegate.StringDelegate( 

MyImplementingClass.TransmitString); 

// gọi phương thức của delegate Writer 

Writer("String passed to Writer

"); 

// gọi phương thức của delegate Logger 

Logger("String passed to Logger

"); 

// gọi phương thức của delegate Transmitter 

Transmitter("String passed to Transmitter

"); 

// thông báo kết nối hai deleagte 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

101 

// thành một multicast deleagte 

Console.WriteLine("myMulticastDelegate = Writer + Logger"); 

// kết nối hai deleagte 

// thành một multicast deleagte

  myMulticastDelegate = Writer + Logger; 

// gọi delegated, hai phương thức được gọi  myMulticastDelegate("First string passed to Collector"); 

// thông báo thêm deleagte thứba 

// vào một multicast deleagte 

Console.WriteLine("

myMulticastDelegate += Transmitter"); 

// thêm delegate thứba 

myMulticastDelegate += Transmitter; 

// gọi delegate, ba phương thức được gọi 

   myMulticastDelegate("Second string passed to Collector"); 

// thông báo loại bỏdelegate logger 

Console.WriteLine("

myMulticastDelegate -= Logger"); 

// bỏdelegate logger 

myMulticastDelegate -= Logger; 

// gọi delegate, hai phương htức còn lại được gọi 

  myMulticastDelegate("Third string passed to Collector"); 

Kết quả: 

Writing string String passed to Writer 

Logging string String passed to Logger 

Transmitting string String passed to Transmitter 

myMulticastDelegate = Writer + Logger 

Writing string First string passed to Collector 

Logging string First string passed to Collector 

myMulticastDelegate += Transmitter 

Writing string Second string passed to Collector 

Logging string Second string passed to Collector 

Transmitting string Second string passed to Collector 

myMulticastDelegate -= Logger 

Writing string Third string passed to Collector 

Transmitting string Third string passed to Collector … 

Sức mạnh của multicast delegatesẽdễhiểu hơn trong khái niệm event. 

12.2 Event (Sựkiện) 

Giao diện người dùng đồhọa (Graphic user inteface - GUI), Windows và các trình 

duyệt yêu cầu các chương trình đáp ứng các sựkiện. Một sựkiện có thểlà một 

button được nhấn, một nục thực đơn được chọn, một tập tin đã chuyển giao hoàn 

tất v.v…. Nói ngắn gọn, là một việc gì đó xảy ra và ta phải đáp trảlại. Ta không thể

tiên đoán trước trình tựcác sựkiện sẽphát sinh. Hệthống sẽim lìm cho đến khi 

một sựkiện xảy ra, khi đó nó sẽthực thi các hành động để đáp trảkiện này. 

Trong môi trường GUI, có rất nhiều điều khiển (control, widget) có thểphát 

sinh sựkiện Ví dụ, khi ta nhấn một button, nó sẽphát sinh sựkiện Click. Khi ta 

thêm vào một drop-down listnó sẽphát sinh sựkiện ListChanged. 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

102 

Các lớp khác sẽquan tâm đến việc đáp trảcác sựkiện này. Cách chúng đáp trảnhư

thếnào không được quan tâm đến (hay không thể) ởlớp phát sinh sựkiện. Nút 

button sẽnói "Tôi được nhấn" và các lớp khác đáp trảphù hợp. 

12.2.1 Publishing và Subcribing 

Trong C#, bất kỳmột lớp nào cũng có thểphát sinh (publish) một tập các sựkiện 

mà các lớp khác sẽbắt lấy (subscribe). Khi một lớp phát ra một sựkiện, tất cả

các lớp subscribe đều được thông báo. 

Với kỹthuật này, đối tượng của ta có thểnói "Đây là các vấn đềmà tôi có thểthông 

báo cho anh biết" và các lớp khác sẽnói "Vâng, hãy báo cho tôi khi nó xảy ra". Ví 

dụnhư, một buttonsẽthông báo cho bất ký các lớp nào quan tâm khi nó được 

nhấn. Button được gọi là publisherbởi vì button publishsựkiện Clickvà 

các lớp khác sẽgọi là subscribersbởi vì chúng subscribesựkiện Click

12.2.2 Event và Delegate 

Eventtrong C# được cài đặt bằng delegate. Lớp publish định nghĩa một 

deleagtemà các lớp subscribephải cài đặt. Khi một sựkiện phát sinh, phương 

thức của lớp subscribesẽ được gọi thông qua delegate. 

Cách quản lý các sựkiện được gọi là event handler (trình giải quyết sựkiện). Ta có 

thểkhai báo một event handlernhưlà ta đã làm với delegate. 

Đểthuận tiện, event handlertrong .NET Framework trảvềkiểu voidvà nhận 

vào 2 tham số. Tham sốthứnhất cho biết nguồn của sựkiện; có nghĩa là đối tượng 

publish. Tham sốthứhai là một đối tượng thừa kếtừlớp EventArgs. Có lời 

khuyên rằng ta nên thiết kếtheo mẫu được qui định này. 

EventArgslà lớp cơsởcho tất cảcác dữliệu vềsựkiện. Ngoại trừhàm khởi tạo, 

lớp EventArgsthừa kếhầu hết các phương thức của lớp Object, mặc dù nó cũng 

có thêm vào một biến thành viên empty đại diện cho một sựkiện không có trạng 

thái (đểcho phép sửdụng có hiệu quảhơn các sựkiện không có trạng thái). Các lớp 

con thừa kếtừ EventArgschứa các thông tin vềsựkiện. 

Events are properties of the class publishing the event. The keyword event controls 

how the event 

property is accessed by the subscribing classes. The event keyword is designed to 

maintain the 

publish/subscribe idiom.

Giảsửta muốn tạo một lớp đồng hồ(Clock) sửdụng event đểthông báo các lớp 

subscribebiết khi nào thời gian thay đổi (theo đơn vịgiây). Gọi sựkiện này là 

OnSecondChange. Ta khai báo sựkiện và event handlertheo cú pháp sau đây: 

[attributes] [modifiers] event type member-name 

Ví dụnhư: 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

103 

public eventSecondChangeHandler OnSecondChange; 

Ví dụnày không có attribute(attributesẽ được đềcập trong chương 18). 

"modifier" có thểlà abstract, new, override, static, virtualhoặc là một 

trong bốn acess modifier, trong trường hợp này là public

Từkhóa eventtheo sau modifier

typelà kiểu  delegateliên kết với  event, trong trường hợp này là 

SecondChangeHandler

member namelà tên của event, trong trường hợp này là OnSecondChange. 

Thông thường nó được bắt đầu bằng từ On(không bắt buộc) 

Tóm lại dòng lệnh này khai báo một eventtên là OnSecondChange, cài đặt một 

delegatecó kiểu là SecondChangeHandler. 

Khai báo của SecondChangeHandlerlà 

public delegate voidSecondChangeHandler( objectclock, 

TimeInfoEventArgs timeInformation ); 

Như đã đềcập, đểcho thuận tiện một event handlerphải trảvềkiểu voidvà 

nhận vào hai tham số: nguồn phát sinh sựkiện (trường hợp này là clock) và một 

đối tượng thừa kếtừlớp  EventArgs, trong trường hợp này là 

TimeInfoEventArgs. TimeInfoEventArgs được khai báo nhưsau: 

public classTimeInfoEventArgs : EventArgs 

publicTimeInfoEventArgs(inthour, intminute, intsecond) 

this.hour = hour; 

this.minute = minute; 

this.second = second; 

public readonly inthour; 

public readonly intminute; 

public readonly intsecond; 

Một đối tượng TimeInfoEventArgssẽcó các thông tin vềgiờ, phút, giây hiện 

hành. Nó định nghĩa một hàm dựng và ba biến thành viên kiểu sốnguyên (int), 

publicvà chỉ đọc. 

Lớp Clockcó ba biến thành viên hour, minutevà secondvà chỉduy nhất một 

phương thức Run(): 

public voidRun( ) 

for(;;) 

// ngủ10 milli giây 

Thread.Sleep(10); 

// lấy giờhiện hành 

System.DateTime dt = System.DateTime.Now; 

// nếu biến giây thay đổi 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

104 

// thông báo cho subscriber 

if(dt.Second != second) 

// tạo đối tượng TimeInfoEventArgs 

// đểtruyền cho subscriber 

TimeInfoEventArgs timeInformation = 

newTimeInfoEventArgs(dt.Hour,dt.Minute,dt.Second); 

// nếu có subscribed, thông báo cho chúng 

if(OnSecondChange != null) 

OnSecondChange(this,timeInformation); 

// cập nhật trạng thái 

this.second = dt.Second; 

this.minute = dt.Minute; 

this.hour = dt.Hour; 

Hàm Runcó vòng lặp forvô tận luôn luôn kiểm tra giờhệthống. Nếu thời gian 

thay đổi nó sẽthông báo đến tất cảcác subscriber. 

Đầu tiên là ngủtrong 10 mili giây 

Thread.Sleep(10); 

Sleeplà phương thức tĩnh của lớp  Thread, thuộc vềvùng tên 

System.Threading. Lời gọi Sleepnhằm ngăn vòng lặp không sửdụng hết tài 

nguyên CPU của hệthống. Sau khi ngủ10 mili giây, kiểm tra giờhiện hành 

System.DateTime dt = System.DateTime.Now; 

Khoảng sau 100 lần kiểm tra , giá trịgiây sẽtăng. Phương thức sẽthông báo thay 

đổi này cho các subscriber. Đểthực hiện điều này, đầu tiên tạo một đối tượng 

TimeInfoEventArgsmới. 

if(dt.Second != second) 

 TimeInfoEventArgs timeInformation = 

newTimeInfoEventArgs(dt.Hour,dt.Minute,dt.Second); 

Sau  đó thông báo cho các  subscriberbằng cách phát ra sựkiện 

OnSecondChange

if(OnSecondChange != null) 

OnSecondChange(this,timeInformation); 

Nếu không có subsrcibernào đăng ký, OnSecondChangecó trị null, kiểm tra 

điều này trước khi gọi. 

Nhớrằng OnSecondChangenhận 2 tham số: nguồn phát sinh sựkiện và đối tượng 

thừa kếtừlớp EventArgs. Quan sát kỹta thấy phương thức dùng từkhóa this

làm tham sốbởi chính clocklà nguồn phát sinh sựkiện. 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

105 

Phát sinh một sựkiện sẽgọi tất cảcác phương thức đã đăng ký với Clockthông 

qua deleagte. Chúng ta xem xét vấn đềnày ngay bây giờ. 

Mỗi lần sựkiện phát sinh, ta cập nhật trạng thái của lớp Clock: 

this.second = dt.Second; 

this.minute = dt.Minute; 

this.hour = dt.Hour; 

Vấn đềcòn lại là tạo lớp subcriber. Ta sẽtạo ra 2 lớp. Lớp thứnhất là 

DisplayClock. Lớp này hiển thịthời gian ra màn hình Console. Ví dụnày đơn 

giản tạo ra 2 phương thức, phương thức thứnhất là Subscribecó nhiệm vụ

subscribesựkiện OnSecondChange. Phương thức thứhai là một  event

handlertên TimeHasChanged 

public classDisplayClock 

public voidSubscribe(Clock theClock) 

theClock.OnSecondChange += 

newClock.SecondChangeHandler(TimeHasChanged); 

public voidTimeHasChanged( 

objecttheClock, TimeInfoEventArgs ti) 

Console.WriteLine("Current Time: {0}:{1}:{2}", 

ti.hour.ToString( ), 

ti.minute.ToString( ), 

ti.second.ToString( )); 

Khi phương thức  đầu,  Subscribe,  được gọi, nó tạo một  delegate

SecondChangeHandlertruyền cho phương thức TimeHasChanged. Việc này 

đăng ký delegatecho sựkiện OnSecondChangecủa Clock

Ta sẽtạo lớp thứhai, lớp này cũng sẽ đáp ứng sựkiện, tên là LogCurrentTime. 

Lớp này chỉ đơn giản ghi lại thời gian vào một tập tin, nhưng để đơn giản lớp này 

cũng xuất ra màn hình console. 

public classLogCurrentTime 

public voidSubscribe(Clock theClock) 

theClock.OnSecondChange += 

newClock.SecondChangeHandler(WriteLogEntry); 

// phương thức sẽghi lên tập tin 

// nhưng để đơn giản ta cũng ghi ra console 

public voidWriteLogEntry( objecttheClock, 

 TimeInfoEventArgs ti)  { 

  Console.WriteLine("Logging to file: {0}:{1}:{2}", 

ti.hour.ToString( ), 

ti.minute.ToString( ), 

ti.second.ToString( )); 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

106 

Mặc dù trong ví dụnày hai lớp tương tựnhưnhau, nhưng bất kỳlớp nào cũng có 

thể subscribemột event. 

Chú ý rằng event được thêm vào bằng toán tử+=. Điều này cho phép các sựkiện 

mới được thêm vào sựkiện OnSecondChangecủa đối tượng Clockmà không làm 

hỏng đi các sựkiện đã đăng ký trước đó. Khi LogCurrentTime subscribevào 

sựkiện OnSecondChanged, ta không cần quan tâm rằng DisplayClock  đã 

subscribehay chưa. 

Ví dụ12-4. Làm việc với event 

usingSystem; 

usingSystem.Threading; 

namespaceProgramming_CSharp 

// lớp giữthông tin vềmột sựkiện 

 // trong trường hợp này là thông tin về đồng hồ

// nhưng tốt hơn là phải có thêm thông tin trạng thái 

public classTimeInfoEventArgs : EventArgs 

publicTimeInfoEventArgs(inthour, intminute, intsecond) 

this.hour = hour; 

this.minute = minute; 

this.second = second; 

public readonly inthour; 

public readonly intminute; 

public readonly intsecond; 

// lớp chính của ta. 

public classClock 

// delegate mà subscribers phải cài đặt 

public delegate voidSecondChangeHandler( objectclock, 

  TimeInfoEventArgs timeInformation);    // sựkiện publish 

public eventSecondChangeHandler OnSecondChange; 

// vận hành đồng hồ

// hàm sẽphát sinh sựkiện sau mỗi giây 

public voidRun( ) 

for(;;) 

// ngủ10 milli giây 

Thread.Sleep(10); 

// lấy giờhiện tại 

System.DateTime dt = System.DateTime.Now; 

// nếu thời gian thay đổi 

// thông báo cho các subscriber 

if(dt.Second != second) 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

107 

// tạo đối tượng TimeInfoEventArgs 

// đểtruyền cho subscriber 

TimeInfoEventArgs timeInformation=newTimeInfoEventArgs( 

   dt.Hour,dt.Minute,dt.Second);    // nếu có subscriber, thông báo cho chúng 

if(OnSecondChange != null) 

OnSecondChange( this,timeInformation ); 

// cập nhật trạng thái 

this.second = dt.Second; 

this.minute = dt.Minute; 

this.hour = dt.Hour; 

private inthour; 

private intminute; 

private intsecond; 

public classDisplayClock 

// subscribe sựkiện SecondChangeHandler của theClock 

public voidSubscribe(Clock theClock) 

theClock.OnSecondChange += 

newClock.SecondChangeHandler(TimeHasChanged); 

// phương thức cài đặt hàm delegated 

public voidTimeHasChanged( objecttheClock,   

  ' TimeInfoEventArgs ti)  { 

Console.WriteLine("Current Time: {0}:{1}:{2}", 

ti.hour.ToString( ), 

ti.minute.ToString( ), 

ti.second.ToString( )); 

public classLogCurrentTime 

public voidSubscribe(Clock theClock) 

theClock.OnSecondChange += 

newClock.SecondChangeHandler(WriteLogEntry); 

// phương thức này nên viết lên tập tin 

// nhưng để đơn giản ta xuất ra màn hình console 

public voidWriteLogEntry(objecttheClock,TimeInfoEventArgs ti) 

Console.WriteLine("Logging to file: {0}:{1}:{2}", 

ti.hour.ToString( ), 

ti.minute.ToString( ), 

ti.second.ToString( )); 

public classTest 

Delegate và Event   Gvhd: Nguyễn Tấn Trần Minh Khang

108 

public static voidMain( ) 

// tạo đồng hồmới 

Clock theClock = newClock( ); 

// tạo một displayClock 

// subscribe với clock vừa tạo 

DisplayClock dc = newDisplayClock( ); 

dc.Subscribe(theClock); 

// tạo đối tượng Log 

// subscribe với clock vừa tạo 

LogCurrentTime lct = newLogCurrentTime( ); 

lct.Subscribe(theClock); 

// bắt đầu chạy 

theClock.Run( ); 

Kết quả: 

Current Time: 14:53:56 

Logging to file: 14:53:56 

Current Time: 14:53:57 

Logging to file: 14:53:57 

Current Time: 14:53:58 

Logging to file: 14:53:58 

Current Time: 14:53:59 

Logging to file: 14:53:59 

Current Time: 14:54:0 

Logging to file: 14:54:0 

12.2.3 Tách rời Publisher khỏi Subsciber 

Lớp Clockchỉnên đơn giản in thời gian hơn là phải phát sinh sựkiện, vậy tại sao 

phải bịlàm phiền bằng việc sửdụng gián tiếp delegate? Thuận lợi của ý tưởng 

publish/subscribelà bất kỳlớp nào (bao nhiêu cũng được) cũng có thể được 

thông báo khi một sựkiện phát sinh. Lớp subscribekhông cần phải biết cách làm 

việc của Clock, và Clockcũng không cần biết chuyện sẽxảy ra khi một sựkiện 

được đáp trả. Tương tựmột buttoncó thểphát ra sựkiện OnClickvà bất kỳlớp 

nào cũng có thể subscribesựkiện này, nhận vềthông báo khi nào buttonbị

nhấn. 

Publishervà Subscriber được tách biệt nhờ delegate. Điều này được mong 

chờnhất vì nó làm cho mã nguồn được mềm dẻo (flexible) và dễhiểu. Lớp Clock

có thểthay đổi cách nó xác định thời gian mà không ảnh hưởng tới các lớp 

subscriber. Tương tựcác lớp subscribercũng có thểthay đổi cách chúng đáp 

trảsựkiện mà không ảnh hưởng tới lớp Clock. Hai lớp này hoàn toàn độc lập với 

nhau, và nó giúp cho mã nguồn dễbảo trì hơn. 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

109 

Chương 13 Lập trình với C# 

Phần này sẽgiới thiệu chi tiết vềcách viết các chương trình .NET, bao gồm 

Windows Forms và Web Forms. Ngoài ra, chúng ta sẽkhảo sát thêm vềcách tương 

tác với cơsởdữliệu (Database) và các dịch vụWeb (Web Services). 

Quan điểm vềkiến trúc .NET là tạo sựdễdàng, thuận tiện khi phát triển các phần 

mềm theo tính hướng đối tượng. Với mục đích này, tầng trên cùng của kiến trúc 

.NET được thiết kế đểbao gồm hai phần: ASP.NET và Windows Form. ASP.NET 

được dùng cho hai mục đích chính: hoặc đểtạo các ứng dụng Web với Web Forms 

hoặc tạo các đối tượng Web (Web Objects) không có giao diện người dùng (User 

Interface: UI) với Web Services. 

Ta sẽkhảo sát chi tiết các mục chính sau : 

1. Cách tạo ra các ứng dụng Windows có tính chuyên nghiệp cao trong môi 

trường phát triển Windows Form một cách nhanh chóng theo mô hình RAD ( 

Rapid Application Development ). 

2. Đểcó thểtruy cập dữliệu trong các hệquản trịdữliệu, ta sẽthảo luận chi tiết 

vềADO.NET và cách tương tác với Microsoft SQL Server và các trình cung 

cấp dữliệu (Providers Data ) khác. 

3. Là sựkết hợp công nghệRAD trong phần (1) và ADO.NET trong phần (2) để

minh họa việc tạo ra các ứng dụng Web với Web Forms. 

4. Không phải tất cảmọi ứng dụng đều có giao diện người dùng thân thiện. Web 

Services giúp tạo các ứng dụng nhưvậy, chúng là những ứng dụng có tính 

phân phối, cung cấp các chức năng dựa trên các nghi thức Web chuẩn, thường 

dùng nhất là XML và HTTP. 

13.1 Ứng dụng Windows với Windows Form 

Trước tiên, chúng ta cần phân biệt sựkhác nhau giữa hai kiểu ứng dụng: Windows 

và Web. Khi các ứng dụng Web đầu tiên được tạo ra, người ta phân biệt hai loại 

ứng dụng trên nhưsau : ứng dụng Windows chạy trên Desktop hay trên một mạng 

cục bộLAN (Local-Area Network), còn ứng dụng Web thì được chạy trên Server ở

xa và được truy cập bằng trình duyệt Web (web browser). Sựphân biệt này không 

còn rõ ràng nữa vì các ứng dụng Windows hiện nay có xu hướng dùng các dịch vụ

của Web. Ví dụnhưphần mềm Outlook chuyển nhận thưthông qua kết nối Web. 

Theo quan điểm của Jesse Liberty, tác giảcủa cuốn sách “Programming C#”, xuất 

bản vào tháng 7 năm 2001. Ông cho rằng điểm phân biệt chủyếu giữa ứng dụng 

Windows và Web là ởchỗ: Cái gì sởhữu UI?, Ứng dụng dùng trình duyệt đểhiển 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

110 

thịhay UI của ứng dụng được xây dựng thành chương trình có thểchạy trên 

Desktop.

Có một sốthuận lợi đối với các ứng dụng Web, ứng dụng có thể được truy cập bởi 

bất kỳtrình duyệt nào kết nối đến Server, việc hiệu chỉnh được thực hiện trên 

Server, không cần phải phân phối thưviện liên kết động (Dynamic Link Libraries -DLLs) mới cần đểchạy ứng dụng cho người dùng. 

.NET cũng có sựphân biệt này, điển hình là có những bộcông cụthích hợp cho 

từng loại ứng dụng: Windows hay Web. Cảhai loại này đều dựa trên khuôn mẫu 

Form và sửdụng các điều khiển (Control) nhưlà Buttons, ListBox, Text … 

Bộcông cụdùng đểtạo ứng dụng Web được gọi là Web-Form, được thảo luận 

trong mục (3). Còn bộcông cụdùng đểtạo ứng dụng Windows được gọi là 

Windows-Form, sẽ được thảo luận ngay trong mục này. 

Chú ý : Theo tác giảJesseLiberty, ông cho rằng hiện nay ứng dụng kiểu 

Windows và Web có nhiều điểm giống nhau, và ông cho rằng .NET nên 

gộp lại thành một bộcông cụchung cho cả ứng dụng Windows và Web 

trong phiên bản tới. 

Trong các trang kế, chúng ta sẽhọc cách tạo một Windows Form đơn giản bằng 

cách dùng trình soạn mã hoặc công cụthiết kế(Design Tool) trong Visual Studio 

.NET. Kếtiếp ta sẽkhảo sát một ứng dụng Windows khác phức tạp hơn, ta sẽhọc 

các dùng bộcông cụkéo thảcủa Visual Studio .NET và một sốkỹthuật lập trình 

C# mà ta đã thảo luận trong phần trước. 

13.1.1 Tạo một Windows Form đơn giản 

Windows Form là công cụdùng đểtạo các ứng dụng Windows, nó mượn các ưu 

điểm mạnh của ngôn ngữVisual Basic : dễsửdụng, hỗtrợmô hình RAD đồng thời 

kết hợp với tính linh động, hướng đối tượng của ngôn ngữC#. Việc tạo ứng dụng 

Windows trởlên hấp dẫn và quen thuộc với các lập trình viên. 

Trong phần này, ta sẽthảo luận hai cách khi tạo một ứng dụng Windows : Dùng bộ

soạn mã đểgõ mã trực tiếp hoặc dùng bộcông cụkéo thảcủa IDE. 

Ứng dụng của chúng ta khi chạy sẽxuất dòng chữ“Hello World!” ra màn hình, khi 

người dùng nhấn vào Button “Cancel” thì ứng dụng sẽkết thúc. 

13.1.1.1 Dùng bộsoạn mã ( Nodepad ) 

Mặc dù Visual Studio .NET cung cấp một bộcác công cụphục vụcho việc kéo thả, 

giúp tạo các ứng dụng Windows một các nhanh chóng và hiệu quả, nhưng trong 

phần này ta chỉcần dùng bộsoạn mã. 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

111 

Hình 13-1 Ứng dụng minh họa việc hiển thịchuỗi và bắt sựkiện của Button. 

Đầu tiên, ta dùng lệnh using  đểthêm vùng tên sau : 

usingSystem.Windows.Forms; 

Ta sẽcho ứng dụng của ta thừa kếtừvùng tên Form : 

public classHandDrawnClass : Form 

Bất kỳmột ứng dụng Windows Form nào cũng đều thừa kếtừ đối tượng Form, ta 

có thểdùng đối tượng này đểtạo ra các cửa sổchuẩn như: các cửa sổtrôi (floating 

form), thanh công cụ(tools), hộp thoại (dialog box) … Mọi Điều khiểntrong bộ

công cụcủa Windows Form (Label, Button, Listbox …) đều thuộc vùng tên này. 

Ta sẽkhai báo 2 đối tượng, một Label  đểgiữchuỗi ‘ Hello World !’ và một Button 

đểbắt sựkiện kết thúc ứng dụng. 

privateSystem.Windows.Forms.Label lblOutput; 

privateSystem.Windows.Forms.Button btnCancel; 

Tiếp theo ta sẽkhởi tạo 2 đối tượng trên trong hàm khởi tạo của Form: 

this.lblOutput = newSystem.Windows.Forms.Label( ); 

this.btnCancel = newSystem.Windows.Forms.Button( ); 

Sau đó ta gán chuỗi tiêu đềcho Form của ta là ‘Hello World‘ : 

this.Text = "Hello World"; 

Chú ý :Do các lệnh trên được đặt trong hàm khởi tạo của Form 

HandDrawClass, vì thếtừkhóa this sẽtham chiếu tới chính nó. 

Gán vịtrí, chuỗi và kích thước cho đối tượng Label : 

lblOutput.Location = newSystem.Drawing.Point (16, 24); 

lblOutput.Text = "Hello World!"; 

lblOutput.Size = newSystem.Drawing.Size (216, 24); 

Vịtrí của Label được xác định bằng một đối tượng Point, đối tượng này cần hai 

thông số: vịtrí so với chiều ngang (horizontal) và đứng (vertical) của thanh cuộn. 

Kích thước của Label cũng được đặt bởi đối tượng Size, với hai thông sốlà chiều 

rộng (width) và cao (height) của Label. Cảhai đối tượng Point và Size đều thuộc 

vùng tên System.Drawing : chứa các đối tượng và lớp dùng cho đồhọa. 

Tương tựlàm với đối tượng Button : 

btnCancel.Location = newSystem.Drawing.Point (150,200); 

btnCancel.Size = newSystem.Drawing.Size (112, 32); 

btnCancel.Text = "&Cancel"; 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

112 

Đểbắt sựkiện click của Button, đối tượng Button cần đăng ký với trình quản lý sự

kiện, đểthực hiện điều này ta dùng ‘delegate’. Phương thức được ủy thác (sẽbắt sự

kiện) có thểcó tên bất kỳnhưng phải trảvềkiểu void và phải có hai thông số: một 

là đối tượng ‘sender’ và một là đối tượng ‘System.EventArgs’. 

protected voidbtnCancel_Click( objectsender, System.EventArgs e) 

//... 

Ta đăng ký phương thức bắt sựkiện theo hai bước. Đầu tiên, ta tạo một trình quản 

lý sựkiện mới System.EventHandler, rồi đẩy tên của phương thức bắt sựkiện vào 

làm tham số: 

newSystem.EventHandler (this.btnCancel_Click); 

Tiếp theo ta sẽ  ủy thác trình quản lý vừa tạo ởtrên cho sựkiện click của 

Button bằng toán tử += 

Mã gộp của hai bước trên : 

one:btnCancel.Click +=newSystem.EventHandler 

(this.btnCancel_Click); 

Đểkết thúc việc viết mã trong hàm khởi tạo của Form, ta sẽthêm hai đối 

tượng Label và button vào Form của ta : 

this.Controls.Add (this.btnCancel); 

this.Controls.Add (this.lblOutput); 

Sau khi ta đã định nghĩa hàm bắt sựkiện click trên Button, ta sẽviết mã thi hành 

cho hàm này. Ta sẽdùng hàm tĩnh ( static ) Exit() của lớp Application đểkết thúc 

ứng dụng : 

protected voidbtnCancel_Click( objectsender, System.EventArgs e) 

Application.Exit(); 

Cuối cùng, ta sẽgọi hàm khởi tạo của Form trong hàm Main(). Hàm Main() là điểm 

vào đầu tiên của Form. 

public static voidMain( ) 

Application.Run(newHandDrawnClass( )); 

Sau đây là mã hoàn chỉnh của toàn bộ ứng dụng 

usingSystem; 

usingSystem.Windows.Forms; 

namespaceProgCSharp 

public classHandDrawnClass : Form 

// Label dùng hiển thịchuỗi ‘Hello World’ 

privateSystem.Windows.Forms.Label lblOutput; 

// Button nhấn ‘Cancel’ 

privateSystem.Windows.Forms.Button btnCancel; 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

113 

publicHandDrawnClass( ) 

// Tạo các đối tượng 

this.lblOutput = newSystem.Windows.Forms.Label ( ); 

this.btnCancel = newSystem.Windows.Forms.Button ( ); 

// Gán tiêu đềcho Form 

this.Text = "Hello World"; 

// Hiệu chỉnh Label 

lblOutput.Location = newSystem.Drawing.Point(16,24); 

lblOutput.Text = "Hello World!"; 

lblOutput.Size = newSystem.Drawing.Size (216, 24); 

// Hiệu chỉnh Button 

btnCancel.Location = newSystem.Drawing.Point(150,20); 

btnCancel.Size = newSystem.Drawing.Size (112, 32); 

btnCancel.Text = "&Cancel"; 

// Đăng ký trình quản lý sựkiện 

btnCancel.Click += 

newSystem.EventHandler (this.btnCancel_Click); 

//Thêm các điều khiển vào Form 

this.Controls.Add (this.btnCancel); 

this.Controls.Add (this.lblOutput); 

// Bắt sựkiện nhấn Button 

protected voidbtnCancel_Click(objectsender, EventArgs e) 

Application.Exit( ); 

// Chạy ứng dụng 

public static voidMain() 

Application.Run(newHandDrawnClass( )); 

13.1.1.2 Dùng kéo thảtrong Visual Studio .NET 

Bên cạnh trình soạn mã, .NET còn cung cấp một bộcác công cụkéo thả đểlàm việc 

trong môi trường phát triển tích hợp IDE( Intergrate Development Enviroment), 

IDE cho phép kéo thảrồi tự động phát sinh mã tương ứng. 

Ta sẽtạo lại ứng dụng trên bằng cách dùng bộcông cụtrong Visual Studio, ta mở

Visual Studio và chọn ‘New Project’. Trong cửa sổ‘New Project’, chọn loại dựán 

là Visual C# và kiểu ứng dụng là ‘Windows Applications’, đặt tên cho ứng dụng là 

ProgCSharpWindowsForm. 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

114 

Hình 13-2 Màn hình tạo ứng dụng Windows mới.

Vs.NET sẽtạo một ứng dụng Windows mới và đặt chúng vào IDEnhưhình dưới : 

Hình 13-3 Môi trường thiết kếkéo thả

Phía bên trái của cửa hình trên là một bộcác công cụ(Toolbox) kéo thảdành cho 

các ứng dụng Windows Form, chính giữa là một Form được .NET tạo sẵn có 

tên Form1. Với bộcông cụtrên, ta có thểkéo và thảmột Label hay Button trực tiếp 

vào Form, nhưhình sau : 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

115 

Hình 13-4 Môi trường phát triển Windows Form. 

Với thanh công cụToolbox ởbên trái, ta có thểthêm các thành phần mới vào nó 

bằng các chọn View/Add Reference. Gó bên phải phía trên là cửa sổduyệt toàn bộ

các tập tin trong giải pháp (Solution, một giải pháp có một hay nhiều dựán con). 

Phía dưới là cửa sổthuộc tính, hiển thịmọi thuộc tính vềmục chọn hiện hành. Ta 

có thểgán giá trịchuỗi hiển thịhoặc thay đổi font cho Label một cách trực tiếp 

trong cửa sổthuộc tính. 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

116 

Hình 13-5 Thay đổi font trực tiếp bằng hộp thoại font. 

Với IDE này, ta có thểkéo thảmột Button và bắt sựkiện click của nó một cách dễ

dàng, chỉcần Nhấn đúp vào Button thì tự động .NET sẽphát sinh ra các mã tương 

ứng trong trang mã của Form (Code-Behind page) như: khai báo, tạo Button và 

hàm bắt sựkiện click của Button. 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

117 

Hình 13-6 Sau khi nhấn đúp vào nút Cancel. 

Bây giờ, ta chỉcần gõ thêm một dòng code nữa trong hàm bắt sựkiện của Button là 

ứng dụng có thểchạy được y như ứng dụng mà ta đã tạo bằng cách gõ code trong 

phần trên. 

Application.Exit( ); 

Sau đây là toàn bộmã được phát sinh bởi IDE và dòng mã bạn mới gõ vào : 

usingSystem; 

usingSystem.Drawing; 

usingSystem.Collections; 

usingSystem.ComponentModel; 

usingSystem.Windows.Forms; 

usingSystem.Data; 

namespaceProgCSharpWindowsForm 

///  

///Summary description for Form1. 

///  

public classForm1 : System.Windows.Forms.Form 

privateSystem.Windows.Forms.Label lblOutput; 

privateSystem.Windows.Forms.Button btnCancel; 

///  

///Required designer variable. 

///  

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

118 

privateSystem.ComponentModel.Container components; 

publicForm1( ) 

InitializeComponent( ); 

public override voidDispose( ) 

base.Dispose( ); 

if(components != null) 

components.Dispose( ); 

#regionWindows Form Designer generated code 

///  

///Required method for Designer support - do not modify 

///the contents of this method with the code editor. 

///  

private voidInitializeComponent( ) 

this.lblOutput = newSystem.Windows.Forms.Label( ); 

this.btnCancel = newSystem.Windows.Forms.Button( ); 

this.SuspendLayout( ); 

// 

// lblOutput 

// 

this.lblOutput.Font = newSystem.Drawing.Font("Arial", 

15.75F, System.Drawing.FontStyle.Bold, 

System.Drawing.GraphicsUnit.Point,((System.Byte)(0))); 

this.lblOutput.Location = newSystem.Drawing.Point(24, 16); 

this.lblOutput.Name = "lblOutput"; 

this.lblOutput.Size = newSystem.Drawing.Size(136, 48); 

this.lblOutput.TabIndex = 0; 

this.lblOutput.Text = "Hello World"; 

// btnCancel 

this.btnCancel.Location = newSystem.Drawing.Point(192, 208); 

this.btnCancel.Name = "btnCancel"; 

this.btnCancel.TabIndex = 1; 

this.btnCancel.Text = "Cancel"; 

this.btnCancel.Click += 

newSystem.EventHandler( this.btnCancel_Click );  

this.AutoScaleBaseSize = newSystem.Drawing.Size(5, 13); 

this.ClientSize = newSystem.Drawing.Size(292, 273); 

this.Controls.AddRange(newSystem.Windows.Forms.Control[]{    this.btnCancel, this.lblOutput}); 

this.Name = "Form1"; 

this.Text = "Form1"; 

this.ResumeLayout(false); 

private voidbtnCancel_Click(objectsender, System.EventArgs e) 

Application.Exit( ); 

#endregion 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

119 

///  

///The main entry point for the application. 

///  

[STAThread] 

static voidMain( ) 

Application.Run(newForm1( )); 

So với đoạn mã ta gõ vào trong ứng dụng trước thì mã do IDE phát sinh không khác 

gì nhiều. Các dòng chú thích được dùng đểlàm sưu liệu báo cáo cho dựán. (mục 

này sẽ được thảo luận sau) 

///  

///Summary description for Form1. 

///  

Các mã tạo và hiệu chỉnh đối tượng thay vì được đặt trực tiếp vào hàm khởi tạo của 

Form, thì ở đây IDE đặt chúng vào trong hàm InitializeComponent(), Sau đó hàm 

này được gọi bởi hàm khởi tạo của Form. Mọi ứng dụng Windows Form đều phát 

sinh ra hàm này. 

13.1.2 Tạo một ứng dụng Windows Form khác 

Trong ứng dụng trên ta đã thảo luận sơqua về ứng dụng Windows Form, phần này 

ta sẽtạo một  ứng dụng Windows khác thực tếhơn. Ứng dụng có tên là 

FileCopier, cho phép chép hay xóa một hoặc nhiều tập tin từvịtrí này sang vị

trí khác. Mục đích của ứng dụng là minh họa sâu hơn vềcác kỹnăng lập trình C# và 

giúp người đọc hiểu thêm vềnamespace Windows.Forms.Giao diện của ứng 

dụng sau khi hoàn chỉnh sẽnhưsau : 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

120 

Hình 13-7 Giao diện người dùng của ứng dụng FileCopier. 

Giao diện của ứng dụng gồm các thành phần sau : 

• Labels: Các tập tin nguồn (Source Files) and Thưmúc đích (Target Directory). 

• Buttons: Bỏcác dấu chọn trên cây bên trái (Clear), Copy, Delete, and Cancel. 

• Checkbox : ghi đè lên nếu đã có sẵn ( "Overwrite if exists" ) 

• Checkbox : hiển thị đường dẫn của mục được trọn ởcây bên phải. 

• Hai cây (TreeView) chứa tập tin. 

Khi người dùng nhấn vào Button ‘Copy’thì tất các tập tin được chọn ởcây bên 

trái sẽ được chép qua cây bên phải, cũng nhưkhi nhấn vào Button ‘Delete’ thì sẽ

xóa các tập tin được chọn. 

13.1.2.1 Tạo giao diện cho ứng dụng 

Đầu tiên ta tạo một dựán Windows Form mới có tên FileCopier. IDE sẽhiển thị

màn hình thiết kế(Designer) lên, ta sẽthực hiện kéo thảcác Label, Button, 

Checkbox và TreeView cho đến khi thích hợp nhưhình dưới đây : 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

121 

Hình 13-8 Tạo giao diện ứng dụng bằng cách kéo thảdùng Designer 

Sau khi tạo giao diện xong, ta đặt thuộc tính CheckBoxes cho cây bên trái có tên 

tvwSource thành true, còn cây bên phải có tên tvwTargetDir thành false, đểthực 

hiện ta đơn giản chỉchọn và sửa đổi trên cửa sổthuộc tính của từng đối tượng. Khi 

ta nhấn đúp lên bất kỳ Điều khiển nào thì tự động Visual Studio .NET sẽphát sinh 

ra mã tương ứng đểbắt sựkiện của Điều khiển đó và đặt con trỏ( Cursor ) vào ngay 

tại hàm đó, ta nhấn đúp vào Button “Cancel” và bổsung mã nhưsau : 

protected voidbtnCancel_Click( objectsender, System.EventArgs e) 

Application.Exit( ); 

13.1.2.2 Quản lý điều khiển TreeView 

Trong ứng dụng này, hai điều khiển TreeView hoạt động tương tựnhau, ngoại trừ

điều khiển cây bên trái tvwTargetDir có thuộc tính CheckBoxes là true và liệt kê 

cảtập tin lẫn thưmục, còn cây bên phải là falsevà chỉliệt ke thưmục. Mặc nhiên 

thì điều khiển cây cho phép chọn nhiều mục một lúc, nhưng ta sẽchỉnh lại sao cho 

chỉcây bên trái tvwSource mới được chọn nhiều mục một lúc,bên phải thì không. 

Ta sẽtạo ra một hàm đẩy dữliệu vào cây : 

private voidFillDirectoryTree(TreeView tvw, boolisSource) 

Có 2 tham số: 

TreeView tvw: điều khiển cây cần đẩy dữliệu vào 

Bool isSource: cờxác định là dữliệu đẩy cho cây. Nếu isSource 

là true thì cây sẽliệt kê cảtập tin và thưmục, false thì chỉcó 

tập tin. 

Hàm này sẽ được dùng chung cho cảhai điều khiển cây : 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

122 

FillDirectoryTree(tvwSource, true); 

FillDirectoryTree(tvwTargetDir, false); 

Đối tượng TreeNode 

Điều khiển TreeView có một thuộc tính Nodes. thuộc tính này nhận vào một đối 

tượng TreeNodeCollection, đối tượng này thực chất là một mảng chứa các đối 

tượng TreeNode, mỗi một TreeNode là một nút trên cây. 

Trước tiên ta cần khởi tạo cây vềrỗng : 

tvw.Nodes.Clear( ); 

Sau đó ta gọi hàm tĩnh GetLogicalDrives() của đối tượng Enviroment đểlấy vềtất 

cảcác ổ đĩa logic hiện đang có trên máy. Đối tượng Enviroment cung cấp các thông 

tin như: tên máy tính, phiên bản hệ điều hành, hệthống thưmục … trên máy tính 

hiện hành. 

string[] strDrives = Environment.GetLogicalDrives( ); 

strDrivessẽchứa tên các ổ đĩa logic hiện có trên máy. 

Sau đó ta sẽduyệt qua từng ổ đĩa bằng cách dùng lệnh foreach. Với mỗi ổ đĩa logic, 

ta gọi hàm GetDirectories() của đối tượng DirectoryInfo. Hàm này sẽtrảvềdanh 

sách các đối tượng DirectoryInfo, chứa tất cảcác tập tin và thưmục trên ổ đĩa logic 

đó. Những tại đây ta không quan tâm đến kết quảmà nó trảvề, mục đích ta gọi hàm 

này chủyếu là đểkiểm tra xem các ổ đĩa có hợp lệhay không, nếu có bất kỳmột lỗi 

nào trên ổ đĩa thì hàm GetDirectories() sẽquăng ra một ngoại lệ. Ta sẽdùng khối 

bắt lỗi try…catch đểbắt lỗi này. 

foreach(stringrootDirectoryName instrDrives) 

 DirectoryInfo dir = newDirectoryInfo(rootDirectoryName); 

dir.GetDirectories( ); 

... 

Khi ỗ đĩa hợp lệ, ta sẽtạo ra một TreeNode ứng với rootDirectoryName ổ đĩa đó, 

chẳng hạn như: “C:\”, “D:\” …Rồi thêm TreeNode này vào điều khiển cây dùng 

hàm Add() thông qua thuộc tính Nodes của cây. 

TreeNode ndRoot = newTreeNode(rootDirectoryName); 

tvw.Nodes.Add(ndRoot); 

Tiếp theo ta tiến hành duyệt trên mọi thưmục con của đối tượng TreeNode gốc trên, 

đểlàm điều này ta gọi hàm GetSubDirectoriesNodes( ), hàm này cần nhận vào các 

đối số: TreeNode gốc, tên của nó và cờxác định là có đẩy cảtập tin vào cây hay 

không. 

if(isSource) 

GetSubDirectoryNodes(ndRoot, ndRoot.Text, true); 

else 

GetSubDirectoryNodes(ndRoot, ndRoot.Text, false); 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

123 

Duyệt đệqui trên các thưmục con 

Hàm GetSubDirectoryNodes() bắt đầu bằng việc gọi hàm GetDirectories() đểnhận 

vềmột danh sách các đối tượng DirectoryInfo : 

private voidGetSubDirectoryNodes( 

TreeNode parentNode, stringfullName, boolgetFileNames) 

 DirectoryInfo dir = newDirectoryInfo(fullName); 

 DirectoryInfo[] dirSubs = dir.GetDirectories( ); 

Ở đây ta thấy node truyền vào có tên là parentNode ( nút cha ), nghĩa là những nút 

sau này sẽ được xem là nút con của nó. Bạn sẽrõ hơn khi tìm hiểu hết hàm này. 

Ta tiến hành duyệt qua danh sách các thưmục con dirSubs, bỏqua các mục có trạng 

thái là ẩn ( Hidden ). 

foreach(Directory dirSub indirSubs) 

if( (dirSub.Attributes & FileSystemAttributes.Hidden) != 0 ) 

continue; 

FileSystemAttributes là biến có kiểu enum, nó chứa một sốgiá trịnhư: Archive, 

Compressed, Encrypted, Hidden, Normal, ReadOnly …Nếu nhưmục hiện hành 

không ởtrạng thái ẩn, ta sẽtạo ra một TreeNode mới với tham sốlà tên của nó. Sau 

đó Thêm nó vào nút cha parentNode : 

TreeNode subNode = newTreeNode(dirSub.Name); 

parentNode.Nodes.Add(subNode); 

Ta sẽgọi lại đệqui hàm GetDirectoriesNodes() đểliệt kê hết mọi mục con trên thư

nút hiện hành, với ba thông số: nút được chuyển vào nhưnút cha, tên đường dẫn 

đầy đủcủa mục hiện hành và cờtrạng thái. 

GetSubDirectoryNodes(subNode,dirSub.FullName,getFileNames); 

Chú ý : Thuộc tính dirSubs.FullName sẽtrảvề đường dẫn đầy đủcủa 

mục hiện hành ( “C:\dir1\dir2\file1” ), còn thuộc tính dirSubs.Name chỉ

trảvềtên của mục hiện hành ( “file1”). Khi ta tạo ra một nút con 

subNode, ta chỉtruyền cho nó tên của mục hiện hành, vì ta chỉmuốn 

hiển thịthịtên của nó trên cây. Còn khi ta gọi  đệqui hàm 

GetSubDirectoryNodes() thì ta cần truyền cho nó tên đường dẫn đầy đủ

của mục hiện hành, đểcó thểliệt kê toàn bộmục con cùa thực mục đang 

xét. 

Đến đây chắc bạn đã hiểu được sựphân cấp của cấu trúc cây và tại sao hàm 

GetSubDirectoryNodes() cần truyền có đối sốFullName.

Lấy vềcác tập tin trong thưmục 

Nếu biến cờgetFileNames là True thì ta sẽtiến hành lấy vềtất cảcác tập tin thuộc 

thưmục. Đểthực hiện ta gọi hàm GetFiles() của đối tượng DirectoryInfo, hàm này 

sẽtrảvềdanh sách các đối tượng FileInfo. Ta sẽduyệt qua danh sách này đểlấy ra 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

124 

tên của từng tập tin một, sau đó tạo ra một nút TreeNode với tên này, nút này sẽ

được thêm vào nút cha parentNode hiện hành. 

13.1.2.3 Quản lý sựkiện trên điều khiển cây 

Trong ứng dụng này, chúng ta sẽphải quản lý một sốsựkiện. Đầu tiên là sựkiện 

người dùng nhấn lên ô CheckBox đểchọn các tập tin hay thưmục ởcây bên phải 

hay nhấn các nút ởcây bên phải. Tiếp theo là các sựkiện nhấn vào Button ‘Cancel’, 

‘Copy’,’Delete’ hay ‘Clear’. 

Ta sẽkhảo sát sựkiện trên điều khiển cây trước. 

Sựkiện chọn một nút trên điều khiển cây bên trái 

Khi người dùng muốn chọn một tập tin hay thưmục đểchép hay xóa. Ứng với mỗi 

lần chọn sẽphát sinh ra một sốsựkiện tương ứng. Ta sẽbắt sựkiện AfterCheck của 

điều khiển cây. Ta gõ vào các đoạn mã sau : 

tvwSource.AfterCheck += 

newTreeViewEventHandler( this.tvwSource_AfterCheck );

Ta viết lệnh thực thi cho hàm bắt sựkiện AfterCheck có tên là 

tvwSource_AfterCheck, hàm này có hai tham số: đầu tiên là biến Sender chứa 

thông tin về  đối tượng phát sinh ra sựkiện, thứhai là  đối tượng 

TreeViewEventArgs chứa thông tin vềsựkiện phát ra. Ta sẽ đánh dấu là chọn cho 

thưmục được chọn và tất cảcác tập tin hay thưmục con của thưmục đó thông qua 

hàm SetCheck() : 

protected voidtvwSource_AfterCheck ( 

objectsender, System.Windows.Forms.TreeViewEventArgs e) 

SetCheck(e.node,e.node.Checked); 

Hàm SetCheck() sẽtiến hành thực hiện đệqui trên nút hiện hành, hàm gồm hai 

tham số: nút cần đánh dấu và cờxác định là đánh dấu hay bỏ đánh dấu chọn, nếu 

thuộc tính Count bằng không ( nghĩa là nút này là nút lá ) thì ta sẽ đánh dấu chọn 

cho nút đó. Nếu không ta gọi đệqui lại hàm SetCheck() : 

private voidSetCheck(TreeNode node, boolcheck) 

 node.Checked = check; 

foreach(TreeNode n innode.Nodes) 

if(node.Nodes.Count == 0) 

node.Checked = check; 

else 

SetCheck(n,check); 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

125 

Sựkiện chọn một nút trên điều khiển cây bên phải 

Khi người dùng chọn một nút ởcây bên phải, ta sẽphải cho hiện đường dẫn đầy đủ

của nút đó lên TextBox ởgóc phíc trên bên phải. Ta sẽbắt sựkiện AfterSelect của 

cây. Sựkiện này sẽ được gọi sau khi người dùng nhấn một nút nào đó trên cây, hàm 

bắt sựkiện này nhưsau : 

protected voidtvwTargetDir_AfterSelect( objectsender, 

System.Windows.Forms.TreeViewEventArgs e) 

stringtheFullPath = GetParentString(e.node); 

Sau khi ta có được đường dẫn đầy đủcủa nút chọn, ta sẽbỏ đi dấu \\ (Backslash) 

nếu có. Rồi cho hiển thịlên hộp thoại TextBox. 

if(theFullPath.EndsWith("\\")) 

theFullPath =theFullPath.Substring(0,theFullPath.Length-1); 

 txtTargetDir.Text = theFullPath; 

Hàm GetParentString() trảvề đường dẫn đầy đủcủa nút được truyền vào làm thông 

số. Hàm này cũng tiến hành lặp đệqui trên nút truyền vào nếu nút này không là nút 

lá và thêm dấu \\ vào nó. Quá lặp sẽkết thúc nếu nút hiện hành là không là nút cha. 

private stringGetParentString(TreeNode node) 

if(node.Parent == null) 

returnnode.Text; 

else 

returnGetParentString(node.Parent) + node.Text + 

      (node.Nodes.Count == 0 ? "" : "\\"); 

Quản lý sựkiện nhấn nút bỏchọn (Clear) 

Ta tiến hành bổsung mã lệnh sau cho hàm bắt sựkiện nhấn vào nút ‘Clear’ : 

protected voidbtnClear_Click( objectsender, System.EventArgs e) 

foreach( TreeNode node intvwSource.Nodes ) 

SetCheck(node, false); 

Hàm này chỉ đơn giản là duyệt qua tất cảcác nút thuộc cây bên trái, sau đó gọi lại 

hàm SetCheck() với biến cờlà false, nghĩa là bỏchọn tất cảcác nút hiện đang được 

chọn trên điều khiển cây. 

Quản lý sựkiện nhấn nút chép tập tin ( Copy ) 

Cái ta cần đểhoàn chỉnh thao tác này là danh sách các đối tượng FileInfo. Đểcó thể

quản lý linh hoạt trên danh sách này ta sẽdùng đối tượng ArrayList, nó cho phép ta 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

126 

thực hiện hầu hết mọi thao tác trêndanh sách một các dễdàng. Đểlất vềdanh sách 

các đối tượng FileInfo, ta sẽgọi hàm GetFileList() của ta : 

protected voidbtnCopy_Click( objectsender, System.EventArgs e) 

 ArrayList fileList = GetFileList( ); 

Lấy vềdanh sách các tập tin 

Đầu tiên ta sẽkhởi tạo một đối tượng ArrayList đểlưu trữdanh sách tên các tập tin 

được chọn, có tên là fileNames : 

privateArrayList GetFileList( ) 

 ArrayList fileNames = newArrayList( ); 

Ta lấy vềdanh sách tên các tập tin được chọn bằng cách duyệt toàn bộcác nút trong 

điều khiển cây bên phải : 

foreach(TreeNode theNode intvwSource.Nodes) 

GetCheckedFiles(theNode, fileNames); 

Hàm GetCheckedFiles() thêm danh sách tên các tập tin được đánh dấu của nút hiện 

hành theNode vào đối tượng fileNames. Nếu nút truyền vào là nút lá và được đánh 

dấu chọn, ta sẽlấy đường dẫn đầy đủcủa nút và thêm vào đối tượng fileNames: 

private voidGetCheckedFiles(TreeNode node, ArrayList fileNames) 

if(node.Nodes.Count == 0) 

if(node.Checked) 

stringfullPath = GetParentString(node); 

fileNames.Add(fullPath); 

Nếu không là nút lá, ta sẽlập đệqui đểtìm nút lá : 

else 

foreach(TreeNode n innode.Nodes) 

GetCheckedFiles(n,fileNames); 

Sau khi thực hiện hết hàm này (nghĩa là duyệt hết cây tvwSource), đối tượng 

fileNames sẽchứa toàn bộcác tập tin được đánh dấu chọn của cây. 

Quay trởlại khảo sát tiếp tục hàm GetFileList(), ta tạo thêm một đối tượng 

ArrayList nữa, tên fileList. Mảng này sẽchứa danh sách các đối tượng FileInfo ứng 

với các tên tập tin tìm được trong mảng fileNames. Thuộc tính Exists của đối tượng 

FileInfo dùng đểkiểm tra là tập tin hay thưmục. Thuộc tính Exists là True thì đối 

tượng FileInfo đó là tập tin và ta sẽthêm vào mảng fileList, ngược lại là thưmục thì 

không thêm . 

foreach(stringfileName infileNames) 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

127 

 FileInfo file = newFile(fileName); 

if(file.Exists) 

fileList.Add(file); 

13.1.2.4 Quản lý sựkiện nhấn chọn nút xóa ( Delete ) 

Trước tiên ta cần đảm bảo rằng người dùng chắc chắn muốn xóa bằng cách cho hiện 

lên một hộp thoại xác nhận xóa. Đểhiển thịhộp thoại ta dùng hàm tĩnh Show() của 

đối tượng MessageBox. 

protected voidbtnDelete_Click( objectsender, System.EventArgs e) 

System.Windows.Forms.DialogResult result = 

MessageBox.Show( "Are you quite sure?",// Thông điệp  "Delete Files",    // Tiêu đềcho hộp thoại 

MessageBox.Buttons.OKCancel,// nút nhấn 

MessageBox.Icon.Exclamation,// biểu tượng hộp thoại 

MessageBox.DefaultButton.Button2); // nút mặc định 

Khi người dùng nhấn nút OK hay Cancel, ta sẽnhận được giá trịtrảvềtừ đối 

tượng DialogResult thuộc namespace Forms và tiến hành xửlý tương ứng : 

if(result == System.Windows.Forms.DialogResult.OK) 

Nếu người dùng chọn nút OK thì ta sẽlấy vềdanh sách tên các tập tin fileNames, 

sau đó duyệt qua từng tên và xóa chúng đi : 

ArrayList fileNames = GetFileList( ); 

foreach(FileInfo file infileNames) 

try 

  lblStatus.Text = "Deleting " + 

  txtTargetDir.Text + "\\" + 

file.Name + "..."; 

Application.DoEvents( ); 

file.Delete( ); 

catch(Exception ex) 

MessageBox.Show(ex.Message); 

lblStatus.Text = "Done."; 

Application.DoEvents( ); 

Sau đây là mã của toàn bộ ứng dụng : 

usingSystem; 

usingSystem.Collections; 

usingSystem.ComponentModel; 

usingSystem.Data; 

usingSystem.Drawing; 

usingSystem.IO; 

usingSystem.Windows.Forms; 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

128 

///  

///chép tập tin – ứng dụng minh họa cho Windows Form 

///  

namespaceFileCopier 

///  

///Form minh họa cho ứng dụng Windows Form 

///  

public classForm1 : System.Windows.Forms.Form 

///  

///lớp bên trong của lớp Form1, so sánh 2 tập tin 

///  

public classFileComparer : IComparer 

public intCompare (objectf1, objectf2) 

FileInfo file1 = (FileInfo) f1; 

FileInfo file2 = (FileInfo) f2; 

if(file1.Length > file2.Length) 

return-1; 

if(file1.Length < file2.Length) 

return1; 

return0; 

publicForm1( ) 

InitializeComponent( ); 

// đẩy dữliệu vào cây bên trái và bên phải 

FillDirectoryTree(tvwSource, true); 

FillDirectoryTree(tvwTargetDir, false); 

///  

///phương thức này dùng để đẩy dữliệu vào cây 

///  

private voidFillDirectoryTree(TreeView tvw, boolisSource) 

// trước khi đẩy dữliệu vào cây, ta phải xóa bỏcác nút 

// hiện đang tồn tại trên cây. 

tvw.Nodes.Clear( ); 

// lấy vềdanh sách các ổ đĩa logic trên máy tính 

// sau đó đẩy chúng vào làm nút gốc của cây 

string[] strDrives = Environment.GetLogicalDrives( ); 

// Duyệt qua các ổ đĩa, dùng khối try/catch đểbắt bất 

// kỳlỗi nào xảy ra trên đĩa, nếu đĩa hợp lệthì ta 

// thêm vào làm nút gốc cho cây. 

// đĩa không hợp lệsẽkhông thêm vào cây : đĩa mềm hay 

// CD trống ... 

foreach(stringrootDirectoryName instrDrives) 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

129 

if(rootDirectoryName != @"C:\") 

continue; 

try

// nếu đĩa không hợp lệta sẽquăng ra một lỗi. 

DirectoryInfo dir = 

newDirectoryInfo(rootDirectoryName); 

dir.GetDirectories( ); 

TreeNode ndRoot = newTreeNode(rootDirectoryName); 

// thêm nút gốc vào cây 

tvw.Nodes.Add(ndRoot); 

     // thêm các nút con vào cây, nếu là cây bên trái 

// thì thêm cảtập tin vào cây   if(isSource) 

GetSubDirectoryNodes(ndRoot, ndRoot.Text, true); 

else 

GetSubDirectoryNodes(ndRoot, ndRoot.Text, false); 

catch(Exception e) 

// thông báo đĩa có lỗi 

MessageBox.Show(e.Message); 

} // kết thúc thao tác đẩy dữliệu vào cây 

///  

///lấy vềtất cảcác thưmục con của nút cha truyền vào, 

///thêm các thưmục con tìm được vào cây 

///hàm này có 3 đối số: nút cha, tên đầy đủcủa nút cha, 

/// và biến cờgetFileNames xác định có lấy tập tin không 

///  

private voidGetSubDirectoryNodes( TreeNode parentNode,    stringfullName, boolgetFileNames) 

DirectoryInfo dir = newDirectoryInfo(fullName); 

   DirectoryInfo[] dirSubs = dir.GetDirectories( ); 

// ứng với mỗi mục con ta thêm vào cây nếu nó không ở

// trạng thái ẩn. 

foreach(DirectoryInfo dirSub indirSubs) 

// bỏqua các thưmục ẩn 

if ( (dirSub.Attributes & FileAttributes.Hidden) != 0 ) 

continue; 

///  

///ta chỉcần tên nút đểthêm vào cây, còn ta phải 

/// truyền vào tên đường dẫn đầy đủcủa nút trên cây 

/// cho hàm lặp đệqui GetSubDirectoryNodes()đểnó có 

/// thểtìm được các nút con cửa nút đó 

///  

TreeNode subNode = newTreeNode(dirSub.Name); 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

130 

parentNode.Nodes.Add(subNode); 

// lặp đệqui hàm GetSubDirectoryNodes(). 

GetSubDirectoryNodes( 

subNode,dirSub.FullName,getFileNames); 

if(getFileNames) 

// lấy mọi tập tin thuộc nút 

    FileInfo[] files = dir.GetFiles( ); 

// thêm các tập tin và nút con 

foreach(FileInfo file infiles) 

TreeNode fileNode = newTreeNode(file.Name); 

parentNode.Nodes.Add(fileNode); 

///  

/// điểm vào chính của ứng dụng. 

///  

[STAThread] 

static voidMain( ) 

Application.Run(newForm1( )); 

///  

///tạo ra một danh sách có thứtựcác tập tin được chọn , 

/// chép chúng sang cây bên phải

///  

private voidbtnCopy_Click(objectsender, 

System.EventArgs e) 

// lấy vềdanh sách tập tin 

   ArrayList fileList = GetFileList( ); 

// tiến hành chép tấp tin 

foreach(FileInfo file infileList) 

try 

     lblStatus.Text = "Copying " + txtTargetDir.Text + 

"\\" + file.Name + "...";  Application.DoEvents( );     

file.CopyTo(txtTargetDir.Text + "\\" + 

file.Name,chkOverwrite.Checked); 

catch // (ta không làm gì ở đây cả) 

lblStatus.Text = "Done."; 

Application.DoEvents( ); 

///  

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

131 

///bắt sựkiện kết thúc ứng dụng 

///  

private voidbtnCancel_Click(objectsender, System.EventArgs e) 

Application.Exit( ); 

///  

///bắt sựkiện xóa bỏcác nút được chọn trên cây bên trái 

///  

private voidbtnClear_Click( objectsender, System.EventArgs e) 

// lấy vềnút gốc trên cây bên trái và 

// tiến hành lặp đệqui 

foreach(TreeNode node intvwSource.Nodes) 

SetCheck(node, false); 

///  

/// đảm bảo người dùng muốn xóa nút đó 

///  

private voidbtnDelete_Click(objectsender, System.EventArgs e) 

// xác nhận xóa 

System.Windows.Forms.DialogResult result = MessageBox.Show( 

"Are you quite sure?", // thông điệp 

"Delete Files", // tiêu đề

MessageBox.Buttons.OKCancel, // nút nhấn 

MessageBox.Icon.Exclamation, // biểu tượng 

MessageBoxDefaultButton.Button2); // nút mặc định 

// nếu đồng ý xóa 

if(result == System.Windows.Forms.DialogResult.OK) 

// duyệt danh sách các tập tin và xoá các tập tin 

/ được chọn 

    ArrayList fileNames = GetFileList( ); 

foreach(FileInfo file infileNames) 

try 

// cập nhật nhãn 

lblStatus.Text = "Deleting " + 

txtTargetDir.Text + "\\" + file.Name + "..."; 

Application.DoEvents( ); 

file.Delete( ); 

catch(Exception ex) 

// hộp thoại thông báo 

MessageBox.Show(ex.Message); 

lblStatus.Text = "Done."; 

Application.DoEvents( ); 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

132 

///  

///lấy đường dẫn đầy đủcủa nút được chọn và gán vào 

/// điều khiển TextBox txtTargetDir 

///  

private voidtvwTargetDir_AfterSelect( objectsender, 

System.Windows.Forms.TreeViewEventArgs e) 

// lấy đường dẫn đầy đủcủa nút 

stringtheFullPath = GetParentString(e.Node); 

// nếu nó không là nút lá, ta sẽxóa 2 ký tựcuối cùng 

// đi, vì đây là dấu // 

if(theFullPath.EndsWith("\\")) 

theFullPath = 

theFullPath.Substring(0,theFullPath.Length-1); 

// gán đường dẫn cho điều khiển TextBox 

txtTargetDir.Text = theFullPath; 

///  

/// đánh dấu chọn nút hiện hành và các nút con của nó 

///  

private voidtvwSource_AfterCheck(objectsender, 

System.Windows.Forms.TreeViewEventArgs e) 

SetCheck(e.Node,e.Node.Checked); 

///  

///lập đệqui việc đánh dấu chọn hay loại bỏdấu chọn trên 

/// nút truền vào dựa vào cờcheck 

///  

private voidSetCheck(TreeNode node, boolcheck) 

// set this node's check mark 

node.Checked = check; 

// tìm tất cảcác nút con của nút 

foreach(TreeNode n innode.Nodes) 

// nếu là nút là thì ta đánh dấu chọn hoặc không chọn 

if(node.Nodes.Count == 0) 

node.Checked = check; 

// nếu không là lá thì ta lặp đệqui 

else 

SetCheck(n,check); 

// lấy vềtất cảcác tập tin đã được đánh dấu chọn thuộc 

// nút 

private voidGetCheckedFiles(TreeNode node,ArrayList fileNames) 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

133 

// nếu là nút lá 

if(node.Nodes.Count == 0) 

// nếu nút là đánh dấu chọn 

if(node.Checked) 

// lấy đường dẫn đầy đủcủa nút và thêm vào cây 

stringfullPath = GetParentString(node); 

fileNames.Add(fullPath); 

else // không là nút lá 

// thực hiện trên tất cảnút con 

foreach(TreeNode n innode.Nodes) 

GetCheckedFiles(n,fileNames); 

///  

///lấy về đường dẫn đầy đủcủa nút truyền vào 

///  

private stringGetParentString(TreeNode node) 

// nếu là nút gốc thì trảvềtên nút ( c:\ ) 

if(node.Parent == null) 

returnnode.Text; 

else

// nếu là nút cha thì thêm dấu // vào chuỗi trảvề

// nếu nút lá ta không thêm gì cả

returnGetParentString(node.Parent) + node.Text + 

     (node.Nodes.Count == 0 ? "" : "\\"); 

///  

///trảvềdanh sách các tập tin được chọn theo thứtự

///  

privateArrayList GetFileList( ) 

// danh sách tên tập tin đầy đủkhông được sắp 

ArrayList fileNames = newArrayList( ); 

// duyệt từng nút của cây và lấy vềdanh sách các tập 

// tin được chọn 

foreach(TreeNode theNode intvwSource.Nodes) 

GetCheckedFiles(theNode, fileNames); 

// danh sách các đối tượng FileInfo 

ArrayList fileList = newArrayList( ); 

// fileNames là tập tin thì ta thêm vào fileList 

foreach(stringfileName infileNames) 

// tạo ra FileInfo tương ứng với fileName 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

134 

FileInfo file = newFileInfo(fileName); 

if(file.Exists) 

fileList.Add(file); 

// tạo ra một thểhiện IComparer 

IComparer comparer = (IComparer) newFileComparer( ); 

// sắp xếp danh sách tập tin 

fileList.Sort(comparer); 

returnfileList; 

13.1.3 Tạo sưu liệu XML bằng chú thích 

Ngôn ngữC# hỗtrợkiểu chú thích mới, bằng ba dấu gạch chéo ( /// ). Trình biên 

dịch C# dùng phần chú thích này đểtạo thành sưu liệu XML. 

Ta có thểtạo tập tin sưu liệu XML này bằng mã lệnh, ví dụnhư đểtạo sưu liệu cho 

ứng dụng FileCopier ởtrên ta gõ các lệnh sau : 

csc filecopier.cs /r:System.Windows.Forms.dll /r:mscorlib.dll 

/r:system.dll /r:system.configuration.dll /r:system.data.dll 

/r:system.diagnostics.dll /r:system.drawing.dll 

/r:microsoft.win32.interop.dll 

/doc:XMLDoc.XML

Ta cũng có thểtạo sưu liệu XML trực tiếp ngay trong Visual Studio .NET, bằng 

cách nhấn chuột phải lên biểu tượng của dựán và chọn ‘Properties’ đểhiện lên hộp 

thoại thuộc tính của dựán (Property Pages), sau đó chọn mục Configuration 

Properties \ Build rồi gõ tên tập tin sưu liệu XML cần tạo ra vào dòng XML 

Document File. Khi biên dịch dựán, tập tin sưu liệu XML sẽtự động được tạo ra 

trong thưmục chứa dựán. Dưới đây là một đoạn mã được trích ra từtập tin sưu liệu 

XML được tạo ra từ ứng dụng FileCopier trên : 

FileCopier 

Form demonstrating Windows Forms implementation 

Required designer variable. 

Tree view of potential target directories 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

135 

Tree view of source directories 

includes check boxes for checking 

chosen files or directories 

Do đoạn mã trên được định dạng dưới kiểu dưới dạng XML, do đó không thuận tiện 

lắm khi quan sát. Ta có thểviết một tập tin theo định dạng XSLT đểchuyển từ định 

dạng XML sang HTML. 

Một cách đơn giản hơn đểtạo sưu liệu XML thành các báo cáo HTML dễ đọc hơn 

là dùng chức năng Tool \ Build CommandWeb Page …, VS.NET sẽtự động tạo ra 

một tập các tập tin sưu liệu HTML tương ứng với tập tin XML. Dưới đây là giao 

diện của màn hình sưu liệu ứng dụng FileCopier được tạo bởi VS.NET : 

Hình 13-9 Sưu liệu dưới dạng Web được tạo bởi Visual Studio .NET 

13.1.4 Triển khai ứng dụng 

Khi ứng dụng đã thực thi hoàn chỉnh, vấn đềbây giờlà làm cách nào đểcó thểtriển 

khai nó. Với các ứng dụng đơn giản, chỉcần chép assemblycủa ứng dụng đó sang 

máy khác và chạy. 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

136 

Ví dụta dịch ứng dụng FileCopier thành tập tin chạy FileCopier.exe, sau đó chép 

sang máy khác và chạy nó. Ứng dụng sẽthực thi tốt. 

13.1.4.1 Việc triển khai các dựán ( Deployment Projects ) 

Đối với các ứng dụng thương mại lớn hơn, khách hàng muốn ứng dụng được cài đặt 

vào một thưmục cụthểvới biểu tượng đặc trưng của họ…, khi đó cách đơn giản 

trên chưa đủ. Visual Studio .NET đã cung cấp thêm một phần mởrộng khác đểhỗ

trợviệc cài đặt và triển khai (Setup and Deployment Projects) ứng dụng. 

Giảsửta đang ởtrong một dựán nào đó, ta chọn File\Add Project \ New Project \ 

Setup and Deployment Projects. Ta sẽthấy hộp thoại sau : 

Hình 13-10 Hộp thoại tạo dựán mới. 

Ta có nhiều nhiều kiểu dựán triển khai khác nhau : 

• Setup Project: Tạo ra tập tin cài đặt, tập tin này có thểtựcài đặt các tập tin 

và tài nguyên của ứng dụng. 

• Cab Project:Giống nhưmột tập tin ZIP, dựán loại này nén các tập tin thành 

một gói ( Package ) . Chọn lựa này có thểkết hợp với các loại khác. 

• Merge Module: Nếu ứng dụng của ta có nhiều dựán cùng dùng chung một 

sốtập tin, thì sựchọn lựa này giúp ta trộn chúng thành các module trung gian 

chung. Ta có thểtích hợp các module này vào các dựán khác. 

• Setup Wizard: Giúp thực hiện một trong các loại dựán trên được dễdàng. 

• Web Setup Project: Giúp triển khai các dựán Web. 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

137 

Đểhiểu rõ, ta sẽthửtạo một dựán triển khai kiểu Cab Project, thường thì khi dựán 

của ta có nhiều tập tin .Html, .Gif hay một sốloại tài nguyên khác mà cần phải kèm 

theo với ứng dụng thì ta triển khai dựán theo kiểu này. Thêm dựán loại Cab Project 

vào dựán với tên là FileCopierCabProject. 

Hình 13-11 Dựán được thêm vào ứng dụng. 

Hình 13-12 Hai kiểu thêm trong dựán loại CAB 

Nhấn chuột phải trên dựán triển khai FileCopierCabProject. Có 2 dạng đóng gói tập 

tin CAB : Project Output… và File… . Ở đây ta chọn Add \ Project Output, hộp 

thoại chọn lựa kiểu kết xuất cho dựán ( Add Project Output Group ) xuất hiện : 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

138 

Hình 13-13 Lựa chọn loại kết xuất để đóng gói. 

Ở đây, ta sẽchọn loại Primary Output đểtạo tập tin FileCopier.exe cho ứng dụng 

FileCopiercủa ta. Khi chạy chương trình thì .NET sẽtạo ra một gói có tên là 

FileCopierCabProject.CAB. Trong gói này có chứa 2 tập tin : 

• FileCopier.exe  : tập tin chạy của ứng dụng 

• Osd8c0.osd   : tập tin này mô tảgói .CAB theo dạng XML. 

Mã mô tảXML tập tin .CAB 

"http://www.microsoft.com/standards/osd/osd.dtd"> 

href="http://www.microsoft.com/standards/osd/msicd.dtd" 

as="MSICD"?> 

FileCopierCabProject  

   

    

   

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

139 

13.1.4.2 Việc cài đặt dựán ( Setup Project ) 

Đểtạo được chương trình tự động cài đặt cho ứng dụng, ta thêm dựán khác và chọn 

kiểu là Setup Project, dựán kiểu này khá linh động. Nó tạo thành tập tin có phần 

mởrộng là MSI, có thểtựcài đặt ứng dụng của ta lên máy tính khác. 

UI phục vụchủyếu cho việc cài đặt là cửa số View Menu: 

Hình 13-14 Giao diện người dùng View Menu. 

Ứng với mỗi chọn lựa trên View Menu, ta sẽthấy các hiển thịtương ứng trong hai 

cửa sổbên trái. Chẳng hạn như, khi ta chọn View \ File Systemthì ởbên trái sẽ

hiện ra hai cửa sổnhưnhưhình bên dưới, ta có thểthêm các tập tin hay tài nguyên 

liên quan đến ứng dụng theo ý muốn : 

Hình 13-15 Cửa sổFile System của ứng dụng FileCopier 

13.1.4.3 Triển khai trên các vịtrí khác nhau 

Mặc nhiên thì ứng dụng sẽ được cài đặt trên thưmục sau : 

[ProgramFilesFolder]\[Manufacturer]\[Product Name. 

ProgramFilesFolder: thưmục Program Files trên máy người dùng 

Manufacturer: tên của nhà sản xuất 

Product Name: tên của ứng dụng 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

140 

Ta có thểthay đổi các thông sốnày trong cửa sổthuộc tính FileCopierSetupProject 

hoặc trong quá trình cài đặt. 

Tạo biểu tượng của ứng dụng trên màn hình Desktop. 

Đểtạo biểu tượng cho ứng dụng, ta chỉcần chọn Application Folder\Primary 

output from FileCopier ( Active ) ởcửa sổbên trái, sau đó nhấn chuột phải và 

chọn Create Shortcut to Primary output from FileCopier ( Active ).Ta hiệu 

chỉnh đường dẫn của biểu tượng cho thích hợp. 

Thêm các mục vào thưmục My Documents 

Ta cũng có thểthêm các tài liệu cần thiết vào thưmục My Documentstrên máy của 

người dùng khi cài đặt, bằng cách đặt chúng vào thưmục User’s Personal Data 

Folder

Tạo biểu trượng trong cửa sổStart Menu 

Đểthêm các thành phần khác của ứng vào cửa sổ Start / Programs, ta thêm chúng 

vào thưmục User’s Program Menu ởcửa sổbên phải. 

13.1.4.4 Thêm các chức năng cài đặt khác cho ứng dụng triển khai 

Ngoài bốn mục được liệt kê trong hình trên, ta có thểbổsung thêm các thưmục 

khác, nhưhình dưới đây : 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

141 

Hình 13-16 Bổsung các thưmục trong cửa sổFile System. 

13.1.4.5 Các thành phần khác 

Ta đã khảo sát qua cách cài đặt dùng File System trong Menu View, nay ta tìm 

hiểu thêm một sốcách khác. 

Tạo các thay đổi trong sổ đăng ký ( Registry ) 

Cửa sổ đăng ký của View Menu cho phép làm cho trình cài đặt của chúng ta tạo ra 

các thay đổi trong sổ đăng ký chương trình trên các máy cài đặt ứng dụng. Nhấn 

chuột phải trên các thưmục được liệt trong hình dưới đây đểhiệu chỉnh sổ đăng ký 

theo ý muốn : 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

142 

Hình 13-17 Hiệu chỉnh sổ đăng ký. 

Chú ý: Phải rất thật cẩn thận khi cài đặt sổ đăng ký. Trong hầu hết các 

ứng dụng .NET đều không cần thiết phải liên quan đến sổ đăng ký, .NET 

quản lý ứng dụng không cần dùng sổ đăng ký (Registry). 

Quản lý giao diện người dùng trong quá trình cài đặt 

Đểcó thểkiểm soát các chữhay giao diện đồhọa hiển thịtrong suốt quá trình cài 

đặt ứng dụng, ta chọn mục User Interface trong View Menu. Hình dưới đây thểhiện 

luồng công việc trong quá trình cà đặt ứng dụng : 

Hình 13-18 Luồng công việc trong quá trình cài đặt. 

Khi ta nhấn chuột phải trên một bước nào đó trong tiến trình cài đặt và chọn 

Propertiesthì sẽhiện lên một cửa sổtương ứng với mục đó, nhờhộp thoại thuộc 

tính này ta có thểhiệu chỉnh các chuỗi hay ảnh hiển thịthích hợp. Ta cũng có thể 

Lập trình với C#   Gvhd: Nguyễn Tấn Trần Minh Khang

143 

thêm hay bớt đi một bước trong luồng công việc cài đặt. Ví dụhộp thoại thuộc tính 

của mục Welcome. 

Hình 13-19 Cửa sổthuộc tính của mục Welcome trong quá trình cài đặt. 

Hiệu chỉnh thêm cho quá trình cài đặt 

Nếu quá trình cài đặt trong cửa sổUser Interface vẫn không đủthỏa mãn nhu cầu 

cài đặt của ứng dụng, thì ta có thểtựhiệu chỉnh các thông sốcho tiến trình cài đặt : 

điều kiện đểchạy tiến trình cài đặt … Ta chọn mục Custom Option trong View 

Menu đểthực hiện mục đích này. 

Kết thúc việc tạo chương trình cài đặt 

Sau khi hoàn tất mọi hiệu chỉnh, ta chỉcần chạy ứng dụng và .NET sẽtạo ra một tập 

tin cài đặt MSD đểcài đặt ứng dụng của ta. 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

144 

Chương 14 Truy cập dữliệu với ADO.NET 

Trong thực tế, có rất nhiều ứng dụng cần tương tác với cơsởdữliệu. .NET 

Framework cung cấp một tập các đối tượng cho phép truy cập vào cơsởdữliệu, tập 

các đối tượng này được gọi chung là ADO.NET. 

ADO.NET tương tựvới ADO, điểm khác biệt chính ởchỗADO.NET là một kiến 

trúc dữliệu rời rạc, không kết nối (Disconnected Data Architecture). Với kiến trúc 

này, dữliệu được nhận vềtừcơsởdữliệu và được lưu trên vùng nhớcache của 

máy người dùng. Người dùng có thểthao tác trên dữliệu họnhận vềvà chỉkết nối 

đến cơsởdữliệu khi họcần thay đổi các dòng dữliệu hay yêu cầu dữliệu mới. 

Việc kết nối không liên tục đến cơsởdữliệu đã đem lại nhiều thuận lợi, trong đó 

điểm lợi nhất là việc giảm đi một lưu lượng lớn truy cập vào cơsởdữliệu cùng một 

lúc, tiết kiệm đáng kểtài nguyên bộnhớ. Giảm thiểu đáng kểvấn đềhàng trăm 

ngàn kết nối cùng truy cập vào cơsởdữliệu cùng một lúc. 

ADO.NET kết nối vào cơsởdữliệu đểlấy dữliệu và kết nối trởlại đểcập nhật dữ

liệu khi người dùng thay đổi chúng. Hầu hết mọi ứng dụng đều sửdụng nhiều thời 

gian cho việc đọc và hiển thịdữliệu, vì thếADO.NET đã cung cấp một tập hợp 

con các đối tượng dữliệu không kết nối cho các ứng dụng đểngười dùng có thể đọc 

và hiển thịchúng mà không cần kết nối vào cơsởdữliệu. 

Các đối tượng ngắt kết nối này làm việc tương tự đối với các ứng dụng Web. 

14.1 Cơsởdữliệu và ngôn ngữtruy vấn SQL 

Đểcó thểhiểu rõ được cách làm việc của ADO.NET, chúng ta cần phải nắm được 

một sốkhái niệm cơbản vềcơsởdữliệu quan hệvà ngôn ngữtruy vấn dữliệu, 

như: khái niệm vềdòng, cột, bảng, quan hệgiữa các bảng, khóa chính, khóa ngoại 

và cách truy vấn dữliệu trên các bảng bằng ngôn ngữtruy vấn SQL : SELECT, 

UPDATE, DELETE … hay cách viết các thủtục ( Store Procedure) …. Trong 

phạm vi của tài liệu này, chúng ta sẽkhông đềcập đến các mục trên. 

Trong các ví dụsau, chúng ta sẽdùng cơsởdữliệu NorthWind, được cung cấp bởi 

Microsoft đểminh họa cho các ví dụcủa chúng ta. 

14.2 Một sốloại kết nối hiện đang sửdụng 

1982 ra đời ODBC driver (Open Database Connectivity) của Microsoft. Chỉtruy 

xuất được thông tin quan hệ, không truy xuất được dữliệu không quan hệnhư: tập 

tin văn bản, email …Ta phải truy cập ODBC thông qua DSN. 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

145 

Đểtruy cập được tất cảDatastore, dùng OLEDB provider thông qua ODBC. Là vỏ

bọc của ODBC hoặc không. OLEDB dễsửdụng hơn ODBC, nhưng chỉcó 1 sốít 

ngôn ngữcó thểhiểu được (C++), vì thếra đời ADO. OLEDB là giao diện ởmức 

lập trình hệthống đểquản lý dữliệu. OLEDB đơn giản chỉlà một tập các giao diện 

COM đóng gói thành các system service đểquản trịcác CSDL khác nhau. Gồm 4 

đối tượng chính : Datasource, Session, Command, Rowset. 

ADO là một COM, do đó được dùng với bất kỳngôn ngữnào tương thích với 

COM. ADO không độc lập OS, nhưng độc lập ngôn ngữ: C++,VB, JavaScript, 

VBScript …Là vỏbọc của OLEDB và ADO gồm 3 đối tượng chính : Connection, 

Command, Recordset. 

Remote Data Services ( RDS ) của Microsoft cho phép dùng ADO thông qua các 

giao thức HTTP, HTTPS và DCOM đểtruy cập dữliệu qua Web. 

Microsoft Data Access Components (MDAC) là tổhợp của ODBC, OLEDB, ADO 

và cảRDS. 

Ta có thểkết nối dữliệu bằng một trong các cách: dùng ODBC driver (DSN), dùng 

OLEDB thông qua ODBC hoặc OLEDB khôngthông qua ODBC. 

14.3 Kiến trúc ADO.NET 

ADO.NET được chia ra làm hai phần chính rõ rệt, được thểhiện qua hình 

Hình 14-1 Kiến trúc ADO.NET 

D

DataSet là thành phần chính cho đặc trưng kết nối không liên tục của kiến trúc 

ADO.NET. DataSet được thiết kế đểcó thểthích ứng với bất kỳnguồn dữliệu nào. 

DataSet chứa một hay nhiều đối tượng DataTable mà nó được tạo từtập các dòng 

và cột dữliệu, cùng với khoá chính, khóa ngoại, ràng buộc và các thông tin liên 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

146 

quan đến đối tượng DataTable này. Bản thân DataSet được dạng nhưmột tập tin 

XML. 

Thành phần chính thứhai của ADO.NET chính là NET Provider Data, nó chứa các 

đối tượng phục vụcho việc thao tác trên cơsởdữliệu được hiệu quảvà nhanh 

chóng, nó bao gồm một tập các đối tượng Connection, Command, DataReader và 

DataAdapter. Đối tượng Connection cung cấp một kết nối đến cơsởdữliệu, 

Command cung cấp một thao tác đến cơsởdữliệu, DataReader cho phép chỉ đọc 

dữliệu và DataAdapter là cấu nối trung gian giữa DataSet và nguồn dữliệu. 

14.4 Mô hình đối tượng ADO.NET 

Có thểnói mô hình đối tượng của ADO.NET khá uyển chuyển, các đối tượng của 

nó được tạo ra dựa trên quan điểm đơn giản và dễdùng. Đối tượng quan trọng nhất 

trong mô hình ADO.NET chính là Dataset. Dataset có thể được xem nhưlà thể

hiện của cảmột cơsởdữliệu con, lưu trữtrên vùng nhớcache của máy người dùng 

mà không kết nối đến cơsởdữliệu. 

14.4.1 Mô hình đối tượng của Dataset 

Hình 14-2 Mô hình đối tượng Dataset 

DataSet bao gồm một tập các đối tượng DataRelation cũng nhưtập các đối tượng 

DataTable. Các đối tượng này đóng vai trò nhưcác thuộc tính của DataSet. 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

147 

14.4.2 Đối tượng DataTable và DataColumn 

Ta có thểviết mã C# đểtạo ra đối tượng DataTablehay nhận vềtừkết quảcủa câu 

truy vấn đến cơsởdữliệu. DataTablecó một sốthuộc tính dùng chung ( public ) 

nhưthuộc tính Columns, từthuộc tính này ta có thểtruy cập đến đối tượng 

DataColumnsCollection thông qua chỉmục hay tên của cột đểnhận vềcác đối 

tượng DataColumn thích hợp, mỗi DataColumn tương ứng với một cột trong một 

bảng dữliệu. Ví dụ: 

DataTable dt = newDataTable("tenBang"); 

DataColumn dc = dt.Columns["tenCot"]; 

14.4.3 Đối tượng DataRelation 

Ngoài tập các đối tượng DataTable được truy cập thông qua thuộc tính Tables, 

DataSetcòn có một thuộc tính Relations. Thuộc tính này dùng đểtruy cập đến đối 

tượng DataRelationCollectionthông qua chỉmục hay tên của quan hệvà sẽtrảvề

đối tượng DataRelationtương ứng. Ví dụ: 

DataSet ds = newDataSet("tenDataSet");   

DataRelation dre = ds.Relations["tenQuanHe"]; 

14.4.4 Các bản ghi ( Rows ) 

Tương tựnhưthuộc tính Columnscủa đối tượng DataTable, đểtruy cập đến các 

dòng ta cũng có thuộc tính Rows. ADO. NET không đưa ra khái niệm RecordSet, 

thay vào đó đểduyệt qua các dòng ( Row ), ta có thểtruy cập các dòng thông qua 

thuộc tính Rowsbằng vòng lặp foreach. 

14.4.5 Đối tượng SqlConnection và SqlCommand 

Đối tượng SqlConnection đại diện cho một kết nối đến cơsởdữliệu, đối tượng này 

có thể được dùng chung cho các đối tượng SqlCommand khác nhau. Đối tượng 

SqlCommand cho phép thực hiện một câu lệnh truy vấn trực tiếp : nhưSELECT, 

UPDATE hay DELETE hay gọi một thủtục (Store Procedure) từcơsởdữliệu. 

14.4.6 Đối tượng DataAdapter 

ADO.NET dùng DataAdapter nhưlà chiếc cầu nối trung gian giữa DataSet và 

DataSource ( nguồn dữliệu ), nó lấy dữliệu từcơsởdữliệu sau đó dùng phương 

Fill() để đẩy dữliệu cho đối tượng DataSet. Nhờ đối tượng DataAdapter này mà 

DataSet tồn tại tách biệt, độc lập với cơsởdữliệu và một DataSet có thểlà thểhiện 

của một hay nhiều cơsởdữliệu. Ví dụ: 

//Tạo đối tượng SqlDataAdapter 

SqlDataAdapter sda = newSqlDataAdapter(); 

// cung cấp cho sda một SqlCommand và SqlConnection ... 

// lấy dữliệu ... 

//tạo đối tượng DataSet mới 

DataSet ds = newDataSet("tenDataSet");   

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

148 

//Đẩy dữliệu trog sda vào ds bằng hàm Fill(); 

sda.Fill(ds); 

14.5 Trình cung cấp dữliệu (.NET Data Providers) 

.NET Framework hỗtrợhai trình cung cấp dữliệu là SQL Server .NET Data 

Provider( dành cho phiên bản SQL Server 7.0 của Microsoft trởlên ) và OLE DB 

.NET Data Provider( dành cho các hệquản trịcơsởdữliệu khác ) đểtruy cập vào 

cơsởdữliệu. 

Hình 14-3 So sánh SQL Server .NET Data Provider và the OLE DB .NET Data 

Provider 

SQL Server .NET Data Provider có các đặc điểm : 

• Dùng nghi thức riêng đểtruy cập cơsởdữliệu 

• Truy xuất dữliệu sẽnhanh hơn và hiệu quảhơn do không phải thông qua lớp 

OLE DB Provider hay ODBC 

• Chỉ được dùng với hệquản trịcơsởdữliệu SQL Server 7.0 trởlên. 

• Được Mircrosoft hỗtrợkhá hoàn chỉnh. 

OLE DB .NET Data Provider có các đặc điểm : 

• Phải thông qua 2 lớp vì thếsẽchậm hơn 

• Thực hiện được các dịch vụ“Connection Pool” 

• Có thểtruy cập vào mọi Datasource có hỗtrợOLE DB Provider thích hợp 

14.6 Khởi sựvới ADO.NET 

Đểcó thểhiểu rõ được ADO.NET, ngoài lý thuyết ra, chúng ta sẽkhảo sát chi tiết 

vềcách chúng hoạt động ra bằng mã lệnh cụthể. 

Ví dụWindows Form dưới đây sẽdùng một ListBox đểlấy dữliệu từbảng 

Custommers trong cơsởdữliệu NorthWind. 

Đầu tiên ta sẽtạo ra đối tượng DataAdapter : 

SqlDataAdapter DataAdapter = newSqlDataAdapter( 

commandString, connectionString); 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

149 

Hàm khởi tạo của  đối tượng này gồm hai tham sốcommandString và 

connectionString. commandString là chuỗi chứa câu lệnh truy vấn trên dữliệu mà ta 

muốn nhận về: 

stringcommandString = 

   "Select CompanyName, ContactName from Customers"; 

Biến connectString chứa các thông số đểkết nối đến cơsởdữliệu. Ứng dụng của ta 

dùng hệquản trịcơsởdữliệu SQL Server, vì thế để đơn giản ta sẽ để đối số

password là trống, uid là sa, máy chủserver là localhost và tên cơsởdữliệu là 

NorthWind : 

stringconnectionString = 

  "server=localhost; uid=sa; pwd=; database=northwind"; 

Với đối tượng DataAdapter được tạo ởtrên, ta sẽtạo ra một đối tượng DataSet mới 

và đẩy dữliệu vào nó bằng phương thức Fill() của đối tương DataAdapter. 

DataSet dataSet = newDataSet( ); 

DataAdapter.FillDataSet(dataSet,"Customers"); 

Đối tượng DataSet chứa một tập các DataTable, nhưng ở đây ta chỉcần lấy dữliệu 

của bảng đầu tiên là “Customers” : 

DataTable dataTable = dataSet.Tables[0]; 

Ta sẽduyệt qua từng dòng của bảng bằng vòng lặp foreach đểlấy vềtừng DataRow 

một, sau đó sẽtruy cập đến trường cần lấy dữliệu thông qua tên cột, rồi thêm vào 

ListBox. 

foreach(DataRow dataRow indataTable.Rows) 

lbCustomers.Items.Add(  dataRow["CompanyName"] + 

  " (" + dataRow["ContactName"] + ")" ); 

Sau đây là đoạn mã đầy đủcủa ứng dụng : 

usingSystem; 

usingSystem.Drawing; 

usingSystem.Collections; 

usingSystem.ComponentModel; 

usingSystem.Windows.Forms; 

usingSystem.Data; 

usingSystem.Data.SqlClient; 

namespaceProgrammingCSharpWinForm 

public classADOForm1 : System.Windows.Forms.Form 

privateSystem.ComponentModel.Container components; 

privateSystem.Windows.Forms.ListBox lbCustomers; 

publicADOForm1( ) 

InitializeComponent( ); 

// kết nối đến máy chủ, cơsởdữliệu northwind 

stringconnectionString = 

"server=localhost; uid=sa; pwd=; database=northwind"; 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

150 

// lấy các dòng dữliệu từbảng Customers 

stringcommandString = 

   "Select CompanyName, ContactName from Customers"; 

// tạo ra đối tượng DataAdapter và DataSet   

SqlDataAdapter DataAdapter = 

newSqlDataAdapter(commandString, connectionString); 

DataSet DataSet = newDataSet( ); 

// đẩy dữliệu vào DataSet 

DataAdapter.Fill(DataSet,"Customers"); 

// lấy vềmột bảng dữliệu 

DataTable dataTable = DataSet.Tables[0]; 

// duyệt từng dòng đểlấy dữliệu thêm vào ListBox 

foreach(DataRow dataRow indataTable.Rows) 

lbCustomers.Items.Add(dataRow["CompanyName"] + 

     " (" + dataRow["ContactName"] + ")" ); 

public override voidDispose( ) 

base.Dispose( ); 

components.Dispose( ); 

private voidInitializeComponent( ) 

this.components = newSystem.ComponentModel.Container(); 

this.lbCustomers = newSystem.Windows.Forms.ListBox(); 

lbCustomers.Location = newSystem.Drawing.Point(48, 24); 

lbCustomers.Size = newSystem.Drawing.Size(368, 160); 

lbCustomers.TabIndex = 0; 

this.Text = "ADOFrm1"; 

this.AutoScaleBaseSize = newSystem.Drawing.Size(5, 13); 

this.ClientSize = newSystem.Drawing.Size(464, 273); 

this.Controls.Add(this.lbCustomers); 

public static voidMain(string[] args) 

Application.Run(newADOForm1( )); 

Chỉvới một sốdòng mã ta đã có thểlấy dữliệu và hiện thịtrong hộp ListBox : 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

151 

Hình 14-4 Kết xuất của ví dụtrên. 

Đểhoàn chỉnh giao tác trên, ta cần thực hiện tám dòng mã chính : 

• Tạo ra chuỗi kết nối vào cơsởdữliệu 

stringconnectionString = 

  "server=myServer; uid=sa; pwd=; database=northwind"; 

• Tạo câu lênh truy vấn chọn dữliệu 

stringcommandString = 

   "Select CompanyName, ContactName from Customers"; 

• Tạo đối tượng DataAdapter và chuyển cho nó chuỗi truy vấn và kết nối 

SqlDataAdapter DataAdapter = newSqlDataAdapter( 

 commandString, connectionString);  • Tạo đối tượng DataSet mới 

DataSet dataSet = newDataSet( ); 

• Đẩy bảng dữliệu Customers lấy từDataAdapter vào dataSet 

DataAdapter.Fill(dataSet,"Customers"); 

• Trích đối tượng DataTable từdataSet trên 

DataTable dataTable = DataSet.Tables[0]; 

• Đẩy dữliệu trong bảng dataTable vào ListBox 

foreach(DataRow dataRow indataTable.Rows) 

lbCustomers.Items.Add(dataRow["CompanyName"] + 

 " (" + dataRow["ContactName"] + ")" ); 

14.7 Sửdụng trình cung cấp dữliệu được quản lý 

Ởví dụtrên chúng ta đã khảo sát qua cách truy cập dữliệu thông qua trình cung cấp 

dữliệu SQL Server .NET Data Provider. Trong phần này chúng ta sẽtiếp tục 

khảo sát sang trình cung cấp dữliệu OLE DB .NET Data Provider, với trình cung 

cấp dữliệu này ta có thểkết nối đến bất kỳhệquản trịcơsởdữliệu nào có hỗtrợ

trình cung cấp dữliệu OLE DB Providers, cụthểlà Microsoft Access. 

So với ứng dụng trên, ta chỉcần thay đổi một vào dòng mã là có thểhoạt động 

được. Đầu tiên là chuỗi kết nối : 

stringconnectionString = "provider=Microsoft.JET.OLEDB.4.0; " 

 + "data source = c:\

orthwind.mdb"; 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

152 

Chuỗi trên sẽkết nối đến cơsởdữliệu northwind trên ổ đĩa C. 

Kếtiếp ta thay  đổi  đối tượng DataAdapter từSqlDataAdapter sang 

OleDbDataAdapter 

OleDbDataAdapter DataAdapter = newOleDbDataAdapter(  

 commandString, connectionString);  Chúng ta phải đảm bảo là namespace OleDb được thêm vào ứng dụng : 

usingSystem.Data.OleDb; 

Phần mã còn lại thì tương tựnhư ứng dụng trên, sau đây sẽtrích ra một đoạn mã 

chính phục vụcho việc kết nối theo cách này : 

publicADOForm1( ) 

InitializeComponent( ); 

// chuỗi kết nối đến cơsởdữliệu 

stringconnectionString = "provider=Microsoft.JET.OLEDB.4.0;" 

  + "data source = c:\

wind.mdb";  // chuỗi truy vấn dữliệu 

stringcommandString = 

"Select CompanyName, ContactName from Customers"; 

// tạo đối tượng OleDbDataAdapter và DataSet mới 

 OleDbDataAdapter DataAdapter = newOleDbDataAdapter( 

 commandString, connectionString);   DataSet dataSet = newDataSet( ); 

// đẩy dữliệu vào dataSet 

DataAdapter.Fill(DataSet,"Customers"); 

// lây vềbảng dữliệu Customers 

 DataTable dataTable = DataSet.Tables[0]; 

// duyệt qua từng dòng dữliệu 

foreach(DataRow dataRow indataTable.Rows) 

lbCustomers.Items.Add(dataRow["CompanyName"] + 

     " (" + dataRow["ContactName"] + ")" ); 

14.8 Làm việc với các điều khiển kết buộc dữliệu 

ADO.NET hỗtrợkhá hoàn chỉnh cho các điều khiển kết buộc dữliệu (DataBound), các điều khiển này sẽnhận vào một DataSet, sau khi gọi hàm DataBind() 

thì dữliệu sẽtự động được hiển thịlên điều khiển. 

14.8.1 Đẩy dữliệu vào điều khiển lưới DataGrid 

Ví dụsau sẽdùng điều khiển lưới DataGrid đểthực hiện kết buộc dữliệu, điều 

khiển lưới này được hỗtrợcho cả ứng dụng Windows Forms và WebForms. 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

153 

Trong ứng dụng trước, ta phải duyệt qua từng dòng của đối tượng DataTable đểlấy 

dữliệu, sau đó hiển thịchúng lên điều khiển ListBox. Trong ứng dụng này công 

việc hiển thịdữliệu lên điều khiển được thực hiện đơn giản hơn, ta chỉcần lấy về

đối tượng DataView của DataSet, sau đó gán DataView này cho thuộc tính 

DataSource của điều khiển lưới, sau đó gọi hàm DataBind() thì tự động dữliệu sẽ

được đẩy lên điều khiển lưới dữliệu. 

CustomerDataGrid.DataSource = 

  DataSet.Tables["Customers"].DefaultView;  Trước tiên ta cần tạo ra đối tượng lưới trên Form bằng cách kéo thả, đặt tên lại cho 

điều khiển lưới là CustomerDataGrid. Sau đây là mã hoàn chỉnh của ứng dụng kết 

buộc dữliệu cho điều khiển lưới : 

usingSystem; 

usingSystem.Drawing; 

usingSystem.Collections; 

usingSystem.ComponentModel; 

usingSystem.Windows.Forms; 

usingSystem.Data; 

usingSystem.Data.SqlClient; 

namespaceProgrammingCSharpWindows.Form 

public classADOForm3 : System.Windows.Forms.Form 

privateSystem.ComponentModel.Container components; 

privateSystem.Windows.Forms.DataGrid CustomerDataGrid; 

publicADOForm3( ) 

InitializeComponent( ); 

// khởi tạo chuỗi kết nối và chuỗi truy vấn dữliệu 

stringconnectionString = 

"server=localComputer; uid=sa; pwd=;database=northwind"; 

stringcommandString = 

    "Select CompanyName, ContactName, ContactTitle, " 

+ "Phone, Fax from Customers"; 

// tạo ra một SqlDataAdapter và DataSet mới, 

// đẩy dữliệu cho DataSet 

SqlDataAdapter DataAdapter = 

newSqlDataAdapter(commandString, connectionString); 

DataSet DataSet = newDataSet( ); 

DataAdapter.Fill(DataSet,"Customers"); 

// kết buộc dữliệu của DataSet cho lưới 

CustomerDataGrid.DataSource= 

DataSet.Tables["Customers"].DefaultView; 

public override voidDispose( ) 

base.Dispose( ); 

components.Dispose( ); 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

154 

private voidInitializeComponent( ) 

this.components = newSystem.ComponentModel.Container(); 

this.CustomerDataGrid =newDataGrid(); 

CustomerDataGrid.BeginInit(); 

CustomerDataGrid.Location = 

newSystem.Drawing.Point (8, 24); 

CustomerDataGrid.Size = newSystem.Drawing.Size (656, 224); 

CustomerDataGrid.DataMember = ""; 

CustomerDataGrid.TabIndex = 0; 

CustomerDataGrid.Navigate += 

newNavigateEventHandler(this.dataGrid1_Navigate); 

this.Text = "ADOFrm3"; 

this.AutoScaleBaseSize = newSystem.Drawing.Size(5, 13); 

this.ClientSize = newSystem.Drawing.Size (672, 273); 

this.Controls.Add (this.CustomerDataGrid); 

CustomerDataGrid.EndInit ( ); 

public static voidMain(string[] args) 

Application.Run(newADOForm3()); 

Điều khiển lưới sẽhiển thịy hệt mọi dữliệu hiện có trong bảng Customers, tên của 

các cột trên lưới cũng chính là tên của các cột trong bản dữliệu. Giao diện của ứng 

dụng sau khi chạy chương trình : 

Hình 14-5 Kết buộc dữliệu cho điều khiển lưới DataGrid. 

14.8.2 Tạo đối tượng DataSet 

Trong ví dụtrước, tạo ra đối tượng SqlDataAdapter bằng cách gắn trực tiếp chuỗi 

kết nối và chuỗi truy vấn vào nó. Đối tượng Connection và Command sẽ được tạo 

và tích hợp vào trong đối tượng DataAdapter này. Với cách này, ta sẽbịhạn chế

trong các thao tác liên quan đến cơsởdữliệu. 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

155 

SqlDataAdapter DataAdapter = 

newSqlDataAdapter(commandString, connectionString); 

Ví dụsau đây sẽminh họa việc lấy về đối tượng DataSet bằng cách tạo ra các đối 

tượng Connection và Command một cách riêng biệt, khi ta cần dùng lại chúng hay 

muốn thực hiện hoàn chỉnh một thao tác thì sẽthuận lợi hơn. 

Đầu tiên ta sẽkhai báo bốn biến thành viên thuộc lớp, nhưsau : 

privateSystem.Data.SqlClient.SqlConnection myConnection; 

privateSystem.Data.DataSet myDataSet; 

privateSystem.Data.SqlClient.SqlCommand myCommand; 

privateSystem.Data.SqlClient.SqlDataAdapter DataAdapter; 

Đối tượng Connection sẽ được tạo riêng với chuỗn kết nối : 

stringconnectionString = 

  "server=localhost; uid=sa; pwd=; database=northwind"; 

myConnection = new

System.Data.Sql.SqlConnection(connectionString); 

Sau đó ta sẽmởkết nối : 

myConnection.Open( ); 

Ta có thểthực hiện nhiều giao tác trên cơsởdữliệu khi kết nối được mởvà sau khi 

dùng xong ta chỉ đơn giản đóng kết nối lại. Tiếp theo ta sẽtạo ra đối tượng DataSet: 

myDataSet = newSystem.Data.DataSet( ); 

Và tiếp tục tạo đối tượng Command, gắn cho nó đối tượng Connection đã mởvà 

chuỗi truy vấn dữliệu : 

myCommand = newSystem.Data.SqlClient.SqlCommand( ) 

myCommand.Connection=myConnection; 

myCommand.CommandText = "Select * from Customers"; 

Cuối cùng ta cần tạo ra đối tượng SqlDataAdapter, gắn đối tượng SqlCommand vừa 

tạo ởtrên cho nó, đồng thời phải tiến hành ánh xạbảng dữliệu nó nhận được từcâu 

truy vấn của đối tượng Command đểtạo sự đồng nhất vềtên các cột khi đẩy bảng 

dữliệu này vào DataSet. 

DataAdapter = newSystem.Data.SqlClient.SqlDataAdapter( ); 

DataAdapter.SelectCommand= myCommand; 

DataAdapter.TableMappings.Add("Table","Customers"); 

DataAdapter.Fill(myDataSet); 

Bây giờta chỉviệc gắn DataSet vào thuộc tính DataSoucre của điều khiển lưới: 

dataGrid1.DataSource=myDataSet.Tables["Customers"].DefaultView; 

Dưới đây là mã hoàn chỉnh của ứng dụng này : 

usingSystem; 

usingSystem.Drawing; 

usingSystem.Collections; 

usingSystem.ComponentModel; 

usingSystem.Windows.Forms; 

usingSystem.Data; 

usingSystem.Data.SqlClient; 

namespaceProgrammingCSharpWindows.Form 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

156 

public classADOForm1 : System.Windows.Forms.Form 

privateSystem.ComponentModel.Container components; 

privateSystem.Windows.Forms.DataGrid dataGrid1; 

privateSystem.Data.SqlClient.SqlConnection myConnection; 

privateSystem.Data.DataSet myDataSet; 

privateSystem.Data.SqlClient.SqlCommand myCommand; 

privateSystem.Data.SqlClient.SqlDataAdapter DataAdapter; 

publicADOForm1( ) 

InitializeComponent( ); 

// tạo đối tượng connection và mởnó 

stringconnectionString = 

    "server=Neptune; uid=sa; pwd=oWenmEany;" + 

"database=northwind"; 

myConnection = new SqlConnection(connectionString); 

myConnection.Open(); 

// tạo đối tượng DataSet mới 

myDataSet = newDataSet( );    

// tạo đối tượng command mới và gắn cho đối tượng 

// connectio và chuỗi truy vấn cho nó 

myCommand = newSystem.Data.SqlClient.SqlCommand( ); 

myCommand.Connection=myConnection; 

   myCommand.CommandText = "Select * from Customers"; 

// tạo đối tượng DataAdapter với đối tượng Command vừa 

// tạo ởtrên, đồng thời thực hiện ánh xạbảng dữliệu 

DataAdapter = newSqlDataAdapter( ); 

DataAdapter.SelectCommand= myCommand; 

DataAdapter.TableMappings.Add("Table","Customers"); 

// đẩy dữliệu vào DataSet 

DataAdapter.Fill(myDataSet); 

// gắn dữliệu vào lưới 

dataGrid1.DataSource = 

myDataSet.Tables["Customers"].DefaultView; 

public override voidDispose() 

base.Dispose(); 

components.Dispose(); 

private voidInitializeComponent( ) 

this.components = newSystem.ComponentModel.Container(); 

this.dataGrid1 = newSystem.Windows.Forms.DataGrid(); 

dataGrid1.BeginInit(); 

dataGrid1.Location = newSystem.Drawing.Point(24, 32); 

dataGrid1.Size = newSystem.Drawing.Size(480, 408); 

dataGrid1.DataMember = ""; 

dataGrid1.TabIndex = 0; 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

157 

this.Text = "ADOFrm1"; 

this.AutoScaleBaseSize = newSystem.Drawing.Size(5, 13); 

this.ClientSize = newSystem.Drawing.Size(536, 501); 

this.Controls.Add(this.dataGrid1); 

dataGrid1.EndInit( ); 

public static voidMain(string[] args) 

Application.Run(newADOForm1()); 

Giao diện của ví dụnày cũng tương tựnhưcác ví dụtrên. 

14.8.3 Kết hợp giữa nhiều bảng 

Các ví dụ ởtrên chỉ đơn thuần lấy dữliệu từtrong một bảng. Ởví dụnày ta sẽtìm 

hiểu vềcách lấy dữliệu trên hai bảng. Trong cơsởdữliệu của ta, một khách hàng 

có thểcó nhiều hóa đơn khác nhau, vì thếta sẽcó quan hệmột nhiều giữa bảng 

khách hàng (Customers)và bảng hóa đơn (Orders). Bảng Orders sẽchứa thuộc tính 

CustomersId của bảng Customers, thuộc tính này đóng vai trò là khóa chính đối 

bảng Customers và khóa ngoại đối với bảng Orders. 

Ứng dụng của ta sẽhiển thịdữliệu của hai bảng Customers và Orders trên cùng 

một lưới và thểhiện quan hệmột nhiều của hai bảng ngay trên lưới. Đểlàm được 

điều này ta chỉcần dùng chung một đối tượng Connetion, hai đối tượng tượng 

SqlDataAdapter và hai đối tượng SqlCommand. 

Sau khi tạo đối tượng SqlDataAdapter cho bảng Customers tương tựnhưví dụtrên, 

ta tiến tạo tiếp đối tượng SqlDataAdapter cho bảng Orders : 

myCommand2 = newSystem.Data.SqlClient.SqlCommand(); 

DataAdapter2 = newSystem.Data.SqlClient.SqlDataAdapter(); 

myCommand2.Connection = myConnection; 

myCommand2.CommandText = "SELECT * FROM Orders"; 

Lưu ý là ở đây đối tượng DataAdapter2 có thểdùng chung đối tượng Connection ở

trên, nhưng đối tượng Command thì khác. Sau đó gắn đối tượng Command2 cho 

DataAdapter2, ánh xạbảng dữliệu và đẩy dữliệu vào DataSet ởtrên. 

DataAdapter2.SelectCommand = myCommand2; 

DataAdapter2.TableMappings.Add ("Table", "Orders"); 

DataAdapter2.Fill(myDataSet); 

Tại thời điểm này, ta có một đối tượng DataSet nhưng chứa hai bảng dữliệu : 

Customers và Orders. Do ta cần thểhiện cảquan hệcủa hai bảng ngay trên điều 

khiển lưới, cho nên ta cần phải định nghĩa quan hệnày cho đối tượng DataSet của 

chúng ta. Nếu không làm điều này thì đối tượng DataSet sẽbỏqua quan hệgiữa 2 

bảng này. 

Do đó ta cần khai báo thêm đối tương DataRelation : 

System.Data.DataRelation dataRelation; 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

158 

Do mỗi bảng Customers và Orders đều có chứa một thuộc tính CustomersId, nên ta 

cũng cần khái báo thêm hai đối tượng DataColumn tương ứng với hai thuộc tính 

này. 

System.Data.DataColumn dataColumn1; 

System.Data.DataColumn dataColumn2; 

Mỗi một DataColumn sẽgiữgiá trịcủa một cột trong bảng của đối tượng DataSet : 

dataColumn1 = myDataSet.Tables["Customers"].Columns["CustomerID"]; 

dataColumn2 = myDataSet.Tables["Orders"].Columns["CustomerID"]; 

Ta tiến hành tạo quan hệcho hai bảng bằng cách gọi hàm khởi tạo của đối tượng 

DataRelation, truyền vào cho nó tên quan hệvà hai cột cần tạo quan hệ: 

dataRelation = newSystem.Data.DataRelation("CustomersToOrders", 

  dataColumn1, dataColumn2);  Sau khi tạo được đối tượng DataRelation, ta thêm vào DataSet của ta. Sau đó ta cần 

tạo một đối tượng quản lý khung nhìn DataViewManager cho DataSet, đối tượng 

khung nhìn này sẽ được gán cho lưới điều khiển đểhiển thị: 

myDataSet.Relations.Add(dataRelation); 

DataViewManager DataSetView = myDataSet.DefaultViewManager; 

dataGrid1.DataSource = DataSetView; 

Do điều khiển lưới phải hiển thịquan hệcủa hai bảng dữliệu, nên ta phải chỉcho 

nó biết là bảng nào sẽlà bảng cha. Ở đây bảng cha là bảng Customers : 

dataGrid1.DataMember= "Customers"; 

Sau đây là mã hoàn chỉnh của toàn bộ ứng dụng : 

usingSystem; 

usingSystem.Drawing; 

usingSystem.Collections; 

usingSystem.ComponentModel; 

usingSystem.Windows.Forms; 

usingSystem.Data; 

usingSystem.Data.SqlClient; 

namespaceProgrammingCSharpWindows.Form 

public classADOForm1 : System.Windows.Forms.Form 

privateSystem.ComponentModel.Container components; 

privateSystem.Windows.Forms.DataGrid dataGrid1; 

privateSystem.Data.SqlClient.SqlConnection myConnection; 

privateSystem.Data.DataSet myDataSet; 

privateSystem.Data.SqlClient.SqlCommand myCommand; 

privateSystem.Data.SqlClient.SqlCommand myCommand2; 

privateSystem.Data.SqlClient.SqlDataAdapter DataAdapter; 

privateSystem.Data.SqlClient.SqlDataAdapter DataAdapter2; 

publicADOForm1( ) 

InitializeComponent( ); 

// tạo kết nối 

stringconnectionString = "server=Neptune; uid=sa;" + 

 " pwd=oWenmEany; database=northwind"; 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

159 

myConnection = new SqlConnection(connectionString); 

myConnection.Open( ); 

// tạo DataSet 

myDataSet = newSystem.Data.DataSet( ); 

// tạo đối tượng Command và DataSet cho bảng Customers 

myCommand = newSystem.Data.SqlClient.SqlCommand( ); 

myCommand.Connection=myConnection; 

   myCommand.CommandText = "Select * from Customers"; 

DataAdapter =newSystem.Data.SqlClient.SqlDataAdapter(); 

DataAdapter.SelectCommand= myCommand; 

DataAdapter.TableMappings.Add("Table","Customers"); 

DataAdapter.Fill(myDataSet); 

// tạo đối tượng Command và DataSet cho bảng Orders 

myCommand2 = newSystem.Data.SqlClient.SqlCommand( ); 

DataAdapter2=newSystem.Data.SqlClient.SqlDataAdapter(); 

myCommand2.Connection = myConnection; 

   myCommand2.CommandText = "SELECT * FROM Orders"; 

DataAdapter2.SelectCommand = myCommand2; 

DataAdapter2.TableMappings.Add ("Table", "Orders"); 

DataAdapter2.Fill(myDataSet); 

// thiết lập quan hệgiữa 2 bảng 

System.Data.DataRelation dataRelation; 

System.Data.DataColumn dataColumn1; 

System.Data.DataColumn dataColumn2; 

dataColumn1 = 

myDataSet.Tables["Customers"].Columns["CustomerID"]; 

dataColumn2 = 

myDataSet.Tables["Orders"].Columns["CustomerID"]; 

dataRelation = newSystem.Data.DataRelation( 

"CustomersToOrders", dataColumn1, dataColumn2); 

// thêm quan hệtrên vào DataSet 

myDataSet.Relations.Add(dataRelation); 

// Đặt khung nhìn và bảng hiển thịtrước cho lưới 

DataViewManager DataSetView = 

  myDataSet.DefaultViewManager;  dataGrid1.DataSource = DataSetView; 

dataGrid1.DataMember= "Customers"; 

public override voidDispose( ) 

base.Dispose( ); 

components.Dispose( ); 

private voidInitializeComponent( ) 

this.components = newSystem.ComponentModel.Container(); 

this.dataGrid1 = newSystem.Windows.Forms.DataGrid(); 

dataGrid1.BeginInit( ); 

dataGrid1.Location = newSystem.Drawing.Point(24, 32); 

dataGrid1.Size = newSystem.Drawing.Size(480, 408); 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

160 

dataGrid1.DataMember = ""; 

dataGrid1.TabIndex = 0; 

this.Text = "ADOFrm1"; 

this.AutoScaleBaseSize = newSystem.Drawing.Size(5, 13); 

this.ClientSize = newSystem.Drawing.Size (536, 501); 

this.Controls.Add (this.dataGrid1); 

dataGrid1.EndInit ( ); 

public static voidMain(string[] args) 

Application.Run(newADOForm1( )); 

Khi chạy ứng dụng sẽcó giao diện nhu sau : 

Hình 14-6 Quan hệmột nhiều giữa hai bảng Customers và Orders 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

161 

Hình 14-7 Danh sách các hóa đơn tương ứng với khách hàng được chọn 

14.9 Thay đổi các bản ghi của cơsởdữliệu 

Tới lúc này, chúng ta đã học cách lấy dữliệu từcơsởdữliệu sau đó hiển thịchúng 

ra màn hình dựa vào các điều khiển có hay không kết buộc dữliệu. Phần này chúng 

ta sẽtìm hiểu cách cập nhật vào cơsởdữliệu. Các thao tác trên cơsởdữliệu như: 

Thêm, xóa và sửa một dòng trong các bảng dữliệu. Sau đây là luồng công việc 

hoàn chỉnh khi ta có một thao tác cập nhật cơsởdữliệu : 

1. Đẩy dữliệu của bảng vào DataSet bằng câu truy vấn SQL hay gọi thủtục từ

cơsởdữliệu 

2. Hiển thịdữliệu trong các bảng có trong DataSet bằng cách kết buộc hay duyệt 

qua các dòng dữliệu. 

3. Hiệu chỉnh dữliệu trong các bảng DataTable với các thao tác thêm, xóa hay 

sửa trên dòng DataRow. 

4. Gọi phương thúc GetChanges() đểlấy vềmột DataSet khác chứa tất cảcác 

thay đổi trên dữliệu. 

5. Kiểm tra lỗi trên DataSet mới được tạo này bằng thuộc tính HasErrors. Nếu có 

lỗi thì ta sẽtiến hành kiểm tra trên từng bảng DataTable của DataSet, khi gặp 

một bảng có lỗi thì ta tiếp tục dùng hàm GetErrors()đểlấy vềcác dòng 

DataRow có lỗi, ứng với từng dòng ta sẽdùng thuộc tính RowError trên dòng 

đểxác định xem dòng đó có lỗi hay không đểcó thể đưa ra xửlý thích hợp. 

6. Trộn hai DataSet lại thành một. 

7. Gọi phương thức Update() của đối tượng DataAdapter với đối sốtruyền vào là 

DataSet vừa có trong thao tác trộn ởtrên đểcập nhật các thay đổi vào cơsởdữ

liệu. 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

162 

8. Gọi phương thức AcceptChanges() của DataSet đểcập nhật các thay đổi vào 

DataSet này hay phương thức RejectChanges() nếu từchối cập nhật thay đổi 

cho DataSet hiện hành. 

Với luồng công việc trên, cho phép ta có thểkiểm soát tốt được việc thay đổi trên 

cơsởdữliệu hay việc gỡlỗi cũng thuận tiện hơn. Trong ví dụdưới đây , ta sẽcho 

hiện thịdữliệu trong bảng Customers lên một ListBox, sau đó ta tiến hành các thao 

tác thêm, xóa hay sửa trên cơsởdữliệu. Đểdễhiểu, ta giảm bớt một sốthao tác 

quản lý ngoại lệhay lỗi, chỉtập trung vào mục đích chính của ta. Giao diện chính 

của ứng dụng sau khi hoàn chỉnh : 

Hình 14-8 Hiệu chỉnh dữliệu trên bảng Customers. 

Trong Form này, ta có một ListBox lbCustomers liệt kê các khách hàng, một Button 

btnUpdate cho việc cập nhật dữliệu, một Button Xóa, ứng với nút thêm mới ta có 

tám hộp thoại TextBox đểnhận dữliệu gõ vào từngười dùng. Đồng thời ta có thêm 

một lblMessage đểhiển thịcác thông báo ứng với các thao tác trên. 

14.9.1 Truy cập và hiển thịdữliệu 

Ta sẽtạo ra ba biến thành viên : DataAdapter, DataSet và Command : 

privateSqlDataAdapter DataAdapter; 

privateDataSet DataSet; 

privateDataTable dataTable; 

Việc khai báo các biến thành viên nhưvậy sẽgiúp ta có thểdùng lại cho các 

phương thức khác nhau. T khai báo chuỗi kết nối và truy vấn : 

stringconnectionString = 

 "server=localhost; uid=sa; pwd=; database=northwind"; 

stringcommandString = "Select * from Customers"; 

Các chuỗi được dùng làm đối số đểtạo đối tượng DataAdapter : 

DataAdapter=newSqlDataAdapter(commandString,ConnectionString); 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

163 

Tạo ra đối tượng DataSet mới, sau đó đẩy dữliệu từDataAdapter vào cho nó: 

DataSet = newDataSet(); 

DataAdapter.Fill(DataSet,"Customers"); 

Đểhiển thịdữliệu, ta sẽgọi hàm PopulateDB()để đẩy dữliệu vào ListBox: 

dataTable = DataSet.Tables[0]; 

lbCustomers.Items.Clear( ); 

foreach(DataRow dataRow indataTable.Rows) 

lbCustomers.Items.Add(dataRow["CompanyName"] + 

    " (" + dataRow["ContactName"] + ")" ); 

14.9.2 Cập nhật một dòng dữliệu 

Khi người dùng nhấn Button Update (cập nhật), ta sẽlấy chỉmục được chọn trên 

ListBox, và lấy ra dòng dữliệu DataRow trong bảng ứng với chỉmục trên. Sau đó 

cập nhật DataSet với dòng dữliệu mới này nếu sau khi kiểm tra thấy chúng không 

có lỗi nào cả. Chi tiết vềquá trình thực hiện cập nhật : 

Đầu tiên ta sẽlấy vềdòng dữliệu người dùng muốn thay đổi từ đối tượng dataTable 

mà ta đã khai báo làm biến thành viên ngay từ đầu : 

DataRow targetRow = dataTable.Rows[lbCustomers.SelectedIndex]; 

Hiển thịchuỗi thông báo cập nhật dòng dữliệu đó cho người dùng biết. Đểlàm điều 

này ta sẽgọi phương thức tình DoEvents() của đối tượng Application, hàm này sẽ

giúp sơn mới lại màn hình với thông điệp hay các thay đổi khác. 

lblMessage.Text = "Updating " + targetRow["CompanyName"]; 

Application.DoEvents(); 

Gọi hàm BeginEdit() của đối tượng DataRow, đểchuyển dòng dữliệu sang chế độ

hiệu chỉnh ( Edit ) và EndEdit()đểkết thúc chế độhiệu chỉnh dòng. 

targetRow.BeginEdit(); 

targetRow["CompanyName"] = txtCustomerName.Text; 

targetRow.EndEdit(); 

Lấy vềcác thay đổi trên đối tượng DataSet đểkiểm tra xem các thay đổi có xảy ra 

bất kỳlỗi nào không. Ở đây ta sẽdùng một biến cờcó kiểu true/false đểxác định 

là có lỗi là true, không có lỗi là false.Kiểm tra lỗi bằng cách dùng hai vòng lặp 

tuần tựtrên bảng và dòng của DataSet mới lấy về ởtrên, ta dùng thuộc tính 

HasErrors đểkiểm tra lỗi trên bảng, phương thức GetErrors()đểlấy vềcác dòng có 

lỗi trong bảng. 

DataSet DataSetChanged; 

DataSetChanged = DataSet.GetChanges(DataRowState.Modified); 

boolokayFlag = true; 

if(DataSetChanged.HasErrors) 

okayFlag = false; 

stringmsg = "Error in row with customer ID "; 

foreach(DataTable theTable inDataSetChanged.Tables) 

if(theTable.HasErrors) 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

164 

   DataRow[] errorRows = theTable.GetErrors( ); 

foreach(DataRow theRow inerrorRows) 

msg = msg + theRow["CustomerID"]; 

 lblMessage.Text = msg; 

Nếu biến cờokagFlag là true,thì ta sẽtrộn DataSet ban đầu với DataSet thay đổi 

thành một, sau đó cập nhật DataSet sau khi trộn này vào cơsởdữliệu. 

if(okayFlag) 

DataSet.Merge(DataSetChanged); 

DataAdapter.Update(DataSet,"Customers"); 

Tiếp theo hiển thịcâu lệnh truy vấn cho người dùng biết, và cập nhật những thay 

đổi cho DataSet đầu tiên, rồi hiển thịdữliệu mới lên đối tượng ListBox. 

lblMessage.Text = DataAdapter.UpdateCommand.CommandText; 

Application.DoEvents( ); 

DataSet.AcceptChanges( ); 

PopulateLB( ); 

Nếu cờokayFlag là false, có nghĩa là có lỗi trong quá trình hiệu chỉnh dữliệu, ta sẽ

từchối các thay đổi trên DataSet. 

else 

DataSet.RejectChanges( ); 

14.9.3 Xóa một dòng dữliệu 

Mã thực thi của sựkiện xóa thì đơn giản hơn một chút, ta nhận vềdòng cần xóa : 

DataRow targetRow = dataTable.Rows[lbCustomers.SelectedIndex]; 

Giữlại dòng cần xóa đểdùng làm thông điệp hiển thịcho người dùng biết trước khi 

xóa dòng này khỏi cơsởdữliệu. 

stringmsg = targetRow["CompanyName"] + " deleted. ";, 

Bắt đầu thực hiện xóa trên bảng dữliệu, cập nhật thay đổi vào DataSet và cập nhật 

luôn vào cơsởdữliệu : 

dataTable.Rows[lbCustomers.SelectedIndex].Delete( ); 

DataSet.AcceptChanges( ); 

DataAdapter.Update(DataSet,"Customers"); 

Khi gọi hàm AccceptChanges()đểcập nhật thay đổi cho DataSet thì nó sẽlần lượt 

gọi hàm này cho các DataTable, sau đó cho các DataRow đểcập nhật chúng. Ta 

cũng cần chú ý khi gọi hàm xóa trên bảng Customers, dòng dữliệu DataRow của 

khách hàng này chỉ được xóa nếu nó không vi phạm ràng buộc trên các bảng khác, 

ở đây khách hàng chỉ được xóa nếu nếu khách hàng không có một hóa đơn nào trên 

bảng Orders. Nếu có ta phải tiến hành xóa trên bảng hóa đơn trước, sau đó mới xóa 

trên bảng Customers. 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

165 

14.9.4 Tạo một dòng dữliệu mới 

Sau khi người dùng cung cấp các thông tin vềkhách hàng cần tạo mới và nhấn 

Button tạo mới ( New ), ta sẽviết mã thực thi trong hàm bắt sựkiện nhấn nút tạo 

mới này. Đầu tiên ta sẽtạo ra một dòng mới trên đối tượng DataTable, sau đó gán 

dữliệu trên các TextBox cho các cột của dòng mới này : 

DataRow newRow = dataTable.NewRow( ); 

newRow["CustomerID"] = txtCompanyID.Text; 

newRow["CompanyName"] = txtCompanyName.Text; 

newRow["ContactName"] = txtContactName.Text; 

newRow["ContactTitle"] = txtContactTitle.Text; 

newRow["Address"] = txtAddress.Text; 

newRow["City"] = txtCity.Text; 

newRow["PostalCode"] = txtZip.Text; 

newRow["Phone"] = txtPhone.Text; 

Thêm dòng mới với dữliệu vào bảng DataTable, cập nhật vào cơsởdữliệu, hiển 

thịcâu truy vấn, cập nhật DataSet, hiển thịdữliệu mới lên hộp ListBox. Làm trắng 

các điều khiển TextBox bằng hàm thành viên ClearFields(). 

dataTable.Rows.Add(newRow); 

DataAdapter.Update(DataSet,"Customers"); 

lblMessage.Text = DataAdapter.UpdateCommand.CommandText; 

Application.DoEvents( ); 

DataSet.AcceptChanges( ); 

PopulateLB( ); 

ClearFields( ); 

Đểhiểu rõ hoàn chỉnh ứng, ta sẽxem mã hoàn chỉnh của toàn ứng dụng : 

usingSystem; 

usingSystem.Drawing; 

usingSystem.Collections; 

usingSystem.ComponentModel; 

usingSystem.Windows.Forms; 

usingSystem.Data; 

usingSystem.Data.SqlClient; 

namespaceProgrammingCSharpWindows.Form 

public classADOForm1 : System.Windows.Forms.Form 

privateSystem.ComponentModel.Container components; 

privateSystem.Windows.Forms.Label label9; 

privateSystem.Windows.Forms.TextBox txtPhone; 

privateSystem.Windows.Forms.Label label8; 

privateSystem.Windows.Forms.TextBox txtContactTitle; 

privateSystem.Windows.Forms.Label label7; 

privateSystem.Windows.Forms.TextBox txtZip; 

privateSystem.Windows.Forms.Label label6; 

privateSystem.Windows.Forms.TextBox txtCity; 

privateSystem.Windows.Forms.Label label5; 

privateSystem.Windows.Forms.TextBox txtAddress; 

privateSystem.Windows.Forms.Label label4; 

privateSystem.Windows.Forms.TextBox txtContactName; 

privateSystem.Windows.Forms.Label label3; 

privateSystem.Windows.Forms.TextBox txtCompanyName; 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

166 

privateSystem.Windows.Forms.Label label2; 

privateSystem.Windows.Forms.TextBox txtCompanyID; 

privateSystem.Windows.Forms.Label label1; 

privateSystem.Windows.Forms.Button btnNew; 

privateSystem.Windows.Forms.TextBox txtCustomerName; 

privateSystem.Windows.Forms.Button btnUpdate; 

privateSystem.Windows.Forms.Label lblMessage; 

privateSystem.Windows.Forms.Button btnDelete; 

privateSystem.Windows.Forms.ListBox lbCustomers; 

privateSqlDataAdapter DataAdapter; 

// biết thành viên DataSet và dataTable cho phép ta sử

// dụng trên nhiều hàm khác nhau 

privateDataSet DataSet; 

privateDataTable dataTable; 

publicADOForm1( ) 

InitializeComponent( ); 

stringconnectionString = "server=Neptune; uid=sa;" + 

" pwd=oWenmEany; database=northwind"; 

stringcommandString = "Select * from Customers"; 

DataAdapter = 

newSqlDataAdapter(commandString, connectionString); 

DataSet = newDataSet( ); 

DataAdapter.Fill(DataSet,"Customers"); 

PopulateLB( ); 

// Đẩy dữliệu vào điều khiển ListBox 

private voidPopulateLB( ) 

dataTable = DataSet.Tables[0]; 

lbCustomers.Items.Clear( ); 

foreach(DataRow dataRow indataTable.Rows) 

    lbCustomers.Items.Add( dataRow["CompanyName"] + " (" + 

  dataRow["ContactName"] + ")" );  } 

public override voidDispose( ) 

base.Dispose( ); 

components.Dispose( ); 

private voidInitializeComponent( ) 

this.components = newSystem.ComponentModel.Container(); 

this.txtCustomerName=newSystem.Windows.Forms.TextBox(); 

this.txtCity = newSystem.Windows.Forms.TextBox(); 

this.txtCompanyID = newSystem.Windows.Forms.TextBox(); 

this.lblMessage = newSystem.Windows.Forms.Label(); 

this.btnUpdate = newSystem.Windows.Forms.Button(); 

this.txtContactName= newSystem.Windows.Forms.TextBox(); 

this.txtZip = newSystem.Windows.Forms.TextBox(); 

this.btnDelete = newSystem.Windows.Forms.Button(); 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

167 

this.txtContactTitle=newSystem.Windows.Forms.TextBox(); 

this.txtAddress = newSystem.Windows.Forms.TextBox(); 

this.txtCompanyName=newSystem.Windows.Forms.TextBox( ); 

this.label5 = newSystem.Windows.Forms.Label( ); 

this.label6 = newSystem.Windows.Forms.Label( ); 

this.label7 = newSystem.Windows.Forms.Label( ); 

this.label8 = newSystem.Windows.Forms.Label( ); 

this.label9 = newSystem.Windows.Forms.Label( ); 

this.label4 = newSystem.Windows.Forms.Label( ); 

this.lbCustomers = newSystem.Windows.Forms.ListBox( ); 

this.txtPhone = newSystem.Windows.Forms.TextBox( ); 

this.btnNew = newSystem.Windows.Forms.Button( ); 

this.label1 = newSystem.Windows.Forms.Label( ); 

this.label2 = newSystem.Windows.Forms.Label( ); 

this.label3 = newSystem.Windows.Forms.Label( );  

txtCustomerName.Location = 

newSystem.Drawing.Point(256, 120); 

txtCustomerName.TabIndex = 4; 

txtCustomerName.Size = newSystem.Drawing.Size(160, 20); 

txtCity.Location = newSystem.Drawing.Point(384, 245); 

txtCity.TabIndex = 15; 

txtCity.Size = newSystem.Drawing.Size (160, 20); 

txtCompanyID.Location = 

newSystem.Drawing.Point (136, 216); 

txtCompanyID.TabIndex = 7; 

txtCompanyID.Size = newSystem.Drawing.Size (160, 20); 

lblMessage.Location = newSystem.Drawing.Point(32, 368); 

   lblMessage.Text = "Press New, Update or Delete"; 

lblMessage.Size = newSystem.Drawing.Size (416, 48); 

lblMessage.TabIndex = 1; 

btnUpdate.Location = newSystem.Drawing.Point (32, 120); 

btnUpdate.Size = newSystem.Drawing.Size (75, 23); 

btnUpdate.TabIndex = 0; 

btnUpdate.Text = "Update"; 

btnUpdate.Click += 

newSystem.EventHandler (this.btnUpdate_Click); 

txtContactName.Location = 

newSystem.Drawing.Point(136, 274); 

txtContactName.TabIndex = 11; 

txtContactName.Size = newSystem.Drawing.Size (160, 20); 

txtZip.Location = newSystem.Drawing.Point (384, 274); 

txtZip.TabIndex = 17; 

txtZip.Size = newSystem.Drawing.Size (160, 20); 

btnDelete.Location = newSystem.Drawing.Point(472, 120); 

btnDelete.Size = newSystem.Drawing.Size(75, 23); 

btnDelete.TabIndex = 2; 

btnDelete.Text = "Delete"; 

btnDelete.Click += 

newSystem.EventHandler (this.btnDelete_Click); 

txtContactTitle.Location = 

newSystem.Drawing.Point(136, 303); 

txtContactTitle.TabIndex = 19; 

txtContactTitle.Size = newSystem.Drawing.Size(160, 20); 

txtAddress.Location = newSystem.Drawing.Point(384, 216); 

txtAddress.TabIndex = 13; 

txtAddress.Size = newSystem.Drawing.Size (160, 20); 

txtCompanyName.Location= newSystem.Drawing.Point (136, 245); 

txtCompanyName.TabIndex = 9; 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

168 

txtCompanyName.Size = newSystem.Drawing.Size (160, 20); 

label5.Location = newSystem.Drawing.Point (320, 252); 

label5.Text = "City"; 

label5.Size = newSystem.Drawing.Size (48, 16); 

label5.TabIndex = 14; 

label6.Location = newSystem.Drawing.Point (320, 284); 

label6.Text = "Zip"; 

label6.Size = newSystem.Drawing.Size (40, 16); 

label6.TabIndex = 16; 

label7.Location = newSystem.Drawing.Point (40, 312); 

label7.Text = "Contact Title"; 

label7.Size = newSystem.Drawing.Size (88, 16); 

label7.TabIndex = 18; 

label8.Location = newSystem.Drawing.Point (320, 312); 

label8.Text = "Phone"; 

label8.Size = newSystem.Drawing.Size (56, 16); 

label8.TabIndex = 20; 

label9.Location = newSystem.Drawing.Point (120, 120); 

   label9.Text = "New Customer Name:"; 

label9.Size = newSystem.Drawing.Size (120, 24); 

label9.TabIndex = 22; 

label4.Location = newSystem.Drawing.Point (320, 224); 

label4.Text = "Address"; 

label4.Size = newSystem.Drawing.Size (56, 16); 

label4.TabIndex = 12; 

lbCustomers.Location = newSystem.Drawing.Point(32, 16); 

lbCustomers.Size = newSystem.Drawing.Size (512, 95); 

lbCustomers.TabIndex = 3; 

txtPhone.Location = newSystem.Drawing.Point (384, 303); 

txtPhone.TabIndex = 21; 

txtPhone.Size = newSystem.Drawing.Size (160, 20); 

btnNew.Location = newSystem.Drawing.Point (472, 336); 

btnNew.Size = newSystem.Drawing.Size (75, 23); 

btnNew.TabIndex = 5; 

btnNew.Text = "New"; 

btnNew.Click += newSystem.EventHandler(this.btnNew_Click); 

label1.Location = newSystem.Drawing.Point (40, 224); 

label1.Text = "Company ID"; 

label1.Size = newSystem.Drawing.Size (88, 16); 

label1.TabIndex = 6; 

label2.Location = newSystem.Drawing.Point (40, 252); 

label2.Text = "Company Name"; 

label2.Size = newSystem.Drawing.Size (88, 16); 

label2.TabIndex = 8; 

label3.Location = newSystem.Drawing.Point (40, 284); 

label3.Text = "Contact Name"; 

label3.Size = newSystem.Drawing.Size (88, 16); 

label3.TabIndex = 10; 

this.Text = "Customers Update Form"; 

this.AutoScaleBaseSize = newSystem.Drawing.Size(5, 13); 

this.ClientSize = newSystem.Drawing.Size (584, 421); 

this.Controls.Add (this.label9); 

this.Controls.Add (this.txtPhone); 

this.Controls.Add (this.label8); 

this.Controls.Add (this.txtContactTitle); 

this.Controls.Add (this.label7); 

this.Controls.Add (this.txtZip); 

this.Controls.Add (this.label6); 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

169 

this.Controls.Add (this.txtCity); 

this.Controls.Add (this.label5); 

this.Controls.Add (this.txtAddress); 

this.Controls.Add (this.label4); 

this.Controls.Add (this.txtContactName); 

this.Controls.Add (this.label3); 

this.Controls.Add (this.txtCompanyName); 

this.Controls.Add (this.label2); 

this.Controls.Add (this.txtCompanyID); 

this.Controls.Add (this.label1); 

this.Controls.Add (this.btnNew); 

this.Controls.Add (this.txtCustomerName); 

this.Controls.Add (this.btnUpdate); 

this.Controls.Add (this.lblMessage); 

this.Controls.Add (this.btnDelete); 

this.Controls.Add (this.lbCustomers); 

// Quản lý sựkiện nhấn nút tạo mới (New) 

protected voidbtnNew_Click( objectsender, System.EventArgs e) 

// tạo một dòng mới 

DataRow newRow = dataTable.NewRow( ); 

newRow["CustomerID"] = txtCompanyID.Text; 

newRow["CompanyName"] = txtCompanyName.Text; 

newRow["ContactName"] = txtContactName.Text; 

newRow["ContactTitle"] = txtContactTitle.Text; 

newRow["Address"] = txtAddress.Text; 

newRow["City"] = txtCity.Text; 

newRow["PostalCode"] = txtZip.Text; 

newRow["Phone"] = txtPhone.Text; 

// thêm một dòng mới vào bảng 

dataTable.Rows.Add(newRow); 

// cập nhật vào cơsởdữliệu 

DataAdapter.Update(DataSet,"Customers"); 

// thông báo cho người dùng biết câu truy vấn thay đổi 

lblMessage.Text = DataAdapter.UpdateCommand.CommandText; 

Application.DoEvents( ); 

DataSet.AcceptChanges( ); 

// hiển thịlại dữliệu cho điều khiển ListBox 

PopulateLB( ); 

// Xoá trằng các TextBox 

ClearFields( ); 

// Xóa trắng các TextBox 

private voidClearFields( ) 

txtCompanyID.Text = ""; 

txtCompanyName.Text = ""; 

txtContactName.Text = ""; 

txtContactTitle.Text = ""; 

txtAddress.Text = ""; 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

170 

txtCity.Text = ""; 

txtZip.Text = ""; 

txtPhone.Text = ""; 

// quản lý sựkiện nhất nút chọn cập nhật (Update) 

protected voidbtnUpdate_Click( objectsender, EventArgs e) 

// lấy vểdòng được chọn trên ListBox 

DataRow targetRow = 

dataTable.Rows[lbCustomers.SelectedIndex]; 

// thông báo cho người biết dòng cập nhật 

   lblMessage.Text = "Updating " + targetRow["CompanyName"]; 

Application.DoEvents( ); 

// hiệu chỉnh dòng 

targetRow.BeginEdit( ); 

targetRow["CompanyName"] = txtCustomerName.Text; 

targetRow.EndEdit( ); 

// lấy vềcác dòng thay đổi 

DataSet DataSetChanged = 

DataSet.GetChanges(DataRowState.Modified); 

// đảm bảo không có dòng nào có lỗi 

boolokayFlag = true; 

if(DataSetChanged.HasErrors) 

okayFlag = false; 

stringmsg = "Error in row with customer ID "; 

// kiểm tra lỗi trên từng bảng 

foreach(DataTable theTable inDataSetChanged.Tables) 

// nếu bảng có lỗi thì tìm lỗi trên dòng cụthể

if(theTable.HasErrors) 

// lấy các dòng có lỗi 

DataRow[] errorRows = theTable.GetErrors( ); 

// duyệt qua từng dòng có lỗi đểthống báo. 

foreach(DataRow theRow inerrorRows) 

msg = msg + theRow["CustomerID"]; 

lblMessage.Text = msg; 

// nếu không có lỗi 

if(okayFlag) 

// trộn các thay đổi trong 2 DataSet thành một 

DataSet.Merge(DataSetChanged); 

// cập nhật cơsởdữliệu 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

171 

DataAdapter.Update(DataSet,"Customers"); 

// thông báo câu truy vấn cho người dùng 

lblMessage.Text = DataAdapter.UpdateCommand.CommandText; 

Application.DoEvents( ); 

// cập nhật DataSet và 

// hiển thịdữliệu mới cho ListBox 

DataSet.AcceptChanges( ); 

PopulateLB( ); 

else // nếu có lỗi 

DataSet.RejectChanges( ); 

// quản lý sựkiện xóa 

protected voidbtnDelete_Click( objectsender, EventArgs e) 

// lấy vềdòng được chọn trên ListBox 

DataRow targetRow = 

dataTable.Rows[lbCustomers.SelectedIndex]; 

// chuẩn bịthông báo cho người dùng 

stringmsg = targetRow["CompanyName"] + " deleted. "; 

// xóa dòng được chọn 

dataTable.Rows[lbCustomers.SelectedIndex].Delete( ); 

// cập nhật thay đổi cho DataSet 

DataSet.AcceptChanges( ); 

// cập nhật cơsởdữliệu 

DataAdapter.Update(DataSet,"Customers"); 

// hiển thịlại ListBox với dữliệu thay đổi 

PopulateLB( ); 

// thông báo cho người dùng biết 

lblMessage.Text = msg; 

Application.DoEvents( ); 

public static voidMain(string[] args) 

Application.Run(newADOForm1( )); 

Giao diện thực thi của ứng dụng : 

Truy cập dữliệu với ADO.NET   Gvhd: Nguyễn Tấn Trần Minh Khang

172 

Hình 14-9 Cung cấp dữliệu cho các TextBox đểthêm mới một dòng 

Hình 14-10 Sau khi thêm một dòng vào cuối ListBox 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

173 

Chương 15  Ứng dụng Web với Web Forms 

Công nghệ.NET được dùng đểxây dựng các ứng dụng Web là ASP.NET, nó cung 

cấp hai vùng tên khá mạnh và đầy đủphục vụcho việc tạo các ứng dụng Web là 

System.Webvà System.Web.UI. Trong phần này chúng ta sẽtập trung chủyếu 

vào việc dùng ngôn ngữC# đểlập trình với ASP.NET. 

Bộcông cụWeb Form cũng được thiết kế đểhỗtrợmô hình phát triển nhanh 

(RAD). Với Web Form, ta có thểkéo thảcác điều khiển trên Form thiết kếcũng 

nhưcó thểviết mã trực tiếp trong tập tin .aspxhay .aspx.cs. Ứng dụng Web sẽ

được triển khai trên máy chủ, còn người dùng sẽtương tác với ứng dụng thông qua 

trình duyệt. .NET còn hỗtrợta bộcung cụ đểtạo ra các ứng dụng tuân theo mô 

hình n - lớp (tầng - n tier), giúp ta có thểquản lý được ứng dụng được dễdàng hơn 

và nhờthếnâng cao hiệu suất phát triển phần mềm. 

1.1 Tìm hiểu vềWeb Forms 

Web Form là bộcông cụcho phép thực thi các ứng dụng mà các trang Web do nó 

tạo động ra được phân phối đến trình duyệt thông qua mạng Internet. 

Với Web Forms, ta tạo ra các trang HTML với nội dung tĩnh và dùng mã C# chạy 

trên Server đểxửlý dữliệu tĩnh này rồi tạo ra trang Web động, gửi trang này về

trình duyệt dưới mã HTML chuẩn. 

Web Forms được thiết đểchạy trên bất kỳtrình duyệt nào, trang HTML gửi vềsẽ

được gọt giũa sao cho thích hợp với phiên bản của trình duyệt. Ngoài dùng C#, ta 

cũng có thểdùng ngôn ngữVB.NET đểtạo ra các ứng dụng Web tương tự. 

Web Forms chia giao diện người dùng thành hai phần : phần thấy trực quan ( hay 

UI ) và phần trang mã phía sau của UI. Quan điểm này thì tương tựvới Windows 

Form, nhưng với Web Forms, hai phần này nằm trên hai tập tin riêng biệt. Phần 

giao diện UI được lưu trữtrong tập tin có phần mởrộng là .aspx, còn mã được lưu 

trữtrong tập tin có phần mởrộng là .aspx.cs. 

Với môi trường làm việc được cung cấp bởi bộVisual Studio .NET, tạo các ứng 

dụng Web đơn giản chỉlà mởForm mới, kéo thảvà viết mảquản lý sựkiện thích 

hợp. Web Forms được tích hợp thêm một loạt các điều khiển thực thi trên Server, có 

thểtựkiểm tra sựhợp lệcủa dữliệu ngay trên máy khách mà ta không phải viết mã 

mô tảgì cà. 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

174 

15.1 Các sựkiện của Web Forms 

Một sựkiện (Events) được tạo ra khi người dùng nhấn chọn một Button, chọn một 

mục trong ListBox hay thực hiện một thao tác nào đó trên UI. Các sựkiện cũng có 

thể được phát sinh hệthống bắt đầu hay kết thúc. 

Phương thức đáp ứng sựkiện gọi là trình quản lý sựkiện, các trình quản lý sựkiện 

này được viết bằng mã C# trong trang mã (code-behind) và kết hợp với các thuộc 

tính của các điều khiển thuộc trang. 

Trình quản lý sựkiện là một “Delegate”, phương thức này sẽtrảvềkiểu void, và có 

hai đối số. Đối số đầu tiên là thểhiện của đối tượng phát sinh ra sựkiện, đối sốthứ

hai là đối tượng EventArg hay một đối tượng khác được dẫn xuất từ đối tượng 

EventArgs. Các sựkiện này được quản lý trên Server. 

15.1.1 Sựkiện PostBack và Non-PostBack 

PostBack là sựkiện sẽkhiến Form được gửi vềServer ngay lập tức, chẳng hạn sự

kiện đệtrình một Form với phương thức Post. Đối lập với PostBack là NonPostBack, sựkiện này không gửi Form nên Server mà nó lưu sựkiện trên vùng nhớ

Cache cho tới khi có một sựkiện PostBack nữa xảy ra. Khi một điều khiển có thuộc 

tính AutoPostBack là true thì sựkiện PostBack sẽcó tác dụng trên điều khiển đó : 

mặc nhiên thuộc tính AutoPostBach của điều khiển DropDownList là false,ta phải 

đặt lại là truethì sựkiện chọn một mục khác trong DropDownList này mới có tác 

dụng. 

15.1.2 Trạng thái của ứng dụng Web (State) 

Trạng thái của ứng dụng Web là giá trịhiện hành của các điều khiển và mọi biến 

trong phiên làm việc hiện hành của người dùng. Web là môi trường không trạng 

thái, nghĩa là mỗi sựkiện Post lên Server đều làm mất đi mọi thông tin vềphiên làm 

việc trước đó. Tuy nhiên ASP.NET đã cung cấp cơchếhỗtrợviệc duy trì trạng thái 

vềphiên của người dùng. 

Bất kỳtrang nào khi được gửi lên máy chủServer đều được máy chủtổng hợp 

thông tin và tái tạo lại sau đó mới gửi xuống trình duyệt cho máy khách. ASP.NET 

cung cấp một cơchếgiúp duy trì trạng thái của các điều khiển phía máy chủ( 

Server Control ) một cách tự động. Vì thếnếu ta cung cấp cho người dùng một 

danh sách dữliệu ListBox, và người dùng thực hiện việc chọn lựa trên ListBox này, 

sựkiện chọn lựa này sẽvẫn được duy trì sau khi trang được gửi lên máy chủvà gửi 

vềcho trình duyệt cho máy khách. 

15.1.3 Chu trình sống của một Web-Form 

Khi có yêu cầu một trang Web trên máy chủWeb sẽtạo ra một chuỗi các sựkiện ở

máy chủ đó, từlúc bắt đầu cho đến lúc kết thúc một yêu cầu sẽhình thành một chu 

trình sống ( Life-Cycle ) cho trang Web và các thành phần thuộc nó. Khi một trang 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

175 

Web được yêu cầu, máy chủsẽtiến hành mở( Load ) nó và khi hoàn tất yêu cầu 

máy chủsẽ đóng trang này lại, kết xuất của yêu cầu này là một trang HTML tương 

ứng sẽ được gửi vềcho trình duyệt. Dưới đây sẽliệt kê một sốsựkiện, ta có thểbắt 

các sựkiện đểxửlý thích hợp hay bỏqua đểASP.NET xửlý mặc định. 

Khởi tạo (Initialize)Là sựkiện đầu tiên trong chu trình sống của trang, ta có thể

khởi bất kỳcác thông sốcho trang hay các điều khiển thuộc trang. 

Mởtrạng thái vùng quan sát (Load View State)  Được gọi khi thuộc tính 

ViewState của điều khiển được công bốhay gọ. Các giá trịtrong ViewState sẽ

được lưu trữtrong một biến ẩn ( Hidden Field ), ta có thểlấy giá trịnày thông qua 

hàm LoadViewState() hay lấy trực tiếp. 

Kết thúc (Dispose)Ta có thểdùng sựkiện này đểgiải phóng bất kỳtài nguyên 

nguyên nào : bộnhớhay hủy bỏcác kết nối đến cơsởdữliệu. 

15.2 Hiển thịchuỗi lên trang 

Đầu tiên ta cần chạy Visual Studio .NET, sau đó tạo một dựán mới kiểu Web 

Application, ngôn ngữchọn là C# và ứng dụng sẽcó tên là ProgrammingCSharpWeb. 

Url mặc nhiên của ứng dụng sẽcó tên là http://localhost/ ProgrammingCSharpWeb. 

Hình 15-1 Cửa sổtạo ứng dụng Web mới 

Visual Studio .NET sẽ đặt hầu hết các tập tin nó tạo ra cho ứng dụng trong thưmục 

Web mặc  định trên máy người dùng, ví dụ: 

D:\Inetpub\wwwroot\ProgrammingCSharpWeb.Trong .NET, một giải pháp 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

176 

(Solution) có một hay hiều dựán (Project), mỗi dựán sẽtạo ra một thưviện liên kết 

động (DLL) hay tập tin thực thi (EXE). Đểcó thểchạy được ứng dụng Web Form, 

ta cần phải cài đặt IIS và FrontPage Server Extension trên máy tính. 

Khi ứng dụng Web Form được tạo, .NET tạo sẵn một sốtập tin và một trang Web 

có tên mặc định là WebForm1.aspx chỉchứa mã HTML và WebForm1.cschứa mã 

quản lý trang. Trang mã .cs không nằm trong cửa sổSolution Explorer, đểhiển thị

nó ta chọn Project\Show All Files, ta chỉcần nhấn đúp chuột trái trên trang Web là 

cửa sổsoạn thảo mã (Editor) sẽhiện nên, cho phép ta viết mã quản lý trang. Để

chuyển từcửa sốthiết kếkéo thảsang cửa sổmã HTML của trang, ta chọn hai Tab 

ởgóc bên trái phía dưới màn hình. 

Đặt tên lại cho trang Web bằng cách nhấn chuột phải lên trang và chọn mục 

Rename để đổi tên trang thành HelloWeb.aspx, .NET cũng sẽtự động đổi tên trang 

mã của trang thành HelloWeb.cs. NET cũng tạo ra một sốmã HTML cho trang : 

Hình 15-2 

.NET đã phát sinh ra một sốmã ASP.NET : 

<%@ Page 

Codebehind="HelloWeb.cs" 

AutoEventWireup="false" 

Inherits="ProgrammingCSharpWeb.WebForm1" %> 

Thuộc tính language chỉra ngôn ngữlập trình được dùng trong trang mã đểquản lý 

trang, ở đây là C#. Codebehidexác định trang mã quản lý có tên HelloWeb.csvà 

thuộc tính Inheritschỉtrang Web được thừa kếtừlớp WebForm1 được viết trong 

HelloWeb.cs : 

public classWebForm1 : System.Web.UI.Page 

Ta thấy trang này được thừa kếtừlớp System.Web.UI.Page, lớp này do ASP.NET 

cung cấp, xác định các thuộc tính, phương thức và các sựkiện chung cho các trang 

phía máy chủ. Mã HTML phát sinh định dạng thuộc tính của Form : 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

177 

Thuộc tính id làm định danh cho Form, thuộc tính methodcó giá trịlà “POST” 

nghĩa là Form sẽ được gởi lên máy chủngay lập tức khi nhận một sựkiện do người 

dùng phát ra ( nhưsựkiện nhấn nút ) và cờ IsPostBacktrên máy chủkhi đó sẽcó 

giá trịlà true. Biến cờnày có giá trịlà false nếu Form được đệtrình với phương 

thức “GET” hay lần đầu tiên trang được gọi. Bất kỳ điều khiển nào hay Form có 

thuộc tính runat=”server”thì  điều khiển hay Form này sẽ  được xửlý bởi 

ASP.NET Framework trên máy chủ. Thuộc tính MS_POSITIONING = 

“GridLayout” trong thẻ, cho biết cách bốtrí các điều khiển trên Form theo 

dạng lưới, ngoài ra ta còn có thểbốtrí các điều khiển trôi lổi trên trang, bằng cách 

gán thuộc tính MS_POSITIONING thành “FlowLayout”. 

Hiện giờForm của ta là trống, đểhiển thịmột chuỗi gì đó lên màn hình, ta gõ dòng 

mã sau trong thẻ: 

Hello World! It isnow <% = DateTime.Now.ToString( ) %> 

Giống với ASP, phần nằm trong dấu <% %> được xem nhưlà mã quản lý cho 

trang, ở đây là mã C#. Dấu =chỉra một giá trịnhận được từmột biến hay một đối 

tượng nào đó, ta cũng có thểviết mã trên lại nhưsau với cùng chức năng : 

Hello World! It isnow 

<% Response.Write(DateTime.Now.ToString( )); %> 

Thực thi trang này ( Ctrl-F5 ), kết quảsẽhiện trên trình duyệt nhưsau : 

Hình 15-3 Hiển thịchuỗi thời gian 

Đểthêm các điều khiển cho trang, hoặc là ta có thểviết mã trong của sổHTML 

hoặc là kéo thảcác điều khiển trên bộcông của Web Form vào cửa sổthiết kếtrang. 

ASP.NET sẽtự động phát sinh ra kết quảtừmã HTML thành các điều khiển cũng 

nhưtừcác điều khiển trên trang thiết thành mã HTML tương ứng. Ví dụ, kéo hai 

RadioButton vào trang và gán cùng một giá trịnào đó cho thuộc tính GroupName

của cảhai điều khiển, thuộc tính này sẽlàm cho các nút chọn loại trừlẫn nhau. Mã 

HTML của trang trong thẻ

do ASP.NET phát sinh sẽnhưsau : 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

178 

Hình 15-4 

Các điều khiển của ASP.NET, có thêm chữ“asp:” phía trước tên của điều khiển đó, 

được thiết kếmang tính hướng đối tượng nhiều hơn. 

   

Ngoài các điều khiển của ASP.NET, các điều khiển HTML chuẩn cũng được 

ASP.NET hỗtrợ. Tuy nhiên các điều khiển không tạo sựdễ đọc trong mã nguồn do 

tính đối tượng trên chúng không rõ ràng, các điều khiển HTML chuẩn ứng với năm 

điều khiển trên là : 

15.3 Điều khiển xác nhận hợp 

ASP.NET cung cấp một tập các điều khiển xác nhận hợp lệdữliệu nhập phía máy 

chủcũng như ởdưới trình duyệt của máy khách. Tuy nhiên việc xác nhận hợp lệ

dưới máy khách chỉlà một chọn lựa, ta có thểtắt nó đi, nhưng việc xác nhận hợp lệ

trên máy chủthông qua các điều khiển này là bắt buộc, nhằm phòng ngừa một số

trường hợp dữliệu nhập là giảmạo. Việc kiểm tra hợp lệcủa mã trên máy chủlà đề

phòng các trường hợp. Một sốloại xác nhận hợp lệ: dữliệu không được rỗng, thỏa 

một định dạng dữliệu nào đó … 

Các điều khiển xác nhận hợp lệphải được gắn liền với một điều khiển nhận dữliệu 

HTML nào đó, các điều khiển nhập được liệt trong bảng sau : 

Bảng 15-1 Các điều khiển nhập HTML dùng đểxác nhận hợp lệ

Các điều khiển nhập Thuộc tính xác nhận hợp lệ 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

179 

Các điều khiển nhập Thuộc tính xác nhận hợp lệ

Ứng với một điều khiển nhập HTML, ta có thểgắn nhiều điều khiển xác nhận hợp 

lệcho nó, bảng dưới đây sẽliệt kê các điều khiển nhập hiện có : 

Bảng 15-2 Các điều khiển xác nhận hợp lệ

Điều khiển Mục đích 

CompareValidator  So sánh các giá trịcủa hai điều khiển đểxem có bằng nhau hay 

không 

CustomValidator Gọi một hàm do người dùng định nghĩa đểthi hành việc kiểm tra 

RangeValidator Kiểm tra xem một mục có nằm trong một miền đã cho hay không 

RegularExpressionvalidator Kiểm tra người dùng có sửa đổi một mục ( mà giá trịcủa nó khác 

với một giá trị đã chỉ định ban đầu, ngầm định giá trịban đầu là 

một chuỗi trống ) hay không 

ValidationSummary Thông báo sựhợp lệtrên các điều khiển 

15.4 Một sốví dụmẫu minh họa 

Một cách thuận tiện nhất đểhọc một công nghệmới chính là dựa vào các ví dụ, vì 

vậy trong phần này chúng ta sẽkhảo sát một vài ví dụ đểminh họa cho phần lý 

thuyết của chúng ta. Nhưta đã biết, ta có thểviết mã quản lý theo hai cách : hoặc là 

viết trong tập tin .cs hoặc là viết trực tiếp trong trang chứa mã HTML. Ở đây đểdễ

tập trung vào các ví dụcủa chúng ta, ta sẽviết mã quản lý trực tiếp trên trang 

HTML. 

15.4.1 Kết buộc dữliệu 

15.4.1.1 Không thông qua thuộc tính DataSource 

Ứng dụng của chúng ta đơn giản chỉhiện lên trang tên khách hàng và sốhóa đơn 

bằng cách dùng hàm DataBind(). Hàm này sẽkết buộc dữliệu của mọi thuộc tính 

hay của bất kỳ đối tượng. 

// mã quản lý C# sẽ được viết trong thẻ

//

Ứng dụng Web với Web Forms Gvhd: Nguyễn Tấn Trần Minh Khang

180

// trang sẽgọi hàm này đầu tiên, ta sẽthực hiện kết buộc

// trực tiếp trong hàm này

voidPage_Load(Object sender, EventArgs e) {

Page.DataBind();

}

// lấy giá trịcủa thuộc tính thông qua thuộc tính // get

stringcustID{

get{

return"ALFKI";

}

}

intorderCount{

get{

return11;

}

}

// ]]>

<fontface="Verdana">Ket buoc khong dung DataSource

<formrunat=server>

Khach hang: <%#custID %>

So hoa don: <%#orderCount %>

Hình 15-5 Giao diện của ví dụ

15.4.1.2 Điều khiển DataList với DataSource 

Trong ví dụnày, ta sẽdùng thuộc tính DataSource của điều khiển  

đểkết buộc dữliệu, ta sẽcung cấp cho thuộc tính DataSource này một bảng dữliệu 

giả, sau đó dùng hàm DataBinder.Eval()đểkết buộc dữliệu trong DataSource theo 

một định dạng ( Format ) thích hợp mong muốn. Dữliệu sẽ được hiển thịlên màn 

hình dưới dạng một bảng các hóa đơn sau khi ta gọi hàm DataBind(). 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

181 

//Không gian tên chứa các đối tượng của ADO.NET 

<%@ Import %>

<scriptlanguage="C#">

voidPage_Load(Object sender, EventArgs e) { 

// nếu trang được gọi lần đầu tiên 

if(!Page.IsPostBack) { 

// tạo ra một bảng dữliệu mới gồm 4 cột , sau đó thêm dữ

// liệu giảcho bảng 

DataTable dt = newDataTable(); 

DataRow dr; 

// thêm 4 cột DataColumn vào bảng, mỗi cột có các 

// kiểu dữliệu riêng

dt.Columns.Add(newDataColumn("IntegerValue", typeof(Int32))); 

dt.Columns.Add(newDataColumn("StringValue", typeof(string))); 

dt.Columns.Add(newDataColumn("DateTimeValue", typeof(DateTime))); 

dt.Columns.Add(new DataColumn("BoolValue", typeof(bool))); 

// thêm 9 dòng dữliệu cho bảng bằng cách tạo ra 

// một dòng mới dùng phương thức NewRow() của đối 

// tượng DataTable, sau đó gán dữliệu giảcho 

   // dòng này và thêm dòng dữliệu này vào bảng 

for(inti = 0; i < 9; i++) { 

 dr = dt.NewRow(); 

dr[0] = i; 

dr[1] = "Item " + i.ToString(); 

dr[2] = DateTime.Now; 

dr[3] = (i % 2 != 0) ? true: false; 

dt.Rows.Add(dr); 

 } 

// gán bảng dữliệu cho thuộc tính DataSource của điều 

// khiển DataList, sau đó thực hiện kết buộc bằng hàm 

// DataBind() 

dataList1.DataSource = newDataView(dt); 

dataList1.DataBind(); 

<fontface="Verdana">Ket buoc du lieu dung DataSource thong qua 

ham DataBind.Eval()

<formrunat=server> 

// điều khiển danh sách cho phép ta kết buộc dữliệu khá 

// linh động, ta chỉcần cung cấp cho nó một DataSource 

// thích hợp, sau đó gọi hàm DataBind()đểhiển thịdữliệu // lên 

trang 

RepeatColumns="3" 

   Width="80%" 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

182 

   BorderColor="black"                >

// đây là một thuộc tính của lưới, khi gọi hàm 

// DabaBind(), dữliệu trong DataSource sẽ được trích ra 

// (nếu là danh các đối tượng thì mỗi lần trích sẽlấy ra 

// một phần tửkiểu đối tượng đó, sau đó dùng hàm 

// DataBinder.Eval()đểgán giá trị, còn nếu là một bảng 

// dữliệu thì mỗi lần kết buộc sẽlấy ra một dòng dữ

// liệu, hàm DataBind.Eval() sẽlấy dữliệu của từng 

// trường) đểkết buộc lên trang. Nó sẽlặp lại thao tác 

// này cho tới khi dữliệu được kết buộc hết. 

//lấy dữliệu trên cột đầu tiên đểkết buộc 

Ngay hoa don: <%#DataBinder.Eval(Container.DataItem, 

"DateTimeValue", "{0:d}") %> 

//lấy dữliệu trên cốt thứ2 

So luong: <%#DataBinder.Eval(Container.DataItem, "IntegerValue", 

"{0:N2}") %>

//cột thứ3 

Muc: <%#DataBinder.Eval(Container.DataItem, "StringValue") %>

//cột thứ4 

Ngay hoa don:

runat=server/>

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

183 

Hình 15-6 Giao diện của ví dụsau khi thực thi 

15.4.1.3 Kết buộc với điều khiển DataGrid 

Trong ví trước, ta đã tìm hiểu sơqua vềcách đẩy dữliệu vào thuộc tính DataSource 

của điều khiển DataList thông qua hàm kết buộc DataBind().Ví dụnày chúng ta sẽ

khảo sát thêm vềcách kết buộc trên điều khiển lưới DataGrid và cách dùng điều 

khiển xác nhận hợp lệtrên dữliệu. Khi ứng dụng chạy sẽhiển thịmột bảng dữliệu 

lên trang, người dùng có thểhiệu chỉnh bất kỳmột dòng nào trên bảng dữliệu bằng 

cách nhấn vào chuỗi lệnh hiệu chỉnh ( Edit ) trên lưới, gõ vào các dữliệu cần hiệu 

chỉnh, khi muốn hủy bỏthao tác hiệu chỉnh ta nhấn chọn chuỗi bỏqua (Cancel). Để

tập trung vào mục đích của ví dụ, chúng ta sẽdùng bảng dữliệu giả, cách làm sẽ

tương tựtrên bảng dữliệu lấy ra từcơsởdữliệu. Sau đây là mã của ví dụ: 

//không gian tên cần thiết đểtruy cập đến các đối tương ADO.NET 

<%@ Import %>

<scriptlanguage="C#">

//khai báo đối tượng bảng và khung nhìn 

DataTable Cart; 

DataView CartView; 

// lấy dữliệu trong Session, nếu không có thì ta sẽtạo ra một 

// bảng dữliệu khác 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

184 

voidPage_Load(Object sender, EventArgs e) { 

if(Session["DG6_ShoppingCart"] == null) { 

Cart = newDataTable(); 

//bảng sẽcó 3 cột đều có kiểu là chuỗi 

Cart.Columns.Add(newDataColumn("Qty", typeof(string))); 

Cart.Columns.Add(newDataColumn("Item", typeof(string))); 

Cart.Columns.Add(newDataColumn("Price", typeof(string))); 

//đẩy định danh của bảng vào phiên làm việc hiện thời 

Session["DG6_ShoppingCart"] = Cart; 

// tạo dữliệu mẫu cho bảng

for(inti=1; i<5; i++) { 

DataRow dr = Cart.NewRow(); 

dr[0] = ((int)(i%2)+1).ToString(); 

dr[1] = "Item " + i.ToString(); 

dr[2] = ((double)(1.23 * (i+1))).ToString(); 

Cart.Rows.Add(dr); 

else{ 

//nếu bảng đã có sẵn trong Session, ta sẽlấy ra dùng 

Cart = (DataTable)Session["DG6_ShoppingCart"]; 

// tạo ra khung nhìn cho bảng, sau đó sắp xếp khung nhìn theo // cột 

Item 

CartView = newDataView(Cart); 

CartView.Sort = "Item"; 

// nếu trang được gọi lần đầu tiên thì kết buộc dữliệu thông// qua 

hàm BindGrid()của ta 

if(!IsPostBack) { 

BindGrid(); 

// sựkiện nhấn chuỗi hiệu chỉnh (Edit) trên lưới, ta sẽlấy chỉ// 

mục của dòng cần hiệu chỉnh thông qua đối tượng 

// DataGridCommandEventArgs, sau đó truyền chỉmục này cho điều // 

khiển lưới của ta và gọi hàm kết buộc của ta để đẩy dữliệu 

// lên lưới 

public voidMyDataGrid_Edit(Object sender, DataGridCommandEventArgs 

e) { 

 MyDataGrid.EditItemIndex = (int)e.Item.ItemIndex; 

BindGrid(); 

//sựkiện nhấn bỏqua trên lưới (Cancel) 

public voidMyDataGrid_Cancel(Object sender, 

DataGridCommandEventArgs e) { 

 MyDataGrid.EditItemIndex = -1; 

BindGrid(); 

//sau khi hiệu chỉnh dữliệu, người dùng tiến hành cập nhậtpublic

voidMyDataGrid_Update(Object sender, DataGridCommandEventArgs e) { 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

185 

// lấy dữliệu trên TextBox 

stringitem = e.Item.Cells[1].Text; 

stringqty = ((TextBox)e.Item.Cells[2].Controls[0]).Text; 

stringprice = ((TextBox)e.Item.Cells[3].Controls[0]).Text; 

// Ở đây, do chúng ta dùng dữliệu giảlưu trên bộnhớchính, // nếu 

dùng cơsởdữliệu thì chúng ta sẽtiến hành hiệu chỉnh // trực tiếp 

trong cơsởdữliệu bằng các câu truy vấn : 

// UPDATE, SELECT, DELETE 

//xóa dòng cũ

CartView.RowFilter = "Item='"+item+"'"; 

if(CartView.Count > 0) { 

CartView.Delete(0); 

CartView.RowFilter = ""; 

//tạo dòng mới và thêm vào bảng

DataRow dr = Cart.NewRow(); 

dr[0] = qty; 

dr[1] = item; 

dr[2] = price; 

Cart.Rows.Add(dr); 

MyDataGrid.EditItemIndex = -1; 

BindGrid(); 

//kết buộc dữliệu thông qua thuộc tính DataSource của lưới 

public voidBindGrid() { 

 MyDataGrid.DataSource = CartView; 

 MyDataGrid.DataBind(); 

<bodystyle="font: 10pt verdana">

<formrunat="server">

<fontface="Verdana">Using an Edit Command Column in 

DataGrid

//Khai báo các thông sốcho lưới, các sựkiện trên lưới : 

OnEditCommand : khi người dùng nhấn chuỗi hiệu chỉnh (Edit) 

OnCancelCommand : nhấn chuỗi bỏqua hiệu chỉnh (Cancel) 

OnUpdateCommand : nhấn chuỗi cập nhật hiệu chỉnh (Update)

BorderColor="black" 

BorderWidth="1" 

CellPadding="3" 

Font-Name="Verdana" 

Font-Size="8pt" 

HeaderStyle-BackColor="#aaaadd" 

OnEditCommand="MyDataGrid_Edit" 

OnCancelCommand="MyDataGrid_Cancel" 

OnUpdateCommand="MyDataGrid_Update" 

AutoGenerateColumns="false" 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

186 

// các thông sốhiệu chỉnh trên cột, ở đây ta chỉcho người 

// dùng hiệu chỉnh trên cột sốlượng và giá hóa đơn 

EditText="Edit" 

CancelText="Cancel" 

UpdateText="Update" 

ItemStyle-Wrap="false" 

HeaderText="Edit Command Column" 

HeaderStyle-Wrap="false" 

/>

DataField="Item"/>

Giao diện của ví dụkhi chạy : 

Hình 15-7 Hiệu chỉnh trực tiếp trên lưới dữliệu 

Sau khi người dùng chọn nút Edit trên lưới, màn hình ứng dụng sẽnhưsau : 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

187 

Hình 15-8 Người dùng sau khi nhấn chuỗi Edit 

15.4.2 Điều khiển xác nhận hợp lệ

Việc xác nhận hợp lệlà cần thiết với các ứng dụng cần yêu cầu nhập liệu, việc đưa 

ra các điều khiển có khảnăng xác nhận hợp lệtrực tiếp dưới máy khách lẫn ởtrên 

máy chủ, đây có thểlà một tính năng mới của ASP.NET, ta không cần phải viết mã 

kiểm tra gì cả, mã kiểm tra dưới trình duyệt ( chẳng hạn nhưJava Script ) sẽ được 

ASP.NET tự động phát sinh. Đểgắn một điều khiển bắt lỗi vào một điều khiển cần 

bắt lỗi ta chỉcần gán thuộc tính ControlToValidate của điều khiển bắt lỗi bằng giá 

trị định danh idcủa điều khiển cần bắt lỗi, ví dụ: Đểbắt lỗi điều khiển TextBox 

không được trống, ta viết má nhưsau : 

//điều khiển cần bắt lỗi 

//điều khiển bắt lỗi hộp nhập liệu TextBox1 

ControlToValidate="TextBox1" 

ErrorMessage="Card Number. " 

Display="Static" 

Width="100%" runat=server>

Ví dụcủa chúng ta sẽcho hiển thị2 hộp thoại DropDownList, 2 nút chọn 

RadioButton và một hộp thoại nhập TextBox, nếu tồn tại mục nhập nào trống khi 

nhấn nút xác nhận Validate, thì các điều khiển xác nhận hợp lệsẽhiển thịlỗi tương 

ứng. Thông điệp lỗi có thể được hiển thịtheo ba cách khác nhau : liệt kê theo danh 

sách (List), liệt kê trên cùng một dòng ( Single Paragraph ), liệt kê danh sách với 

dấu chấm tròn ở đầu ( Bullet List ). Mã hoàn chỉnh của ví dụ được liệt kê nhưsau : 

// không cho phép điều khiển xác nhận hợp lệdưới máy khách bằng 

// cách gán thuộc tính clienttarget = downlevel 

<%@ Page clienttarget=downlevel %> 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

188 

<scriptlanguage="C#" runat=server>

// thay đổi chế độhiển thịlỗi bằng cách chọn 1 trong 3 mục 

// trong hộp thoại ListBox 

voidListFormat_SelectedIndexChanged(Object Sender, EventArgs E ) 

valSum.DisplayMode = (ValidationSummaryDisplayMode) 

ListFormat.SelectedIndex; 

<fontface="Verdana">Ví dụvềxác nhận điều khiển hợp lệ

ValidationSummary

<formrunat="server">

<tablecellpadding=10> 

<tablebgcolor="#eeeeee" cellpadding=10><tdcolspan=3> 

<fontface=Verdana size=2>Credit Card 

Information

<tdalign=right>

<fontface=Verdana size=2>Card Type: 

// danh sách các nút chọn được bắt lỗi bởi điều //khiển xác nhận hợp 

lệRequireFieldValidator1

runat=server>

MasterCard

Visa

//điều khiển xác nhận hợp lệcho các nút chọn //RadioButtonList1 

<tdalign=middle rowspan=1>

 ControlToValidate="RadioButtonList1"  ErrorMessage="Card Type. " 

Display="Static" 

InitialValue="" runat=server>

<tdalign=right>

<fontface=Verdana size=2>Card Number:

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

189 

//điều khiển xác nhận hợp lệtrên hộp thoại //nhập liệu TextBox, nếu 

chuỗi là trống khi //nhấn nút Validate thì sẽbịbắt lỗi.

ControlToValidate="TextBox1" 

ErrorMessage="Card Number. " 

Display="Static" 

Width="100%" runat=server>

<tdalign=right>

<fontface=Verdana size=2>Expiration Date:

//hộp thoại DropDownList dùng đểhiển thị//danh sách các ngày, nếu 

người dùng chọn //mục trống trong DropDownList này thì sẽbị//điều 

khiển xác nhận hợp lệ//RequireFieldValidator3 bắt lỗi 

06/00

07/00

08/00

09/00

10/00

11/00

01/01

02/01

03/01

04/01

05/01

06/01

07/01

08/01

09/01

10/01

11/01

12/01

//điều khiển xác nhận hợp lệtrên //DropDownList1 hiển thịngày hết 

hạn, nếu //người dùng chọn một mục trống trên //DropDownList thì 

điều khiển này sẽphát //sinh ra lỗi

ControlToValidate="DropDownList1" 

ErrorMessage="Expiration Date. " 

Display="Static" 

InitialValue="" 

Width="100%" 

runat=server>

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

190 

//nút nhấn đểxác định hợp lệ

<tdvalign=top>

<tablecellpadding=20> 

//điều khiển dùng đểhiện thịcác lỗi lên trang, //nó sẽbắt bất kỳ

lỗi nào được phát sinh bởi các //điều khiển DropDownList đểhiển thị

HeaderText="You must enter a value in the following fields:" 

Font-Name="verdana" 

Font-Size="12" 

/>

<fontface="verdana">Select the type of validation summary 

display you wish:  

//Danh sách liệt kê 3 cách hiển thịlỗi 

OnSelectedIndexChanged="ListFormat_SelectedIndexChanged" 

runat=server >

List

Bulleted List

Single Paragraph

Giao diện của ví dụkhi chạy : 

Ứng dụng Web với Web Forms   Gvhd: Nguyễn Tấn Trần Minh Khang

191 

Hình 15-9 Khi chưa nhấn nút xác nhận Validate 

Hình 15-10 Hiển thịlỗi do bỏtrống trên TextBox theo dạng dấu chấm tròn Bullet 

Các dịch vụWeb   Gvhd: Nguyễn Tấn Trần Minh Khang

192 

Chương 16 Các dịch vụWeb 

Hiện nay, vẫn còn một sốhạn chếlớn trong các ứng dụng Web. Người dùng bịgiới 

hạn chỉthực hiện được những nội dung đã được cấu trúc cho một trang cụthểvà 

xem dữliệu thông qua một sốgiao diện cụthểnào đó đã được thiết kếtrên máy 

chủ. Do đó người dùng muốn lấy được thông tin được linh động và hiệu quảhơn. 

Hơn nữa, thay vì ta hiển thịthông tin thông qua trình duyệt Web, ta muốn chạy một 

phần mềm trực tiếp trên máy khách mà có thểtrao đổi dữliệu trên máy chủtuỳý. 

Công nghệ.NET cho phép xây dụng cách dịch vụWeb ( Web Services ) đáp ứng 

được các yêu cầu trên. Ý tưởng chính là : thay vì liệt kê các thông tin theo dạng 

HTML, trang tạo sẵn một loạt các lệnh gọi hàm. Các lệnh gọi hàm này có thểtrao 

đổi thông tin qua lại giữa các hệcơsởdữliệu trên máy chủ. Các hàm này có thể

chấp nhận các tham sốvà có thểtrảvềmột giá trịtùy ý. 

Các dịch vụWeb vẫn dựa trên giao thức HTTP đểtruyền dữliệu, đồng thời nó cần 

phải sửdụng thêm một loại giao thức đểphục vụcho việc gọi hàm. Hiện nay có hai 

giao thức được dùng chủyếu là : SOAP ( Simple Object Access Protocol ) và SDL

( Service Description Language, đây là giao thức riêng của Microsoft ). Cảhai giao 

thức này đều được xây dụng dựa trên XML, mục đích chung của chúng là giúp định 

nghĩa các lệnh gọi hàm, tham sốvà giá trị. 

Ngoài ra, Microsoft cũng đưa ra thêm một ý tưởng mới vềtập tin Discovery File, 

có phần mởrộng là .disco. Tập tin dạng này dùng đểcung cấp các thông tin cho các 

trình duyệt đểcác trình duyệt này có thểxác định được các trang trên các máy chủ

mà có chứa các dịch vụWeb. 

Sau đây, ta sẽtìm hiểu một ví dụnhằm minh họa việc tạo ra một dịch vụWeb, đóng 

vai trò là một thưviện chứa một tập các hàm tiện ích. Trang Web của chúng ta sẽsử

dụng các hàm của dịch vụnày. Dịch vụWeb của chúng sẽcó tên MathService, đơn 

giản là định nghĩa bốn phương thức cộng, trừ, nhân, chia trên hai sốthực bất kỳ. 

Mỗi phương thức đều nhận vào hai đối sốkiểu sốthực và trảvềkết quảcũng có 

kiểu sốthực. 

Đầu tiên ta cần tạo một dựán kiểu Web Service bằng cách chọn : New 

Project\Visual C# Project\ASP.NET Web Service và đặt tên cho dựán là 

MathService và đổi tên dịch vụthành MathService.asmx. NET có tạo sẵn cho 

chúng ta một sốtập tin như: 

• Service1.asmx: được trình duyệt yêu cầu, tương tựvới tập tin .aspx. 

• WebService1.cs: trang chứa mã C# quản lý. 

• DiscoFile1.disco: tập tin khám phá. 

Các dịch vụWeb   Gvhd: Nguyễn Tấn Trần Minh Khang

193 

Trong ví dụnày, chúng ta sẽtạo ra một Web Form mới và thiết giao diện nhưsau : 

Web Form sẽgọi thực thi các hàm của dịch vụWeb. 

Dựán của ta sẽthừa kếnamespace là System.Web.Services.WebService,nơi chứa 

các thuộc tính và phương thức cần thiết đểtạo dịch vụWeb. 

public classMathService : System.Web.Services.WebService 

Trên mỗi phương thức ta cần khai báo thuộc tính [WebMethod], đểchỉra đây là 

phương thức sẽ được sửdụng cho dịch vụWeb. Mã của tập tin dịch vụsẽnhưsau : 

usingSystem; 

usingSystem.Collections; 

usingSystem.ComponentModel; 

usingSystem.Data; 

usingSystem.Diagnostics; 

usingSystem.Web; 

usingSystem.Web.Services; 

namespaceMathService 

public classMathService :System.Web.Services.WebService 

publicMathService() 

InitializeComponent(); 

#regionComponent Designer generated code 

privateIContainer components = null; 

private voidInitializeComponent() 

protected override voidDispose( booldisposing ) 

Các dịch vụWeb   Gvhd: Nguyễn Tấn Trần Minh Khang

194 

if(disposing && components != null) 

components.Dispose(); 

base.Dispose(disposing);  

#endregion

//4 hàm toán học của dịch vụWeb, trên mỗi phương thức 

//ta cần khai báo thuộc tính [WebMethod] đểchỉ đây là 

//phương thức dành cho dịch vụWeb.

[WebMethod] 

public floatAdd(floata, floatb) 

returna + b; 

[WebMethod] 

public floatSubtract(floata, floatb) 

returna - b; 

[WebMethod] 

public floatMultiply(floata, floatb) 

returna * b; 

[WebMethod] 

public floatDivide(floata, floatb) 

if(b==0) return-1; 

returna / b; 

Bây giờchúng ta sẽviết mã thực thi cho trang Web. Trang Web của chúng ta sẽgọi 

các hàm của dịch vụtương ứng với các phép cộng, trừ, nhân, chia . Sau đây là mã 

của trang Web: 

<%@ Import %> 

//

floatoperand1 = 0;

floatoperand2 = 0;

public voidSubmit_Click(Object sender, EventArgs E)

{

try

{

operand1 = float.Parse(Operand1.Text);

operand2 = float.Parse(Operand2.Text);

}

catch(Exception) { /* bỏqua lỗi nếu có */}

Các dịch vụWeb Gvhd: Nguyễn Tấn Trần Minh Khang

195

//tạo ra một đối tượng dịch vụMathService đểcó thểtruy cập đến

//các hàm thành viên của chúng.

MathService service = newMathService();

switch(((Control)sender).ID)

{

case"Add" : Result.Text = "Result = " +

service.Add(operand1, operand2).ToString(); break;

case"Subtract" : Result.Text = "Result = " +

service.Subtract(operand1, operand2).ToString(); break;

case"Multiply" : Result.Text = "Result = " +

service.Multiply(operand1, operand2).ToString(); break;

case"Divide" : Result.Text = "Result = " +

service.Divide(operand1, operand2).ToString(); break;

}

}

// ]]>

Using a Simple Math Service 

Operand 1:

/>

Operand 2:

/>

OnServerClick="Submit_Click"> 

OnServerClick="Submit_Click"> 

OnServerClick="Submit_Click"> 

OnServerClick="Submit_Click"> 

Assemblies và Versioning   Gvhd: Nguyễn Tấn Trần Minh Khang

196 

Chương 17 Assemblies và Versioning 

Đơn vịcơbản trong lập trình .NET là Assembly. Một Assembly là một tập hợp các 

tập tin mà đối với người sửdụng, họchỉthấy đó là một tập tin DLL hay EXE. 

.NET định nghĩa Assembly là một đơn vịcó khảnăng tái sửdụng (re-use), mang số

hiệu phiên bản (versioning), bảo mật (security) và cuối cùng là khảnăng triển khai 

(deployment) 

Asssembly có thểchứa đựng nhiều thành phần khác ngoài mã chương trình ứng 

dụng nhưtài nguyên (resource, ví dụtập tin .GIF), thông tin mô tảkiểu (type 

definition), siêu dữliệu (metadata) vềmã và dữliệu. 

17.1 Tập tin PE 

Assembly được lưu trữtrên dĩa từtheo dạng thức tập tin Portable Executable (PE). 

Dạng thức tập tin PE của .NET cũng giống nhưtập tin PE bình thường của 

Windows NT. Dạng thức PE được cài đặt thành dạng thức tập tin DLL và EXE. 

Vềmặt logic, assembly chứa đựng một hay nhiều module. Mỗi module được tổ

chức thành một DLL và đồng thời mỗi module là một cấu thành của assembly. Các 

module tựbản thân chúng không thểchạy được, các module phải kết hợp với nhau 

thành assembly thì mới có thểlàm được việc gì đó hữu ích. 

17.2 Metadata 

Metadata là thông tin được lưu trữbên trong assembly với mục đích là đểmô tảcác 

kiểu dữliệu, các phương thức và các thông tin khác vềassembly. Do có chứa 

metadata nên assembly có khảnăng tựmô tả. 

17.3 Ranh giới an ninh 

Assembly tạo ra một ranh giới an ninh (securityboundary). Các kiểu dữliệu định 

nghĩa bên trong assembly bịgiới hạn phạm vi tại ranh giới assembly. Đểcó thểsử

dụng chung một kiểu dữliệu giữa 2 assembly, cần phải chỉ định rõ bằng tham chiếu 

(reference) trong IDE hoặc dòng lệnh. 

17.4 Sốhiệu phiên bản (Versioning) 

Mỗi assembly có sốhiệu phiên bản riêng. Một “phiên bản” ám chỉtoàn bộnội dung 

của một assembly bao gồm cảkiểu dữliệu và resource. 

17.5 Manifest 

Manifest chính là một thành phần của metadata. Manifest mô tảmột assembly chứa 

những gì, ví dụnhư: thông tin nhận dạng (tên, phiên bản), danh sách các kiểu dữ 

Assemblies và Versioning   Gvhd: Nguyễn Tấn Trần Minh Khang

197 

liệu, danh sách các resource, danh sách các assembly khác được assembly này tham 

chiếu đến, … 

17.5.1 Các module trong manifest 

Một assembly có thểchứa nhiều module, do đó manifest trong assembly còn có thể

chứa mã băm (hash code) của mỗi module lắp ghép thành assembly đểbảo đảm 

rằng khi thực thi, chỉcó thểnạp các module đúng phiên bản. 

Chỉcần một sựthay đổi rất rất nhỏtrong module là mã băm sẽthay đổi. 

17.5.2 Manifest trong module 

Mỗi module cũng chứa riêng phần manifest mô tảcho chính nó giống nhưassembly 

chứa manifest mô tảcho assembly vậy. 

17.5.3 Các assembly cần tham chiếu 

Manifest của assembly cũng có thểchứa tham chiếu đến các assembly khác. Mỗi 

tham chiếu chứa đựng tên, phiên bản, văn hóa (culture), nguồn gốc (originator),… 

Thông tin vềnguồn gốc chính là chữký số(digital signature) của lập trình viên hay 

của công ty nơi cung cấp assembly mà assembly hiện tại đang tham chiếu đến. 

Văn hóa là một đối tượng chứa thông tin vềngôn ngữ, cách trình bày của mỗi quốc 

gia. Ví dụnhưcách thểhiện ngày tháng: D/M/Y hay M-D-Y 

17.6 Đa Module Assembly 

Một assembly đơn module là một assembly chỉgồm một module, module này có 

thểlà một tập tin EXE hoặc DLL. Manifest cho assembly đơn module được nhúng 

vào trong module. 

Một assembly đa module là một assembly bao gồm nhiều tập tin (ít nhất một tập tin 

EXE hoặc DLL). Manifest cho assembly đa module có thể được lưu trữthành một 

tập tin riêng biệt hoặc được nhúng vào một module nào đó bất kỳ. 

17.6.1 Lợi ích của đa module assembly 

Nếu một dựán có nhiều lập trình viên mà dựán đó chỉxây dựng bằng một 

assembly, việc kiểm lỗi, biên dịch dựán,… là một “ác mộng” vì tất cảcác lập trình 

viên phải hợp tác với nhau, phải kiểm tra phiên bản, phải đồng bộhóa mã nguồn,… 

Nếu một ứng dụng lớn được xây dựng bằng nhiều assembly, khi cần cập nhật 

(update) đểsửa lỗi chẳng hạn, thì chỉcần cập nhật một / vài assembly mà thôi. 

Nếu một ứng dụng lớn được tổchức từnhiều assembly, chỉcó những phần mã 

chương trình thường sửdụng / quan trọng thuộc một vài assembly là được nạp vào 

bộnhớ, do đó làm giảm bớt chi phí bộnhớ, tăng hiệu suất hệthống. 

Assemblies và Versioning   Gvhd: Nguyễn Tấn Trần Minh Khang

198 

17.7 Assembly nội bộ(private assembly) 

Có 2 loại Assembly: nội bộ(private) và chia sẻ(shared). Assembly nội bộ được dự

định là chỉdùng cho một ứng dụng, còn assembly chia sẻthì ngược lại, dùng cho 

nhiều ứng dụng. 

Các assembly nội bộ được ghi trên dĩa từthành một tập tin EXE hoặc DLL trong 

cùng thưmục với assembly chính hoặc trong các thưmục con của thưmục chứa 

assembly chính. Đểthực thi trên máy khác chỉcần sao chép đúng cấu trúc thưmục 

là đủ, không cần phải đăng ký với Registry. 

17.8 Assembly chia sẻ(shared assembly) 

Khi viết ra một assembly đại loại nhưmột control chẳng hạn, nếu tác giảcủa 

control đó có ý định chia sẻcho các lập trình viên khác thì anh / chịta phải xây 

dựng assembly đó đáp ứng các yêu cầu sau: 

• Assembly đó phải có tên “mạnh” (strong name). Tên mạnh có nghĩa là chuỗi 

biểu diễn tên đó phải là duy nhất (globally unique) 

• Phải có thông tin vềphiên bản đểtránh hiện tượng các phiên bản “dẫm chân 

lên nhau” 

• Đểcó thểchia sẻassembly, assembly đó phải được đặt vào nơi gọi là Global 

Assembly Cache (GAC). Đây là nơi được quy định bởi Common Language 

Runtime (CLR) dùng đểchứa assembly chia sẻ. 

17.8.1 Chấm dứt “địa ngục DLL” 

Giảsửbạn cài đặt một ứng dụng A lên một máy và nó chạy tốt. Sau đó bạn cài đặt 

ứng dụng B, bỗng nhiên ứng dụng A không chịu hoạt động. Sau quá trình tìm hiểu, 

cuối cùng nguyên nhân là do ứng dụng B đã cài một phiên bản khác đè lên một tập 

tin DLL mà ứng dụng A sửdụng. Tình huống trên gọi là “địa ngục DLL” 

Sựra đời của assembly đã chấm dứt tình trạng trên. 

17.8.2 Phiên bản 

Assembly chia sẻtrong .NET được định vịbằng tên duy nhất (unique) và phiên bản. 

Phiên bản được biểu diễn bởi 4 sốphân cách bằng dấu ‘:’ ví dụnhư1:2:6:1246 

Số đầu tiên mô tảphiên bản chính (major version) 

Sốthứ2 mô tảphiên bản phụ(minor version) 

Sốthứ3 mô tảthứtựbản xây dựng (build) 

Sốcuối cùng mô tảlần xem xét cập nhật (revision) đểsửa lỗi 

Assemblies và Versioning   Gvhd: Nguyễn Tấn Trần Minh Khang

199 

17.8.3 Tên mạnh 

Một tên mạnh là một chuỗi các ký tựhexa mang thuộc tính là duy nhất trong toàn 

cầu (globally unique). Ngoài ra chuỗi đó còn được mã hóa bằng thuật toán khóa 

công khai 

1

đểbảo đảm rằng assembly không bịthay đổi vô tình hay cốý. 

Đểtạo ra tên mạnh, một cặp khóa công khai-bí mật được tạo ra cho assembly. Một 

mã băm (hash code) được tạo ra từtên, nội dung của các tập tin bên trong assembly 

và chuỗi biểu diễn khóa công khai. Sau đó mã băm này được mã hóa bằng khóa bí 

mật, kết quảmã hóa được ghi vào manifest. Quá trình trên được gọi là ký xác nhận 

vào assembly (signing the assembly). 

Khi assembly được CLR nạp vào bộnhớ, CLR sẽdùng khóa công khai trong 

manifest giải mã mã băm đểxác định xem assembly có bịthay đổi không. 

17.8.4 Global Assembly Cache (GAC) 

Sau khi đã tạo tên mạnh và ghi vào assembly, việc còn lại đểthực hiện chia sẻ

assembly là đặt assembly đó vào thưmục GAC. Đó là một thưmục đặc biệt dùng 

đểchứa assembly chia sẻ. Trên Windows, đó là thưmục \WinNT\assembly. 

1

Mã hóa khóa công khai – bí mật: đó là một thuật toán mã hóa đặc biệt, đầu tiên dùng một 

thuật toán riêng tạo ra 2 khóa, một khóa phổbiến rộng rãi nên gọi là khóa công khai, khóa còn lại 

do chủnhân của nó cất nơi an toàn nên gọi là bí mật. Sau đó dùng thuật toán mã hóa đểmã hóa dữ

liệu. Một khi dữliệu bịmã hóa bằng một khóa thì dữliệu đó chỉcó thể được giải mã bằng khóa kia 

và ngược lại. 

Attributes và Reflection   Gvhd: Nguyễn Tấn Trần Minh Khang

200 

Chương 18 Attributes và Reflection 

Xin được nhắc lại rằng một ứng dụng .NET bao gồm mã chương trình, dữliệu, 

metadata. Metadata chính là thông tin vềdữliệu mà ứng dụng sửdụng nhưkiểu dữ

liệu, mã thực thi, assembly,… 

Attributes là cơchế đểtạo ra metadata. Ví dụnhưchỉthịcho trình biên dịch, những 

dữliệu khác liên quan đến dữliệu, phương thức, lớp, … 

Reflection là quá trình một ứng dụng đọc lại metadata của chính nó đểcó cách thể

hiện, ứng xửthích hợp cho từng người dùng. 

18.1 Attributes 

Một attribute là một đối tượng, trong đối tượng đó chứa một mẩu dữliệu, mà lập 

trình viên muốn đính kèm với một phần tử(element) nào đó trong ứng dụng. Phần 

tử(element) mà lập trình viên muốn đính kèm attribute gọi là mục tiêu (target) của 

attribute. Ví dụattribute: 

[NoIDispatch] 

được đính kèm với một lớp hay một giao diện đểnói rằng lớp đích (target class) nên 

được thừa kếtừgiao diện IUnknown chứkhông phải thừa kếtừIDispatch. 

18.2 Attribute mặc định (intrinsic attributes) 

Có 2 loại attribute: 

• Attribute mặc định: là attribute được CLR cung cấp sẵn. 

• Attribute do lập trình viên định nghĩa (custom attribute) 

18.2.1 Đích của Attribute 

Mỗi attribute chỉ ảng hưởng đến một đích (target) mà nó khai báo. Đích có thểlà 

lớp, giao diện, phương thức … Bảng sau liệt kê tất cảcác đích 

Bảng 18-1 Các đích của attribute 

Loại  Ý nghĩa 

All Áp dụng cho tất cà các loại bên dưới 

Assembly Áp dụng cho chính assembly 

Class Áp dụng cho một thểhiện của lớp 

ClassMembers Áp dụng cho các loại từsau hàng này trở đi 

Attributes và Reflection   Gvhd: Nguyễn Tấn Trần Minh Khang

201 

Constructor Áp dụng với hàm dựng 

Delegate Áp dụng cho delegate 

Enum Áp dụng cho kiểu liệt kê 

Event Áp dụng cho sựkiện 

Field Áp dụng cho biến thành viên (tĩnh lẫn không tĩnh) 

Interface Áp dụng cho giao diện 

Method Áp dụng cho phương thức 

Module Áp dụng cho module 

Parameter Áp dụng cho tham số

Property Áp dụng cho property 

ReturnValue Áp dụng cho trịtrảvề

Struct Áp dụng cho cấu trúc 

18.2.2 Áp dụng Attribute 

Lập trình viên áp dụng attribute lên mục tiêu bằng cách đặt attribute trong ngoặc 

vuông [] liền trước mục tiêu. Ví dụattribute “Assembly” được áp dụng: 

[assembly: AssemblyDelaySign(false)] 

[assembly: AssemblyKeyFile(“keyfile.snk”)] 

Cách sau cũng tương đương với cách trên: 

[assembly: AssemblyDelaySign(false), assembly: 

AssemblyKeyFile(“keyfile.snk”)] 

Attribute thường dùng trong lập trình C# là “Serializable”

[serializable] 

class MySerClass 

Attribute trên báo cho compiler biết rằng lớp MySerClasscần được bảo đảm trong 

việc ghi nội dung, trạng thát xuống dĩa từhay truyền qua mạng. 

18.3 Attribute do lập trình viên tạo ra 

Lập trình viên hoàn toàn tựdo trong việc tạo ra các attribute riêng và đem sửdụng 

chúng vào nơi nào cảm thấy thích hợp. 

18.3.1 Khai báo Attribute tựtạo 

Đầu tiên là thừa kếmột lớp từlớp System.Attribute: 

Public class XYZ : System.Attribute 

Sau đó là báo cho compiler biết attribute này có thể đem áp dụng lên mục tiêu nào. 

[AttributeUsage(AttributeTargets.Class | 

AttributeTargets.Constructor | 

AttributeTargets.Field | 

AttributeTargets.Method | 

AttributeTargets.Property, 

AllowMultiple = true)] 

Attributes và Reflection   Gvhd: Nguyễn Tấn Trần Minh Khang

202 

Attribute “AttributeUsage”trên có mục tiêu áp dụng là Attribute khác: gọi là 

meta-attribute. 

18.3.2 Đặt tên một attribute 

Lập trình viên hoàn toàn tựdo trong việc đặt tên cho attribute. Tuy nhiên, compiler 

của .NET còn có thêm khảnăng tựnối thêm chuỗi “Attribute” vào tên. Điều đó có 

nghĩa là nếu lập trình viên định nghĩa một attribute có tên là “MyBugFix”thì khi 

tìm kiếm hoặc truy xuất attribute trên, lập trình viên có thểviết tên attribute: 

“MyBugFix”hoặc  “MyBugFixAttribute”.Nếu một attribute có tên là 

“MyBugFixAttribute”thì lập trình viên cũng có thểghi tên attribute là 

“MyBugFix”hoặc “MyBugFixAttribute”. 

Ví dụ: 

[MyBugFix(123, "Jesse Liberty", "01/01/05",)] 

18.3.3 Khởi tạo Attribute 

Mỗi attribute phải có ít nhất một contructor. Attribute nhận 2 kiểu đối số: kiểu vịtrí 

(positional) và kiểu tên (named). 

Trong ví dụ MyBugFix ởphần trước, phần tên và ngày tháng là kiểu vịtrí, phần ghi 

chú (comment) là kiểu tên. 

Các đối sốkiểu vịtrí phải được truyền vào contructor đúng theo thứtựkhai báo. Ví 

dụ: 

public BugFixAttribute(int bugID, string programmer, 

string date) 

 this.bugID = bugID; 

 this.programmer = programmer; 

 this.date = date; 

Đối sốkiểu tên thì được cài đặt nhưlà properties: 

public string Comment 

 get { return comment; } 

 set { comment = value; } 

18.3.4 Sửdụng Attribute 

Một khi đã định nghĩa attribute, lập trình viên sửdụng nó bằng cách đặt nó ngay 

trước mục tiêu (target) của nó. Ví dụ: 

[BugFixAttribute(121,"Jesse Liberty","01/03/05")] 

BugFixAttribute(107,"Jesse Liberty","01/04/05", 

Comment="Fixed off by one errors")] 

public class MyMath 

Ví dụtrên áp dụng attribute MyBugFix vào lớp MyMath. 

Attributes và Reflection   Gvhd: Nguyễn Tấn Trần Minh Khang

203 

18.4 Reflection 

Đểcho việc lưu attribute trong metadata có ích, cần phải có cơchếtruy xuất chúng 

vào lúc chạy. Các lớp trong vùng tên (namespace) Reflection, cùng với các lớp 

trong System.Type và System.TypeReference, cung cấp sựhỗtrợtruy xuất 

metadata. 

Reflection là một khái niệm chung cho bất kỳthao tác nào trong các thao tác sau 

đây: 

• Xem xét metadata 

• Tìm hiểu kiểu dữliệu (type discovery): lớp, interface, phương thức, đối sốcủa 

phương thức, properties, event, field, … 

• Nối kết trễcác phương thức và properties (late binding to methods and 

properties) 

• Tạo ra một kiểu dữliệu mới ngay trong lúc thực thi. Lập trình viên có thể định 

nghĩa một assembly mới ngay lúc chạy, có thểlưu xuống dĩa từ đểdùng lại. 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

204 

Chương 19 Marshaling và Remoting 

Ngày nay, các ứng dụng không còn đơn thuần chỉgồm một module, khi thực thi thì 

chỉcần chạy trong một process mà là một tập hợp nhiều thành phần (component) 

phức tạp. Các thành phần đó không chỉphân cách với nhau bằng ranh giới giữa các 

process mà còn có thểphân cách với nhau qua ranh giới máy - mạng - máy. 

Tiến trình di chuyểnmột đối tượng vượt qua một ranh giới (process, máy, …) được 

gọi là Remoting. 

Tiến trình chuẩn bị đểmột đối tượng thực hiện remoting được gọi là Marshaling. 

Giảsử đối tượng A nằm trên máy X muốn sửdụng dịch vụcủa đối tượng B nằm 

trên máy Y. Đểphục vụ đối tượng A, đối tượng B chuyển cho A một đối tượng

proxy. Những yêu cầu từ đối tượng A sẽ được proxy chuyển vềcho B, những kết 

quảtrảlời của B được gởi đến proxy, proxy sẽgởi lại cho đối tượng A. Giữa đối 

tượng A và đối tượng B có nhiều đối tượng sink, công việc của các đối tượng sink 

là áp đặt an ninh lên kênh liên lạc giữa 2 đối tượng. Các thông điệp được chuyển tải 

giữa A và B trên một đối tượng channel. Đối tượng channel lại yêu cầu sựgiúp đỡ

của đối tượng formatter. Công việc của formatter là định dạng lại thông điệp để2 

phía có thểhiểu nhau (ví dụchuyển mã hóa endian của dãy byte). 

19.1 Miền Ứng Dụng (Application Domains) 

Theo lý thuyết, một process là một ứng dụng đang thực thi (đang chạy). Mỗi một 

application thực thi trong một process riêng của nó. Nếu trên máy hiện có Word, 

Excel, Visual Studio thì tương ứng trên máy đang có 3 process. 

Bên trong mỗi process, .NET chia nhỏra thành các phần nhỏhơn gọi là miền ứng 

dụng (Application Domains viết tắt là app domains). Có thểxem mỗi miền ứng 

dụng là một process “nhẹcân”, miền ứng dụng hành xửy nhưlà một process nhưng 

điểm khác biệt là nó sửdụng ít tài nguyên hơn process. 

Các miền ứng dụng trong một process có thểkhởi động (started) hay bịtreo (halted) 

độc lập với nhau. Miền ứng dụng cung cấp khảnăng chịu lỗi (fault tolerance); nếu 

khởi động một đối tượng trong một miền ứng dụng khác với miền ứng dụng chính 

và đối tượng vừa khởi động gây lỗi, nó chỉlàm crash miền ứng dụng của nó chứ

không làm crash toàn bộ ứng dụng. 

Mỗi process lúc bắt đầu thực thi có một miền ứng dụng ban đầu (initial app domain) 

và có thểtạo thêm nhiều miền ứng dụng khác nếu lập trình viên muốn. Thông 

thường, ứng dụng chỉcần một miền ứng dụng là đủ. Tuy nhiên, trong những ứng 

dụng lớn cần sửdụng những thưviện do người khác viết mà thưviện đó không 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

205 

được tin cậy lắm thì cần tạo ra một miền ứng dụng khác dùng đểchứa thưviện 

không tin cập đó, tách thưviện đó khỏi miền ứng dụng chính đểcô lập lỗi, nếu lỗi 

xảy ra thì không làm crash ứng dụng. 

Miền ứng dụng khác với thread. Một thread luôn chạy bên trong một miền ứng 

dụng. Trong một miền ứng dụng có thểtồn tại nhiều thread. 

19.1.1 Marshaling vượt qua biên miền ứng dụng 

Marshaling là quá trình chuẩn bịmột đối tượng đểdi chuyển qua một ranh giới nào 

đó. Marshaling có thể được tiến hành theo 2 cách: bằng giá trị(by value) và bằng 

tham chiếu (by reference). 

• Khi một đối tượng được marshaling bằng giá trị, một bản sao của đối tượng 

được tạo ra và truyền đến nơi nhận. Những thay đổi trên bản sao này không 

làm thay đổi đối tượng gốc ban đầu. 

• Khi một đối tượng được marshaling bằng tham chiếu, một đối tượng đặc biệt 

gọi là proxy được gởi đến nơi nhận. Những thay đổi, những lời gọi hàm trên 

đối tượng proxy sẽ được chuyển vềcho đối tượng ban đầu xửlý. 

19.1.1.1 Tìm hiểu marshaling và proxy 

Khi marshaling đối tượng bằng reference, .NET CLR cung cấp cho đối tượng đang 

thực hiện lời gọi từxa một đối tượng proxy “trong suốt” (transparent proxy - TP). 

Công việc của đối tượng TP là nhận tất cảnhững thông tin gì liên quan đến việc gọi 

hàm (giá trịtrảvề, thông sốnhập, …) từtrong stack rồi đóng gói vào một đối tượng 

riêng mà đối tượng đó đã cài đặt giao diện IMessage. Sau đó đối tượng IMessage 

đó được trao cho đối tượng RealProxy. 

RealProxy là một lớp cơsởtrừu tượng (abstract base class) mà từ đó các đối tượng 

proxy thừa kế. Lập trình viên có thểtựtạo ra các đối tượng proxy mới thừa kếtừ

RealProxy. Đối tượng proxy mặc định của hệthống sẽtrao IMessage cho một chuỗi 

các đối tượng sink. Sốlượng các sink phụthuộc vào sốlượng chính sách bảo an 

(policy) mà nhà quản trịmuốn duy trì, tuy nhiên đối tượng sink cuối cùng là đối 

tượng đặt IMessage vào Channel. 

Channel được chia ra thành channel phía clientvà channel phía server, công việc 

chính của channel là di chuyển thông điệp (message) vượt qua một ranh giới 

(boundary). Channel chịu trách nhiệm tìm hiểu nghi thức truyền thông (transport 

protocol). Định dạng thật sựcủa thông điệp di chuyển qua ranh giới được quản lý 

bởi formatter. Khung ứng dụng (framework) .NET cung cấp 2 formatter: 

• Simple Object Access Protocol(SOAP) dùng cho HTTP channel 

• Binary dùng cho TCP/IP channel 

Lập trình viên cũng có thểtạo đối tượng formatter riêng và nếu muốn cũng có thể

tạo ra channel riêng. 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

206 

Một khi message vượt qua ranh giới, nó được nhận bởi channel và formatter phía 

server. Channel phía server sẽtái tạo lại đối tượng IMessage, sau đó channel phía 

server trao đối tượng IMessage cho một hay nhiều đối tượng sink phía server. Đối 

tượng sink cuối cùng trong chuỗi sink là một đối tượng StackBuilder, công việc 

của StackBuilder là nhận IMessage rồi tái tạo lại stack frame đểcó thểthực hiện lời 

gọi hàm. 

19.1.1.2 Chỉ định phương pháp Marshaling 

Một đối tượng bình thường hoàn toàn không có khảnăng marshaling. 

Đểmarshaling một đối tượng bằng giá trị(by value), chỉcần đánh dấu attribute 

Serializable lúc định nghĩa đối tượng. 

[Serializable] 

public class Point 

Đểmarshaling một đối tượng bằng tham chiếu (by reference), đối tượng đó cần 

thừa kếtừ MarshalByRefObject. 

public class Shape : MarshalByRefObject 

19.2 Context 

Miền ứng dụng (app domain) đến lượt nó lại được chia ra thành các context. 

Context có thểxem nhưmột ranh giới mà các đối tượng bên trong context có cùng 

quy tắc sửdụng (usage rules). Các quy tắc sửdụng như: đồng bộhóa giao dịch 

(synchronization transaction), … 

19.2.1 Đối tượng loại Context-Bound và Context-Agile 

Các đối tượng có thểlà Context-Bound hoặc Context-Agile. 

Nếu các đối tượng là Context-Bound, chúng tồn tại trong một context riêng, khi 

giao tiếp lẫn nhau, các thông điệp của chúng được marshaling đểvượt qua biên 

context. 

Nếu các đối tượng là Context-Agile, chúng hoạt động bên trong context của đối 

tượng yêu cầu (calling). Do đó, khi một đối tượng A triệu gọi phương thức của đối 

tượng B, phương thức của B được thực thi bên trong context của A. Vì vậy việc 

marshaling là không cần thiết. 

Giảsử đối tượng A cần giao tiếp với cơsởdữliệu, giảsử đối tượng A có thiết lập 

vềgiao dịch (transaction). Do đó A cần tạo một context. Tất cảcác phương thức của 

A sẽ được thực thi trong context của transaction. 

Giảsửcó một đối tượng B khác thuộc loại context-agile. Giảsửrằng đối tượng A 

trao một tham chiếu cơsởdữliệu cho đối tượng B và triệu gọi một phương thức X 

của B. Lại giảsửrằng phương thức X của B mà A triệu gọi lại gọi một phương thức 

Y khác của A. Bởi vì B thuộc loại context-agile do đó phương thức X đó của B 

được thực thi trong context của đối tượng A. Vì context của A có sựbảo vệcủa giao 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

207 

dịch nên nếu A có roll-back cơsởdữliệu thì những thay đổi mà phương thức X của 

B lên cơsởdữliệu cũng sẽ được roll-back. 

Giảsử đối tượng C triệu gọi một phương thức Z của B, phương thức Z có thực hiện 

thay đổi cơsởdữliệu, do B thuộc loại Context-Agile nên Z được thực thi trong 

context của C. Vì vậy những thay đổi mà Z thực hiện lên cơsởdữliệu sẽkhông thể

được roll-back. 

Nếu B thuộc loại Context-Bound, khi A tạo ra B, context của B sẽ được thừa kếtừ

context của A. Khi C triệu gọi phương thức Z của B, lời triệu gọi đó được 

marshaling tửcontext của C đến context của B rồi được thực hiện trong context của 

B (thừa kếtừA). Do đó phương thức Z thực thi trong sựbảo vệcủa transaction. 

Những thay đổi mà Z thực hiện lên cơsởdữliệu có thể được rooll-back. 

Một đối tượng có thểcó 3 lựa chọn vềContext: 

• Context-Agile 

• Context-Bound không chỉ định attribute. Thực hiện bằng cách thừa kếtừ

ContextBoundObject. Phương thức của đối tượng thuộc loại này thực thi trong 

Context thừa kếtừContext của đối tượng tạo ra nó 

• Context-Bound có chỉ định attribute Context. Các phương thức hoạt động 

trong Context do nó tạo riêng. 

19.2.2 Marshaling vượt qua ranh giới Context 

Khi truy cập đối tượng Context-Agile trong cùng miền ứng dụng thì không cần 

proxy. Khi một đối tượng A trong một context truy cập một đối tượng ContextBound B trong một context khác, đối tượng A đó truy cập đối tượng B thông qua 

proxy của B. 

Đối tượng được marshaling khác nhau vượt qua ranh giới context phụthuộc vào 

cách nó được tạo ra: 

• Đối tượng bình thường không có marshaling; bên trong miền ứng dụng các đối 

tượng thuộc loại context-agile 

• Đối tượng có đánh dấu attribute Serializable thì thuộc loại context-agile và 

được marshaling bởi giá trị(by value) vượt qua ranh giới miền ứng dụng 

• Đối tượng thừa kếtừMarshalByRefObject thì thuộc loại context-agile và 

được marshaling bởi tham chiếu (by reference) vượt qua ranh giới miền ứng 

dụng 

• Đối tượng thừa kếtừContextBoundObject được marshaling bởi tham chiếu 

vượt qua ranh gới miền ứng dụng và ranh giới context 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

208 

19.3 Remoting 

Cùng với việc marshaling đối tượng vượt qua ranh giới context và miền ứng dụng, 

đối tượng có thể được marshaling vượt qua ranh giới process và thậm chí qua ranh 

giới máy - mạng - máy. Khi một đối tượng được marshaling vượt qua ranh giới 

process hay máy - mạng – máy, nó được gọi là Remoted. 

19.3.1 Tìm hiểu các kiểu dữliệu phía Server 

Có 2 kiểu đối tượng phía server phục vụcho việc Remotingtrong .NET: nổi tiếng 

(well-known)và client kích hoạt (client activated). Kênh liên lạc với đối tượng 

nổi tiếng được thiết lập mỗi khi client gởi thông điệp (message). Kênh liên lạc đó 

không được giữthường trực nhưtrong trường hợp của đối tượng client kích hoạt. 

Đối tượng nổi tiếng chia thành 2 loại nhỏ: singletonvà single-call. 

• Đối tượng nổi tiếng kiểu singleton: tất cảcác thông điệp từclient gởi đến đối 

tượng được phân phối (dispatch) cho một đối tượng đang chạy trên server. 

Đối tượng này được tạo ra khi server khởi động và nằm chờtrên server để

phục vụcho bất kỳclient nào. Vì vậy đối tượng loại này phải có contructor 

không tham số. 

• Đối tượng nổi tiếng kiểu single-call: mỗi thông điệp mới từclient gởi đi được 

giải quyết bởi một đối tượng mới. Mô hình này thường dùng đểcân bằng tải 

hệthống. 

Đối tượng client kích hoạt thường được sửdụng bởi các lập trình viên có công việc 

chính là tạo ra các server riêng phục vụcho việc lập trình, đối tượng client kích hoạt 

duy trì kết nối với client cho đến khi nào toàn bộyêu cầu của client được đáp ứng. 

19.3.2 Mô tảmột server bằng Interface 

Sau đây là ví dụxây dựng một lớp máy tính (calculator) với 4 chức năng. 

Tạo một tập tin ICalc.cs với nội dung 

namespace Programming_CSharp 

using System; 

public interface ICalc 

 double Add(double x, double y); 

 double Sub(double x, double y); 

 double Mult(double x, double y); 

 double Div(double x, double y); 

Tạo một project mới kiểu C# Class Library, mởmenu Build, ra lệnh Build. Kết quả

là một tập tin Icalc.DLL 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

209 

19.3.3 Xây dựng một Server 

Tạo một project kiểu C# Console Application, tạo một tập tin mới CalcServer.cs, 

sau đó ra lệnh Build. 

Lớp Calculator thừa kếtừMarshalByRefObect nên nó sẽtrao cho client một proxy 

khi được triệu gọi. 

public class Calculator : MarshalByRefObject, ICalc 

Công việc đầu tiên là tạo channel và đăng ký, sửdụng HTTPChannel cung cấp bởi 

.NET: 

HTTPChannel chan = new HTTPChannel(65100); 

Đăng ký channel với .NET Channel Services: 

ChannelServices.RegisterChannel(chan); 

Đăng ký đối tượng (cần cung cấp endpointcho hàm đăng ký; endpoint chính là tên 

liên kết với đối tượng) 

Type calcType = Type.GetType("Programming_CSharp.Calculator"); 

Đăng ký kiểu Singleton 

RemotingConfiguration.RegisterWellKnownServiceType 

 ( calcType, "theEndPoint",WellKnownObjectMode.Singleton ); 

Sau đây là toàn bộnội dung: 

using System; 

using System.Runtime.Remoting; 

using System.Runtime.Remoting.Channels; 

using System.Runtime.Remoting.Channels.Http; 

namespace Programming_CSharp 

 // implement the calculator class 

 public class Calculator : MarshalByRefObject, ICalc 

public Calculator( ) 

Console.WriteLine("Calculator constructor"); 

  // implement the four functions 

  public double Add(double x, double y) 

   Console.WriteLine("Add {0} + {1}", x, y); 

return x+y; 

  public double Sub(double x, double y) 

   Console.WriteLine("Sub {0} - {1}", x, y); 

return x-y; 

  public double Mult(double x, double y) 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

210 

   Console.WriteLine("Mult {0} * {1}", x, y); 

return x*y; 

  public double Div(double x, double y) 

   Console.WriteLine("Div {0} / {1}", x, y); 

return x/y; 

 public class ServerTest 

  public static void Main( ) 

// tạo một channel và đăng ký nó 

   HttpChannel chan = new HttpChannel(65100); 

ChannelServices.RegisterChannel(chan); 

Type calcType = 

Type.GetType("Programming_CSharp.Calculator"); 

   // register our well-known type and tell the server 

   // to connect the type to the endpoint "theEndPoint" 

RemotingConfiguration.RegisterWellKnownServiceType 

( calcType, "theEndPoint",  

WellKnownObjectMode.Singleton ); 

   // "They also serve who only stand and wait."); (Milton) 

Console.WriteLine("Press [enter] to exit..."); 

Console.ReadLine( ); 

19.3.4 Xây dựng Client 

Client cũng phải đăng ký channel, tuy nhiên client không lắng nghe trên channel đó 

nên client sửdụng channel 0 (zero) 

HTTPChannel chan = new HTTPChannel(0); 

ChannelServices.RegisterChannel(chan); 

Client kết nối với dịch vụtừxa, trao cho hàm kết nối kiểu đối tượng mà nó cần 

cộng với URI của lớp cài đặt dịch vụ

MarshalByRefObject obj = RemotingServices.Connect( 

typeof(Programming_CSharp.ICalc), 

"http://localhost:65100/theEndPoint"); 

Vì đối tượng từxa có thểkhông sẵn sàng (mạng đứt, máy chứa đối tượng không tồn 

tại,…) nên đểgọi hàm cần khối thử

try 

 Programming_CSharp.ICalc calc = obj as 

Programming_CSharp.ICalc; 

 double sum = calc.Add(3,4); 

Bây giờclient đã có đối tượng proxy của đối tượng Calculator. Sau đây là toàn bộ

mã nguồn 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

211 

namespaceProgramming_CSharp 

usingSystem; 

usingSystem.Runtime.Remoting; 

usingSystem.Runtime.Remoting.Channels; 

usingSystem.Runtime.Remoting.Channels.Http; 

public classCalcClient 

public static voidMain( ) 

int[] myIntArray = new int[3]; 

Console.WriteLine("Watson, come here I need you..."); 

// create an Http channel and register it 

// uses port 0 to indicate won't be listening 

HttpChannel chan = newHttpChannel(0); 

ChannelServices.RegisterChannel(chan); 

// get my object from across the http channel 

MarshalByRefObject obj = 

(MarshalByRefObject) RemotingServices.Connect 

(typeof(Programming_CSharp.ICalc), 

"http://localhost:65100/theEndPoint"); 

try 

// cast the object to our interface 

Programming_CSharp.ICalc calc = 

obj asProgramming_CSharp.ICalc; 

// use the interface to call methods 

doublesum = calc.Add(3.0,4.0); 

doubledifference = calc.Sub(3,4); 

doubleproduct = calc.Mult(3,4); 

doublequotient = calc.Div(3,4); 

// print the results 

Console.WriteLine("3+4 = {0}", sum); 

Console.WriteLine("3-4 = {0}", difference); 

Console.WriteLine("3*4 = {0}", product); 

Console.WriteLine("3/4 = {0}", quotient); 

catch( System.Exception ex ) 

Console.WriteLine("Exception caught: "); 

Console.WriteLine(ex.Message); 

Kết xuất phía client 

Watson, come here I need you... 

3+4 = 7 

3-4 = -1 

3*4 = 12 

3/4 = 0.75 

Kết xuất phía server: 

Calculator constructor 

Press [enter] to exit... 

Add 3 + 4 

Sub 3 - 4 

Mult 3 * 4 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

212 

Div 3 / 4 

19.3.5 Sửdụng SingleCall 

Đểthấy sựkhác biệt giữa Singleton và Single Call, thay đổi một dòng trong mã 

nguồn Calculator.cs 

RemotingServices. RegisterWellKnownServiceType( 

"CalcServerApp","Programming_CSharp.Calculator", 

"theEndPoint",WellKnownObjectMode.Singleton ); 

thành 

RemotingServices. RegisterWellKnownServiceType( 

"CalcServerApp","Programming_CSharp.Calculator", 

"theEndPoint",WellKnownObjectMode.SingleCall );

Và đây là kết quảkết xuất phía server 

Calculator constructor 

Press [enter] to exit... 

Calculator constructor 

Add 3 + 4 

Calculator constructor 

Sub 3 - 4 

Calculator constructor 

Mult 3 * 4 

Calculator constructor 

Div 3 / 4 

19.3.6 Tìm hiểu RegisterWellKnownServiceType 

Khi gọi phương thức RegisterWellKnownServiceType, điều gì đã xảy ra ? 

Xin nhớlại rằng bạn đã tạo một đối tượng Typecho lớp Calculator

Type.GetType("Programming_CSharp.Calculator"); 

Sau đó bạn gọi RegisterWellKnownServiceType(), trao cho phương thức đó đối 

tượng Type, endpoint và Singleton. Điều đó báo cho CLR biết phải tạo một thểhiện 

của Calculator và liên kết endpoint với thểhiện đó. 

Bạn có thểtựlàm lại quá trình đó bằng cách thay đổi hàm Main() nhưsau: 

Ví dụ19-1 Manually instantiating and associating Calculator with an endpoint 

public static voidMain( ) 

// create a channel and register it 

HttpChannel chan = newHttpChannel(65100); 

ChannelServices.RegisterChannel(chan); 

// make your own instance and call Marshal directly 

 Calculator calculator = newCalculator( ); 

RemotingServices.Marshal(calculator,"theEndPoint"); 

// "They also serve who only stand and wait."); (Milton) 

 Console.WriteLine("Press [enter] to exit..."); 

Console.ReadLine( ); 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

213 

19.3.7 Tìm hiểu EndPoint 

Chuyện gì xảy ra khi bạn đăng ký endpoint ? Rõ ràng là server liên kết endpoint với 

đối tượng bạn vừa tạo, và khi client kết nối, server sửdụng chỉmục trong một bảng 

đểcó thểtrảvề đối tượng proxy tương ứng với đối tượng yêu cầu. 

Bạn có thểkhông cung cấp endpoint, thay vào đó bạn ghi các thông tin về đối tượng 

Calculator rồi gởi vềclient. Client “phục chế” lại đối tượng proxy đểcó thểdùng 

liên lạc với Calculator trên server. 

Đểcó thểhiểu làm cách nào bạn có thểgọi một đối tượng mà không biết endpoint, 

hãy thay đổi hàm Main() một lần nữa, thay vì gọi Marshal()với một endpoint, hãy 

trao một đối tượng: 

ObjRef objRef = RemotingServices.Marshal(calculator) 

Hàm Marshal() trảvềmột đối tượng ObjRef. Đối tượng ObjRef chứa tất cảthông 

tin cần thiết đểkích hoạt và liên lạc với đối tượng ởxa. Khi bạn cung cấp một 

endpoint, server tạo một bảng và liên kết endpoint với một ObjRef đểkhi client có 

yêu cầu, server có thểtạo một proxy cung cấp cho client. ObjRef chứa tất cảthông 

tin cần thiết cho client đểclient có thểtạo một proxy. ObjRef có khảnăng 

Serializable. 

Mởmột stream tập tin, tạo một SOAP formatter, bạn có thểserialize đối tượng 

ObjRef thành một tập tin bằng cách gọi hàm Serialize() của formatter, sau đó đưa 

cho formatter tham chiếu đến stream tập tin và tham chiếu của ObjRef, vậy là bạn 

đã có tất cảthông tin cần thiết đểtạo một proxy dưới dạng một tập tin 

Ví dụ19-2 Marshaling an object without a well-known endpoint 

public static voidMain( ) 

// create a channel and register it 

HttpChannel chan = newHttpChannel(65100); 

ChannelServices.RegisterChannel(chan); 

// make your own instance and call Marshal directly 

 Calculator calculator = newCalculator( ); 

 ObjRef objRef = RemotingServices.Marshal(calculator); 

 FileStream fileStream = 

newFileStream("calculatorSoap.txt",FileMode.Create); 

 SoapFormatter soapFormatter = newSoapFormatter( ); 

soapFormatter.Serialize(fileStream,objRef); 

fileStream.Close( ); 

// "They also serve who only stand and wait."); (Milton) 

Console.WriteLine( 

 "Exported to CalculatorSoap.txt. Press ENTER to exit..."); 

Console.ReadLine( ); 

Bạn hãy trao tập tin chứa đối tượng đã serialize đó cho client. Đểclient có thểtái 

tạo lại đối tượng, client cần tạo một channel và đăng ký nó. 

FileStream fileStream = new FileStream ("calculatorSoap.txt", 

FileMode.Open); 

Marshaling và Remoting   Gvhd: Nguyễn Tấn Trần Minh Khang

214 

Sau đó tạo một thểhiện của đối tượng SoapFormatterrồi gọi hàm DeSerialize() 

của formatter đểnhận lại đối tượng ObjRef 

SoapFormatter soapFormatter = 

new SoapFormatter ( ); 

try 

 ObjRef objRef = 

(ObjRef) soapFormatter.Deserialize (fileStream); 

Tiến hành gỡbỏmarshall, nhận lại ICalc 

ICalc calc = (ICalc) RemotingServices.Unmarshal(objRef); 

Bây giờclient có thểtriệu gọi phương thức trên server thông qua ICalc. 

Ví dụ19-3 Replacement of Main( ) from Example 19-4 (the client) 

public static voidMain( ) 

int[] myIntArray = new int[3]; 

 Console.WriteLine("Watson, come here I need you..."); 

// create an Http channel and register it 

// uses port 0 to indicate you won't be listening 

HttpChannel chan = newHttpChannel(0); 

ChannelServices.RegisterChannel(chan); 

 FileStream fileStream = 

newFileStream ("calculatorSoap.txt", FileMode.Open); 

 SoapFormatter soapFormatter = 

newSoapFormatter ( ); 

try 

ObjRef objRef = 

(ObjRef) soapFormatter.Deserialize (fileStream); 

ICalc calc = 

(ICalc) RemotingServices.Unmarshal(objRef); 

// use the interface to call methods 

doublesum = calc.Add(3.0,4.0); 

doubledifference = calc.Sub(3,4); 

doubleproduct = calc.Mult(3,4); 

doublequotient = calc.Div(3,4); 

// print the results 

  Console.WriteLine("3+4 = {0}", sum); 

  Console.WriteLine("3-4 = {0}", difference); 

  Console.WriteLine("3*4 = {0}", product); 

  Console.WriteLine("3/4 = {0}", quotient); 

catch( System.Exception ex ) 

Console.WriteLine("Exception caught: "); 

Console.WriteLine(ex.Message); 

Thread và Sự Đồng BộGvhd: Nguyễn Tấn Trần Minh Khang

215 

Chương 20 Thread và Sự Đồng Bộ

Thread là một process “nhẹcân” cung cấp khảnăng multitasking trong một ứng 

dụng. Vùng tên System.Threading cung cấp nhiều lớp và giao diện đểhỗtrợlập 

trình nhiều thread. 

20.1 Thread 

Thread thường được tạo ra khi bạn muốn làm đồng thời 2 việc trong cùng một 

thời điểm. Giảsử ứng dụng của bạn đang tiến hành đọc vào bộnhớmột tập tin có 

kích thước khoảng 500MB, trong lúc đang đọc thì dĩnhiên ứng dụng không thể đáp 

ứng yêu cầu xửlý giao diện. Giảsửngười dùng muốn ngưng giữa chừng, không 

cho ứng dụng đọc tiếp tập tin lớn đó nữa, do đó cần một thread khác đểxửlý giao 

diện, lúc này khi người dùng ấn nút Stop thì ứng dụng đáp ứng được yêu cầu trong 

khi thread ban đầu vẫn đang đọc tập tin. 

20.1.1 Tạo Thread 

Cách đơn giản nhất là tạo một thểhiện của lớp Thread. Contructor của lớp Thread 

nhận một tham sốkiểu delegate. CLR cung cấp lớp delegate ThreadStart nhằm mục 

đích chỉ đến phương thức mà bạn muốn thread mới thực thi. Khai báo delegate 

ThreadStart nhưsau: 

public delegate void ThreadStart( ); 

Phương thức mà bạn muốn gán vào delegate phải không chứa tham sốvà phải trảvề

kiểu void. Sau đây là ví dụ: 

Thread myThread = new Thread( new ThreadStart(myFunc) ); 

myFunc phải là phương thức không tham sốvà trảvềkiểu void. 

Xin lưu ý là đối tượng Thread mới tạo sẽ khôngtựthực thi (execute), để đối tượng 

thực thi, bạn càn gọi phương thức Start() của nó. 

Thread t1 = new Thread( new ThreadStart(Incrementer) ); 

Thread t2 = new Thread( new ThreadStart(Decrementer) ); 

t1.Start( ); 

t2.Start( ); 

Thread sẽchấm dứt khi hàm mà nó thực thi trởvề(return). 

20.1.2 Gia nhập Thread 

Hiện tượng thread A ngưng chạy và chờcho thread B chạy xong được gọi là thread 

A gia nhập thread B. 

Đểthread 1 gia nhập thread 2: 

Thread và Sự Đồng BộGvhd: Nguyễn Tấn Trần Minh Khang

216 

t2.Join( ); 

Nếu câu lệnh trên được thực hiện bởi thread 1, thread 1 sẽdừng lại và chờcho đến 

khi thread 2 kết thúc. 

20.1.3 Treo thread lại (suspend thread) 

Nếu bạn muốn treo thread đang thực thi lại một khoảng thời gian thì bạn sửdụng 

hàm Sleep() của đối tượng Thread. Ví dụ đểthread ngưng khoảng 1 giây: 

Thread.Sleep(1000); 

Câu lệnh trên báo cho bộ điều phối thread (của hệ điều hành) biết bạn không muốn

bộ điều phối thread phân phối thời gian CPU cho thread thực thi câu lệnh trên trong 

thời gian 1 giây. 

20.1.4 Giết một Thread (Kill thread) 

Thông thường thread sẽchấm dứt khi hàm mà nó thực thi trởvề. Tuy nhiên bạn có 

thểyêu cầu một thread “tựtử” bằng cách gọi hàm Interrupt()của nó. Điều này sẽ

làm cho exception ThreadInterruptedException được ném ra. Thread bịyêu cầu 

“tựtử” có thểbắt exception này đểtiến hành dọn dẹp tài nguyên. 

catch (ThreadInterruptedException) 

 Console.WriteLine("[{0}] Interrupted! Cleaning up...", 

Thread.CurrentThread.Name); 

20.2 Đồng bộhóa (Synchronization) 

Khi bạn cần bảo vệmột tài nguyên, trong một lúc chỉcho phép một thread thay đổi 

hoặc sửdụng tài nguyên đó, bạn cần đồng bộhóa. 

Đồng bộhóa được cung cấp bởi một khóa trên đối tượng đó, khóa đó sẽngăn cản 

thread thứ2 truy cập vào đối tượng nếu thread thứnhất chưa trảquyền truy cập đối 

tượng. 

Sau đây là ví dụ cầnsự đồng bộhóa. Giảsử2 thread sẽtiến hành tăng tuần tự 1 

đơn vịmột biến tên là counter. 

int counter = 0; 

Hàm làm thay đổi giá trịcủa Counter: 

public void Incrementer( ) 

try 

  while (counter < 1000) 

int temp = counter; 

temp++; // increment 

  // simulate some work in this method 

Thread.Sleep(1); 

  // assign the Incremented value 

Thread và Sự Đồng BộGvhd: Nguyễn Tấn Trần Minh Khang

217 

  // to the counter variable 

  // and display the results 

counter = temp; 

Console.WriteLine( 

  "Thread {0}. Incrementer: {1}", 

Thread.CurrentThread.Name, 

counter); 

Vấn đề ởchỗthread 1 đọc giá trịcounter vào biến tạm rồi tăng giá trịbiến tạm, 

trước khi thread 1 ghi giá trịmới từbiến tạm trởlại counter thì thread 2 lại đọc giá 

trịcounter ra biến tạm của thread 2. Sau khi thread 1 ghi giá trịvừa tăng 1 đơn vị

trởlại counter thì thread 2 lại ghi trởlại counter giá trịmới bằng với giá trịmà 

thread 1 vừa ghi. Nhưvậy sau 2 lần truy cập giá trịcủa biến counter chỉtăng 1 đơn 

vịtrong khi yêu cầu là phải tăng 2 đơn vị. 

20.2.1 Sửdụng Interlocked 

CLR cung cấp một sốcơchế đồng bộtừcơchế đơn giản Critical Section(gọi là 

Locks trong .NET) đến phức tạp như Monitor. 

Tăng và giảm giá trịlàm một nhu cầu phổbiến, do đó C# cung cấp một lớp đặc biệt 

Interlockednhằm đáp ứng nhu cầu trên. Interlocked có 2 phương thức Increment()

và Decrement()nhằm tăng và giảm giá trị trong sựbảo vệcủa cơchế đồng bộ. Ví 

dụ ởphần trước có thểsửa lại nhưsau: 

public void Incrementer( ) 

try 

  while (counter < 1000) 

Interlocked.Increment(ref counter); 

   // simulate some work in this method 

Thread.Sleep(1); 

// assign the decremented value 

// and display the results 

Console.WriteLine( 

"Thread {0}. Incrementer: {1}", 

Thread.CurrentThread.Name, 

counter); 

Khối catch và finally không thay đổi so với ví dụtrước. 

20.2.2 Sửdụng Locks 

Lock đánh dấu một đoạn mã “gay cấn” (critical section) trong chương trình của bạn, 

cung cấp cơchế đồng bộcho khối mã mà lock có hiệu lực. 

Thread và Sự Đồng BộGvhd: Nguyễn Tấn Trần Minh Khang

218 

C# cung cấp sựhỗtrợcho lock bằng từchốt (keyword) lock. Lock được gỡbỏkhi 

hết khối lệnh. Ví dụ: 

public void Incrementer( ) 

try 

  while (counter < 1000) 

lock (this) 

{ // lock bắt đầu có hiệu lực

int temp = counter; 

temp ++; 

Thread.Sleep(1); 

counter = temp; 

} // lock hết hiệu lực -> bịgỡbỏ

// assign the decremented value 

// and display the results 

Console.WriteLine( "Thread {0}. Incrementer: {1}", 

Thread.CurrentThread.Name, counter); 

Khối catch và finally không thay đổi so với ví dụtrước. 

20.2.3 Sửdụng Monitor 

Đểcó thể đồng bộhóa phức tạp hơn cho tài nguyên, bạn cần sửdụng monitor. Một 

monitor cho bạn khảnăng quyết định khi nào thì bắt đầu, khi nào thì kết thúc đồng 

bộvà khảnăng chờ đợi một khối mã nào đó của chương trình “tựdo”. 

Khi cần bắt đầu đồng bộhóa, trao đối tượng cần đồng bộcho hàm sau: 

Monitor.Enter(đối tượng X); 

Nếu monitor không sẵn dùng (unavailable), đối tượng bảo vệbởi monitor đang 

được sửdụng. Bạn có thểlàm việc khác trong khi chờ đợi monitor sẵn dùng 

(available) hoặc treo thread lại cho đến khi có monitor (bằng cách gọi hàm Wait()) 

Ví dụbạn đang download và in một bài báo từWeb. Đểhiệu quảbạn cần tiến hành 

in sau hậu trường (background), tuy nhiên bạn cần chắc chắn rằng 10 trang đã được 

download trước khi bạn tiến hành in. 

Thread in ấn sẽchờ đợi cho đến khi thread download báo hiệu rằng sốlượng trang 

download đã đủ. Bạn không muốn gia nhập (join) với thread download vì sốlượng 

trang có thểlên đến vài trăm. Bạn muốn chờcho đến khi ít nhất 10 trang đã được 

download. 

Đểgiảlập việc này, bạn thiết lập 2 hàm đếm dùng chung 1 biến counter. Một hàm 

đếm tăng 1 tương ứng với thread download, một hàm đếm giảm 1 tương ứng với 

thread in ấn. 

Trong hàm làm giảm bạn gọi phương thức Enter(), sau đó kiểm tra giá trịcounter, 

nếu < 5 thì gọi hàm Wait() 

Thread và Sự Đồng BộGvhd: Nguyễn Tấn Trần Minh Khang

219 

if (counter < 5) 

Monitor.Wait(this); 

Lời gọi Wait() giải phóng monitor nhưng bạn đã báo cho CLR biết là bạn muốn lấy 

lại monitor ngay sau khi monitor được tựdo một lần nữa. Thread thực thi phương 

thức Wait() sẽbịtreo lại. Các thread đang treo vì chờ đợi monitor sẽtiếp tục chạy 

khi thread đang thực thi gọi hàm Pulse(). 

Monitor.Pulse(this); 

Pulse() báo hiệu cho CLR rằng có sựthay đổi trong trạng thái monitor có thểdẫn 

đến việc giải phóng (tiếp tục chạy) một thread đang trong tình trạng chờ đợi. 

Khi thread hoàn tất việc sửdụng monitor, nó gọi hàm Exit() đểtrảmonitor. 

Monitor.Exit(this); 

Source code ví dụ: 

namespaceProgramming_CSharp 

usingSystem; 

usingSystem.Threading; 

classTester 

static voidMain( ) 

// make an instance of this class 

Tester t = newTester( ); 

// run outside static Main 

t.DoTest( ); 

public voidDoTest( ) 

// create an array of unnamed threads 

Thread[] myThreads = { 

newThread( newThreadStart(Decrementer) ), 

newThread( newThreadStart(Incrementer) ) }; 

// start each thread 

intctr = 1; 

foreach(Thread myThread inmyThreads) 

myThread.IsBackground=true; 

myThread.Start( ); 

    myThread.Name = "Thread" + ctr.ToString( ); 

ctr++; 

Console.WriteLine("Started thread {0}",myThread.Name); 

Thread.Sleep(50); 

// wait for all threads to end before continuing 

foreach(Thread myThread inmyThreads) 

myThread.Join( ); 

// after all threads end, print a message 

   Console.WriteLine("All my threads are done."); 

Thread và Sự Đồng BộGvhd: Nguyễn Tấn Trần Minh Khang

220 

voidDecrementer( ) 

try 

// synchronize this area of code 

Monitor.Enter(this); 

// if counter is not yet 10 

// then free the monitor to other waiting 

// threads, but wait in line for your turn 

if(counter < 10) 

Console.WriteLine( 

     "[{0}] In Decrementer. Counter: {1}. Gotta Wait!", 

Thread.CurrentThread.Name, counter); 

Monitor.Wait(this); 

while(counter >0) 

longtemp = counter; 

temp--; 

Thread.Sleep(1); 

counter = temp; 

Console.WriteLine("[{0}] In Decrementer. Counter: {1}.", 

Thread.CurrentThread.Name, counter); 

finally 

Monitor.Exit(this); 

voidIncrementer( ) 

try 

Monitor.Enter(this); 

while(counter < 10) 

longtemp = counter; 

temp++; 

Thread.Sleep(1); 

counter = temp; 

Console.WriteLine("[{0}] In Incrementer. Counter: {1}", 

Thread.CurrentThread.Name, counter); 

// I'm done incrementing for now, let another 

// thread have the Monitor 

Monitor.Pulse(this); 

finally 

Console.WriteLine("[{0}] Exiting...", 

Thread.CurrentThread.Name); 

Monitor.Exit(this); 

private longcounter = 0; 

Thread và Sự Đồng BộGvhd: Nguyễn Tấn Trần Minh Khang

221 

Kết quả: 

Started thread Thread1 

[Thread1] In Decrementer. Counter: 0. Gotta Wait! 

Started thread Thread2 

[Thread2] In Incrementer. Counter: 1 

[Thread2] In Incrementer. Counter: 2 

[Thread2] In Incrementer. Counter: 3 

[Thread2] In Incrementer. Counter: 4 

[Thread2] In Incrementer. Counter: 5 

[Thread2] In Incrementer. Counter: 6 

[Thread2] In Incrementer. Counter: 7 

[Thread2] In Incrementer. Counter: 8 

[Thread2] In Incrementer. Counter: 9 

[Thread2] In Incrementer. Counter: 10 

[Thread2] Exiting... 

[Thread1] In Decrementer. Counter: 9. 

[Thread1] In Decrementer. Counter: 8. 

[Thread1] In Decrementer. Counter: 7. 

[Thread1] In Decrementer. Counter: 6. 

[Thread1] In Decrementer. Counter: 5. 

[Thread1] In Decrementer. Counter: 4. 

[Thread1] In Decrementer. Counter: 3. 

[Thread1] In Decrementer. Counter: 2. 

[Thread1] In Decrementer. Counter: 1. 

[Thread1] In Decrementer. Counter: 0. 

All my threads are done. 

20.3 Race condition và DeadLock 

Đồng bộhóa thread khá rắc rối trong những chương trình phức tạp. Bạn cần phải 

cẩn thận kiểm tra và giải quyết các vấn đềliên quan đến đồng bộhóa thread: race 

condition và deadlock 

20.3.1 Race condition 

Một điều kiện tranh đua xảy ra khi sự đúng đắn của ứng dụng phụthuộcvào thứtự

hoàn thành không kiểm soát được của 2 thread độc lậpvới nhau. 

Ví dụ: giảsửbạn có 2 thread. Thread 1 tiến hành mởtập tin, thread 2 tiến hành ghi 

lên cùng tập tin đó. Điều quan trọng là bạn cần phải điều khiển thread 2 sao cho nó 

chỉtiến hành công việc sau khi thread 1 đã tiến hành xong. Nếu không, thread 1 sẽ

không mở được tập tin vì tập tin đó đã bịthread 2 mở đểghi. Kết quảlà chương 

trình sẽném ra exception hoặc tệhơn nữa là crash. 

Đểgiải quyết vấn đềtrong ví dụtrên, bạn có thểtiến hành join thread 2 với thread 1 

hoặc thiết lập monitor. 

20.3.2 Deadlock 

Giảsửthread A đã nắm monitor của tài nguyên X và đang chờmonitor của tài 

nguyên Y. Trong khi đó thì thread B lại nắm monitor của tài nguyên Y và chờ 

Thread và Sự Đồng BộGvhd: Nguyễn Tấn Trần Minh Khang

222 

monitor của tài nguyên X. 2 thread cứchờ đợi lẫn nhau mà không thread nào có thể

thoát ra khỏi tình trạng chờ đợi. Tình trạng trên gọi là deadlock. 

Trong một chương trình nhiều thread, deadlock rất khó phát hiện và gỡlỗi. Một 

hướng dẫn đểtránh deadlock đó là giải phóng tất cảlock đang sởhữu nếu tất cảcác 

lock cần nhận không thểnhận hết được. Một hướng dẫn khác đó là giữlock càng ít 

càng tốt. 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

223 

Chương 21 Luồng dữliệu. 

Khi muốn đọc hay ghi dữliệu vào/ra tập tin hay muốn truyền dữliệu từmáy này 

sang máy khác, ta phải tổchức dữliệu theo cấu trúc tuần tựcác byte hay các gói tin 

…. Điều này dễliên tưởng dữliệu nhưlà các luồng dữliệu chảy từtừnguồn đến 

đích. 

Thưviện .NET Framework cung cấp các lớp Stream(Streamvà các lớp thừa kế

từnó) đểchương trình có thểsửdụng trong các thao tác nhập xuất dữliệu như

doc/ghi tập tin, truyền dữliệu qua mạng … 

21.1 Tập tin và thưmục 

Các lớp đềcập trong chương này thuộc vềvùng tên System.IO. Các lớp này bao 

gồm lớp Filemô phỏng cho một tập tin trên đĩa, và lớp Directorymô phỏng 

cho một thưmục trên đĩa. 

21.1.1 Làm việc với thưmục 

Lớp Directorycó nhiều phương thức dành cho việc tạo, di chuyển, duyệt thư

mục. Các phương thức trong lớp Directory đều là phương thức tĩnh;vì vậy không 

cần phải tạo một thểhiện lớp Directorymà có thểtruy xuất trực tiếp từtên lớp. 

Lớp DirectoryInfolà lớp tương tựnhưlớp Directory. Nó cung các tất cảcác 

phương thức mà lớp Directorycó đồng thời bổsung nhiều phương thức hữu ích 

hơn cho việc duyệt cấu trúc cây thưmục. Lớp DirectoryInfothừa kếtừlớp 

FileSystemInfo, và vì vậy cũng thừa kếlớp MarshalByRefObj. Lớp 

DirectoryInfokhông có phương thức tĩnh, vì vậy cần tạo một thểhiện lớp trước 

khi sửdụng các phương thức. 

Có một khác biệt quan trong giữa Directoryvà DirectoryInfolà các phương 

thức của lớp Directorysẽ được kiểm tra vềbảo mật mỗi khi được gọi trong khi 

đối tượng DirectoryInfochỉkiểm tra một lần vào lúc khởi tạo, các phương thức 

vì vậy sẽthực hiện nhanh hơn. 

Dưới đây là bảng liệt kê các phương thức quan trọng của hai lớp 

Bảng 21-1 Các phương thức lớp Directory 

Phưong thức  Giải thích 

CreateDirectory()  Tạo tất cảcác thưmục và thưmục con trong đường dẫn tham số. 

Delete()Xoá thưmục và các nội dung của nó. 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

224 

Exists( ) 

Trảvềkết quảkiểu logic, đúng nếu đường dẫn đến thưmục tồn tại (có 

nghĩa là thưmục tồn tại). 

GetCreationTime( ) 

SetCreationTime( ) 

Lấy/thiết đặt ngày giờtạo thưmục 

GetCurrentDirectory( ) 

SetCurrentDirectory( ) 

Lấy/thiết đặt thưmục hiện hành 

GetDirectories( )  Lấy vềmột mảng các thưmục con một thưmục 

GetDirectoryRoot( )  Trảvềthưmục gốc của đường dẫn 

GetFiles( )  Trảvềmảng chuỗi tên các tập tin chứa trong một thưmục 

GetLastAccessTime( ) 

SetLastAccessTime( ) 

Lầy/thiết đặt ngày giờlần truy cập cuối cùng đến thưmục 

GetLastWriteTime( ) 

SetLastWriteTime( ) 

Lầy/thiết đặt ngày giờlần chỉnh sửa cuối cùng lên thưmục 

GetLogicalDrives( )  Trảvềtên của tất cảcác ổ đĩa logic theo định dạng <ổ_đĩa>:\ 

GetParent() Trảvềthưmục cha của một đường dẫn. 

Move() Di chuyển thưmục (cảnội dung) đến một vịtrí khác. 

Bảng 21-2 Các phương thức/property lớp DirectoryInfo 

Phưong thức/property  Ý nghĩa 

Attributes Thừa kếtừFileSystemInfo, lấy/thiết đặt thuộc tính của tập tin hiện hành. 

CreationTime Thừa kếtừFileSystemInfo, lấy/thiết đặt thời gian tạo tập tin 

Exists Trảvề đúng nếu thưmục tồn tại 

Extension Thừa kếtừFileSystemInfo, phần mởrộng tập tin 

FullName Thừa kếtừFileSystemInfo, đường dẫn đầy đủcủa tập tin hay thưmục 

LastAccessTime Thừa kếtừFileSystemInfo, ngày giờtruy cập cuối cùng 

LastWriteTime Thừa kếtừFileSystemInfo, ngày giờchỉnh sửa cuối cùng 

Name Tên thưmục 

Parent Lấy thưmục cha 

Root Lấy thưmục gốc của đường dẫn. 

Create( )  Tạo một thưmục 

CreateSubdirectory() Tạo một hoặc nhiều thưmục con 

Delete( )  Xóa một thưmục và nội dung của nó 

GetDirectories( )  Trảvềdanh sách các thưmục con của thưhiện hiện có 

GetFiles( )  Lấy danh mục các tập tin của thưmục 

GetFileSystemInfos() Nhận vềmảng các đối tượng FileSystemInfo 

MoveTo( )  Di chuyển DirectoryInfo và nội dung của nó sang đường dẫn khác 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

225 

Phưong thức/property  Ý nghĩa 

Refresh( )  Làm tươi trạng thái đối tượng 

21.1.2 Tạo đối tượng DirectoryInfo 

Đểduyệt cấu trúc cây thưmục, ta cần tạo một thểhiện của lớp DirectoryInfo. 

Lớp DirectoryInfokhông chỉcung cấp phương thức lấy vềtên các tập tin và thư

mục con chứa trong một thưmục mà còn cho phép lấy vềcác đối tượng FileInfo

và DirectoryInfo, cho phép ta thực hiện việc quản lý các cấu trúc cây thưmục, 

hay thực hiện các thao tác đệqui. 

Khởi tạo một đối tượng DirectoryInfobằng tên của thưmục muốn tham chiếu. 

DirectoryInfo dir = new DirectoryInfo(@"C:\winNT"); 

Ta có thểthực hiện các phương thức đã liệt kê ởbảng trên. Dưới đây là đoạn mã 

nguồn ví dụ. 

Ví dụ21-1. Duyệt các thưmục con 

usingSystem; 

usingSystem.IO; 

namespaceProgramming_CSharp 

classTester 

public static voidMain() 

Tester t = newTester( ); 

// một một thưmục 

stringtheDirectory = @"c:\WinNT"; 

// duyệt thưmục và hiển thịngày truy cập gần nhất 

// và tất cảcác thưmục con 

DirectoryInfo dir = newDirectoryInfo(theDirectory); 

t.ExploreDirectory(dir); 

// hoàn tất. in ra sốlượng thống kê 

Console.WriteLine( "

{0} directories found.

", 

  dirCounter);  } 

// với mỗi thưmục tìm thấy, nó gọi chính nó 

private voidExploreDirectory(DirectoryInfo dir) 

indentLevel++; // cấp độthưmục 

// định dạng cho việc trình bày 

for(inti = 0; i < indentLevel; i++) 

Console.Write(" "); // hai khoảng trắng cho mỗi cấp 

// in thưmục và ngày truy cập gần nhất 

Console.WriteLine("[{0}] {1} [{2}]

", 

indentLevel, dir.Name, dir.LastAccessTime); 

// lấy tất cảthưmục con của thưmục hiện tại 

// đệquy từng thưmục 

   DirectoryInfo[] directories = dir.GetDirectories( ); 

foreach(DirectoryInfo newDir indirectories) 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

226 

dirCounter++; //tăng biến đếm 

ExploreDirectory(newDir); 

indentLevel--; // giảm cấp độthưmục 

// các biến thành viên tĩnh cho việc thống kê và trình bày 

static intdirCounter = 1; 

static intindentLevel = -1; // so first push = 0 

Kết quả(một phần): 

 [2] logiscan [5/1/2001 3:06:41 PM] 

 [2] miitwain [5/1/2001 3:06:41 PM] 

[1] Web [5/1/2001 3:06:41 PM] 

 [2] printers [5/1/2001 3:06:41 PM] 

  [3] images [5/1/2001 3:06:41 PM] 

 [2] Wallpaper [5/1/2001 3:06:41 PM] 

363 directories found. 

Chương trình tạo một đối tượng DirectoryInfogắn với thưmục WinNT. Sau đó 

gọi hàm ExploreDirectoryvới tham sốlà đối tượng DirectoryInfovừa tạo. 

Hàm sẽhiển thịcác thông tin vềthưmục này và sau đó lấy tất cảcác thưmục con. 

Đểliệt kê danh sách các thưmục con, hàm gọi phương thức GetDirectories. 

Phương thức này trảvềmảng các đối tượng DirectoryInfo. Bằng cách gọi đệ

qui chính nó, hàm liệt kê xuống các thưmục con và thưmục con của thưmục con 

… Kết quảcuối cùng là cấu trúc cây thưmục được hiển thị. 

21.1.3 Làm việc với tập tin. 

Đối tượng DirectoryInfocũng trảvềdanh sách các đối tượng FileInfo là các tập 

tin chứa trong thưmục. Các đối tượng này mô tảthông tin vềtập tin. Thưviện 

.NET cũng cung cấp hai lớp Filevà FileInfotương tựnhưvới trường hợp thư

mục. Lớp Filechỉcó các phương thức tĩnh và lớp FileInfothì không có phương 

thức tĩnh nào cả. 

Hai bảng dưới đây liệt kê các phương thức cũa hai lớp này 

Bảng 21-3 Các phương thức lớp File 

Phương thức  Giải thích 

AppendText() Tạo một StreamWriter cho phép thêm văn bản vào tập tin 

Copy() Sao chép một tập tin từtập tin đã có 

Create() Tạo một tập tin mới 

CreateText() Tạo một StreamWriter cho phép viết mới văn bản vào tập tin 

Delete() Xoá một tập tin 

Exists() Trảvề đúng nếu tập tin tồn tại 

GetAttributes() Lấy/ thiết đặt các thuộc tính của một tập tin 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

227 

Phương thức  Giải thích 

SetAttributes() 

GetCreationTime() 

SetCreationTime() 

Lấy / thiết đặt thời gian tạo tập tin 

GetLastAccessTime() 

SetLastAccessTime() 

Lấy / thiết đặt thời gian truy cập tập tin lần cuối 

GetLastWriteTime() 

SetLastWriteTime() 

Lấy / thiết đặt thời gian chỉnh sửa tập tin lần cuối 

Move() Di chuyển tập tin đến vịtrí mới, có thểdùng để đổi tên tập tin 

OpenRead() Mởmột tập tin để đọc (không ghi) 

OpenWrite() Mởmột tập tin cho phép ghi. 

Bảng 21-4 Các phương thức / property lớp FileInfo 

Phương thức / property  Giải thích 

Attributes() Thừa kếtừFileSystemInfo. Lấy/thiết đặt thuộc tính tập tin 

CreationTime Thừa kếtừFileSystemInfo. Lấy/thiết đặt thời gian tạo tập tin 

Directory Lấy thưmục cha 

Exists Xác định tập tin có tồn tại chưa? 

Extension Thừa kếtừFileSystemInfo. Phần mởrộng của tập tin 

FullName Thừa kếtừFileSystemInfo. Đường dẫn đầy đủcủa tập tin 

LastAccessTime Thừa kếtừFileSystemInfo. Thời điểm truy cập gần nhất 

LastWriteTime Thừa kếtừFileSystemInfo. Thời điểm ghi gần nhất. 

Length Kívh thước tập tin 

Name Tên tập tin 

AppendText() Tạo đối tượng StreamWriter đểghi thêm vào tập tin 

CopyTo()  Sao chép sang một tập tin mới 

Create() Tạo một tập tin mới 

Delete() Xóa tập tin 

MoveTo() Dịch chuyển tập tin, cũng dùng để đổi tên tập tin 

Open() Mởmột tập tin với các quyền hạn 

OpenRead() Tạo đối tượng FileStream cho việc đọc tập tin 

OpenText() Tạo đối tượng StreamReader cho việc đọc tập tin 

OpenWrite() Tạo đối tượng FileStream cho việc ghi tập tin 

Ví dụ21-2 sửa lại từví dụ12-1, thêm đoạn mã lấy FileInfocủa mỗi thưmục. 

Đối tượng này dùng đểhiển thịtên, kích thước và ngày truy cấp cuối cùng của tập 

tin. 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

228 

Ví dụ21-2. Duyệt tập tin và thưmục con 

usingSystem; 

usingSystem.IO; 

namespaceProgramming_CSharp 

classTester 

public static voidMain( ) 

Tester t = newTester( ); 

stringtheDirectory = @"c:\WinNT"; 

DirectoryInfo dir = newDirectoryInfo(theDirectory); 

t.ExploreDirectory(dir); 

Console.WriteLine( 

    "

{0} files in {1} directories found.

", 

fileCounter,dirCounter );

private voidExploreDirectory(DirectoryInfo dir) 

indentLevel++; 

for(inti = 0; i < indentLevel; i++) 

Console.Write(" ");

Console.WriteLine("[{0}] {1} [{2}]

", 

indentLevel, dir.Name, dir.LastAccessTime); 

// lấy tất cảcác tập tin trong thưmục và 

// in tên, ngày truy cập gần nhất, kích thước của chúng 

   FileInfo[] filesInDir = dir.GetFiles( ); 

foreach(FileInfo file infilesInDir) 

// lùi vào một khoảng phía dưới thưmục 

// phục vụviệc trình bày 

for(inti = 0; i < indentLevel+1; i++) 

Console.Write(" "); // hai khoảng trắng cho mỗi cấp 

    Console.WriteLine("{0} [{1}] Size: {2} bytes", 

file.Name, file.LastWriteTime,  file.Length); 

fileCounter++; 

   DirectoryInfo[] directories = dir.GetDirectories( ); 

foreach(DirectoryInfo newDir indirectories) 

dirCounter++;

ExploreDirectory(newDir); 

indentLevel--; 

// các biến tĩnh cho việc thống kê và trình bày 

static intdirCounter = 1; 

static intindentLevel = -1;

static intfileCounter = 0; 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

229 

Kết quả(một phần): 

[0] WinNT [5/1/2001 3:34:01 PM] 

 ActiveSetupLog.txt [4/20/2001 10:42:22 AM] Size: 10620 bytes 

 actsetup.log [4/20/2001 12:05:02 PM] Size: 8717 bytes 

 Blue Lace 16.bmp [12/6/1999 4:00:00 PM] Size: 1272 bytes 

 [2] Wallpaper [5/1/2001 3:14:32 PM] 

  Boiling Point.jpg [4/20/2001 8:30:24 AM] Size: 28871 bytes 

  Chateau.jpg [4/20/2001 8:30:24 AM] Size: 70605 bytes 

  Windows 2000.jpg [4/20/2001 8:30:24 AM] Size: 129831 bytes 

8590 files in 363 directories found. 

21.1.4 Chỉnh sửa tập tin 

Đối tượng FileInfocó thểdùng đểtạo, sao chép, đổi tên và xoá một tập tin. Ví 

dụdưới đậy tạo một thưmục con mới, sao chép một tập tin, đổi tên vài tập tin, và 

sau đó xóa toàn bộthưmục này. 

Ví dụ21-3. Tạo thưmục con và thao tác các tập tin 

usingSystem; 

usingSystem.IO; 

namespaceProgramming_CSharp 

classTester 

public static voidMain( ) 

Tester t = newTester( ); 

stringtheDirectory = @"c:\test\media"; 

DirectoryInfo dir = newDirectoryInfo(theDirectory); 

t.ExploreDirectory(dir); 

private voidExploreDirectory(DirectoryInfo dir) 

// tạo mới một thưmục con 

stringnewDirectory = "newTest"; 

DirectoryInfo newSubDir = 

dir.CreateSubdirectory(newDirectory); 

// lấy tất cảcác tập tin trong thưmục và 

// sao chép chúng sang thưmục mới 

   FileInfo[] filesInDir = dir.GetFiles( ); 

foreach(FileInfo file infilesInDir) 

stringfullName = newSubDir.FullName + 

"\\" + file.Name; 

file.CopyTo(fullName); 

Console.WriteLine("{0} copied to newTest", 

file.FullName); 

// lấy các tập tin vừa sao chép 

filesInDir = newSubDir.GetFiles( ); 

// hủy hoặc đổi tên một vài tập tin 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

230 

intcounter = 0; 

foreach(FileInfo file infilesInDir) 

stringfullName = file.FullName; 

if(counter++ %2 == 0) 

file.MoveTo(fullName + ".bak"); 

Console.WriteLine("{0} renamed to {1}", 

fullName,file.FullName); 

else 

file.Delete( ); 

Console.WriteLine("{0} deleted.", fullName); 

newSubDir.Delete(true); // hủy thưmục con này 

Kết quả(một phần): 

c:\test\media\Bach's Brandenburg Concerto No. 3.RMI 

copied to newTest 

c:\test\media\Beethoven's 5th Symphony.RMI copied to newTest 

c:\test\media\Beethoven's Fur Elise.RMI copied to newTest 

c:\test\media\canyon.mid copied to newTest 

c:\test\media

ewTest\Bach's Brandenburg Concerto 

No. 3.RMI renamed to 

c:\test\media

ewTest\Bach's Brandenburg Concerto 

No. 3.RMI.bak 

c:\test\media

ewTest\Beethoven's 5th Symphony.RMI deleted. 

c:\test\media

ewTest\Beethoven's Fur Elise.RMI renamed to 

c:\test\media

ewTest\Beethoven's Fur Elise.RMI.bak 

c:\test\media

ewTest\canyon.mid deleted. 

21.2 Đọc và ghi dữliệu 

Đọc và ghi dữliệu là nhiệm vụchính của các luồng, Stream. Streamhỗtrợcảhai 

cách đọc ghi đồng bộhay bất đồng bộ. .NET Framework cung cấp sẵn nhiều lớp 

thừa kếtừlớp  Stream, bao gồm FileStream,  MemoryStreamvà 

NetworkStream. Ngoài ra còn có lớp BufferedStreamcung cấp vùng đệm xuất 

nhập được dùng thêm với các luồng khác. Bảng dưới đây tóm tắt ý nghĩa sửdụng 

của các luồng 

Bảng 21-5 Ý nghĩa các luồng 

Lớp  Giải thích 

Stream Lớp trừu tượng cung cấp hỗtrợ đọc / ghi theo byte 

BinaryReader / 

BinaryWriter 

Đọc / ghi các kiểu dữliệu gốc (primitive data type) theo trịnhịphân 

File, FileInfo, Directory, 

DirectoryInfo 

Cung cấp các cài đặt cho lớp FileSystemInfo, bao gồm việc tạo, 

dịch chuyển, đổi tên, xoá tập tin hay thưmục 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

231 

FileStream 

Để đọc từ/ ghi lên tập tin. Mặc định mởtập tin đồng bộ, hỗtrợtruy 

cập tập tin bất đồng bộ. 

TextReader, TextWriter, 

StringReader, StringWriter 

TextReader và TextWriter là hai lớp trừu tượng được thiết kếcho 

việc xuất nhập ký tựUnicode.StringReader và StringWrite cài đặt 

hai lớp trên dành cho việc đọc ghi vào một chuỗi 

BufferedStream 

Luồng dùng đểlàm vùng đệm cho các luồng khác như

NetworkStream. Lớp FileStream tựcài đặt sẵn vùng đệm. Lớp này 

nhằm tăng cường hiệu năng cho luồng gắn với nó. 

MemoryStream 

Luồng dữliệu trực tiếp từbộnhớ. Thường được dùng nhưvùng 

đệm tạm. 

NetworkStream Luồng cho kết nối mạng. 

21.2.1 Tập tin nhịphân 

Phần này sẽbắt đầu sửdụng lớp cơsở Stream để đọc tập tin nhịphân. Lớp 

Streamcó rất nhiều phương thức nhưng quan trọng nhất là năm phương thức 

Read(), Write(), BeginRead(), BeginWrite()và Flush(). 

Đểthao tác tập tin nhịphân (hay đọc tập tin theo kiểu nhịphân), ta bắt đầu tạo một 

cặp đối tượng Stream, một để đọc, một đểviết. 

Stream inputStream = File.OpenRead(@"C:\test\source\test1.cs"); 

Stream outputStream = File.OpenWrite(@"C:\test\source\test1.bak"); 

Đểmởmột tập tin để đọc và viết, ta sửdụng hai hàm tĩnh OpenRead()và 

OpenWrite()của lớp Filevới tham sốlà đường dẫn tập tin. 

Tiếp theo ta đọc dữliệu từ inputStreamcho đến khi không còn dữliệu nữa và sẽ

ghi dữliệu đọc được vào outputStream. Hai hàm lớp Streamphục vụviệc đọc 

ghi dữliệu là Read()và Write().

while( (bytesRead = inputStream.Read(buffer,0,SIZE_BUFF)) > 0 ) 

outputStream.Write(buffer,0,bytesRead); 

Hai hàm có cùng một sốlương và kiểu tham sốtruyền vào. Đầu tiên là một mảng 

các byte(được gọi là vùng đệm buffer) dùng đểchứa dữliệu theo dang byte. 

Tham sốthứhai cho biết vịtrí bắt đầu đọc hay ghi trên vùng đệm, tham sốcuối 

cùng cho biết sốbyte cần đọc hay ghi. Đối với hàmRead()còn trảvềsốbyte mà 

Stream đọc được, có thểbằng hay khác giá trịtham sốthứba. 

Ví dụ21-4. Cài đặt việc đọc và ghi tập tin nhịphân 

usingSystem; 

usingSystem.IO; 

namespaceProgramming_CSharp 

classTester 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

232 

const intSizeBuff = 1024; 

public static voidMain( ) 

Tester t = newTester( ); 

t.Run( ); 

private voidRun( ) 

// đọc từtập tin này 

Stream inputStream = File.OpenRead( 

@"C:\test\source\test1.cs"); 

// ghi vào tập tin này 

Stream outputStream = File.OpenWrite( 

@"C:\test\source\test1.bak"); 

// tạo vùng đệm chứa dữliệu 

byte[] buffer = newByte[SizeBuff]; 

intbytesRead; 

// sau khi đọc dữliệu xuất chúng ra outputStream

while( (bytesRead = 

inputStream.Read(buffer,0,SizeBuff)) > 0 ) 

outputStream.Write(buffer,0,bytesRead); 

// đóng tập tin trước khi thoát 

inputStream.Close( ); 

outputStream.Close( ); 

Kết quảsau khi chay chương trình là một bản sao của tập tin đầu vào (test1.cs) được 

tạo trong cùng thưmục với tên test1.bak 

21.2.2 Luồng có vùng đệm 

Trong ví dụtrước ta thực hiện việc ghi lên tập tin theo từng khối buffer, nhưvậy 

hệ điều hành sẽthực thi việc ghi tập tin ngay sau lệnh Write(). Điều này có thể

làm giảm hiệu năng thực thi do phải chờcác thao tác cơhọc của đĩa cứng vốn rất 

chậm. 

Luồng có vùng đệm sẽkhắc phục nhược điểm này bằng cách sau: khi có lệnh 

Write() dữliệu, luồng sẽkhông gọi hệ điều hành ngay mà sẽgiữtrên vùng đệm 

(thực chất là bộnhớ), chờcho đến khi dữliệu đủlớn sẽghi một lượt lên tập tin. Lớp 

BufferedStreamlà cài đặt cho luồng có vùng đệm. 

Đểtạo một luồng có vùng đệm trước tiên ta vẫn tạo luồng Streamnhưtrên 

Stream inputStream = File.OpenRead(@"C:\test\source\folder3.cs"); 

Stream outputStream = File.OpenWrite(@"C:\test\source\folder3.bak"); 

Sau đó truyền các luồng này cho hàm dựng của BufferedStream

BufferedStream bufferedInput = new BufferedStream(inputStream); 

BufferedStream bufferedOutput = new BufferedStream(outputStream); 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

233 

Từ  đây ta sửdụng  bufferedInputvà  bufferedOutputthay cho 

inputStreamvà outputStream. Cách sửdụng là nhưnhau: cũng dùng phương 

thức Read()và Write()

while((bytesRead = bufferedInput.Read(buffer,0,SIZE_BUFF))>0 ) 

bufferedOutput.Write(buffer,0,bytesRead); 

Có một khác biệt duy nhất là phải nhớgọi hàm Flush() đểchắc chắn dữliệu đã 

được "tống" từvùng bufferlên tập tin. 

bufferedOutput.Flush( ); 

Lệnh này nhằm yêu cầu hệ điều hành sao chép dữliệu từvùng nhớ bufferlên đĩa 

cứng. 

Ví dụ21-5. Cài đặt luồng có vùng đệm 

usingSystem; 

usingSystem.IO; 

namespaceProgramming_CSharp 

classTester 

const intSizeBuff = 1024; 

public static voidMain( ) 

Tester t = newTester( ); 

t.Run( ); 

private voidRun( ) 

// tạo một luồng nhịphân 

Stream inputStream = File.OpenRead( 

@"C:\test\source\folder3.cs"); 

Stream outputStream = File.OpenWrite( 

@"C:\test\source\folder3.bak"); 

// tạo luồng vùng đệm kết buộc với luồng nhịphân

BufferedStream bufferedInput = 

newBufferedStream(inputStream); 

BufferedStream bufferedOutput = 

newBufferedStream(outputStream); 

byte[] buffer = newByte[SizeBuff]; 

intbytesRead; 

while( (bytesRead = 

bufferedInput.Read(buffer,0,SizeBuff)) > 0 ) 

bufferedOutput.Write(buffer,0,bytesRead); 

bufferedOutput.Flush( ); 

bufferedInput.Close( ); 

bufferedOutput.Close( ); 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

234 

Với tập tin có dung lượng lớn, chương trình này sẽchạy nhanh hơn chương trình ví 

dụtrước. 

21.2.3 Làm việc với tập tin văn bản 

Đối với các tập tin chỉchứa văn bản, ta sửdụng hai luồng StreamReadervà 

StreamWritercho việc đọc và ghi. Hai lớp này được thiết kế đểthao tác với văn 

bản dễdàng hơn. Ví dụnhưchúng cung cấp hàm ReadLine()và WriteLine()

để đọc và ghi một dòng văn bản. 

Đểtạo một thểhiện StreamReaderta gọi phương thức OpenText()của lớp 

FileInfo. 

FileInfotheSourceFile = 

new FileInfo (@"C:\test\source\test1.cs"); 

StreamReader stream = theSourceFile.OpenText( ); 

Ta đọc từng dòng văn bản của tập tin cho đến hết 

do 

 text = stream.ReadLine( ); 

} while (text != null); 

Đểtạo đối tượng StreamWriterta truyền cho hàm khởi dựng đường dẫn tập tin 

StreamWriter writer = new 

StreamWriter(@"C:\test\source\folder3.bak",false); 

tham sốthứhai thuộc kiểu bool, nếu tập tin đã tồn tại, giá trị truesẽghi dữliệu 

mới vào cuối tập tin, giá trị falsesẽxóa dữliệu cũ, dữliệu mới sẽghi đè dữliệu 

cũ. 

Ví dụ21-6. Đọc và ghi tập tin văn bản 

usingSystem; 

usingSystem.IO; 

namespaceProgramming_CSharp 

classTester 

public static voidMain( ) 

Tester t = newTester( ); 

t.Run( ); 

private voidRun( ) 

// mởmột tập tin 

FileInfo theSourceFile = newFileInfo( 

@"C:\test\source\test.cs"); 

// tạo luồng đọc văn bản cho tập tin 

   StreamReader reader = theSourceFile.OpenText( ); 

// tạo luồng ghi văn bản cho tập tin xuất 

StreamWriter writer = newStreamWriter( 

@"C:\test\source\test.bak",false); 

// tạo một biến chuỗi lưưgiữmột dòng văn bản 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

235 

stringtext; 

// đọc toàn bộtập tin theo từng dòng 

// ghi ra màn hình console và tập tin xuất 

do 

text = reader.ReadLine( ); 

writer.WriteLine(text); 

Console.WriteLine(text); 

} while(text != null); 

// đóng tập tin 

reader.Close( ); 

writer.Close( ); 

Khi thực thi chương trình nội dung tập tin nguồn được ghi lên tập tin mới đồng thời 

xuất ra màn hình console. 

21.3 Bất đồng bộnhập xuất 

Các ví dụ được trình bày ởtrên sửdụng kỹthuật đồng bộhóa trong nhập xuất dữ

liệu (synchronous I/O), có nghĩa là chương trình sẽtạm ngưng trong lúc hệ điều 

hành thực hiện việc đọc hay ghi dữliệu. Điều này có thểlàm chương trình tốn thời 

gian vô ích, đặc biệt khi làm việc với các ổ đĩa có tốc độchậm hay dung lượng 

đường truyền mạng thấp. 

Kỹthuật bất đồng bộnhập xuất (asynchronous I/O) được dùng đểgiải quyết vấn đề

này. Ta có thểthực hiện các công việc khác trong khi chờhệthống hập xuất đọc/ghi 

dữliệu. Kỹthuật này  được cài đặt trong phương thức BeginRead()và 

BeginWrite()của lớp Stream. 

Mấu chốt của phương thức Begin*()là khi được gọi một tiểu trình mới sẽ được 

tạo và làm công việc nhập xuất, tiểu trình cũsẽthực hiện công việc khác. Sau khi 

hoàn tất việc đọc/ghi, thông báo được gởi đến hàm callbackthông qua một 

deleagte. Ta có thểthao tác với các dữliệu vừa được đọc/ghi, thực hiện một công 

việc đọc/ghi khác và lại quay đi làm công việc khác. 

Phương thức BeginRead()yêu cầu năm tham số, ba tham sốtương tựhàm Read, 

hai tham số(tùy chọn) còn lại là:  delegate  AsyncCallback  đểgọi hàm 

callbackvà tham sốcòn lại là objectdùng đểphân biệt giữa các thao tác nhập 

xuất bất đồng bộkhác nhau. 

Trong ví dụdụnày ta sẽtạo một mảng  bytelàm vùng đệm, và một đối tượng 

Stream

public classAsynchIOTester 

privateStream inputStream; 

private byte[] buffer; 

const intBufferSize = 256; 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

236 

một biến thành viên kiểu delegatemà phương thức BeginRead()yêu cầu 

privateAsyncCallback myCallBack; // delegated method 

Delegate AsyncCallbackkhai báo trong vùng tên Systemnhưsau 

publicdelegate void AsyncCallback (IAsyncResult ar); 

Tạo một hàm callback để đóng gói trong delegate

voidOnCompletedRead(IAsyncResult asyncResult) 

Dưới đây là cách hoạt động của ví dụ. Trong hàm Main()ta khởi tạo và cho thực 

thi lớp kiểm thử AsyncIOTester

public static voidMain( ) 

 AsynchIOTester theApp = newAsynchIOTester( ); 

theApp.Run( ); 

Hàm dựng khởi tạo các biến thành viên 

AsynchIOTester( ) 

 inputStream = File.OpenRead(@"C:\test\source\AskTim.txt"); 

buffer = new byte[BufferSize]; 

myCallBack = newAsyncCallback(this.OnCompletedRead); 

Phương thức Run()sẽgọi BeginRead() 

inputStream.BeginRead( 

buffer, // chứa kết quả

0, // vịtrí bắt đâu 

buffer.Length, // kích thước vùng đệm 

myCallBack, // callback delegate 

null); // đối tượng trạng thái 

Sau đó thực hiện công việc khác, trường hợp này là vòng lặp forthực hiện 500.000 

lần. 

for(longi = 0; i < 500000; i++) 

if(i%1000 == 0) 

Console.WriteLine("i: {0}", i); 

Sau khi việc đọc hoàn tất hàm callback được gọi 

voidOnCompletedRead(IAsyncResult asyncResult) 

Điều đầu tiên là phải biết sốlượng byte thật sự đọc được bằng cách gọi hàm 

EndRead()

intbytesRead = inputStream.EndRead(asyncResult); 

Sau đó thao tác trên dữliệu đọc được (in ra console), và lại gọi tiếp một 

BeginRead() đểthực hiện nhập xuất bất đồng bộmột lần nữa, 

if(bytesRead > 0) 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

237 

strings =  Encoding.ASCII.GetString (buffer, 0, bytesRead); 

Console.WriteLine(s); 

inputStream.BeginRead(buffer, 0, buffer.Length, 

  myCallBack, null); 

Hiệu quảcủa chương trình là ta có thểthực hiện các công việc không cần kết quả

của việc đọc dữliệu khác. Ví dụhoàn chỉnh lệit kê dưới đây 

Ví dụ21-7. cài đặt nhập xuất bất đồng bộ

usingSystem; 

usingSystem.IO; 

usingSystem.Threading; 

usingSystem.Text; 

namespaceProgramming_CSharp 

public classAsynchIOTester 

privateStream inputStream; 

// delegated 

privateAsyncCallback myCallBack; 

// vùng nhớbuffer lưu giữliệu đọc được 

private byte[] buffer; 

// kích thước buffer 

const intBufferSize = 256; 

AsynchIOTester( ) 

// mởmột luồng nhập 

inputStream = File.OpenRead( 

 @"C:\test\source\AskTim.txt");    // cấp phát vùng buffer 

buffer = new byte[BufferSize]; 

// gán một hàm callback 

myCallBack = newAsyncCallback(this.OnCompletedRead); 

public static voidMain( ) 

AsynchIOTester theApp =  newAsynchIOTester(); 

theApp.Run( ); 

voidRun() 

inputStream.BeginRead( 

buffer, // chứa kết quả

0, // vịtrí bắt đầu trên buffer 

buffer.Length, // kích thước buffer 

myCallBack, // callback delegate 

null); // đối tượng trạng thái cục bộ

// làm chuyện gì đó trong lúc đọc dữliệu 

for(longi = 0; i < 500000; i++) 

if(i%1000 == 0) 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

238 

Console.WriteLine("i: {0}", i); 

// hàm callback 

voidOnCompletedRead(IAsyncResult asyncResult) 

intbytesRead = 

inputStream.EndRead(asyncResult); 

// nếu đọc có dữliệu 

if(bytesRead > 0) 

// chuyển nó thành chuỗi

String s = 

Encoding.ASCII.GetString(buffer, 0, bytesRead); 

Console.WriteLine(s); 

inputStream.BeginRead( 

buffer, 0, buffer.Length, myCallBack, null); 

Kết quả(một phần) 

i: 47000 

i: 48000 

i: 49000 

Date: January 2001 

From: Dave Heisler 

To: Ask Tim 

Subject: Questions About O'Reilly 

Dear Tim, 

I've been a programmer for about ten years. I had heard of 

O'Reilly books,then... 

Dave, 

You might be amazed at how many requests for help with 

school projects I get; 

i: 50000 

i: 51000 

i: 52000 

Trong các ứng dụng thực tế, ta sẽtương tác với người dùng hoặc thực hiện các tính 

toán trong khi công việc nhập xuất tập tin hay cơsởdữliệu được thực hiện một 

cách bất đồng bộ ởmột tiểu trình khác. 

21.4 Serialization 

Serialize có nghĩa là sắp theo thứtự. Khi ta muốn lưu một đối tượng xuống tập tin 

trên đĩa từ đểlưu trữ, ta phải định ra trình tựlưu trữcủa dữliệu trong đối tượng. 

Khi cần tái tạo lại đối tượng từthông tin trên tập tin đã lưu trữ, ta sẽ"nạp" đúng 

theo trình tự đã định trước đó. Đây gọi là quá trình Serialize. 

Nói chính xác hơn, serializelà tiến trình biến đổi trạng thái của đối tượng theo 

một định dạng có thể được lưu trữhay dịch chuyển (transfer). 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

239 

.NET Framework cung cấp 2 kỹthuất serialize: 

• Binary serialize (serialize nhịphân): cách này giữnguyên kiểu dữliệu, thích 

hợp cho việc giữnguyên cấu trúc đối tượng. Có thểdùng kỹthuật này đểchia 

sẻ đối tương giữa các ứng dụng bằng cách serialize vào vùng nhớclipboard; 

hoặc serialize vào các luồng, đĩa từ, bộnhớ, trên mạng …; hoặc truyền cho 

máy tính ởxa nhưmột tham trị("by-value object") 

• XML và SOAP Serialize: chỉserialize các thuộc tính public, và không giữ

nguyên kiểu dữliệu. Tuy nhiên XML và SOAP là các chuẩn mởnên kỹthuất 

không bịcác hạn chếvềgiao tiếp giữa các ứng dụng. 

Các đối tượng cớsở đều có khảnăng serialize. Để đối tượng của ta có thể

serialize, trước tiên cần thêm khai báo attribute [Serialize]cho lớp đối 

tượng đó. Nếu đối tượng có chứa các đối tượng khác thì các đối tượng đó phải có 

khảnăng serialize. 

21.4.1 Làm việc với Serialize 

Trước tiên, ta tạo một đối tượng Sumoflàm ví dụcho việc Serialize. Đối tượng 

có các biến thành viên sau: 

private intstartNumber = 1; 

private intendNumber; 

private int[] theSums; 

mảng theSums  đuợc mô tả: phần tử  theSum[i]chứa giá trịlà tổng từ

startNumbercho đến startNumber + i.

21.4.1.1 serialize đối tượng 

Trước tiên thêm attribute [Serialize]vào trước khai báo đối tượng 

[Serializable] 

classSumOf 

Ta cần một tập tin đểlưtrữ đối tượng này, tạo một FileStream

FileStream fileStream = newFileStream("DoSum.out",FileMode.Create); 

Sau khi tạo một Formatter, gọi phương thức Serializecủa nó. 

binaryFormatter.Serialize(fileStream,this); 

Đối tượng Sumof đã được Serialize. 

21.4.1.2 Deserialize đối tượng 

Deserializelà tiến trình ngược với serialize, tiến trình này đọc dữliệu được 

serialize đểtái tạo lại đối tượng. 

Khai báo phương thức tĩnh DeSerializecho tiến trình này 

public staticSumOf DeSerialize( ) 

 FileStream fileStream = newFileStream("DoSum.out",FileMode.Open); 

 BinaryFormatter binaryFormatter = newBinaryFormatter( ); 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

240 

return(SumOf) binaryFormatter.Deserialize(fileStream); 

fileStream.Close( ); 

Ví dụ21-1 Serialize và Deserialize đối tượng 

usingSystem; 

usingSystem.IO; 

usingSystem.Runtime.Serialization; 

usingSystem.Runtime.Serialization.Formatters.Binary; 

namespaceProgramming_CSharp 

[Serializable] 

classSumOf 

public static voidMain( ) 

   Console.WriteLine("Creating first one with new..."); 

SumOf app = newSumOf(1,10); 

Console.WriteLine("Creating second one with 

 deserialize...");  SumOf newInstance = SumOf.DeSerialize( ); 

newInstance.DisplaySums( ); 

publicSumOf(intstart, intend) 

startNumber = start; 

endNumber = end; 

ComputeSums( ); 

DisplaySums( ); 

Serialize( ); 

private voidComputeSums( ) 

intcount = endNumber - startNumber + 1; 

theSums = new int[count]; 

theSums[0] = startNumber; 

for(inti=1,j=startNumber + 1;i<count;i++,j++) 

    theSums[i] = j + theSums[i-1]; 

private voidDisplaySums( ) 

foreach(inti intheSums) 

Console.WriteLine("{0}, ",i); 

private voidSerialize( ) 

Console.Write("Serializing..."); 

// tạo một file stream để đọhay ghi 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

241 

FileStream fileStream = 

newFileStream("DoSum.out",FileMode.Create); 

//sửdung binary formatter 

BinaryFormatter binaryFormatter = 

newBinaryFormatter( ); 

// serialize 

binaryFormatter.Serialize(fileStream,this); 

Console.WriteLine("...completed"); 

fileStream.Close( ); 

public staticSumOf DeSerialize( ) 

FileStream fileStream = 

newFileStream("DoSum.out",FileMode.Open); 

BinaryFormatter binaryFormatter = 

newBinaryFormatter( ); 

return(SumOf) binaryFormatter.Deserialize(fileStream); 

fileStream.Close( ); 

private intstartNumber = 1; 

private intendNumber; 

private int[] theSums; 

Kết quả: 

Creating first one with new... 

1, 

3, 

6, 

10, 

15, 

21, 

28, 

36, 

45, 

55, 

Serializing......completed 

Creating second one with deserialize... 

1, 

3, 

6, 

10, 

15, 

21, 

28, 

36, 

45, 

55, 

21.4.2 Handling Transient Data 

Theo cách nhìn nào đó thì serializekiểu Ví dụ21-1 rất lãng phí. Giá trịcác 

phần tửtrong mảng có thểtính bằng thuật toán vì vậy không nhất thiết phải 

serializemảng này (và làm giảm đáng kểdung lượng tập tin lưu trữ). 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

242 

ĐểCLR biết ta không muốn Serializebiến thành viên này, ta đặt attribute 

[NonSerialize]trước khai báo: 

[NonSerialized] private int[] theSums; 

Theo logic, khi deserialize, ta không thểcó ngay mảng và vì vậy cần thực hiện 

lại công việc tính toán một lần nữa. Ta có thểthực hiện trong hàm Deserialize, 

nhưng CLR cung cấp giao diện IDeserializationCallback, ta sẽcài đặt giao 

diện này 

[Serializable] 

classSumOf : IDeserializationCallback 

Giao diện này có một phương thức duy nhất là OnDeserialization()mà ta 

phải cài đặt: 

public virtual void OnDeserialization (Object sender) 

ComputeSums( ); 

Khi tiến trình Deserialize, phương thức này sẽ được gọi và mảng theSums

được tính toán và khởi gán. Cái giá mà ta phải trảchính là thời gian dành cho việc 

tính toán này. 

Ví dụ21-2 Làm việc với dối tượng nonserialize 

usingSystem; 

usingSystem.IO; 

usingSystem.Runtime.Serialization; 

usingSystem.Runtime.Serialization.Formatters.Binary; 

namespaceProgramming_CSharp 

[Serializable] 

classSumOf : IDeserializationCallback 

public static voidMain( ) 

   Console.WriteLine("Creating first one with new..."); 

SumOf app = newSumOf(1,5); 

Console.WriteLine("Creating second one with 

   deserialize...");  SumOf newInstance = SumOf.DeSerialize( ); 

newInstance.DisplaySums( ); 

publicSumOf(intstart, intend) 

startNumber = start; 

endNumber = end; 

ComputeSums( ); 

DisplaySums( ); 

Serialize( ); 

private voidComputeSums( ) 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

243 

intcount = endNumber - startNumber + 1; 

theSums = new int[count]; 

theSums[0] = startNumber; 

for(inti=1,j=startNumber + 1;i<count;i++,j++) 

    theSums[i] = j + theSums[i-1]; 

private voidDisplaySums( ) 

foreach(inti intheSums) 

Console.WriteLine("{0}, ",i); 

private voidSerialize( ) 

Console.Write("Serializing..."); 

FileStream fileStream = 

newFileStream("DoSum.out",FileMode.Create); 

BinaryFormatter binaryFormatter = newBinaryFormatter(); 

binaryFormatter.Serialize(fileStream,this); 

Console.WriteLine("...completed"); 

fileStream.Close( ); 

public staticSumOf DeSerialize( ) 

FileStream fileStream = 

newFileStream("DoSum.out",FileMode.Open); 

BinaryFormatter binaryFormatter = 

newBinaryFormatter( ); 

return(SumOf) binaryFormatter.Deserialize(fileStream); 

fileStream.Close( ); 

public virtual voidOnDeserialization( Object sender ) 

ComputeSums( ); 

private intstartNumber = 1; 

private intendNumber; 

[NonSerialized] private int[] theSums; 

Kết quả: 

Creating first one with new... 

1, 

3, 

6, 

10, 

15, 

Serializing......completed 

Creating second one with deserialize... 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

244 

1, 

3, 

6, 

10, 

15, 

21.5 Isolate Storage 

Outlook Express (OE) là trình nhận/chuyển thư điện tửcủa Microsoft. Khi chạy trên 

môi trường đa người dùng nhưWindows 2000, nó cung cấp cho mỗi người dùng 

một hộp thưriêng. Các hộp thưnày lưu trữtrên đĩa cứng thành nhiều tập tin khác 

nhau ởcác thưmục thuộc quyền của người dùng tương ứng. Ngoài ra OE còn lưu 

giữcảc các thiết đặt (nhưcác cửa sổhiển thị, tài khỏan kết nối …) của từng người 

dùng. 

.NET Framework cung cấp các lớp thực hiện các công việc này. Nó tương tựnhư

các tập tin .ini của Windows cũ, hay gần  đây hơn là khóa 

HKEY_CURRENT_USER trong Registry. Lớp thực hiện việc này là luồng 

IsolatedStorageFileStream. Cách sửdụng tương tựnhưcác luồng khác. Ta 

khởi tạo bằng cách truyền cho hàm dựng tên tập tin, các công việc khác hoàn toàn 

do luồng thực hiện. 

Ví dụ21-3 Isolated Storage 

usingSystem; 

usingSystem.IO; 

usingSystem.IO.IsolatedStorage; 

namespaceProgramming_CSharp 

public classTester 

public static voidMain( ) 

Tester app = newTester( ); 

app.Run( ); 

private voidRun( ) 

// tạo một luồng cho tập tin cấu hình    IsolatedStorageFileStream configFile = new

IsolatedStorageFileStream("Tester.cfg",FileMode.Create); 

// tạo một writer đểghi lên luồng 

StreamWriter writer = newStreamWriter(configFile); 

// ghi dữliệu lr6n tập tin config 

String output; 

System.DateTime currentTime = System.DateTime.Now; 

   output = "Last access: " + currentTime.ToString( ); 

writer.WriteLine(output); 

   output = "Last position = 27,35"; 

writer.WriteLine(output); 

// tống sạch dữliệu 

writer.Flush( ); 

writer.Close( ); 

Luồng dữliệu. Gvhd: Nguyễn Tấn Trần Minh Khang

245 

configFile.Close( ); 

Sau khi chạy đoạn mã này ta, thực hiện việc tìm kiếm tập tin test.cfg, ta sẽthấy 

nó trong đường dẫn sau: 

c:\Documents and Settings\Administrator\ApplicationData\ 

Microsoft\COMPlus\IsolatedStorage\0.4\ 

Url.wj4zpd5ni41dynqxx1uz0x0aoaraftc\ 

Url.wj4zpd5ni41dynqxx1uz0ix0aoaraftc\files 

Mởtập tin này bằng Notepad, nội dung tập tin nhưsau 

Last access: 5/2/2001 10:00:57 AM 

Last position = 27,35 

Ta cũng có thể đọc tập tin này bằng chính luồng IsolatedStorageFileStream 

Lập trình .NET và COM   Gvhd: Nguyễn Tấn Trần Minh Khang

246 

Chương 22 Lập trình .NET và COM 

Chương này nói vềnhững điều còn lại của C# (và .NET Framework). 

Khi xây dựng và công bốchuẩn OLE 2.0, sau đó là COM và ActiveX, Microsoft đã 

quảng cáo một cách rầm rộvề"khảnăng" lập trình các thành phần, sau đó gắn 

chúng lại đểcó các ứng dụng. Bên cạnh đó là khảnăng viết một lần dùng cho tất cả

ngôn ngữcủa COM. Tuy nhiên COM vẫn vướng mắc một sốhạn chếnhưvấn đề

phiên bản và khá "khó nuốt". 

.NET Framework mới ra đời lại mang một kiến trúc khác, không hạn chếvềngôn 

ngữ, giải quyết "xong" vấn đềphiên bản. Tuy nhiên trong các công ty hiện nay vẫn 

còn "vô sốCOM", và .NET Framework buộc phải tiếp tục hỗtrợCOM. Dưới đây là 

các vấn đềmà .NET Framework giải quyết được: 

• Hiểu và cho phép sửdụng các ActiveX control trong môi trường Vs.NET 

• Hiểu và cho phép sửdụng các đối tượng COM 

• Cho phép chuyển một lớp .NET thành một COM 

Ngoài ra, như đã giới thiệu C# hỗtrợkiểu con trỏcủa C++ với mục đích có được sự

mềm dẻo của C/C++. Kiểu con trỏ được khuyên không nên sửdụng vì đoạn mã 

dùng con trỏ được xem là không an toàn. Nó chỉthích hợp cho các thao tác với các 

COM, các thưviện hàm DLL, hay gọi trực tiếp đến các Win API. 

22.1 P/Invoke 

Khởi đầu Platform invoke facility (P/Invoke - dễdàng gọi các giao diện lập trình 

của hệ điều hành/sàn diễn) được dự định cung cấp một cách thức đểtruy cập đến 

các hàm Windows API, nhưng ta có thểdùng nó đểgọi các hàm thưviện DLL. 

Ví dụsắp trình bày sửdụng hàm Win API MoveFile của thưviên kernal32.dll. Ta 

khai báo phương thức static extern bằng attribute DllImport nhưsau: 

[DllImport("kernel32.dll",, 

ExactSpelling=false, CharSet=CharSet.Unicode, 

SetLastError=true)] 

static extern boolMoveFile( 

stringsourceFile, stringdestinationFile); 

Lớp DllImport (cũng là lớp DllImportAttribute) đểchỉra một phương thức không 

được quản lý (unmanaged) được gọi thông qua P/Invoke. Các tham số được giải 

thích nhưsau: 

EntryPoint: Tên hàm được gọi 

ExactSpelling: đặt giá trịfalse đểkhông phân biệt hoa thường 

CharSet: tập ký tựthao tác trên các tham sốkiểu chuỗi 

Lập trình .NET và COM   Gvhd: Nguyễn Tấn Trần Minh Khang

247 

SetLastError: đặt giá trịtrue để được phép gọi hàm 

GetLastError (Win API) kiểm tra lỗi 

Ví dụ22-1 Sửdụng P/Invoke đểgọi WinAPI 

usingSystem; 

usingSystem.IO; 

usingSystem.Runtime.InteropServices; 

namespaceProgramming_CSharp 

classTester 

// khai báo hàm WinAPI muốn gọi P/Invoke 

[DllImport("kernel32.dll",, 

ExactSpelling=false, CharSet=CharSet.Unicode, 

SetLastError=true)] 

static extern bool MoveFile( stringsourceFile,    stringdestinationFile); 

public static voidMain( ) 

Tester t = newTester( ); 

stringtheDirectory = @"c:\test\media"; 

DirectoryInfo dir = newDirectoryInfo(theDirectory); 

t.ExploreDirectory(dir); 

private voidExploreDirectory(DirectoryInfo dir) 

stringnewDirectory = "newTest"; 

DirectoryInfo newSubDir = 

 dir.CreateSubdirectory(newDirectory);     FileInfo[] filesInDir = dir.GetFiles( ); 

foreach(FileInfo file infilesInDir) 

stringfullName = newSubDir.FullName + 

"\\" + file.Name; 

file.CopyTo(fullName); 

Console.WriteLine("{0} copied to newTest", 

file.FullName); 

filesInDir = newSubDir.GetFiles( ); 

// xóa một vài tập tin và 

// đổi tên một vài tập tin 

intcounter = 0; 

foreach(FileInfo file infilesInDir) 

stringfullName = file.FullName; 

if(counter++ %2 == 0) 

// P/Invoke Win API 

Tester.MoveFile(fullName, fullName + ".bak"); 

Console.WriteLine("{0} renamed to {1}", 

fullName,file.FullName); 

else 

Lập trình .NET và COM   Gvhd: Nguyễn Tấn Trần Minh Khang

248 

file.Delete( ); 

Console.WriteLine("{0} deleted.", fullName); 

newSubDir.Delete(true); 

Kết quả(một phần): 

c:\test\media

ewTest\recycle.wav renamed to 

c:\test\media

ewTest\recycle.wav 

c:\test\media

ewTest\ringin.wav renamed to 

c:\test\media

ewTest\ringin.wav 

Một lần nữa, chỉnên gọi P/Invoke trong trường bất khảkháng. Sửdụng các lớp 

.NET Framework đểcó đoạn mã được quản lý. 

22.2 Con trỏ

Như đã đềcập ởtrên, chỉnên sửdụng con trỏkhi làm việc với các COM, WinAPI, 

hàm DLL. 

Các toán tửsửdụng với con trỏtương tựnhưC/C++ 

&: toán tửlấy địa chỉ

*: toán tửlấy nội dung con trỏ

->: toán tử đến các thành viên của con trỏ

Ví dụdưới đây sửdụng con trỏlàm tham sốcho hai hàm WinAPI CreatFile và 

ReadFile. 

Ví dụ22-2 Sửdụng con trỏtrong C# 

usingSystem; 

usingSystem.Runtime.InteropServices; 

usingSystem.Text; 

classAPIFileReader 

// import hai phương thức, phải có từkhóa unsafe 

[DllImport("kernel32", SetLastError=true)] 

static extern unsafe intCreateFile( 

stringfilename, 

uintdesiredAccess, 

uintshareMode, 

uintattributes, 

uintcreationDisposition, 

uintflagsAndAttributes, 

uinttemplateFile); 

// API phải dùng con trõ

[DllImport("kernel32", SetLastError=true)] 

static extern unsafe boolReadFile( 

inthFile, 

void* lpBuffer, 

Lập trình .NET và COM   Gvhd: Nguyễn Tấn Trần Minh Khang

249 

intnBytesToRead, 

int* nBytesRead, 

intoverlapped); 

// hàm dựng: mởmột tập tin đã tồn tại 

publicAPIFileReader(stringfilename) 

fileHandle = CreateFile( 

filename, // tập tin 

GenericRead, // cách truy xuất - desiredAccess 

UseDefault, // shareMode 

UseDefault, // attributes 

OpenExisting, // creationDisposition 

UseDefault, // flagsAndAttributes 

UseDefault); // templateFile 

// unsafe: cho phép tạo con trỏvà 

// ngữcảnh unsafe (unsafe context)

public unsafe intRead(byte[] buffer, intindex, intcount) 

intbytesRead = 0; 

// fixed: cấm CLR dọn dẹp rác

fixed(byte* bytePointer = buffer) 

ReadFile( 

fileHandle, // hfile 

bytePointer + index, // lpBuffer 

count, // nBytesToRead 

&bytesRead, // nBytesRead 

0); // overlapped 

returnbytesRead; 

const uintGenericRead = 0x80000000; 

const uintOpenExisting = 3; 

const uintUseDefault = 0; 

intfileHandle; 

classTest 

public static voidMain( ) 

APIFileReader fileReader = 

newAPIFileReader("myTestFile.txt"); 

// tạo buffer và ASCII coder 

const intBuffSize = 128; 

byte[] buffer = new byte[BuffSize]; 

ASCIIEncoding asciiEncoder = newASCIIEncoding( ); 

// đọc tập tin vào buffer và hiển thịra màn hình console 

while(fileReader.Read(buffer, 0, BuffSize) != 0) 

Console.Write("{0}", asciiEncoder.GetString(buffer)); 

Lập trình .NET và COM   Gvhd: Nguyễn Tấn Trần Minh Khang

250 

Phần 2 

Xây dựng một ứng dụng minh họa 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

251 

Chương 23  Website dạy học ngôn ngữC#

23.1 Hiện trạng và yêu cầu 

Trước tiên chúng ta sẽtìm hiểu sơqua vềnhững gì đang diễn ra trong thực tế, và 

ứng dụng của ta liên quan đến khía cạnh nào. Sau đó ta phải xác định rõ các yêu 

cầu mà ứng dụng cần phải thực hiện. Việc xác định thật rõ và đúng các yêu cầu 

mà ứng dụng cần phải thực hiện là bước rất quan trọng, nó sẽ định hướng cho 

toàn bộ ứng dụng của chúng ta. 

23.1.1 Hiện trạng thực tế

23.1.1.1 Hiện trạng 

Hiện nay, lĩnh vực công nghệthông tin trên toàn thếgiới đang phát triển hết sức 

nhanh chóng cảvềhướng công nghệphần mềm và lẫn hướng công nghệphần 

cứng. Chỉcần một vài tháng là sẽcó rất nhiều thay đổi, vì thếta cần phải có một 

phương pháp tốt đểtiếp cận chúng. 

Mặc dù có rất nhiều công cụ, ngôn ngữgiúp các nhà phát triển phần mềm tạo ra 

hàng loạt các ứng dụng mạnh mẽ, nhưng giường nhưchưa đủ. Họvẫn luôn muốn 

tìm tòi những cái mới, công cụtốt hơn đểcó thểtăng hiệu suất phát triển phần 

mềm thật nhanh và thật hiệu quả. Một sốtổchức cung cấp các bộphát triển phần 

mềm nổi tiếng như: 

1. Microsoft với hệ điều hành Windows, bộVisual Studio 6.0 với các ngôn 

ngữlập trình như: Visual Basic, Visual C++ … 

2. Tổchức Sun với ngôn ngữJava đã từng nổi tiếng một thời, thống trịtrong 

các ứng dụng Web. 

Những năm đầu của thếkỷ21, năm 2000 – 2002. Micrsoft đã tung ra thị

trường một công nghệmới Microsoft Development Enviroment .NET với 

mục đích : 

3. Đánh bại các đối thủkhác : ngôn ngữlập trình Java của Sun hay hệquản trị

cơsởdữliệu Oracle … 

4. Trởthành công cụmạnh nhất đểphát triển các ứng dụng Web ( chữNET 

viết tắt của Network ). 

Nhằm minh họa quá trình tìm hiểu ngôn ngữC# (đọc là Csharp) trong bộcông cụ

.NET, chúng tôi đã viết nên ứng dụng Web dạy học C# này. 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

252 

23.1.1.2 Quá trình tìm hiểu thực tế

Để ứng dụng phù hợp với thực tếvà xác định rõ được các yêu cầu mà ứng dụng 

cần thực hiện, chúng tôi cũng đã tìm hiểu qua một sốWeb-Site dạy học trên 

mạng. Sau đây là một sốhình ảnh minh họa quá trình tìm hiểu : 

Trang chủdạy học chủ đềCSS 

Trang này sẽliệt kê tất cảcác mục thuộc chủ đềnày, đồng thời trang này cũng 

cho phép các liên kết ( Link ) tới các trang con khác : các tham chiếu tới các địa 

chỉkhác có liên quan, trắc nghiệm của chủ đềvà minh họa lý thuyết qua các ví 

dụnếu có. 

Hình 23-1 Trang Chủdạy học ngôn ngữCSS 

Trang hiển thịlý thuyết của chương thuộc chủ đề

Trong một chủ đềsẽcó nhiều chương. Khi chọn một chương nào đó, thì sẽhiển 

thịphần lý thuyết chính mô tảcho chương đó. 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

253 

Hình 23-2 Trang giới thiệu vềchương : Introduction CSS 

Liệt kê các ví dụminh họa lý thuyết thuộc chủ đề

Phần này sẽliệt kê tất cảcác ví dụhiện có thuộc chủ đềtheo từng nhóm cụthể. 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

254 

Hình 23-3 Liệt kê các ví dụminh họa lý thuyết thuộc chủ đềtheo nhóm 

Minh họa lý thuyết qua ví dụ

Sau khi tìm hiểu lý thuyết, người dùng muốn tìm hiểu rõ hơn lý thuyết qua phần 

mã nguồn của các ví dụthuộc chủ đề đó. Tùy từng loại chủ đềmà người dùng 

được hỗtrợchức năng gõ mã tiếp vào cửa sổText, sau đó sẽgửi trang mã này 

lên máy chủ đểnhận được kết quảcủa phần mã vừa gõ vào. 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

255 

Hình 23-4 Minh họa lý thuyết qua ví dụ, cho phép người dùng tựgõ mã của họ

vào 

Tổchức thi trắc nghiệm cho chủ đề

Trong mỗi chủ đề, người dùng có thểtham gia thi trắc nghiệm kiến thức của 

mình : 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

256 

Hình 23-5 Tổchức thi trắc nghiệm cho chủ đề

23.1.2 Xác định yêu cầu 

Ứng dụng phải giúp người dùng có thểnắm bắt được kiến thức một cách nhanh 

nhất, tránh tràn lan, dài dòng và phải hỗtrợchức năng hiệu chỉnh ( Admin ). 

23.1.2.1 Yêu cầu chức năng 

Ứng dụng dự định sẽcó 5 chức năng chính sau : 

1. Hiển thịlý thuết 

2. Minh họa lý thuyết qua ví dụ

3. Tổchức thi trắc nghiệm 

4. Cho phép quảng cáo, giới thiệu sách và Link tới các Site liên quan khác 

5. Cho phép chức năng hiệu chỉnh ( thêm, xóa, sửa ) các thành phần trên 

Mô tảchi tiết vềcác yêu cầu chức năng trên : 

Lưu trữ

• Lý thuyết vềtừng chủ đề, các chương thuộc chủ đềvà từng mục chính, ý 

chính của chương, các tập tin đính kèm. 

• Các ví dụminh họa của mỗi chủ đề, tập tin đính kèm nếu có. 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

257 

• Danh sách các bài trắc nghiệm và các câu hỏi, không lưu trữbài làm 

• Các địa chỉWeb-Site đểtham chiếu, các hình ảnh và thông tin vềviệc 

quảng cáo sách ứng với mỗi chủ đề. 

• Một sốthông tin khác : thông tin người dùng Admin,các tham sốhiệu 

chỉnh … 

Tra cứu 

• Thông tin vềmỗi chủ đề, chương, mục chính và các ý chính thuộc chương. 

• Thông tin vềcác bài ví dụminh họa lý thuyết. 

• Thông tin vềcác bài thi trắc nghiệm. 

• Một sốthông tin khác liên quan nếu cần thiết. 

Tính toán 

• Sốcâu đúng cho các bài trắc nghiệm và tính điểm cho chúng. 

Kết xuất 

• Hiển thịtheo cấu trúc phân cấp dạng cây vềlý thuyết theo chủ đề, chương, 

mục chính, ý chính. 

• Ví dụminh họa lý thuyết 

• Các bài trắc nghiệm và kết quảtương ứng 

• Danh sách các tham chiếu đến các Web-Site, các hình ảnh và thông tin về

sách cần quảng cáo. 

• Các thông tin vềlý thuyết, ví dụ, trắc nghiệm, sách cần đểhiệu chỉnh. 

• Các báo cáo thống kê vềcác mục trên nếu có. 

23.1.2.2 Yêu cầu chất lượng 

2. Ứng dụng phải hiển thịthông tin một cách súch tích, ngắn gọn, trực quan 

đối với người dùng, phải giúp người dúng nắm bắt thông tin được nhanh 

nhất. 

3. Phải cho phép hiệu chỉnh (Admin) trực tiếp trên ứng dụng. 

4. Nội dung phải chất lượng, phù hợp với từng chủ đề, dễhiểu. 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

258 

23.2 Phân tích hướng đối tượng 

23.2.1 Sơ đồlớp đối tượng 

Hình 23-6 Lớp đối tượng 

23.2.2 Ý nghĩa các đối tượng chính 

23.2.2.1 CHỦ ĐỀ

Ứng dụng có thểcó nhiều chủ đề, mỗi chủ đềsẽlưu trữcác thông tin vềchính nó 

như: các thành phần thuộc chủ đề, giới thiệu vềchủ đề, có nhiều chương. 

23.2.2.2 CHƯƠNG 

Chương thuộc vềmột chủ đềnào đó, mỗi chương có một hay nhiều mục chính, 

ví dụ, trắc nghiệm. 

23.2.2.3 MỤC CHÍNH 

Mục chính thuộc vềmột chương nào đó, mỗi mục chính có một hay nhiều ý 

chính con và một tập tin mô tảchi tiết cho mục chính này nếu có. 

23.2.2.4 VÍ DỤ

Ví dụthuộc vềmột chương nào đó, mỗi ví dụsẽcó phần mã nguồn và kết xuất 

tương ứng với mã nguồn này, có thểsẽcó một tập tin mô tảchi tiết vềví dụnếu 

cần thiết. 

23.2.2.5 CÂU TRẮC NGHIỆM 

Trắc nghiệm thuộc vềmột chương, một bài trắc nghiệm sẽcó nhiều câu hỏi, mỗi 

câu hỏi sẽcó một đáp án và các chọn lựa tương ứng với câu hỏi đó. 

23.2.3 Bảng thuộc tính các đối tượng chính 

Thuộc tính đối tượng : CHỦ ĐỀ

Stt  Tên thuộc tính  Loại  Kiểu dữliệu  Ý nghĩa 

1 MACHUDE  PK  VARCHAR(50)  Mỗi chủ đềcó một mã chủ

đềduy nhất 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

259 

2 TEN   VARCHAR(50)  Tên chủ đề

3 NOIDUNG   VARCHAR(1000) Nội dung của chủ đề

4 SOCHUONG   INT  Sốchương có trong chủ đề

5 TENTP   VARCHAR(50)  Tên thành phần : Lý 

thuyết, Vì dụ, Trắc 

nghiệm, Quản trị

6 NOIDUNGTP   VARCHAR(500) Nội dung tổng quan của 

thành phần thuộc chủ đề

Thuộc tính đối tượng : CHƯƠNG 

Stt  Tên thuộc tính  Loại  Kiểu dữliệu  Ý nghĩa 

1 MACHUONG  PK  INT  Mỗi chương có một mã 

duy nhất 

2 TEN   VARCHAR(50)  Tên của chương 

3 NOIDUNG   VARCHAR(1000) Nội dung của chương 

4 SOMUCCHINH   INT  Sốmục chính trong một 

chương 

Thuộc tính đối tượng : MỤC CHÍNH 

Stt  Tên thuộc tính  Loại  Kiểu dữliệu  Ý nghĩa 

1 MAMUCCHINH PK  INT  Mã mục chính 

2 TENMC   VARCHAR(100) Tên mục chính 

3 GIOITHIEUMC   VARCHAR(2000) Giới thiệu tổng quan tổng 

quan vềmục chính 

4 TAPTIN   VARCHAR(50)  Tên tập tin mô tảchi tiết 

vềmục chính nếu có 

5 NOIDUNGCTMC  VARCHAR(1000) Nội dung của một ý chính 

con thuộc mục chính 

Thuộc tính đối tượng : VÍ DỤ

Stt  Tên thuộc tính  Loại  Kiểu dữliệu  Ý nghĩa 

1  MAVD  PK  INT  Mã ví dụ

2 TENVIDU   VARCHAR(100) Tên của ví dụ

3 MANGUON   VARCHAR(1000) Mã nguồn của một ví dụ

4 KETQUA   VARCHAR(1000) Kết xuất của mã ví dụ

5 TAPTIN   VARCHAR(50)  Tên tập tin mô tảchi tiết 

vềmột ví dụ 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

260 

Thuộc tính đối tượng : CÂU TRẮC NGHIỆM 

Stt  Tên thuộc tính  Loại  Kiểu dữliệu  Ý nghĩa 

1  MACAUTRACNGHIEM  PK  INT  Mã câu trắc 

nghiệm 

2 TEN   VARCHAR(500) Tên của câu trắc 

nghiệm 

3 NOIDUNG   VARCHAR(1000) Nội dung của câu 

trắc nghiệm 

4 TRALOI1, TRALOI2, 

TRALOI3, TRALOI4 

VARCHAR(500) Một câu hỏi có 4 

câu trảlời, ứng với 

4 thuộc tính 

5 DAPAN   INT  Đáp án của câu 

hỏi, có 4 giá trị: 1, 

2, 3, 4 

23.2.4 Các xửlý trên các đối tượng chính 

Xửlý trên đối tượng : CHỦ ĐỀ

Stt  Tên xửlý  Loại  Ý nghĩa 

1 Hiển thịthông tin vềchủ đềQui định Hiển thịthông tin vềcác thành phần 

trong chủ đề

2 Thêm chủ đềmới Qui định Dữliệu nhập là tên và nội dung 

3 Xóa chủ đề đã có sẵn Qui định Xóa toàn bộthông tin vềchủ đề

4 Sửa đổi thông tin vềchủ đềQui định Hiệu chỉnh tên, nội dung chủ đề

5 Kiểm tra hợp lệviệc thay đổi chủ

đề

Qui định Kiểm tra các ràng buộc và tính hợp 

lệkhi thêm, xóa, sửa chủ đề

6 Lập báo cáo, thống kê  Qui định Tổng kết toàn bộthông tin vềsố

chương, ví dụ, câu trắc nghiệm 

Xửlý trên đối tượng : CHƯƠNG 

Stt  Tên xửlý  Loại  Ý nghĩa 

1 Hiển thịthông tin vềchương Qui định Hiển thịdanh sách tên chương theo 

từng chủ đềvà nội dung 

2 Thêm chương mới Qui định Dữliệu nhập là tên và nội dung 

3 Xóa chương đã có sẵn Qui định Xóa toàn bộthông tin vềchương : 

các mục chính, ý chính 

4 Sửa đổi thông tin vềchương Qui định Hiệu chỉnh tên, nội dung chương 

5 Kiểm tra hợp lệviệc thay đổi 

chương 

Qui định Kiểm tra các ràng buộc và tính hợp 

lệkhi thêm, xóa, sửa 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

261 

Xửlý trên đối tượng : MỤC CHÍNH 

Stt  Tên xửlý  Loại  Ý nghĩa 

1 Hiển thịthông tin vềmục chính 

thuộc chương 

Qui định Hiển thịdanh sách các ý chính con 

của mục chính 

2 Thêm mục chính mới Qui định Dữliệu nhập là tên, nội dung mục 

chính và các ý chính con, và cảtên 

tập tin mô tảmục chính nếu cần 

thiết 

3 Xóa mục chính đã có sẵn Qui định Xóa toàn bộthông tin vềmục chính 

: các ý chính, tên tập tin 

4 Sửa đổi thông tin vềmục chính  Qui định Hiệu chỉnh tên, nội dung nội dung 

mục chính và các ý chính con nếu 

cần thiết 

5 Kiểm tra hợp lệviệc thay đổi mục 

chính 

Qui định Kiểm tra các ràng buộc và tính hợp 

lệkhi thêm, xóa, sửa 

Xửlý trên đối tượng : VÍ DỤ

Stt  Tên xửlý  Loại  Ý nghĩa 

1 Hiển thịthông tin vềví dụQui định Hiển thịdanh sách các ví dụcủa 

chương 

2 Thêm ví dụmới  Qui định Dữliệu nhập là tên, giới thiệu sơvề

ví dụ, mã nguồn, kết xuất tương ứng 

và cảtên tập tin mô tảví dụnếu cần 

thiết 

3 Xóa ví dụ đã có sẵn Qui định Xóa toàn bộthông tin vềví dụ: tên, 

mã nguồn, kết xuất và tên tập tin 

4 Sửa đổi thông tin vềví dụQui định Hiệu chỉnh tên, giới thiệu ví dụ, mã 

nguồn, kết xuất và tên tập tin nếu 

có. 

5 Kiểm tra hợp lệviệc thay đổi ví 

dụ

Qui định Kiểm tra các ràng buộc và tính hợp 

lệkhi thêm, xóa, sửa 

Xửlý trên đối tượng : CÂU TRẮC NGHIỆM 

Stt  Tên xửlý  Loại  Ý nghĩa 

1 Hiển thịthông tin vềcâu trắc 

nghiệm thuộc chương 

Qui định Hiển thịdanh sách các câu trắc 

nghiệm, các câu trảlời và đáp án 

tương ứng của câu. 

2  Thêm câu trắc nghiệm mới Qui định Dữliệu nhập là tên, câu hỏi trắc 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

262 

nghiệm, các câu chọn lựa và đáp án 

của câu. 

3 Xóa câu trắc nghiệm đã có sẵn Qui định Xóa toàn bộthông tin vềcâu trắc 

nghiệm : tên, câu hỏi trắc nghiệm, 

các câu chọn lựa và đáp án của câu. 

4 Sửa đổi thông tin vềcâu trắc 

nghiệm 

Qui định Hiệu chỉnh tên, câu hỏi trắc nghiệm, 

các câu chọn lựa và đáp án của câu. 

5 Kiểm tra hợp lệviệc thay đổi câu 

trắc nghiệm 

Qui định Kiểm tra các ràng buộc và tính hợp 

lệkhi thêm, xóa, sửa 

23.3 Thiết kếhướng đối tượng 

23.3.1 Thiết kếdữliệu 

23.3.1.1 Sơ đồlogic 

Hình 23-7 Sơ đồlogic 

23.3.1.2 Bảng thuộc tính các đối tượng phụ

Thuộc tính đối tượng : THAM SỐ

Stt  Tên thuộc tính  Loại  Kiểu dữliệu  Ý nghĩa 

1 MATHAMSO  PK  INT  Mã tham số

2 TEN   VARCHAR(100) Tên tham số

3 GIATRI   INT  Giá trịtham số

4 DIENGIAI   VARCHAR(100) Ý nghĩa của tham 

số

Thuộc tính đối tượng : NGƯỜI DÙNG 

Stt  Tên thuộc tính  Loại  Kiểu dữliệu  Ý nghĩa 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

263 

MÀN 

HÌNH 

CHÍNH 

Tổng quan lý thuết 

Tổng quan Ví dụ

Tổng quan Trắc nghiệm 

Quản trị

Đăngnhập

Lý thuyết theo chương 

Ví dụtheo chương 

Trắc nghiệm theo chương

Lý thuyết Ví dụTrắc nghiệm 

Thông 

báo lỗi 

Báo cáo 

Thống kê 

1 TENNGUOIDUNG  PK  VARCHAR(100) Tên người dùng 

2 MATMA   VARCHAR(100) Mật mã 

3 EMAIL   VARCHAR(100)  Địa chỉmail 

4 DIENTHOAI   VARCHAR(100)  Điện thoại 

5 FAX   VARCHAR(100) Sốfax 

23.3.2 Thiết kếgiao diện 

23.3.2.1 Tổng quan Sơ đồmàn hình 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

264 

23.3.2.2 Một sốgiao diện của ứng dụng 

Giao diện chính 

Hình 23-8 Trang chủ ứng dụng dạy học CSharp 

• 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

265 

Giao diện chi tiết lý thuyết từng chương 

Hình 23-9 Liệt kê chi tiết các mục chính, ý chính con trong chương số1 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

266 

Giao diện tổng quan vềví dụ

Hình 23-10 Liệt kê tất cảví dụtrong mỗi chương 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

267 

Giao diện mô tảchi tiết ví dụ

Hình 23-11 Minh họa lý thuyết bằng mã ví dụvà kết xuất của nó 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

268 

Giao diện các câu trắc nghiệm kiến thức 

Hình 23-12 Trắc nghiệm kiến thức chương số1 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

269 

Kết quảbài làm trắc nghiệm 

Hình 23-13 Kết quảchấm điểm bài làm trắc nghiệm 

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

270 

Giao diện hiệu chỉnh chủ đề

Hình 23-14 Cho phép thêm, xóa và sửchủ đề

Website dạy học ngôn ngữC#   Gvhd: Nguyễn Tấn Trần Minh Khang

271 

Giao diện tổng quan hiệu

Bạn đang đọc truyện trên: Truyen2U.Pro