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.
1
Phần 1
Tìm hiểu ngôn ngữC#
C# và .Net Framework Gvhd: Nguyễn Tấn Trần Minh Khang
2
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
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
4
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
5
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
6
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
7
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
8
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
9
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
8
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();
3
4 // sửdung property set
5 dgMoi.HoTen = "Nguyễn Văn A";
6
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