You are on page 1of 226

Giáo trình

Lập trình với C#


Chương 1 - Kiến trúc của C# và .NET
Chương 2 - Căn bản C#
Chương 3 - Hướng đối tượng trong C#
Chương 4 - Những chủ đề tiến bộ trong C#
Chương 5 - C# và các lớp cơ sở
Chương 6 - Lập trình trong môi trường .NET
Chương 7 - Windows Applications
Chương 8 - Assemblies
Chương 9 - Truy cập cơ sở dữ liệu với .NET
Chương
- Viewing .NET Data
10
Chương
- Thao tác XML
11
Chương
- File and Registry Operations
12
Chương
- Làm việc với Active Directory
13
Biên dịch từ cuốn Professional C#, 2nd Edition, Xuất bản bởi Wrox Press Ltd .

Chương 1: C# và kiến trúc .NET


Tổng quan:
Tôi muốn nhấn mạnh rằng đừng bao giờ xem xét ngôn ngữ C# một cách tách biệt, nó luôn đồng hành với "Bộ khung .NET". C#
là một trình biên dịch hướng .NET, nghĩa là tất cả các mã của C# luôn luôn chạy trên trên môi trường .NET Framework. Điều đó
dẫn đến 2 hệ quả sau:

• Cấu trúc và các lập luận C# được phản ánh các phương pháp luận của .NET ngầm bên dưới.

• Trong nhiều trường hợp, các đặc trưng của C# thậm chí được quyết định dựa vào các đặc trưng của .NET, hoặc thư
viện lớp cơ sở của .NET.

Chính bởi tầm quan trọng của .NET, nên các bạn cần phải biết sơ qua về .NET trước khi đi vào ngôn ngữ C#. Đây cũng chính là
mục đích của chương này.

Chúng ta sẽ tìm hiểu xem chuyện gì sẽ xảy ra khi mã của các ngôn ngữ hướng .NET (bao gồm C#) được biên dịch và thực thi.
Đây là một lĩnh vực rộng, chúng ta sẽ tìm hiểu kĩ hơn về Microsoft Intermediate Language (MS-IL), ngôn ngữ trung gian
trong .NET mã của các ngôn ngữ khác đều phải được biên dịch về ngôn ngữ này trước khi thực thi. Cụ thể chúng ta sẽ tìm hiểu
xem cách thức mà MS-IL với phần dùng chung Common Type System (CTS) và Common Language Specification (CLS) có
thể cung cấp cho chúng ta sự tương hoạt giữa các ngôn ngữ hướng .NET. Chúng ta cũng sẽ trình bày các ngôn ngữ biết .NET
khác bao gồm VB và C++.

Sau đó chúng ta sẽ xem xét các đặc trưng khác của .NET, bao gồm các assembly, các namespace, và thư viện lớp cơ bản của
.NET. Chúng ta sẽ kết thúc chương này bằng việc liệt kê vắn tắt về các loại ứng dụng mà chúng ta có thể tạo ra trong C#.

Mối quan hệ giữa C# và .NET


C# là một ngôn ngữ lập trình mới, và được biết đến với hai lời chào:

• Nó được thiết kế riêng để dùng cho Microsoft's .NET Framework (Một nền khá mạnh cho sự phát triển, triển khai, hiện
thực và phân phối các ứng dụng)

• Nó là một ngôn ngữ hoàn toàn hướng đối tượng được thiết kế dựa trên kinh nghiệm của các ngôn ngữ hướng đối tượng
khác.

Một điều quan trọng cần nhớ C# là một ngôn ngữ độc lập. Nó được thiết kế để có thể sinh ra mã đích trong môi trường .NET, nó
không phải là một phần của .NET bởi vậy có một vài đặc trưng được hỗ trợ bởi .NET nhưng C# không hỗ trợ và bạn cũng đừng
ngạc nhiên khi có những đặc trưng C# hỗ trợ mà .NET không hỗ trợ (chẳng hạn như quá tải toán tử)

Để tạo được những ứng dụng mang tính hiệu quả cao, chúng ta sẽ xem qua về hoạt động của .NET

Common Language Runtime


Trung tâm của .NET framework là môi trường thời gian chạy, gọi là Common Language Runtime (CLR) hoặc .NET runtime.
Mã của các điều khiển trong CLR thường là mã có quản.

Tuy nhiêu, trước khi được thực thi bởi CLR, mã được phát triển trong C# (hoặc các ngôn ngữ khác) cần phải được biên dịch.Quá
trình biên dịch trong .NET xảy ra theo hai bước:

1. Dịch mã nguồn thành Microsoft Intermediate Language (MS-IL)


2. Dịch IL thành mã nền cụ thể bởi CLR

Mới nhìn có vẻ hơi dài dòng. Nhưng thật sự, một tiến trình dịch hai mức là rất cần thiết, bởi vì trạng thái của Microsoft
Intermediate Language (mã có quản) là chìa khóa cung cấp nhiều lợi ích trong .NET.

Các lợi ích của mã có quản


Microsoft Intermediate Language (thường được viết tắt là"Intermediate Language", hay "IL") tương tự như ý tưởng về mã Java
byte, nó là một ngôn ngữ cấp thấp với những cú pháp đơn giản (dựa trên cơ sở mã số hơn là text), chính điều này sẽ làm cho quá
trình dịch sang mã máy nhanh hơn. Hiểu kĩ các cú pháp này sẽ mang lại những lợi ích đáng kể.
Độc lập nền
Trước tiên, nó có nghĩa là các file chứa mã lệnh có thể chạy trên bất kì nền nào, vào thời gian chạy trình biên dịch cuối sẽ hoạt
động và mã có thể chạy trên một nền cụ thể. Nói cách khác việc dịch mã nguồn sang Intermediate Language cho phép độc lập
nền trong .NET, nó giống như cách dịch mã nguồn sang Java byte code cung cấp sự độc lập nền trong Java.

Bạn cũng nên biết rằng sự độc lập nền của .NET chỉ là trên lí thuyết bởi vì tại thời điểm này, .NET chỉ có sẵn trong Windows.
Tuy nhiên việc chuyển .NET sang những nền khác đang được khảo sát tỉ mỉ (xem ví dụ Mono project, một sự cố gắng tạo một
thực thi mã nguồn mở trong .NET, tại địa chỉ http://www.go-mono.com/).

Sự cải tiến trong thực thi


Mặc dù chúng ta đã so sánh với Jave, IL thật sự có một chút khả quan hơn Java. IL luôn là trình biên dịch Just-In-Time, ngược
lại Java byte code thì thường là thông dịch. Một trong những bất lợi của Java là vào lúc thực thi quá trình dịch từ java byte code
sang mã máy tốn nhiều tài nguyên.

Thay vì phải dịch toàn bộ ứng dụng một lần, trình biên dịch JIT sẽ biên dịch từng phần mã khi nó được gọi. Khi mã được dịch
rồi, mã kết quả sẽ được giữ lại cho tới khi thoát khỏi ứng dụng, chính vì thế nó không phải biên dịch lại trong lần chạy kế tiếp.
Microsoft quả quyết rằng cách xử lí này có hiệu lực cao hơn là dịch toàn bộ ứng dụng, bởi vì có trường hợp một khối lượng lớn
mã của ứng dụng không bao giờ được sử dụng trong thời gian chạy. Khi sử dụng trình biên dịch JIT , các đoạn mã này sẽ không
bao giờ được dịch.

Chính vì thế chúng ta hi vọng rằng mã IL sẽ thực thi nhanh như là mã máy. Microsof cam kết chúng ta sẽ có một thay đổi lớn
trong thực thi. Lời lí giải là, là lần dịch cuối cùng trong thời gian chạy, trình biên dịch JIT sẽ biết chính xác loại vi xử lí mà
chương trình sẽ chạy. Có nghĩa là nó có thể tối ưu mã thi hành cuối cùng bằng cách tham chiếu đến các đặc trưng của từng các bộ
lệnh ứng với các loại vi xử lí đó.

Các trình biên dịch truyền thống đều có tối ưu mã, nhưng chúng chỉ có thể tối ưu độc lập không quan tâm đến loại vi xử lý mà
chương trình sẽ chạy. Bởi vì trình biên dịch truyền thống biên dịch toàn bộ ứng dụng sang mã máy trước khi thực thi. Có nghĩa là
trình biên dịch không hề biết loại vi xử lí mà chương trình sẽ được chạy, chẳng hạn nó có thể là một vi xử lí tương thích x86 hoặc
một vi xử lí Alpha. Visual Studio 6, tối ưu cho cho một máy tương thích Pentium, bởi vậy mã mà nó sinh ra không đem lại lợi ích
gì đối với các đặc trưng phần cứng của vi xử lí Pentium III. Trong khi đó, trình biên dịch JIT có thể thực hiện tối ưu giống như
Visual Studio 6, ngoài ra nó còn có thể tối ưu cho các loại vi xử lí cụ thể mà mã chương trình sẽ chạy.

Tương hoạt giữa các ngôn ngữ


Chúng ta đã biết cách thức IL cho phép độc lập nền, trình biên dịch JIT có thể cải thiện quá trình thực thi. Tuy nhiên, IL cũng
làm cho tương hoạt giữa các ngôn ngữ trở nên dễ dàng hơn. Bạn có thể biên dịch IL từ một ngôn, và mã này sau đó có thể tương
hoạt với IL được biên dịch bởi một ngôn ngữ khác.

Bây giờ chắc bạn đang tự hỏi rằng những ngôn ngữ nào có thể tương tác với C# trong .NET, hãy xem qua các ngôn ngữ biết
.NET phổ biến sau.

VB.NET

Visual Basic đã được tân trang lại để có thể tương thích với công nghệ .NET. Từ việc phát triển Visual Basic trong những năm
gần đây cho thấy rằng trong các phiên bản trước, Visual Basic 6, nó không tương thích với lập trình .NET. Ví dụ, nó đặt nặng vấn
đề tích hợp COM, và nó chỉ đưa ra các sự kiện để phát triển, hầu hết mã nền không có sẵn trong mã nguồn. Không những thế, nó
không thực sự hỗ trợ tính thừa kế và các kiểu dữ liệu chuẩn của Visual Basic không tương thích với .NET.

Visual Basic đang được hoàn thiện trong Visual Basic .NET, cũng đừng ngạc nhiên khi sự thay đổi này xảy ra trên một diện
rộng. Về phương diện thực hành bạn có thể xem VB.NET như là một ngôn ngữ mới. Mã VB 6 không không thể được biên dịch
trong như mã VB.NET. Sự chuyển đổi từ lập trình VB 6 sang VB.NET yêu cầu một sự thay đổi lớn về mã. Tuy nhiên hầu hết các
sự thay đổi này có thể được thực hiện một cách tự động bởi Visual Studio .NET (sự cải tiến của VS cho việc sử dụng .NET). Nếu
bạn cố gắng đọc một đề án VB 6 trong Visual Studio .NET, nó sẽ cải tiến đề án của bạn, có nghĩa là nó sẽ viết lại mã nguồn VB 6
thành mã nguồn VB.NET. Điều đó có nghĩa là việc này sẽ gặp rắc rối khi bạn cắt ngang, bạn sẽ phải kiểm tra lại mã VB.NET
mới để chắc rằng đề án của bạn vẫn chạy đúng.

Một hiệu ứng phụ là không còn khả năng biên dịch .NET sang mã thực thi. VB.NET chỉ biên dịch sang IL, giống như C#. Nếu
như bạn muốn tiếp tục mã hóa trong VB 6, bạn có thể làm như vậy, nhưng khi mã thực thi quá dài nó sẽ lờ đi .NET Framework,
và bạn cần phải giữ lại Visual Studio 6 đã cài đồng thời phải hoàn toàn tin vào môi trường phát triển trong Visual Studio.

Managed C++

Vào lúc đó trong Visual C++ 6, C++ đã có một khối lượng lớn các mở rộng của Microsoft trong Windows. Với Visual C++
.NET, các mở rộng này được tăng thêm cho việc hỗ trợ .NET framework. Có nghĩa là mã nguồn C++ sẽ vẫn tiếp tục được dịch
sang mã máy không có gì khác biệt. Cũng có nghĩa là nó sẽ chạy độc lập trong môi trường .NET. Nếu bạn không muốn mã C++
của bạn chạy trong môi trường .NET Framework, bạn có thể đơn giãn đặt dòng lệnh sau vào đầu mã nguồn của bạn:
#using <mscorlib.dll>

Bạn cũng có thể bỏ qua cờ /clr trong trình biên dịch, cờ này cho biết rằng bạn muốn biên dịch sang mã có quản, và nó sẽ phát ra
IL thay vì mã máy. Có một điều thú vị trong C++ khi bạn biên dịch sang mã có quản, trình biên dịch có thể phát ra các IL có
nhúng các mã máy. Điều này có nghĩa là bạn có thể pha trộn kiểu có quản và kiểu không quản trong mã C++. Bằng cách mã C+
+:

class MyClass
{

Định nghĩa cho một lớp trong C++ , trong khi đó mã:

__gc class MyClass


{

sẽ cho bạn một lớp có quản, giống như việc bạn viết một lớp trong C# hay VB.NET. Thật vậy, một thuận lợi của managed C++
so với C# là bạn có thể gọi các lớp không quản C++ từ mã có quản C++ bỏ qua tương thích COM.

Trình biên dịch sẽ phát ra một lỗi nếu bạn cố gắng dùng những đặc trưng mà mã có quản của .NET không hỗ trợ trong (ví dụ,
khuôn mẫu hay đa thừa kế). Bạn cũng sẽ nhận ra rằng bạn sẽ phải dùng các đặc trưng không thuần C++ (chẳng hạn từ khoá __gc
trong ví dụ trên) khi sử dụng các lớp có quản.

Bởi vì trong VC++ cho phép giải phóng bộ nhớ thủ công dưới dạng một con trỏ, trình biên dịch C++ không thể phát ra mã cho
kiểu bộ nhớ an toàn CLR. Nếu ứng dụng của bạn thật sự cần phải nhận dạng kiểu bộ nhớ an toàn CLR, bạn cần phải viết mã
nguồn trong các ngôn ngữ khác (như C# hay VB.NET).

J++ and J#

J++ vẫn được hỗ trợ cho chỉ vì mục đích tương thích trước đây. Microsoft không còn phát triển bất kì nền tảng nào hỗ trợ việc
biện dịch sang máy Java ảo. Thay vì đó, Microsoft phát triển hai công nghệ tách biệt Java/J++ phát triển bên dưới ngọn cờ
JUMP (Java User Migration Path) và "JUMP trong .NET".

Công nghệ đầu tiên là Visual J#. Về bản chất nó được thêm vào Visual Studio.NET để cho phép bạn viết và biên dịch mã J++. Sự
khác biệt đó là thay vì biên dịch sang một Java Virtual Machine, J# sẽ biên dịch sang IL, vì vậy nó sẽ hoạt động như là một ngôn
ngữ của .NET. Ngừơi dùng J# sẽ có thể được hưởng các thuận lợi của các đặc tính của VS.NET. Microsoft tin răng người dùng
J++ sẽ nhanh chóng nhận ra điều đó nếu họ thích làm việc trong với .NET.

Sự lựa chọn thứ hai là công cụ tự động hỗ trợ việc chuyển mã J++ sang mã C#. Sự giống nhau giữa J++ và C# làm cho việc
chuyển đổi này trở nên dễ dàng hơn.

Không giống như J# cũng không như các công cụ chuyển đổi ngôn ngữ có sẵn như là một phần của .NET hay trong Visual
Studio. NET, thay vì thế nó được cung cấp riêng. Để biết thêm thông tin liên hệ http://msdn.microsoft.com/visualj/.

Scripting languages

Scripting languages đâu đó vẫn còn tồn tại, dù rằng về mặt tổng quát, tầm quan trọng của chúng đã giảm sút cùng với sự ra đời
của .NET. JScript, được cải tiến lên JScript.NET. ASP.NET (một cải tiến từ ASP dành cho .NET, giải thích sau) các trang có thể
được viết bằng JScript.NET, và bây giờ nó có thể chạy trên JScript.NET như là một ngôn ngữ biên dịch hơn là một ngôn ngữ
thông dịch và nó có thể tạo ra các mã kiểu mã JScript.NET mạnh hơn. Với ASP.NET không có lí do gì để dùng scripting
languages trên cac trang web server-side. VBA vẫn được sử dụng như là một ngôn ngữ cho Microsoft Office và Visual Studio
macros.

COM and COM+

COM và COM+ không là công nghệ chính của .NET, bởi vì các thành phần cơ bản của chúng không thể dịch sang IL (mặc dù
vẫn có thể làm điều đó khi tổ chức thành phần COM bằng mã C++). Tuy nhiên COM+ vẫn còn là một công cụ quan trọng, từ khi
đặc tính của nó được nhân lên trong .NET. Ngoài ra, thành phần COM vẫn còn làm việc và .NET kết hợp chặc chẽ các đặc trưng
tương hoạt COM để mã có quản có thể gọi đến COM và ngược lại (sẽ được bàn thêm ở chương 17).

Assemblies
Một assembly là một đơn vị luận lí chứa mã đã được biên dịch sang .NET. Chúng ta sẽ bàn kĩ về các assemblie trong chương 8,
ở đây chúng ta sẽ nói sơ về nó.

Một assembly là một tự mô tả đầy đủ, và nó giống một đơn vị luận lí hơn là một đơn vị vật lí, điều đó có nghĩa là nó có thể chứa
trong nhiều file (thật vậy các assemblie động được lưu trong bộ nhớ không phải trong file). Nếu một assembly được lưu trong
nhiều file, thì sẽ có một file chính chứa các con trỏ và các mô tả về các file khác của assembly.
Chú ý rằng, câu trúc assembly được dùng chung cho cả mã thi hành và mã thư viện. Sự khác biệt duy nhất là assembly thi hành
chứa lối vào chương trình chính trong khi assembly thư viện thì không có.

Một điểm quan trọng trong các assembly là chúng chứa metadata dùng để mô tả các kiểu và phương thức được định nghĩa trong
mã tương ứng. Một assembly, tất nhiên cũng chứ assembly metadata dùng để mô tả chính assembly đó. Assembly metadata này,
chứa một vùng đựơc hiểu như là manifest, cho phép kiểm tra phiên bản và tình trạng của assembly.

ildasm, một tiện ích có sẵn của Windows, có thể dùng để nghiên cứu nội dung của một assembly, bao gồm manifest metadata.
Chúng ta sẽ lấy vi dụ về ildasm trong chương 8.

Thật vậy một assembly chứa metadata của chương trình nghĩa là các ứng dụng hoặc các assembly khác có thể gọi mã trong môt
assembly mà không cần tham chiếu đến Registry, hoặc một dữ liệu nguồn khác,. Một điểm quan trọng trong cách làm của COM
cũ, các GUID của các thành phần và giao diện interfaces không thể đạt được từ Registry.

Việc dàn trải dự liệu thành 3 định vị khác nhau đồng nghĩa với việc tạo ra mối nguy hiểm trong đồng bộ hoá, nó ngăn không cho
các thành phần khác sử dụng. Với assemblies, sẽ không còn những mối nguy hiểm như vậy, bởi vì tất các các metadata được lưu
trong bộ lệnh thi hành của chương trình. Chú ý rằng dù cho các assemblie được lưu thành một vài file, chúng vẫn không gây vấn
đề gì về đồng bộ hoá dữ liệu. Đó là vì nhờ vào file assembly chính, file này chứa đường dẫn, các thông tin chi tiết, mã băm, và
nội dung của các file khác, điều đó có nghĩa là nếu một file bị thay thế, hay bị phá hoại, nó sẽ được tìm ra và sẽ không cho load.

Assemblies bao gồm 2 loại: các shared và private assembly.

Private Assemblies
Private assemblies là kiểu đơn giản nhất. Nó chứa phần mềm và chỉ được dùng cho phần mềm đó. Với phần mô tả này bạn có thể
chứa đựng các private assemblie hòng cung cấp cho một ứng dụng kiểu thực thi và một số thư viện, các thư viện này chứa mã sẽ
được thi hành bởi ứng dụng đó.

Hệ thống đảm bảo rằng private assemblies sẽ không được dùng bởi phần mềm khác, bởi vì một ứng dụng chỉ có thể load private
assemblies trong cùng folder với chương trình chính hoặc là trong một thư mục con của nó.

Chúng ta không thể tin cậy rằng tất cả các phần mềm luôn được cài đặt trong thư mục của nó, nghĩa là sẽ không bao giờ có
chuyện một gói phần mềm ghi đè, sửa chữa hoặc vô tình load một private assemblies dành riền cho một gói khác. Vậy làm sao để
các Private assemblie chỉ được dùng bởi gói phần mềm mà nó mô tả? Cần có một cơ chế bảo vệ, sao cho khi một sản phẩm
thương mại khác cài đè lên một phiên bản assembly mới (chưa kể đến các chương trình đựơc thiết kế để phá hoại), thì sẽ không
có chuyện tranh chấp tên. Nếu có sự trùng tên trong các assembly, đều đó không quan trọng và các ứng dụng chỉ có thể nhìn thấy
một bộ các assembly.

Bởi vì một private assembly là một tự định nghĩa trọn vẹn, tiến trình xử lí cực kì đơn giản. Bạn đơn giản thay thế các file thích
hợp vào thư mục thíhc hợp trong file hệ thống (Không cần phải đăng kí trong registry). Tiến trình này được gọi là zero impact
(xcopy) installation.

Shared Assemblies
Shared assemblies được dành cho cácc thư viện công cộng có thể dùng cho bất kì ứng dụng nào. Bởi vì bất kì ứng dụng nào cũng
có thể truy xuất một shared assembly, cần phải có các cơ chế để bảo vệ các rủi ro sau:

• Tranh chấp tên, khi một công ty tạo ra các shared assembly trùng tên với các shared assembly sẵn có của bạn. Về mặt lí
thuyết mã của bạn có thể truy xuất vào cả hai assembly này song đây có thể là một vấn đề phức tạp.
• Lỗi của một assembly có thể bị ghi đè bởi một phiên bản khác của cùng same assembly - một phiên bản mới không
tương thích với những gì sẵn có.

Giải pháp cho những vấn đề trên là đặt các shared assembly trong một cây thư mục đặt biệt của hệ thống, có thể xem như là
assembly cache toàn cục. Không giống như các private assembly, nó không đơn giản là copy assembly sang một thư mục thích
hợp - nó cần được cài đặt rõ ràng vào cache. Tiến trình này có thể được thực thi bởi một số tiện ích của .NET, bao gồm luôn quá
trình kiểm tra trên assembly, tương tự như cài đặt một thư mục trong assembly cache để đảm bảo tính toàn vẹn của assembly.

Để tránh tranh chấp tên, shared assemblies đưa ra một được quản lí dựa trên một khóa mật mã chính. Tên này được gọi là strong
name, được bảo đảm về tính độc nhất, và phải được trích dẫn bởi ứng dụng muốn tham chiếu đến một shared assembly.

Vấn đề về tương thích với lỗi do ghi đè một assembly được đánh địa chỉ theo thông tín phiên bản trong assembly manifest, và
cho phép cài đặt song song.

Reflection
Từ khi các assembly được lưu dưới dạng metadata, bao gồm chi tiết về tất cả các kiểu và thành viên của những kiểu này, thì nó
có thể được truy xuất được các metadata programmatically. Để biết chi tiết hơn, xin hãy xem reflection - mã quả có thể xem xét
các mã quản khác, hoặc xem xét chính nó, để nhận ra các thông tin về mã. Bạn có thể dùng các attribute, để có thể sử dụng
phương thức trong lúc chạy điều này tốt hơn là trong lúc biên dịch.

Tìm hiểu về Intermediate Language


Như chúng ta đã biết, Intermediate Language hoạt động như là bản chất của .NET Framework. Là lập trình viên C#, chúng ta nên
biết rằng mã C# sẽ luôn được dịch sang Intermediate Language trước khi nó được thực thi (thật vậy, trình biên dịch C# chỉ dịch
sang mã có quản). Chúng ta hãy cùng khám phá các tính năng chính của IL, bất kì ngôn ngữ nào hướng .NET cũng sẽ hỗ trợ các
đặc tính chính của IL.

Sau đây là những đặc tính chính của Intermediate Language:

• Hướng đối tượng và dùng interfaces


• Sự tách biệt giữa kiểu giá trị và kiểu tham chiếu

• Định kiểu mạnh

• Quản lỗi thông qua các ngoại lệ

• Sự dụng các thuộc tính

Bây giờ chúng ta sẽ cùng khám phá các đặc tính trên.

Hỗ trợ hướng đối tượng và dùng giao diện


Ngôn ngữ độc lập nền của .NET có một vài giới hạn riêng. Cụ thể trong lúc thực thi IL chắc chắn sẽ thực thi một cách thức lập
trình riêng, và các ngôn ngữ khác phải chú ý đến việc tương thích với cách thức lập trình này. IL đã được Microsoft phát triển
như là một ngôn ngữ hướng đối tượng cổ điển hỗ trợ đầy đủ thừa kế đơn giữa các lớp.

Bên cạnh lập trình hướng đối tượng đơn, Intermediate Language còn nêu ra ý tưởng về interfaces (giao diện), cái đã được tích
hợp trong Windows với giao diện COM. .NET nó không giống như giao diện COM; chúng không cần phải hỗ trợ bất kì một kiến
trúc COM nào (ví dụ, chúng không xuất phát từ IUnknown, và chúng cũng không liên quan gì đến các GUID). Tuy nhiên chúng
có thể dùng chung các giao diện COM.

Hướng đối tượng và thực thi chéo ngôn ngữ

Bây chúng ta sẽ tìm hiểu về hoạt động của .NET nghĩa là hoạt động biên dịch sang mã Intermediate Language, điều đó nói lên
rằng bạn cần phải lập trình theo cách thức hướng đối tượng truyền thống. Không những thế chúng còn cung cấp cho chúng ta khả
năng chuyển giao ngôn ngữ. Sau cùng, C++ và Java cả hai đều dùng những biến thể của hướng đối tượng, dù vậy chúng vẫn còn
được quan tâm để có thể thực thi chéo. Chúng ta cần tìm hiểu một chút về thực thi chéo ngôn ngữ.

Trước tiên chúng ta cần hiểu chính xác thực thi ngôn ngữ chéo là gì. Sau cùng, COM cho phép các thành phần được viết bởi các
ngôn ngữ khác nhau có thể thực thi chéo. COM, là một nhị phân chuẩn, cho phép các thành phần có thể hiểu nhau và có thể gọi
các phương thức cũng như thuộc tính lẫn nhau mà không cần quan tâm đến ngôn ngữ đã tạo ra chúng. Để làm được điều đó mỗi
đối tượng phải có khả năng giao tiếp với thời gian chạy của COM, và phải có khả năng truy cập thông qua một giao diện. Các
thành phần chỉ có thể giao tiếp với nhau trong thời gian chạy COM. Dù rằng các thành phần của COM có thể giao tiếp với nhau
bất chấp ngôn ngữ đã tạo ra chúng, tuy nhiên COM không hỗ trợ hoạt động thừa kế, chính vì thế nó đã đánh mất các thuận lợi
của lập trình hướng đối tượng.

Một vấn đề xảy ra khi bẫy lỗi là các thành thành phần phải được bẫy lỗi trong ngôn ngữ đã tạo chúng, và bạn không thể bẫy lỗi
từng bước trên các ngôn ngữ khác nhau. Vậy thực thi chéo ngôn ngữ được hiểu như là các lớp được tạo ra trong một ngôn ngữ có
thể giao tiếp lẫn nhau với các lớp được tạo ra trong các ngôn ngữ khác. Cụ thể là:

• Một lớp được tạo ra trong một ngôn ngữ có thể thừa kế từ một lớp được viết trong một ngôn ngữ khác.
• Một lớp có thể chứa thể hiện của một lớp khác không quan tâm đến ngôn ngữ đã tạo ra hai lớp đó.

• Một đối tượng có thể gọi trực tiếp phương thức của một đối tượng khác được viết bởi một ngôn ngữ khác.

• Các đối tượng (hoặc các tham chiếu đến các đối tượng) có thể được truyền qua lại giữa các hàm

• Bạn có khả năng bẫy lỗi từng bước chương trình nguồn giữa các ngôn khác nhau

Thật bất ngờ về những gì mà .NET và thực thi ngôn ngữ chéo đã làm được. Tiện ích bẫy lỗi từng được giới thiệu như là khả năng
của Visual Studio .NET IDE hơn là CLR.

Sự khác biệt giữa kiểu dữ liệu giá trị và kiểu dữ liệu tham chiếu
Như bất kì ngôn ngữ lập trình nào, IL cung cấp một số tiền định nghĩa về các kiểu dữ liệu nguyên thủy. Một đặc trưng của
Intermediate Language là phân biệt rạch ròi giữa kiểu dữ liệu giá trị và kiểu dữ liệu tham chiếu. Kiểu giá trị là các biến được
dùng để lưu trực tiếp giá trị, trong khi đó kiểu tham chiếu là các biến chứa địa chỉ của dữ liệu.

Trong C++, kiểu tham chiếu có thể coi như là một con trỏ, trong khi đó ở Visual Basic, kiểu tham chiếu có thể coi là các đối
tượng, trong VB 6 luôn truy cập thông qua tham chiếu. Intermediate Language cũng chỉ rõ về cách thức lưu trữ dữ liệu: ví như
một kiểu tham chiếu luôn được lưu trong vùng managed heap của bộ nhớ, trong khi đó kiểu giá trị lại được lưu trong stack (tuy
nhiên nếu kiểu dữ liệu được khai báo là một trường của kiểu tham chiếu, chúng vẫn được lưu ở heap). Chúng ta sẽ bàn về stack
và heap trong chương 3.

Định kiểu mạnh

Một điểm mạnh trong IL là định kiểu mạnh. Nghĩa là tất cả các biếu đều được đánh dấu rõ ràng và chuyên biệt về kiểu dữ liệu
(IL không còn hỗ trợ kiễu Variant cho Visual Basic và ngôn ngữ kịch bản). Cụ thể là IL không cho phép các hoạt động trả về các
kiểu dữ liệu không rõ ràng.

Trong trường hợp là người phát triển VB có lẽ bạn sẽ rất lo lắng về kiểu, bởi vì khi dùng kiểu dữ liệu Variant VB tự động ép kiểu
giúp bạn. Còn là người phát triển C++, có lẽ bạn sẽ dùng các casting pointer giữa các kiểu. Lập trình theo cách này có thể là một
lập trình mạnh, tuy nhiên nó phá vỡ tính an toàn kiểu. Từ bây giờ, nó chỉ còn hỗ trợ trong những trường hợp đặc biệt trong một
số ngôn ngữ có khả năng biên dịch sang mã có quản. Thật vậy, các con trỏ (không phải là tham chiếu) chỉ còn cho phép trong các
khối mã đặc biệt trong C#, trong VB không có (mặc dù nó cho phép trong C++). Nếu dùng con trỏ trong mã nguồn nó sẽ không
chuyển thành mã có quản và sẽ không được kiểm tra bởi CLR.

Bạn cũng nên biết rằng trong một số ngôn ngữ biết .NET, chẳng hạn như VB.NET, vẫn cho phép mơ hồ kiểu, tuy nhiên chỉ để có
thể làm được như thể làm được như vậy thì trình biên dịch đã xác định kiểu bảo vệ kiểu trước khi phát ra IL.

Mặc dù, kiểu bảo vệ lúc đầu có thể sinh ra nhiều cản trở trong lập trình nhưng trong nhiều trường hợp kiểu bảo vệ sẽ mang lại
nhiều lợi ích to lớn trong các dịch vụ được cung cấp bởi .NET. Chẳng hạn các dịch vụ sau:

• Language Interoperability
• Garbage Collection

• Security

• Application Domains

Hãy tìm hiểu xem tại sao kiểu dữ liệu mạnh lại là một trong những đặc tính quan trọng của .NET.

Tầm quan trọng của Strong Data Typing đối với Language Interoperability

Một khía cạnh quan trong của strong data typing là nếu một lớp xuất thân hoặc chứa một lớp khác thì nó cần phải biết tất cả các
kiểu dùng trong các lớp đó. Thật vậy, nó đã từng các chướng ngại lớn trong việc thực thi ngôn ngữ chéo ở các hệ thống không hỗ
trợ trước đấy. Thông tin này không có sẵn trong các file thi hành và DLL chuẩn.

Giả sử rằng một phương thức trong VB.NET được định nghĩa là sẽ trả về một Integer, một trong những kiểu dữ liệu chuẩn của
VB.NET. C# không có kiểu dữ liệu có tên như vậy. Chúng ta chỉ có thể dùng phương thức này để trả về một kiểu của C# nết
trình biên dịch biết cách ánh xạ kiểu VB.NET's Integer đến một trong những kiểu được định nghĩa trong C#. Vậy .NET đã làm
việc đó như thế nào?

Common Type System (CTS)

Vấn đề về kiểu dữ liệu này được .NET giải quyết bằng cách dùng Common Type System (CTS). CTS định nghĩa các kiểu dữ
liệu tiền định và có sẵn trong IL, vì thế tất các các ngôn ngữ hướng .NET framework sẽ sinh ra mã cuối trên cơ sở các kiểu dữ
liệu này.

Trong ví dụ trên, VB.NET's Integer thực tế là một 32-bit signed integer, được ánh xạ từ kiểu Int32 trong IL. Nó phải được biên
dịch thành mã IL. Bởi vì trình biên dịch C# cũng biết kiểu dữ liệu này nên không có vấn đề gì cả. Ở cấp mã nguồn, C# gọi Int32
là int, vì vậy khi biên dịch hàm VB.NET đơn giản trả về một kiểu int.

CTS không chỉ đơn thuần là các kiểu dữ liệu đơn giản, doesn't merely specify primitive data types, mà nó còn cho phép chúng ta
tự định nghĩa kiểu của riêng mình.

Các kiểu được trình bày trong bảng dưới đây:

Kiểu Giải thích

Type Kiểu cơ bản dùng để mô tả các kiểu khác


Kiểu Giải thích

Value Type Kiểu cơ bản dùng để mô tả các kiểu giá trị.

Reference Types Kiểu cơ bản dùng để môt tả các kiểu tham trị.

Built-in Value Types Bao gồm các kiểu giá trị nguyên thủy chuẩn, như các kiểu số, kiểu luận lí, kiểu kí tự.

Enumerations Bộ các giá trị liệt kêSets of enumerated values.

User-defined Value Types Kiểu được định nghĩa trong mã nguồn như là một kiểu giá trị. Trong C# nó có là struct.

Interface Types Các giao diện.

Pointer Types Các con trỏ.

Self-describing Types Kiểu dữ liệu có quản.

Arrays Các kiểu chứa mảng các đối tượng.

Class Types Các kiểu tự mô tả nhưng không phải là mảng.

Delegates Kiểu được thiết kế để tham chiếu đến các phương thức.

User-defined Reference Types Kiểu được định nghĩa trong mã nguồn và được lưu như là kiểu tham chiếu. Trong C#,
nó có nghĩa là một lớp.

Boxed Value Types Một kiểu giá trị được bọc thành một kiểu tham chiếu vì thế nó có thể được lưu trong
heap.

Chúng ta không thể liệt kê tât cả các kiểu giá trị ở đây, bởi vì chúng sẽ được bàn kĩ trong chương 2. Trong C#, mỗi kiểu có sẵn
được nhận dạng bởi trình biên dịch ánh xạ đến một kiểu IL cài sẵn. Điều này cũng đúng cho cả VB.NET.

Common Language Specification (CLS)

Common Language Specification hoạt động cùng với Common Type System để bảo đảm thực thi ngôn ngữ chéo. CLS là một bộ
con chuẩn mà tất cả các trình biên dịch hướng .NET đều phải hỗ trợ. Đều đó có nghĩa là các trình biên dịch đều sẽ hỗ trợ tất cả
những gì được định nghĩa trong CLS.

Chú ý: Các bạn có thể viết các mã non-CLS, tuy nhiên những mã này không đảm bảo việc thực thi ngôn ngữ chéo.

IL là một ngôn ngữ phân biệt loại kí tự. Những nhà phát triển khi làm việc với các ngôn ngữ phân biệt loại kí tự có khả năng tạo
nên sự mềm dẻo khi đặt tên biến. VB.NET, lại không phải là ngôn ngữ phân biệt loại kí tự. CLS xử lí việc này bằng các ra hiệu
cho CLS rằng mã không cho phép hai tên chỉ khác nhau về mặt loại kí tự. Bởi vậy, mã VB.NET có thể hoạt động trong CLS.

CLS hoạt động theo hai định hướng. Trước tiên nó là một trình biên dịch riêng không hỗ trợ đây đủ các đặc trưng của .NET điều
này khuyến khích sự phát triển của các ngôn biết .NET khác. Thứ hai, nó bảo đảm rằng nếu bạn hạn chế các lớp của bạn trong
những đặc tính của CLS, thì nó bảo đảm rằng các mã dùng trong những ngôn ngữ khác có thể dùng các lớp này.

Nét đẹp của ý tưởng này là việc giới hạn trong những đặc tính của CLS chỉ nên áp dụng cho những thành phần public và
protected của các lớp và chỉ dùng cho các lớp public. Trong các thành phần thực thi của các lớp của bạn, bạn có thể viết các mã
non-CLS nếu muốn, bởi các ngôn ngữ khác không bao giờ có thể truy cập vào những phần này.

Chúng ta không đi vào chi tiết của CLS ở đây. Về mặt tổng quát CLS không ảnh hưởng nhiều đến mã C# của bạn vì nó không có
nhiều đặc tính khác CLS.

Garbage Collection

Garbage collector là một thành phần quản lí bộ nhớ của .NET, nó là một đáp án cho việc thu hồi bộ nhớ của các chương trình
thực thi. Từ trước đến giờ có hai công nghệ được sử dụng cho việc huỷ bộ nhớ trong Windows, những tiến trình này được yêu
cầu từ hệ thống:
• Ứng dụng tự làm điều này một cách thủ công.

• Tạo một bộ đếm tham chiếu đến đối tượng.

Việc mã ứng dụng chịu trách nhiệm thu hồi vùng nhớ là một cộng nghệ dùng ở mức thấp, hoặc những ngôn ngữ thực thi cấp cao
như C++. Nó mang tính hiệu quả cao, nó có thuận lợi là tài nguyên sẽ được giải phóng ngay khi không còn cần thiết. Một bất lợi
lớn là nó thường xuyên sinh lỗi. Mã nguồn luôn phải chỉ rõ cho hệ thống biết khi nó không cần dùng bộ nhớ đó nữa . Dễ dàng
nhìn ra rằng kết quả có thể dẫn đến rò rỉ bộ nhớ.

Mặc dù các môi trường phát triển hiện đại có cung cấp một số công cụ giúp đỡ trong việc phát hiện sự rò rỉ bộ nhớ, nhưng rất
khó theo vết, bởi vì nó không có hiệu lực cho đến khi có một khối lượng lớn bộ nhớ bị rò rỉ: Windows buộc phải ngưng các tiến
trình xử lí. Tại thời điểm này máy tính chậm đi thấy rõ một sự trả giá cho các yêu cầu bộ nhớ.

Việc duy trì một bộ đếm các tham chiếu là một ân huệ trong COM. Ý tưởng này cho rằng mỗi thành phần COM chứa một bộ
đếm xem có bao nhiêu ứng dụng đang chứa tham chiếu đến nó. Khi bộ đếm này xuống đến zero, Thành phần có thể tự hủy nó và
giải phóng vùng nhớ cũng như các tài nguyên tương ứng. Vấn đề ở đây là nó vẫn lệ thuộc vào sự thông báo của các ứng dụng khi
chúng không còn dùng đến các thành phần này nữa. Trong một vài trường hợp, nó có khả năng tạo ra một vấn đề nghiêm trọng
hơn là sự kiểu rò rỉ C++ thông thường, bởi vì đối tượng COM có thể nằm trong một tiến trình của riêng nó, điều này có nghĩa là
nó sẽ không bao giờ được hủy bởi hệ thống (chí ít trong rò rỉ kiểu C++, hệ thống có thể giành lại toàn bộ vùng nhớ khi tiến trình
kết thúc).

Thời gian chạy .NET hoàn toàn phụ thuộc vào garbage collector instead. Đây là một chương trình hỗ trợ việc thu dọn bộ nhớ.
Trong ý tưởng này tất cả các yêu cầu bộ nhớ đều được cấp phát trên heap (điều này đúng cho tất cả các ngôn ngữ, trong .NET,
CLR chứa nó trong vùng heap có quản cho tất cả các ứng dụng .NET sử dụng). Thỉnh thoảng .NET sẽ kiểm tra xem vùng heap có
quản có trở nên đầy chưa để nó tiến hành thu dọn, và nó gọi đây là tiến trình thu gôm rác. Trình thu dọn rác sẽ kiểm tra các tham
chiếu từ mã của bạn, ví dụ các tham chiếu từ mã của bạn đến các đối tượng được lưu trên heap được nhận dạng, nó có nghĩa là
đối tượng đó vẫn còn tham chiếu, các đối tượng không còn tham chiếu nữa sẽ bị huỷ.

Trình thu gom rác hoạt động trong .NET bởi vì Intermediate Language được thiết kế để làm điều đó. Phải tuân thủ các nguyên tắc
sau, thứ nhất bạn chỉ có thể tham chiếu đến một đối tượng có sẵn bằng cách sao chép cac tham chiếu có sẵn, thứ hai Intermediate
Language bảo vệ kiểu, điều này có nghĩa là các tham chiếu đến các đối tượng có sẵn luôn chứa đựng thông tín nhận dạng chính
xác của đối tượng đó.

C++ Có thể không sử dụng trình thu gom một cách máy móc, bởi vì C++ cho phép các con trỏ tự do ép kiểu.

Một điều đặc biệt quản trọng là tính không định trước của trình thu gom rác. Hay nói cách khác, bạn không thể bảo đảm được khi
nào trình thu gôm rác sẽ được gọi; nó sẽ được gọi khi CLR cảm thấy cần (nếu bạn không thực hiện lời gọi tường minh).

Bảo mật

.NET thật sự xuất sắc trong việc bổ sung cơ chế bảo mật của Windows bởi vì nó hỗ trợ code-based security trong khi đó
Windows chỉ thật sự hỗ trợ Role-based security.

Role-based security là cơ sở để xác định tài khoản của các tiến trình đang thực thi, hay nói cách khác ai sở hữu các tiến trình
đang thực thi. Code-based security là một cơ chế khác để xác định xem những mã nào và có bao nhiêu mã là đáng tin. Cảm ơn sự
bảo vệ kiểu mạnh của IL, vì nhờ nó mà CLR có thể kiểm tra mã trước khi chạy trong một chế độ bảo vệ được đưa ra.NET cũng
hỗ trợ một cơ chế những mã nào được phép phơi tra trong một cơ chế bảo mật nào đó.

Một điều quan trọng là code-based security có thể làm giảm nguy cơ liên quan đến việc chạy các đoạn mã có xuất xứ không rõ
ràng (chẳng hạn như mã mà bạn downloaded từ Internet). Một ví dụ, nếu mã được chạy dưới quyền administrator, nó có thể sử
dụng code-based security để khai báo rằng mã không còn cho phép thực thi trong những kiểu mà quyền administrator hỗ trợ như:
không thể đọc hoặc viết lên các biến môi trường, đọc hoặc viết lên registry, không truy cập vào các đặc trưng trong .NET.

Security sẽ được bàn kĩ hơn trong chương 23.

Application Domains

Application domains là một cách tân quan trọng trong .NET và nó được thiết kế để có thể dễ dàng xử li các vấn đề khi chạy các
ứng dụng cần sự biệt lập với các ứng dụng khác nhưng vẫn có thể thông tin với các ứng dụng khác. Một ví dụ cổ điển đó lá một
ứng dụng web server application, nó phải phản hồi lại với một số lượng các yêu cấu từ các trình duyệt. Chắc chắn rằng sẽ tồn tại
cùng lúc nhiều thành phần có khả năng phản hồi để phục vụ cho các yêu cầu đó.

Trước thời .NET, sự lựa chọn giữa cho phép các thể hiện đó có thể dùng trong một tiến trình, cái mà sẽ mang lại sự rủi ro có thể
làm giảm độ an toàn của trang web, hay là cho phép các thể hiện đó chạy trên các tiến trình biệt lập, cái mà sẽ mang lại sự gia
tăng sự thực thi.

Giờ đây, đó là sự biệt lập mã thông qua các tiến trình. Khi bạn kích khởi một ứng dụng mới, nó sẽ chạy trong ngữ cảnh của tiếnt
trình. Các tiến trình Windows độc lập nhau thông qua vùng địa chỉ. Trong ý tưởng này mỗi tiến trình sẽ có sẵn 4 gigabytes bộ
nhớ ảo để chữa dữ liệu và mã thực thi (4GB là dành cho hệ thống 32-bit; hệ thống 64-bit có thể nhiều hơn). Windows gián tiếp
thực hiện cơ chê mở rộng để ánh xạ bộ nhớ ảo này với bộ nhớ vật lí thật hay đĩa. Mỗi tiến trình sẽ có sự ánh xạ khác nhau, sao
cho các vùng nhớ vật lí thật sự không trùng lấp nhau. Nó được minh họa bởi sơ đồ sau:
Một cách tổng quat, bất kì tiến trình nào cũng chỉ có thể truy cập đến bộ nhớ thông qua mộ địa chỉ ảo cụ thể - các tiến trình
không thể truy xuất trực tiếp bộ nhớ vật lí. Như vậy nó đơn giản là không cho phép một tiến trình có thể truy xuất đến vùng nhớ
được cấp cho một tiến trình khác. Nó cung cấp một cơ chế bảo đảm rằng những ứng xử tồi của mã không thể làm hỏng bất kì thứ
gì bên ngoài vùng địa chỉ của nó. (chú ý rằng trong Windows 9x, những cơ chế bảo vệ này không đươc thấu đáo như trong
NT/2000/XP, vì thế về mặt lí thuyết các ứng dụng có khả năng phá hủy Windows do viết lên vùng nhớ không thích hợp).

Các tiến trình không chỉ phục vụ như là cách để tạo nên sự tách biệt giữa các thể hiện khác nhau. Trong hệ thống Windows
NT/2000, nó còn làm đơn vị để gán các giấy phép và đặc quyền bảo mật. Mỗi tiến trình có một kí hiệu bảo mật riêng, để báo cho
Windows biết chính xác các thực thi mà tiến trình cho phép.

Cả hai phương pháp này đều có khả năng bảo mật tốt nhưng lại sinh ra một bất lợi lớn đó là thực thi. Thông thường các tiến trình
sẽ hoạt động chung với nhau, bởi vậy cần phải có sự truyền thông giữa chúng. Ví dụ như đâu đó một tiến trình gọi một thành
phần COM khả thi, và bởi vì được yêu cầu chạy trong tiến trình của chúng. Giống như cách mà COM vẫn làm. Khi đó các tiến
trình không thể dùng chung bộ nhớ, một tiến trình phức tạp được sử dụng để sao chép dữ liệu giữa các tiến trình. Nó sẽ gây trở
ngại lớin đến vấn đề thực thi. Nếu bạn muốn các thành phần có thể làm việc với nhau mà không muốn ảnh hưởng đến vấn đề
thực thi, cách duy nhất là sử dụng DLL-based components và mọi thứ sẽ hoạt động trong cùng một vùng đã chỉ (đây là một việc
mạo hiểm vì các thành phần ứng xử tồi sẽ làm hỏng tất cả mọi thứ).

Application domains được thiết kế như là một thành phần riêng biệt không gây ảnh hưởng đến vấn đề thực thi trong lúc các tiến
trình trao đổi dữ liệu. Ý tưởng này cho rằng một tiến trình được chia thành một số các application domains. Mỗi application
domain sẽ trả lời cho một ứng dụng đơn, và các loạt thực thi sẽ hoạt động như là một application domain độc lập:

Nếu các thực thi cúng sử dụng chung một vùng nhớ, rõ ràng chúng có thể dùng chung dữ liệu, bởi vì trên lí thuyết chúng có thể
truy xuất trục tiếp dữ liệu của nhau. Tuy nhiên đó chỉ là nguyên tắc, CLR sẽ bảo đảm rằng điều này sẽ không xảy ra trong thực tế
bằng cách kiểm tra kỹ lưỡng mã trong mỗi ứng dụng, để chắc rằng chúng không lạc ra khỏi vùng dữ liệu của chúng. Trước tiên
hầu như các trò bịp quá đáng sẽ bị loại bỏ, sau đó ứng dụng có thể hoạt động và không phải kích hoạt nó.

Thật sự, nó hoàn toàn có thể làm được điều này vì sự định kiểu mạnh của IL. Trong nhiều trường hợp, nếu mã thật sự dùng kiểu
không quản chẳng hạn như các con trỏ, kiểu dữ liệu đang dùng sẽ bảo đảm vùng nhớ sẽ được truy cập hợp lí. ví dụ, kiểu mảng
.NET sẽ tiến hành kiểm tra và bảo đảm rằng các thao tác trên mảng đều nằm trong phạm vi cho phép. Nếu một thực thi cần trao
đổi thông tin với các thực thi chạy trong các application domain khác chúng phải gọi dịch vụ điều khiển từ xa của .NET.
Mã được kiểm tra xem có truy cập dữ liệu ngoài application domain không được gọi là memory type-safe. Như vậy mã này có
thể hoạt động cùng với mã được bảo vệ ở các application domains khác nhau trong cùng một tiến trình.

Bẫy lỗi thông qua các ngoại lệ

.NET được thiết kế để đơn giản hoá quá trình bẫy lỗi thông qua các ngoại lệ. Những nhà phát triển C++ nên biết rằng, bởi vì IL là
hệ thống định kiểu mạnh, nó không thực thi các mối kết hợp bất lợi thông qua các ngoại lệ trong IL, đây là cách được đưa ra
trong C++. Tất nhiên khối finally cũng được hỗ trợ trong .NET và C#.

Chúng ta sẽ bàn kĩ về ngoại lệ trong chương 4. Sơ qua một chút, ý tưởng ở đây là một vùng mã được thiết kế như là các thủ tục
quản ngoại lệ, mỗi đoạn mã có thể giải quyết một điêu kiện lỗi riêng (ví dụ, một file không được tìm thấy, hoặc không được phép
thực thi một số lệnh). Những điều kiện này có thể được định nghĩa kĩ hoặc sơ qua tuỳ bạn. Cấu trúc ngoại lệ bảo đảm rằng khi
một điều kiện sinh lỗi xảy ra, ngay lập tức luồn thi hành sẽ nhảy đến thủ tục quản ngoại lệ.

Cơ cấu quản ngoại lệ tạo điều kiện thuận lợi để truyền cho một đối tượng thông tin chính xác về các điều kiện sinh ngoại lệ và
một thủ tục quản ngoại lệ. Đối tượng này có thể bao gồm một thông điệp thích hợp cho người dùng và chi tiết về nơi phát sinh
ngoại lệ.

Hầu hết các cơ cấu quản ngoại lệ, bao gồm cả điều khiển của chương trình sẽ treo khi một ngoại lệ được phát sinh, được quản bởi
ngôn ngữ bậc cao (C#, VB.NET, C++), và không một lệnh IL nào hỗ trợ việc đó. Ví dụ C#, quản sự kiện thông qua các khối mã
try{}, catch{}, finally{}, chúng ta sẽ bàn sau trong chương 4.

Những gì mà .NET làm là cung cấp cơ sở cho phép các trình biên dịch hướng .NET hỗ trợ việc quản ngoại lệ. Cụ thể nó cung cấp
một bộ các lớp .NET có thể miêu tả các ngoại lệ, và thực thi ngôn ngữ chéo cho phép truyền các đối tượng ngoại lệ cho các mã
quản ngoại lệ, bất chấp mã quản ngoại lệ được viết trong ngôn ngữ nào. Sự độc lập ngôn ngữ này không được hỗ trợ trong việc
quản ngoại lệ của C++ lẫn Jave, mặc dù nó vẫn tồn tại giới hạn trong cơ cấu COM cho việc quản lỗi: bao gồm việc trả về mã lỗi
trong các phương thức và truyền các đối tượng lỗi. Thật vậy các ngoại lệ đó được quản một cách nhất quán trong các ngôn ngữ
khác nhau nó đóng vai trò quyết định trong phát triển đa ngôn ngữ.

Dùng các thuộc tính

Attributes là một đặc trưng đã thân thuộc với những nhà phát triển C++ để viết các thành phần COM (thông qua việc sử dụng
Microsoft's COM Interface Definition Language (IDL)) dù vậy nó không thân thiện với những nhà phát triển Visual Basic hay
Java. Attribute cung cấp thông tin mở rộng liên quan đến các mục trong chương trình có thể được sử dụng bởi trình biên dịch.

Attributes được hỗ trợ trong .NET - và vì thế giờ đây nó được hỗ trợ trong C++, C#, và VB.NET. Một cái mới là các attribute
trong .NET là một cơ chế cho phép bạn có thể định nghĩa các attribute của riêng bạn trong mã nguồn. Các attribute tự định nghĩa
này có thể thay thế cho các siêu dữ liệu của các phương thức và kiểu dữ liệu tương ứng. .Do tính độc lập ngôn ngữ của .NET mà
các attribute có thể được định nghĩa trong một ngôn ngữ và có thể đọc bằng mã ở các ngôn ngữ khác.

Attributes sẽ được bàn trong chương 5 của cuốn sách này.

Các lớp .NET Framework


Có lẽ một trong những lợi ích lớn nhất của viết mã có quản, ít nhất là đối với một nhà phát triển, đó là bạn có thể sử dụng thư
viện lớp cơ sở của .NET.

Thư viện lớp cơ sở của .NET là một tập hợp lớn các lớp mã có quản được viết bởi Microsoft, những lớp này cho phép bạn thao
tác rất nhiều các tác vụ sẵn có trong Windows. Bạn có thể tạo các lớp của mình từ các lớp có sẵn trong thư viện lớp cơ sở của
.NET dựa trên cơ chế thừa kế đơn.

Thư viện lớp cơ sở của .NET rất trực quan và rất dễ sử dụng. Ví dụ, để tạo một tiến trình mới, bạn đơn giản gọi phương thức
Start() của lớp Thread. Để disable một TextBox, bạn đặt thuộc tính Enabled của đối tượng TextBox là false. Thư viện này được
thiết kế để dễ xài như là Visual Basic và Java. Tất nhiên là nó dễ sử dụng hơn các lớp của C++: các vỏ bọc ngoài các hàm API
thô như GetDIBits(), RegisterWndClassEx(), và IsEqualIID().

Mặt khác, những nhà phát triển C++ luôn dễ dàng truy cập đến các API, ngược lại những nhà phát triển Visual Basic và Java đã
bị giới hạn trong những thao tác hệ thống cơ bản mà ngôn ngữ đã từng ngôn ngữ đã cung cấp sẵn. Cái mới của thư viện lớp cơ sở
.NET là kết hợp tính đơn giản của các thư viện Visual Basic và Java với hầu hết các đặc tính trong các hàm Windows API. Có
nhiều đặc tính của Windows không sẵn có trong các lớp của thư viện .NET, trong trường hợp đó bạn cần phải gọi các hàm API,
những đặc tính này thường là các đặc tính lạ, ít sử dụng. Những đặc tính thông dụng đều đã được hỗ trợ đầy đủ trong thư viện lớp
của .NET. Và nếu bạn muốn gọi một hàm API, .NET gọi là "platform-invoke", cơ chế này luôn bảo đảm tính đúng đắn của kiểu
dữ liệu, vì vậy thao tác này không khó hơn việc gọi trực tiếp từ mã C++, nó được hỗ trợ cho cả C#, C++, và VB.NET.

WinCV, một tiện ích Windows-based, bạn có thể dùng để tham khảo các lớp, cấu trúc, giao diện, kiểu liệt kê trong thư viện .NET
base class. Chúng ta sẽ tìm hiểu WinCV trong chương 6.

Dù rằng chủ đề của chương 5 bàn về các lớp cơ sở, nhưng thực tế, chúng tôi chỉ nói về các cú pháp của ngôn ngữ C#, chủ yếu
quyển sách này chỉ cho các bạn về cách dùng các lớp khác nhau trong thư viện .NET base class. Một cách tổng quát .NET base
classes bao gồm các vấn đề:
• Các đặc tính lõi cung cấp bởi IL (chủ yếu là về các kiểu dữ liệu trong CTS, Chương 5)
• Hỗ trợ Windows GUI và controls (Chương 7)

• Web Forms (ASP.NET, 16)

• Data Access (ADO.NET, 10)

• Directory Access (Chương 13)

• File system và registry access (Chương 12)

• Networking và web browsing (Chương 20)

• .NET attributes và reflection (Chương 5)

• Truy xuất vào hệ điều hành Windows (các biến môi trường vv..., Chương 23)

• COM interoperability (18)

Một cách tình cờ, hầu hết các thư viện lớp cơ sở của .NET được viết bằng C#!

Các Namespace
Namespace là cách mà .NET dùng để chống lại sự xung đột tên giữa các lớp. Chẳng hạn như trường hợp bạn có một lớp mô tả
khách hàng gọi là lớp Customer, và sau đó một người khác cũng có một lớp giống như vậy.

Một namespace không chỉ là một nhóm các kiểu dữ liệu, mà nó làm cho tên của tất cả các kiểu dữ liệu trong cùng một không
gian tên sẽ có tiếp đầu ngữ là tên của namespace đó. Nó cũng cho phép một không gian tên nằm trong một không gian tên khác.
Ví dụ, hầu hết các hỗ trợ chung của các thư viện lớp cơ sở .NET đều nằm trong một không gian tên gọi là System. Lớp cơ sở
Array nằm trong không gian tên này có tên đầy đủ là System.Array.

.NET yêu cầu tất cả các kiểu đều phải được định nghĩa trong một không gian tên, ví dụ bạn có thể đặt lớp Customer của bạn
trong một không gian tên gọi là YourCompanyName. Lớp này sẽ có tên đầy đủ là YourCompanyName.Customer.

Nều một namespace không được khai báo rõ ràng, các kiểu sẽ được đặt vào một namespace toàn cục không tên.

Microsoft khuyên rằng các hỗ trợ của bạn nên đặt vào một namespace ít nhất là 2 cấp, cấp một là tên của công ty của bạn, cấp hai
là tên của công nghệ hoặc là phần mềm của gói sản phẩm đó, chẳng hạn như YourCompanyName.SalesServices.Customer. Làm
như vậy trong hầu hết các trường hợp đảm bảo rằng, các lớp trong ứng dụng của bạn không xung đột tên với các lớp của các tổ
chức khác.

Chúng ta sẽ xem xét thêm về namespace ở chương 2.

Tạo các ứng dụng .NET bằng C#


C# có thể dùng để tạo các ứng dụng console: các ứng dụng thuần văn bản chạy trên DOS window. Hầu như bạn chỉ tạo các ứng
dụng console khi cần kiểm tra các thư viện lớp, hoặc cho các tiến trình daemon Unix/Linux. Tât nhiên, bạn cũng có thể dùng C#
để tạo các ứng dụng dùng cho các công nghệ tương thích .NET. Trong phần này, chúng ta xem qua về các kiểu ứng dụng khác
nhau có thể tạo ra bằng C#.

Tạo các ứng dụng ASP.NET


ASP là một công nghệ của Microsoft dùng để tạo các trang web có nội dung động. Một trang ASP thực chất là một file HTML có
nhúng các khối server-side VBScript hay JavaScript. Khi một trình duyệt khách yêu cầu một trang ASP page, web server sẽ sinh
ra mã HTML, xử lí các server-side script khi chúng đến. Thường thì các script sẽ truy cập vào một cơ sở dữ liệu để lấy dữ liệu,
và biểu diễn trên trang HTML. ASP là cách đơn giản nhất để tạo các ứng dụng browser-based.

ASP tất nhiên cũng có một vài hạn chế. Trước tiên, các trang ASP thỉnh thoảng trở nên rất chậm bởi vì mã server-side được
thông dịch thay vì đựơc biên dịch. Thứ hai, các file ASP khó bảo trì bởi vì chúng không có cấu trúc; mã server-side ASP và
HTML được trộn lẫn với nhau. Thứ ba, ASP đôi khi kho phát triển bởi nó không quan tâm đến bẫy lỗi và kiểm tra kiểu. Cụ thể,
nếu bạn dùng VBScript và muốn bẫy lỗi trên các trang của bạn, bạn phải dung câu lệnh On Error Resume Next, và cho phép tất
cả các thành phần gọi thông qua một Err.Number để chắc rằng tất cả đều tốt.
ASP.NET là một phiên bản mới của ASP đã cải tiến rất nhiều các thiếu xót của nó. Nó không chỉ thay thế ASP; hơn thế, các
trang ASP.NET có thể sống chung với các ứng dụng ASP trên cùng một máy chủ. Tất nhiên bạn có thể lập trình ASP.NET với
C#!

Mặt dù các chương (14-16) sẽ bàn kĩ về ASP.NET, nhưng chúng ta cũng nói qua một vài đặc tính quan trọng của nó.

Các đặc tính của ASP.NET


Trước tiên, và có lẽ là quan trọng nhất, các trang ASP.NET là các trang có cấu trúc. Có nghĩa là mỗi trang là thực tế là một lớp
được thừa kế từ lớp .NET System.Web.UI.Page, và có thể ghi đè một tập các phương thức sẽ dùng trong thời gian sống của trang
web (bạn hãy tưởng tượng rằng nhữn sự kiện này như là anh em bà con với các sự kiện OnApplication_Start và OnSession_Start
trong file global.asa của ASP cũ.) Bởi vì bạn có thể chuyển các thao tác của một trang thành các sự kiện sáng nghĩa hơn, chính vì
thể mà các tramg ASP.NET dễ hiểu hơn.

Một điểm mạnh khác là các trang ASP.NET có thể được tạo trong VS.NET, cùng chung môi trường với các thành phần luận lí và
dữ liệu sẽ được dùng trong các trang web này. Một nhóm đề án VS.NET, hoặc solution, chứa tất cả các file liên quan đển một
ứng dụng. Hơn thế nữa bạn có thể bẫy lỗi các trang ASP của bạn ngay trong trình thiết kế; trước đây, thật là khó khăn để có thể
cấu hình InterDev và các đề án web server để thực hiện bẫy lỗi.

Rõ ràng, đặc tính ASP.NET's code-behind giúp các bạn có thể dễ dàng cấu trúc một trang web. ASP.NET cho phép bạn tách biệt
các chức năng server-side của trang thành một lớp, biên dịch lớp đó thành một DLL, và đặt DLL đó vào một thư mục bên dưới
phần HTML. Một code-behind chi phối đỉnh của một trang web tương đương với file DLL của nó. Khi một trình duyệt yêu cầu
trang, web server phát ra các sự kiện trong lớp của page's code-behind DLL.

Cuối cùng không kém phần quan trọng, ASP.NET thật sự đáng chú ý với khả năng tăng cường sự thực thi. Ngược lại với các
trang ASP được thông dịch cho mỗi yêu cầu, web server lưu giữ lại các trang ASP.NET sau quá trình biên dịch. Nghĩa là các yêu
cầu sau của một trang ASP.NET sẽ thực thi nhanh hơn trang đầu tiên.

ASP.NET dễ tạo các trang hơn bởi vì nó được chiếu bởi trình duyệt, bạnc có thể sử dụng một môi trường mạng intranet. Theo
kinh nghiệm truyền thống thì một ứng dụng form-based thường là tốt hơn một user interface, nhưng cũng khó bảo trì hơn vì nó
chạy trên nhiều máy khác nhau.

Với sự ra đời của Internet Explorer 5 và sự thực thi mơ hồ của Navigator 6, tất nhiên các đặc tính của ứng dụng form-based bị
che mờ. IE 5's hỗ trợ nhất quán và mạnh mẽconsistent cho DHTML cho phép các nhà lập trình tạo các ứng dụng web-based đẹp
là lớn hơn. Tất nhiên, các ứng dụng này bắt buộc phải theo chuẩn của IE và không được hỗ trợ bởi Navigator. Trong nhiều lĩnh
vực công nghiệp, chuẩn này đã trở nên phổ biến.

Web Forms
Để dễ dàng cho việc tạo các trang có cấu truc, Visual Studio .NET cung cấp Web Forms. Chúng cho phép bạn tạo các trang
ASP.NET sinh động như cách mà VB 6 hay C++ Builder windows đã làm; nó cách khác, bằng cách kéo các controls từ toolbox
vào form, sau đó sắp xếp cho đẹp, điền mã quản lí sự kiện thích hợp vào control đó. Khi bạn dung C# để tạo các Web Form, bạn
đang tạo một lớp C# được thừa kế từ lớp Page base, và một trang ASP được chỉ định như là code-behind. Tất nhiên, không bắt
buộc phải dùng C# để tạo một Web Form; bạn có thể dùng VB.NET hoặc một ngôn ngữ biết .NET khác.

In the past, the difficulty of web development has discouraged some teams from attempting it. To succeed in web development,
you had to know so many different technologies, such as VBScript, ASP, DHTML, JavaScript, and so on. By applying the Form
concepts to web pages, Web Forms promise to make web development easier. Only time will tell, however, how successful Web
Forms and Web Controls (which we'll look at next) will be at insulating the developer from the complexities of web design.

Web Controls

Các control thường được cư trú trên một Web Form không phải là các ActiveX control. Hơn nữa, chúng là XML tags trong ASP
namespace và browser có thể chuyển sang HTML và client-side script khi một trang được yêu cầu. Đặc biệt hơn, web server có
thể các điều khiển server-side control theo nhiều cách khác nhau, sinh ra sự biến đổi phù hợp với các yêu cầu của các web
browser riêng biệt. Điều này có nghĩa là sẽ dễ dàng viết các giao diện người dùng tinh vi cho các trang web, đừng bận tâm đến
vấn đề tương thích trình duyệt web– bởi vì Web Forms sẽ làm điều đó cho bạn.

Bạnc có thể dùng C# hay VB.NET để mở rộng hộp công cụ Web Form. Việc tạo một server-side control mới đơn giản là thực thi
lớp .NET System.Web.UI.WebControls.WebControl.

Web Services

Ngày nay, các trang HTML là nguyên nhân của hầu hết các xung đột trên World Wide Web. Với XML, các máy vi tính có một
định dạng device-independent để dùng cho việc truyền thông với các máy khác trên mạng Web. Trong tương lại, các máy tính có
thể sẽ dùng Web và XML để trao đổi thông tin hơn là dùng các line chuyên dụng và theo những định dạng riêng như EDI
(Electronic Data Interchange). Các Web Service được thiết kế cho một web hướng dịch vụ, trong đó các máy tính ở xa cung
cấp cho nhau các thông tin động có thể phân tích và tái định dạng, trước khi trao lại cho người dùng. Một Web Service là cách
đơn giản nhất để một máy tính có thể cung cấp thông tin cho các máy tính khác trên Web dưới định dạng XML.
Về mặt kĩ thuật, một Web Service trong .NET là một trang ASP.NET theo định dạng XML thay vì theo định dạng HTML để yêu
cầu các client. Các trang này có một code-behind DLL chứa một lớp xuất phát từ lớp WebService. VS.NET IDE cung cấp một cơ
chế để tiện cho việc phát triển Web Service.

Có hai lí do chính để một tổ chức chọn Web Services. Lí do thứ nhất là bởi vì chúng đáng tin cậy trên HTTP, Web Services có
thể dùng các mạng có sẵn (the Web) như một môi trường cho việc truyền thông. Một lí do khác là bởi vì các Web Service dùng
XML, một định dạng dữ liệu tự mô tả, mang tính phổ biến, và độc lập nền.

Tạo các Windows Form


Mặc dù C# và .NET được thiết kế để phát triển web, nhưng chúng vẫn hỗ trợ mạnh mẽ cho cái gọi là ứng dụng "fat client", các
ứng dụng có thể được cài đặt trên một máy người dùng cuối. Hỗ trợ này gọi là Windows Forms.

Một Windows Form là câu trả lời của .NET cho VB 6 Form. Dùng để thiết kế một giao diên window sinh động, bạn chỉ đơn giản
kéo các control từ vào trên Windows Form. Để xác định cách xử của window, bạn viết các thủ tục quản lí sự kiện cho form
controls. Một đề án Windows Form được dịch thành một EXE phải được cài đặt trong một môi trường ở máy tính người dùng
cuối. Giống như các kiểu đề án .NET khác, đề án Windows Form được hỗ trợ cho cả VB.NET và C#. Chúng ta sẽ nói kĩ về
Windows Forms trong chương 7.

Windows Controls

Mặc dù Web Forms và Windows Forms được phát triển theo cùng một cách, bạn dùng các loại khác nhau của controls để định vị
chúng. Web Forms dùng Web Controls, và Windows Forms dùng Windows Controls.

Một Windows Control là một ActiveX control. Đằng sau sự thực thi của một Window control, là sự biên dich sang một DLL để
có thể cài đặt trên máy khách. Thật vậy, .NET SDK cung cấp một tiện ích dùng để tạo một vỏ bọc cho các ActiveX control, vì thể
chúng có thể được đặt trong Windows Forms. Giống trường hợp này các Web Control, Windows Control được tạo thành từ một
lớp khác System.Windows.Forms.Control.

Windows Services
Một Windows Service là một chương trình được thiết kế để chạy trên nền Windows NT/2000/XP (không hỗ trợ trên Windows
9x). Các dịch vụ này rất hữu ích khi bạn muốn một chương trình có thể chạy liên tục và sẵn sàng đáp ứng các sự kiện mà không
cần người dùng phải khởi động. Ví dụ như một World Wide Web Service ở trên các web server luôn lắng nghe các yêu cầu từ
trình khách.

Thật dễ dàng để viết các dịch vụ trong C#. Với thư viện lớp cơ sở .NET Framework sẵn có trong không gian tên
System.ServiceProcess namespace chuyên dùng để tổ chức các tác vụ boilerplate kết hợp với các dịch vụ, ngoài ra, Visual Studio
.NET cho phép bạn tạo một đề án C# Windows Service, với các mã nguồn cơ bản ban đầu. Chúng ta sẽ khám cách viết một C#
Windows Services trong chương 22.

Tóm tắt
Chúng ta đã khảo sát nhiều vấn đề trong chương này, chúng ta sẽ tóm lại các vấn đề quan trọng trong .NET Framework và mối
quan hệ của nó với C#. Chúng tôi đã trình bày cách mà tất cả các ngôn ngữ hướng .NET được biên dịch thành Intermediate
Language trước khi được biên dịch và thực thi bởi Common Language Runtime. Chúng tôi cũng đã trình bày vai trò của các đặc
tính sau trong .NET trong quá trình biên dịch và thực thi:

• Các Assembly và thư viện lớp cơ sở của .NET


• Các thành phần COM

• Quá trình biên dịch JIT

• Các Application domain

• Garbage Collection

Lưu đồi sau cho ta một cái nhìn về vài trò của các đặc tính này trong quá trình biên dịch và thực thi:
Chúng tôi cũng đã trình bày những đặc trưng của IL, cụ thể là định nghĩa kiểu mạnh và hướng đối tượng. Chúng tôi đã chú thích
các đặc tính này ảnh hưởng đến các các ngôn ngữ hướng .NET khác, bao gồm C#. Chúng tôi cũng đã chú thich cách mà định
nghĩ kiểu mạnh có thể hỗ trợ tương hoạt ngôn ngữ chéo, cũng như các dịch vụ CLR chẳng hạn như trình thu gom rác và bảo mật.

Ở phần cuối của chương tôi đã nói về cách tạo các ứng dụng C# dựa trên các công nghệ của .NET trong đó có ASP.NET.

Giờ đây chúng ta đã có cái nền, chương tới sẽ chỉ ra cách viết mã trong C#.

Chương 2: Cơ bản C#

Tổng quan :
Trong chương này chúng tôi sẽ cung cấp cho bạn những kiến thức cơ bản nhất của ngôn ngữ lập trình C#. Những chủ đề chính
chúng ta sẽ được học sau đây :
• Khai báo biến
• Khởi tạo và phạm vi hoạt động của biến
• C#'s predefined data types
• Cách sử dụng các vòng lặp và câu lệnh.
• Gọi và hiển thị lớp và phương thức
• Cách sử dụng mảng
• Toán tử
• An toàn kiểu và cách để chuyển các kiểu dữ liệu
• Enumerations
• Namespaces
• Phương thức của hàm Main( )
• Cơ bản trình biên dịch dòng lệnh trong C#
• Using System.Console để thực hiện I/O
• Sử dụng chú thích trong C# và Visual Studio . NET
• Các định danh và từ khoá trong C#
Cuối chương này bạn sẽ có đủ khả năng viết một chương trình đơn giản bằng C# mà bạn không cần phải biết sự kế thừa hay
hướng đối tượng mà chúng tôi sẽ trình bày những phần này ở vài chương tới của quyển sách.
This document is created from a CHM file automatically by an unregistered copy of CHM-2-
Word.
The content of this chapter is skipped, please register CHM-2-Word to get full features.
For registration information, please refer to: http://www.macrobject.com

Chương trình đầu tiên !

Chúng ta sẽ bắt đầu theo cách truyền thống là tạo một chương trình viết bằng C# rồi cho biên dịch và chạy thử nghiệm. Việc
phân tích chương trình con này sẽ dẫn dắt bạn vào những chức năng chủ chốt của ngôn ngữ C#.
Bạn có thể biên dịch chương trình này bằng cách khỏ vào chương trình soạn thảo văn bản đơn giản, Notepad chẳng hạn, rồi
cho cất trữ dưới dạng tập tin với tên mở rộng là .cs (tắt chữ C sharp), rồi cho chạy trình biên dịch C# command_line (scs.exe) ví
dụ tập tin First.cs :
using System;

namespace Wrox.ProCSharp.Basics
{
class MyFirstCSharpClass
{
static void Main()
{
Console.WriteLine("This isn't at all like Java!");
Console.ReadLine();
return;

}
}
}
Một chương trình khả thi mang tên First.exe sẽ được tạo ra, và bạn có thể cho chạy chương trình này từ command line giống
như với DOS hoặc từ Windows Explorer như bất cứ chương trình khả thi nào.Chạy chương trình như sau :
csc First.cs
Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

First
This isn't at all like Java! Download First
Nhưng trước tiên bạn nên biết trên C# cũng như trên các ngôn ngữ C khác chương trình được cấu thành bởi câu lệnh
(statement ) và câu lệnh C# được kết thúc bởi một dấu chấm phẩy (;).Nhiều câu lệnh có thể gộp thành một khối được bao ở hai
đầu bởi cặp dấu ngoặc nghéo {}, câu lệnh nếu dài có thể tiếp tục xuống hàng dưới không cần đến một ký tự báo cho biết câu lệnh
tiếp tục hàng dưới.

Biến và Hằng
Một biến dùng để lưu trữ giá trị mang một kiểu dữ liệu nào đó.
Cú pháp C# sau đây để khai báo một biến :

[ modifier ] datatype identifer ;

Với modifier là một trong những từ khoá : public, private, protected, . . . còn datatype là kiểu dữ liệu (int , long , float. . . ) và
identifier là tên biến.
Thí dụ dưới đây một biến mang tên i kiểu số nguyên int và có thể được truy cập bởi bất cứ hàm nào.
thí dụ :
public int i ;
Ta có thể gán cho biến một giá trị bằng toán tử "=".
i = 10 ;
Ta cũng có thể khai báo biến và khởi tạo cho biến một giá trị như sau :
int i = 10 ;
Nếu ta khai báo nhiều biến có cùng kiểu dữ liệu sẽ có dạng như sau:
int x = 10; y = 20;

int x = 10;
bool y = true ; // khai báo trên đúng

int x = 10 , bool = true // khai báo trên có lỗi

Phạm vi hoạt động của biến (Variable Scope).

Phạm vi hoạt động của biến là vùng đoạn mã mà từ đấy biến có thể được truy xuất.
Trong một phạm vi hoạt động (scope), không thể có hai biến cùng mang một tên trùng nhau.
Thí dụ ta không thể làm như sau :
int x = 20;
// một số câu lệnh ở đây
int x = 30;
Xét ví dụ sau :
using System;
namespace Wrox.ProCSharp.Basics
{
public class ScopeTest
{
public static int Main()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
} // biến i ra khỏi phạm vi
// Chúng ta có thể khai báo thêm biến i ở đây
for (int i = 9; i >= 0; i--)
{
Console.WriteLine(i);
} // biến i ra khỏi phạm vi ở đây
return 0;
}
}
}
Download ScopeTest

Đoạn mã trên đơn giản in ra các số từ 0 đến 9, rồi lộn ngược lại từ 9 đến 0, sử dụng vòng lặp for.Chúng ta sẽ đề cập loại
vòng lặp này. Điều quan trọng là ở đây chúng ta khai báo biến i hai lần trong cùng một hàm ScopeTest.Chúng ta có thể làm được
điều này vì i được khai báo trong vòng lặp nghĩa là biến i cục bộ đối với vòng lặp.Một khi vòng lặp hoàn thành nhiệm vụ thì biến
thoát khỏi phạm vi, và không thể truy xuất được nữa.

Chúng ta xem tiếp một ví dụ khác :

public static int Main()


{
int j = 20;
for (int i = 0; i < 10; i++)
{
int j = 30; // không thể thực thi - j vẫn còn trong phạm vi
Console.WriteLine(j + i);
}
return 0;
}
Đoạn mã trên sẽ được biên dịch mặc dù có hai biến đặc tên j trong phạm vi không có phương thức hàm main( ) biến j được
định nghĩa ở lớp mức và không đi ra ngoài đến khi lớp bị huỷ ( trong trường hợp này chương trình kết thúc khi hàm main( ) kết
thúc), biến j được định nghĩa trong hàm main( ) phương thức ẩn trong lớp mức với biến cùng tên j nên khi chạy chương trình sẽ
hiện giá trị 30.

Ta xem đoạn thí dụ sau :


using System;

namespace Wrox.ProCSharp.Basics
{
class ScopeTest2
{
static int j = 20;

public static void Main()


{
int j = 30;
Console.WriteLine(j);
return;
}
}
}
Chương trình vẫn hoạt động và cho kết quả là 30.
Download ScopeTest2

HẰNG:
Một hằng (constant) là một biến nhưng trị không thể thay đổi được suốt thời gian thi hành chương trình. Đôi lúc ta cũng cần
có những giá trị bao giờ cũng bất biến.
Thí dụ
const int a = 100; // giá trị này không thể bị thay đổi
Trong định nghĩa lớp mà ta sẽ xem sau, người ta thường định nghĩa những mục tin (field) được gọi là read-only variable,
nghĩa là những biến chỉ được đọc mà thôi

Hằng có những đặc điểm sau :


• Hằng bắt buộc phải được gán giá trị lúc khai báo.Một khi đã được khởi gán thì không thể viết đè chồng lên.
• Trị của hằng phải có thể được tính toán vào lúc biên dịch, Do đó không thể gán một hằng từ một trị của một biến. Nếu
muốn làm thế thì phải sử dụng đến một read-only field.
• Hằng bao giờ cũng static, tuy nhiên ta không thể đưa từ khoá static vào khi khai báo hằng.
Có ba thuận lợi khi sử dụng hằng trong chương trình của bạn :
• Hằng làm cho chương trình đọc dễ dàng hơn, bằng cách thay thế những con số vô cảm bởi những tên mang đầy ý nghĩa
hơn.
• Hằng làm cho dễ sữa chương trình hơn.
• Hằng làm cho việc tránh lỗi dễ dàng hơn, nếu bạn gán một trị khác cho một hằng đâu đó trong chương trình sau khi bạn
đã gán giá trị cho hằng, thì trình biên dịch sẽ thông báo sai lầm.

Câu lệnh điều kiện

Câu lệnh điều kiện


Câu lệnh điều kiện if :
Cú pháp như sau:
if (condition)
statement(s)
[else
statement(s)]

Xét ví dụ sau:

Nếu có nhiều hơn một câu lệnh để thi hành trong câu điều kiện chúng ta sẽ đưa tất cả các câu lệnh này vào trong dấu ngoặc móc
({ ... }) giống như ví dụ dưới đây

bool isZero;
if (i == 0)
{
isZero = true;
Console.WriteLine("i is Zero");
}
else
{
isZero = false;
Console.WriteLine("i is Non-zero");
}

Đoạn code trên kiểm tra isZero có bằng 0 hay không.


Xét ví dụ:

Trong ví dụ dưới đây chúng ta dùng câu điều kiện íf . . . else để kiểm tra nhiều điều kiện .

using System;

namespace Wrox.ProCSharp.Basics
{
class MainEntryPoint
{
static void Main(string[] args)
{
Console.WriteLine("Type in a string");
string input;
input = Console.ReadLine();
if (input == "")
{
Console.WriteLine("You typed in an empty string");
}
else if (input.Length < 5)
{
Console.WriteLine("The string had less than 5 characters");
}
else if (input.Length < 10)
{
Console.WriteLine("The string had at least 5 but less than 10
characters");
}
Console.WriteLine("The string was " + input);
}
}
}

Download Conditional

Đoạn code trên không giới hạn bao nhiêu else if's trong câu điều kiện

if (i == 0)
Console.WriteLine("i is Zero"); // câu lệnh chỉ thi hành khi i == 0
Console.WriteLine("i can be anything"); // câu lệnh thi hành bất kì giá trị
của i

Câu lệnh switch


Các câu lệnh if nằm lồng rất khó đọc, khó gỡ rối. Khi bạn có một loạt lựa chọn phức tạp thì nên sử dụng câu lệnh switch.

Cú pháp như sau:

switch (biểu thức)


{ casce biểu thức ràng buộc:
câu lệnh
câu lệnh nhảy
[default: câu lệnh mặc định]
}

Thí dụ sau: Thí dụ sẽ kiểm tra integerA thoả đúng trong các trường hợp 1, 2, 3 không nếu không đúng sẽ thực thi trường hợp
default
switch (integerA)
{
case 1:
Console.WriteLine("integerA =1");
break;
case 2:
Console.WriteLine("integerA =2");
break;
case 3:
Console.WriteLine("integerA =3");
break;
default:
Console.WriteLine("integerA is not 1,2, or 3");
break;
}
Xem các Thí dụ sau để hiểu rõ thêm về switch:
// assume country and language are of type string
switch(country)
{
case "America":
CallAmericanOnlyMethod();
goto case "Britain";
case "France":
language = "French";
break;
case "Britain":
language = "English";
break;
}
switch(country)
{
case "au":
case "uk":
case "us":
language = "English";
break;
case "at":
case "de":
language = "German";
break;
}
Vòng lặp (Loops):
C# cung cấp cho chúng ta 4 vòng lặp khác nhau (for, while, do...while, và foreach)
cho phép chúng ta thực hiện một đoạn mã lặp lại đến khi đúng điều kiện lặp.

Vòng lặp for:

cú pháp:
for (initializer; condition; iterator)
statement(s)
Thí dụ:

Đoạn mã sau sẽ xúât ra tất cả số nguyên từ 0 đến 99:

for (int i = 0; i < 100; i = i+1)

{
Console.WriteLine(i);
}
Xét ví dụ sau:
using System;

namespace Wrox.ProCSharp.Basics
{
class MainEntryPoint
{
static void Main(string[] args)
{
// This loop iterates through rows...
for (int i = 0; i < 100; i+=10)
{
// This loop iterates through columns...
for (int j = i; j < i + 10; j++)
{
Console.Write(" " + j);
}
Console.WriteLine();
}
}
}
}

Kết quả được in ra khi chạy chương trình như sau:

Download Loop for

csc NumberTable.cs
Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

NumberTable
0 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49
50 51 52 53 54 55 56 57 58 59
60 61 62 63 64 65 66 67 68 69
70 71 72 73 74 75 76 77 78 79
80 81 82 83 84 85 86 87 88 89
90 91 92 93 94 95 96 97 98 99

Vòng lặp while (The while Loop)


Cú pháp như sau :

while(condition)
statement(s);
Thí dụ :
bool condition = false;
while (!condition)
{
// Vòng lặp thực hiện đến khi điều kiện đúng
DoSomeWork();
condition = CheckCondition(); // cho rằng CheckCondition() trả về kiểu bool
}

Vòng lặp do . . . while (The do…while Loop)


bool condition;
do
{
// Vòng lặp này sẽ thực hiện ít nhất một lần thậm chí nếu câu điều kiện sai
MustBeCalledAtLeastOnce();
condition = CheckCondition();
} while (condition);

Vòng lặp foreach (The foreach Loop)


Cho phép bạn rảo qua tất cả các phần tử bản dãy hoặc các tập hợp khác, và tuần tự xem xét từng phần tử một.Cú pháp như sau:

foreach (type identifier in expression) statement

thí dụ:
foreach (int temp in arrayOfInts)
{
Console.WriteLine(temp);
}

foreach (int temp in arrayOfInts)


{
temp++;
Console.WriteLine(temp);
}

Câu lệnh goto


goto Label1;
Console.WriteLine("This won't be executed");
Label1:
Console.WriteLine("Continuing execution from here");

Câu lệnh break


Ta dùng câu lệnh break khi muốn ngưng ngang xương việc thi hành và thoát khỏi vòng lặp.

Câu lệnh continue


Câu lệnh continue được dùng trong vòng lặp khi bạn muốn khởi động lại một vòng lặp nhưng lại không muốn thi hành phần lệnh
còn lại trong vòng lặp, ở một điểm nào đó trong thân vòng lặp.

Câu lệnh return


Câu lệnh return dùng thoát khỏi một hàm hành sự của một lớp, trả quyền điều khiển về phía triệu gọi hàm (caller). Nếu hàm có
một kiểu dữ liệu trả về thì return phải trả về một kiểu dữ liệu này; bằng không thì câu lệnh được dùng không có biểu thức.

Cấu trúc chương trình


Trước đây chúng ta đã được giới thiệu vài các phần của main 'building blocks' tạo bởi ngôn ngữ C# bao gồm khai báo biến, các
kiểu dữ liệu và các câu lệnh của chương trình chúng ta cũng đã thấy đoạn mã ngắn về phương thức hàm main(). Cái chúng ta
chưa thấy là làm thế nào để đặt tất cả chúng vào một khung của một chương trình hoàn chỉnh. Để trả lời chúng ta làm việc với
các class.

Lớp
Như bạn đã biết , các class tạo nên một chương trình lớn trong C# , để biết thêm chúng ta sẽ được trình bày ở chương 3 toàn bộ
về lập trình hướng đối tượng trong C#. Tuy nhiên nó thực sự có khả năng viết một chương trình mà không sử dụng đến lớp, ở
đây chúng ta chỉ cần một ít về lớp. Chúng ta sẽ được trang bị cú pháp cơ bản để gọi một lớp, nhưng chúng ta sẽ dành hướng đối
tượng cho chương sau.

Lớp là một khuôn mẫu thiết yếu mà chúng ta cần tạo ra đối tượng. Mỗi đối tượng chứa dữ liệu và các phương thức chế tác truy
cập dữ liệu. Lớp định nghĩa cái mà dữ liệu và hàm của mỗi đối tượng riêng biệt (được gọi là thể hiện) của lớp có thể chứa. Ví dụ
chúng ta có một lớp miêu tả một khách hàng nó được định nghĩa các trường như CustomerID, FirstName, LastName, và Address,
cái mà chúng ta giữ thông tin cụ thể khách hàng. Nó cũng có thể được miêu tả bởi các hành động trong các trường dữ liệu.

Các lớp thành viên


Dữ liệu và các hàm không có lớp đượp biết như là lớp thành viên

Thành phần dữ liệu

Thành phần dữ liệu (Data members) là những thành phần chứa dữ liệu cho lớp – trường (fields), Hằng số (constants), và sự kiện
(events).

Fields là các biến kết hợp với lớp. ví dụ, chúng ta định nghĩa một lớp PhoneCustomer với trường CustomerID, FirstName and
LastName như sau:
class PhoneCustomer
{
public int CustomerID;
public string FirstName;
public string LastName;
}

Once we have instantiated a PhoneCustomer object, we can then access these fields using the Object.FieldName syntax, for
example:

PhoneCustomer Customer1 = new PhoneCustomer();


Customer1.FirstName = "Burton";

Các hằng số có thể kết hợp với lớp như là biến. chúng ta khai báo một hằng số sử dụng từ khoá const. nếu nó khai báo public thì
có thể truy cập ở ngoài lớp.

class PhoneCustomer
{
public const int DayOfSendingBill = 1;
public int CustomerID;
public string FirstName;
public string LastName;
}

Hàm thành phần (Function Members):


Bao gồm các thuộc tính và các phương thức . Chúng ta sử dụng các từ khoá sau để bổ nghĩa cho một phương thức :

Modifier Description

new Phương thức ẩn một phương thức kế thừa với cùng kí hiệu
Phương thức có thể được truy cập bất kỳ
public

protected Phương thức có thể bị truy xuất không từ lớp nó thuộc hoặc từ lớp dẫn xuất;

internal Phương thức có thể được truy cập không cùng assembly

private Phương thức có thể được truy cập từ bên trong lớp nó phụ thuộc

static Phương thức có thể không được tính trên trên một lớp thể hiển cụ thể

virtual Phương thức bị ghi đè bởi một lớp dẫn xúât

abstract Phương thức trừu tượng

override Phương thức ghi đè một phương thức ảo kế thừa hoặc trừu tượng.

sealed Phương thức ghi đè một phương thức ảo kế thừa, nhưng không thể bị ghi đè từ lớp kế thừa này

extern Phương thức được thực thi theo bên ngoài từ một ngôn ngữ khác

Cấu trúc (Structs )


Chúng ta sẽ đề cập ngắn gọn là, ngoài các lớp nó cũng có thể để khai báo cho cấu trúc, cú pháp giống như cơ bản bạn biết
ngoại trừ chúng ta dùng từ khoá struct thay cho class.

Ví dụ chúng ta khai báo một cấu trúc PhoneCustomer được viết như sau :
struct PhoneCustomer
{
public const int DayOfSendingBill = 1;
public int CustomerID;
public string FirstName;
public string LastName;
}

Mảng (Arrays)
Mảng
Array là một cấu trúc dữ liệu cấu tạo bởi một số biến được gọi là những phần tử mảng. Tất cả các phần tử này đều thuộc một kiểu
dữ liệu. Bạn có thể truy xuất phần tử thông qua chỉ số (index). Chỉ số bắt đầu bằng zero.
Có nhiều loại mảng (array): mảng một chiều, mảng nhiều chiều.

Cú pháp :

type[ ] array-name;

thí dụ:
int[] myIntegers; // mảng kiểu số nguyên
string[] myString ; // mảng kiểu chuổi chữ

Bạn khai báo mảng có chiều dài xác định với từ khoá new như sau:

// Create a new array of 32 ints


int[] myIntegers = new int[32];
integers[0] = 35;// phần tử đầu tiên có giá trị 35
integers[31] = 432;// phần tử 32 có giá trị 432
Bạn cũng có thể khai báo như sau:
int[] integers;
integers = new int[32];
string[] myArray = {"first element", "second element", "third element"};

Làm việc với mảng (Working with Arrays)


Ta có thể tìm được chiều dài của mảng sau nhờ vào thuộc tính Length thí dụ sau :

int arrayLength = integers.Length

Nếu các thành phần của mảng là kiểu định nghĩa trước (predefined types), ta có thể sắp xếp tăng dần vào phương thức gọi là
static Array.Sort() method:

Array.Sort(myArray);

Cuối cùng chúng ta có thể đảo ngược mảng đã có nhờ vào the static Reverse() method:

Array.Reverse(myArray);
string[] artists = {"Leonardo", "Monet", "Van Gogh", "Klee"};
Array.Sort(artists);
Array.Reverse(artists);

foreach (string name in artists)


{
Console.WriteLine(name);
}
Mảng nhiều chiều (Multidimensional Arrays in C#)
Cú pháp :
type[,] array-name;
Thí dụ muốn khai báo một mảng hai chiều gồm hai hàng ba cột với phần tử kiểu nguyên :
int[,] myRectArray = new int[2,3];
Bạn có thể khởi gán mảng xem các ví dụ sau về mảng nhiều chiều:
int[,] myRectArray = new int[,]{ {1,2},{3,4},{5,6},{7,8}}; mảng 4 hàng 2 cột
string[,] beatleName = { {"Lennon","John"},
{"McCartney","Paul"},
{"Harrison","George"},
{"Starkey","Richard"} };

chúng ta có thể sử dụng :

string[,,] my3DArray;
double [, ] matrix = new double[10, 10];
for (int i = 0; i < 10; i++)
{
for (int j=0; j < 10; j++)
matrix[i, j] = 4;
}

Mảng jagged
Một loại thứ 2 của mảng nhiều chiều trong C# là Jagged array. Jagged là một mảng mà mỗi phần tử là một mảng với kích thước
khác nhau. Những mảng con này phải đuợc khai báo từng mảng con một.

Thí dụ sau đây khai báo một mảng jagged hai chiều nghĩa là hai cặp [], gồm 3 hàng mỗi hàng là một mảng một chiều:

int[][] a = new int[3][];


a[0] = new int[4];
a[1] = new int[3];
a[2] = new int[1];

Khi dùng mảng jagged ta nên sử dụng phương thức GetLength() để xác định số lượng cột của mảng. Thí dụ sau nói lên điều này:

using System;

namespace Wrox.ProCSharp.Basics
{
class MainEntryPoint
{
static void Main()
{
// Declare a two-dimension jagged array of authors' names
string[][] novelists = new string[3][];
novelists[0] = new string[] {
"Fyodor", "Mikhailovich", "Dostoyevsky"};
novelists[1] = new string[] {
"James", "Augustine", "Aloysius", "Joyce"};
novelists[2] = new string[] {
"Miguel", "de Cervantes", "Saavedra"};

// Loop through each novelist in the array


int i;
for (i = 0; i < novelists.GetLength(0); i++)
{
// Loop through each name for the novelist
int j;
for (j = 0; j < novelists[i].GetLength(0); j++)
{
// Display current part of name
Console.Write(novelists[i][j] + " ");
}
// Start a new line for the next novelist
Console.Write("\n");
}
}
}
}

Kết quả chương trình sau khi chạy:

csc AuthorNames.cs
Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.
AuthorNames
Fyodor Mikhailovich Dostoyevsky
James Augustine Aloysius Joyce
Miguel de Cervantes Saavedra

Operators

Category Operator

Arithmetic +-*/%

Logical & | ^ ~ && || !

String concatenation +

Increment and decrement ++ --

Bit shifting << >>

Comparison == != < > <= >=

Assignment = += -= *= /= %= &= |= ^= <<= >>=

Member access (for objects and structs) .

Indexing (for arrays and indexers) []

Cast ()

Conditional (the Ternary Operator) ?:

Object Creation new

Type information sizeof (unsafe code only) is typeof as

Overflow exception control checked unchecked

Indirection and Address * -> & (unsafe code only) []

Câu sau đây có nghĩa là x bằng với 3:

x = 3;

Nếu chúng ta muốn so sánh x với một giá trị chúng ta sử dụng kí hiệu sau ==:

if (x == 3)

Operator Shortcuts

Bảng dưới đây trình bày một danh sách đầy đủ của shortcut operators có giá trị trong C#:
Shortcut Operator Tương đương

x++, ++x x=x+1

x--, --x x=x-1

x += y x=x+y

x -= y x=x–y

x *= y x=x*y

x /= y x=x/y

x %= y x=x%y

x >>= y x = x >> y

x <<= y x = x << y

x &= y x=x&y

x |= y x=x|y

x ^= y x=x^y

Thí dụ :

int x = 5;
if (++x == 6)
{
Console.WriteLine("This will execute");
}
if (x++ == 7)
{
Console.WriteLine("This won't");
}

x += 5;
x = x + 5;

The Ternary Operator

Cú pháp :

condition ? true_value : false_value

Thí dụ :

int x = 1;
string s = x.ToString() + " ";
s += (x == 1 ? "man" : "men");
Console.WriteLine(s);

is
int i = 10;
if (i is object)
{
Console.WriteLine("i is an object");
}
sizeof
string s = "A string";
unsafe
{
Console.WriteLine(sizeof(int));
}

Operator Precedence

Group Operators

() . [] x++ x-- new typeof sizeof checked unchecked

Unary + - ! ~ ++x --x and casts

Multiplication/Division */%

Addition/Subtraction +-

Bitwise shift operators << >>

Relational < > <= >= is as

Comparison == !=

Bitwise AND &

Bitwise XOR ^

Bitwise OR |

Boolean AND &&

Boolean OR ||

Ternary operator ?:

Assignment = += -= *= /= %= &= |= ^= <<= >>= >>>=

This document is created from a CHM file automatically by an unregistered copy of CHM-2-
Word.
The content of this chapter is skipped, please register CHM-2-Word to get full features.
For registration information, please refer to: http://www.macrobject.com

Enumerations
Phương án thay thế hằng là enumeration (liệt kê), gồm một tập hợp những hằng đuợc đặt tên.

Chúng ta định nghĩa một enumeration giống như sau :


public enum TimeOfDay
{
Morning = 0,
Afternoon = 1,
Evening = 2
}
Ví dụ sau sử dụng enumeration:

class EnumExample
{
public enum TimeOfDay
{
Morning = 0,
Afternoon = 1,
Evening = 2
}
public static int Main()
{
WriteGreeting(TimeOfDay.Morning);
return 0;
}

static void WriteGreeting(TimeOfDay timeOfDay)


{
switch(timeOfDay)
{
case TimeOfDay.Morning:
Console.WriteLine("Good morning!");
break;
case TimeOfDay.Afternoon:
Console.WriteLine("Good afternoon!");
break;
case TimeOfDay.Evening:
Console.WriteLine("Good evening!");
break;
default:
Console.WriteLine("Hello!");
break;
}
}
}

Download EnumExample

Bạn có thể nhận được chuỗi đại diện của một enum. Thí dụ sử dụng enum TimeOfDay sau:

TimeOfDay time = TimeOfDay.Afternoon;


Console.WriteLine(time.ToString());

Kết quả sẽ viết ra chuỗi Afternoon.

Bạn có thể nhận được giá trị enum từ chuỗi.

TimeOfDay time2 = (TimeOfDay) Enum.Parse(typeof(TimeOfDay), "afternoon",


true);
Console.WriteLine((int)time2);

Namespaces
Namespace cung cấp cho ta cách mà chúng ta tổ chức quan hệ giữa các lớp và các kiểu
khác.Namespace( địa bàn hoạt động của các tên) là cách
mà .NET tránh né việc các tên lớp, tên biến, tên hàm. . đụng độ vì trùng tên giữa các lớp.
namespace CustomerPhoneBookApp
{
using System;

public struct Subscriber


{
// Code for struct here...
}
}

Ta có thể khai báo nhiều namespace như sau:

namespace Wrox
{
namespace ProCSharp
{
namespace Basics
{
class NamespaceExample
{
// Code for the class here...
}
}
}
}

Mỗi một namespace được soạn một tên.Như vậytên đầy đủ của ProCSharp namespace là Wrox.ProCSharp, và tên đầy dủ của lớp
NamespaceExample là Wrox.ProCSharp.Basics.NamespaceExample

Chúng ta cũng dùng cấu trúc này để tổ chức namespace trong định nghĩa namespace.Nên đoạn code có thể víêt như sau :

namespace Wrox.ProCSharp.Basics
{
class NamespaceExample
{
// Code for the class here...
}
}

Câu lệnh using


Từ khoá using giúp bạngiảm thiểu việc phải gõ những namespace trước các hàm hành sự hoặc thuộc tính, thí dụ sau sử dụng
namespace Wrox.ProCSharp thay vì phải gõ đầy đủ đường dẫn

using Wrox.ProCSharp;

class Test
{
public static int Main()
{
Basics.NamespaceExample NSEx = new Basics.NamespaceExample();
return 0;
}
}

Bí danh Namespace
Một cách sử dụng khác từ khoá using là gán những bí danh cho các lớp và namespace. Nếu bạn có 1 namespace dài lê thê mà bạn
muốn quy chiếu nhiều chỗ trên đoạn mã . bạn có thể gán một alias cho namespace.

Cú pháp :

using alias = NamespaceName;

Chúng ta sử dụng đối tượng này để trả về tên của lớp namespace thí dụ sau Introduction được đặt cho Wrox.ProCSharp.Basics
namespace đại diện cho đối tượng NamespaceExample Đối tượng này có một phương thức GetNamespace(), sử dụng phương
thức GetType() mà mọi lớp đều có thể truy xuất một đối tượng Type tượng trương cho kiểu dữ liệu của lớp. Chúng ta dùng đối
tượng này để trả về tên namespace cho lớp.

using System;
using Introduction = Wrox.ProCSharp.Basics;
class Test
{
public static int Main()
{
Introduction.NamespaceExample NSEx =
new Introduction.NamespaceExample();
Console.WriteLine(NSEx.GetNamespace());
return 0;
}
}

namespace Wrox.ProCSharp.Basics
{
class NamespaceExample
{
public string GetNamespace()
{
return this.GetType().Namespace;
}
}
}

Các trình biên dịch file C#


Phần này sẽ giới thiệu các bạn một số file khi bạn biên dịch chương trình ngoại trừ csc.exe. Trước tiên ta nên xác định loại file
bạn muốn tạo theo bảng sau:

Option Output

/t:exe Ứng dụng console mặc định

/t:library Lớp thư viện với manifest

/t:module Thành phần không có manifest

/t:winexe Một của sổ ứng dụng


Thí dụ:
namespace Wrox.ProCSharp.Basics
{
public class MathLib
{
public int Add(int x, int y)
{
return x + y;
}
}
}

Chúng ta biên dịch file C# sau thành .NET DLL sử dụng câu lệnh sau:

csc /t:library MathLibrary.cs


Download MathLibrary
using System;

namespace Wrox.ProCSharp.Basics
{
class Client
{
public static void Main()
{
MathLib mathObj = new MathLib();
Console.WriteLine(mathObj.Add(7,8));
}
}
}
biên dịch chương trình
csc MathClient.cs /r:MathLibrary.dll
kết quả là 15 download code

Console I/O
Để đọc một ký tự văn bản từ của sổ console, Chúng ta dùng phương thức Console.Read(), giá trị trả về sẽ là kiểu int hoặc kiểu
string tuỳ ý . Và chúng ta cũng có hai phương thức dùng để viết ra chuổi ký tự như sau

• Console.Write() - Viết một giá trị ra của sổ window


• Console.WriteLine() - tương tự trên nhưng sẽ tự động xuống hàng khi kết thúc lệnh

Thí dụ sau sẽ cho giá trị nhập kiểu int và giá trị in ra kiểu chuổi

int x = Console.Read();
Console.WriteLine((char)x);

Giá trị trả về kiểu string:

string s = Console.ReadLine();
Console.WriteLine(s);

Giả sử có đoạn mã như sau:

int i = 10;
int j = 20;
Console.WriteLine("{0} plus {1} equals {2}", i, j, i + j);

Kết quả hiển thị như sau:

10 plus 20 equals 30
int i = 940;
int j = 73;
Console.WriteLine(" {0,4}\n+{1,4}\n ----\n {2,4}", i, j, i + j);

Kết quả:

940
+ 73
----
1013

Từ định danh và từ khoá


Trong phần cuối của chương cơ bản C# chúng ta khảo sát các quy tắc để đặt tên cho các biến, lớp, các phương thức. . .

Từ định danh là tên chúng ta đặt cho biến, để định nghĩa kiểu sử dụng như các lớp , cấu trúc, và các thành phần của kiểu này. C#
có một số quy tắc để định rõ các từ định danh như sau:

• Chúng phải bắt đầu bằng ký tự không bị gạch dưới


• Chúng ta không được sử dụng từ khoá làm từ định danh

Trong C# có sẵn một số từ khoá (keyword).

abstract do implicit params switch

as double in private this

base else int protected throw

bool enum interface public true


break event internal readonly try

byte explicit is ref typeof

case extern lock return uint

catch false long sbyte ulong

char finally namespace sealed unchecked

checked fixed new short unsafe

class float null sizeof ushort

const for object stackalloc using

continue foreach operator static virtual

decimal goto out string volatile

default if override struct void

delegate while

Tóm tắt
Trong chương này chúng ta đã khảo sát những cú pháp cơ bản của C#, Viết một số đoạn code đơn giản, chương trình C#.
Chúng ta đã họ được nhiều nền tảng cơ bản trong C#, phong cách viết ngôn ngữ C#, tóm tắt các chủ đề các bạn cần phải nắm rõ
như sau:

• Phạm vi của biến và các cấp độ truy cập


• Khai báo biến của các kiểu dữ liệu khác nhau

• Điều khiển thi hành chương trình C#

• Gọi và khai báo các lớp và các phương thức

• Làm việc với mảng

• Toán tử trong C#

• Chuyển đổi dữ liệu giữa các kiểu khác nhau

• Làm thế nào mà stack và heap được thi hành bởi CLR

• Các mã ghi chú

Bây giờ bạn đã nắm được cơ bản nền của C# FrameWork, để đi sau hơn trong ngôn ngữ C# bạn cần phải cần tìm hiểu thêm lập
trình hướng đối tượng của C#, Đây là tính năng mạnh và quan trong bạn không thể bỏ qua, chương tới chúng ta sẽ được học về
nó.

Lớp và Thừa kế:


Chúng ta đã được xem cách sử dụng lớp trong chương 2 nhưng để nắm được mối liên hệ giữa các chương, chúng tôi sẽ tóm tắt
một vài khái niệm về lớp. Lớp trong C# được định nghĩa với cú pháp sau:
class MyClass
{
private int someField;

public string SomeMethod(bool parameter)


{
}
}

Các lớp bao gồm nhiều thành viên, mỗi thành viên là thuật ngữ(term) dùng để chỉ đến một dữ liệu hay một chức năng nào đó
được định nghĩa trong lớp đó. Ví dụ chúng ta dùng thuật ngữ Function để chỉ những thành viên chứa mã như các phương
thức(methods), các thuộc tính(properties), constructor, hay các nạp chồng toán hạng(Operator Overloads).

Tất cả các lớp trong C# là những kiểu tham khảo. Tức là khi bạn khai báo một kiểu lớp thì có một biến lưu trữ sự tham khảo đến
một thể hiện (instance) của lớp đó. Và sử dụng lệnh new để tạo ra một đối tượng. Ví dụ tạo ra đối tượng myObject như sau:

MyClass myObject;
myObject = new MyClass();

Tuy nhiên bạn có thể khai báo và khởi tạo đối tượng cùng một lúc.

MyClass myObject = new MyClass();

Đơn thừa kế:


C# hỗ trợ đơn thừa kế giữa các lớp. Một lớp có thể thừa hưởng những thuộc tính và phương thức từ một lớp khác. Cú pháp:

class MyDerivedClass : MyBaseClass


{
// functions and data members here
}

Cú pháp này khác với C++ về phạm vi, không có bổ từ truy cập(access modifier). Tức là C# không hỗ trợ như C++ về các khái
niệm thừa kế public hay private vì nó làm ngôn ngữ thêm phức tạp. Trong thực tế thì thừa kế private rất ít được sử dụng.

Trong C# một lớp bắt buột phải thừa kế từ một lớp nào đó. C# hỗ trợ một lớp cơ sở toàn diện gọi là System.Object.

Phương thức nạp chồng(Overloading):


C# hỗ trợ phương thức nạp chồng với một vài dạng phương thức khác nhau về những đặc tính sau: tên, số lượng thông số, và
kiểu thông số. Nhưng nó không hỗ trợ những thông số mặc định như C++ và VB. Một cách đơn giản là bạn khai báo những
phương thức cùng tên nhưng khác số lượng và kiểu của thông số:

class ResultDisplayer
{
void DisplayResult(string result)
{
// implementation
}

void DisplayResult(int result)


{
// implementation
}
}

Bởi vì C# không hỗ trợ những thông số tuỳ chọn nên bạn cần sử dụng những phương thức nạp chồng để đạt được cùng một hiệu
quả:

class MyClass
{
int DoSomething(int x) // want 2nd parameter with default value 10
{
DoSomething(x, 10);
}

int DoSomething(int x, int y)


{
// implementation
}
}

Trong bất kỳ một ngôn ngữ nào, phương thức nạp chồng có thể đem đến một lỗi nghiêm trọng nếu nó bị gọi sai. Trong chương
tới ta sẽ bàn cách để tránh đều đó. Trong C# có một vài điểm khác nhỏ về thông số trong các phương thức nạp chồng cần biết
như sau:

• Nó không chấp nhận hai phương thức chỉ khác nhau về kiểu trả về.
• Nó không chấp nhận hai phương thức chỉ khác nhau về đặc tính của một thông số đang được khai báo như ref hay out.

Phương thức Overriden và Hide:


Bằng cách khai báo virtual trong một hàm ở lớp cơ sở thì cho phép hàm đó được overriden trong bất kỳ một lớp thừa hưởng nào.

class MyBaseClass
{
public virtual string VirtualMethod()
{
return "This method is virtual and defined in MyBaseClass";
}
}

Như ví dụ trên, tức là ta có thể tạo ra một sự thực thi khác của VirtualMethod() trong một lớp thừa hưởng của MyBaseClass. Và
khi gọi phương thức trong một thể hiện của lớp thừa hưởng thì phương thức của lớp thừa hưởng sẽ được thi hành mà không quan
tâm đến phương thức đó ở lớp cơ sở. Khác với C++ và Java, trong C# những hàm không được khai báo virtual mặc định mà bạn
phải khai báo virtual một cách rõ ràng và khi một hàm muốn override một hàm khác thì phải sử dụng từ khoá override:

class MyDerivedClass : MyBaseClass


{
public override string VirtualMethod()
{
return "This method is an override defined in MyDerivedClass";
}
}

Những trường thành viên(member fields) và những hàm tĩnh thì không được khai báo Virtual.

Nếu một phương thức có cùng đặc tính trong cả hai khai báo ở lớp cơ sở và lớp thừa hưởng nhưng các phương thức này thì
không khai báo virtual hay overriden thì sẽ được gọi là: "lớp thừa hưởng hide lớp cơ sở đó". Kết quả là: phương thức nào được
gọi phụ thuộc vào kiểu của biến được sử dụng để tham khảo đến thể hiện, chứ không phải kiểu của chính thể hiện đó.

Trong hầu hết mọi trường hợp bạn luôn thích override một phương thức hơn là hide nó. Bởi vì hide dễ gây ra những lỗi nghiêm
trọng. Nhưng cú pháp C# được thiết kế để bảo đảm cho những nhà phát triển sẽ được cảnh báo trong thời gian biên dịch về vấn
đề nghiêm trọng này.

Nếu bạn tạo ra hai phương thức hoàn toàn giống nhau ở cả lớp thừa hưởng và lớp cơ sở mà không có khai báo virtual và override
thì bạn sẽ bị cảnh báo trong khi biên dịch. Trong C#, bạn nên sử dụng từ khoá new để đảm bảo bạn muốn hide phương thức đó.

Gọi các phiên bản cơ sở của các chức năng(base Versions of


Functions):
Trong C# có một cú pháp đặc biệt để gọi những phiên bản cơ sở của một phương thức từ một lớp thừa hưởng. Cú pháp :
base.<methodname>(). Ví dụ:
class CustomerAccount
{
public virtual decimal CalculatePrice()
{
// implementation
}
}

class GoldAccount : CustomerAccount


{
public override decimal CalculatePrice()
{
return base.CalculatePrice() * 0.9M;
}

Các lớp và hàm Abstract :


C# cho phép cả lớp và phương thức có thể khai báo abstract. Một lớp abstract không được thể hiện và một phương thức abstract
không được thực thi mà phải được overriden trong bất kỳ lớp thừa hưởng không abstract nào. Một phương thức abstract sẽ tự
động được khai báo virtual. Nếu một lớp có phương thức abstract thì nó cũng là lớp abstract và được khai báo như sau:

abstract class Building


{
public abstract decimal CalculateHeatingCost(); // abstract method
}

Sealed các lớp và phương thức:


C# cho phép các lớp và phương thức được khai báo sealed. Nếu là lớp có nghĩa là bạn không được quyền thừa kế lớp đó, nếu là
phương thức tức là bạn không được phép override nó.

C# sử dụng từ khoá sealed trước tên lớp và phương thức:

sealed class FinalClass


{
// etc
}

class DerivedClass : FinalClass // wrong. Will give compilation error


{
// etc
}
class MyClass
{
public sealed override void FinalMethod()
{
// etc.
}
}

class DerivedClass : MyClass


{
public override void FinalMethod() // wrong. Will give compilation error
{
}
}

Những bổ từ truy cập(access modifiers):


Trong C# cung cấp một số lượng những bổ từ để cho biết sự tồn tại của một thành viên của một lớp. C# có 5 bổ từ như sau:
Accessibility Mô tả

public Biến và phương thức có thể được truy cập từ bất kỳ nơi đâu.

internal Biến và phương thức chỉ có thể được truy cập trong cùng một gói.

protected Biến hay phương thức chỉ có thể được truy cập từ kiểu của nó hay từ những kiểu thừa kế kiểu đó.

protected internal Biến hay phương thức có thể được truy cập từ gói hiện tại, hay từ những kiểu thừa kế kiểu hiện tại.

private Biến hay phương thức chỉ có thể truy cập từ trong kiểu của nó.

Thuộc tính(properties):
Để định nghĩa thuộc tính trong C# bạn dùng cú pháp sau:

public string SomeProperty


{
get
{
return "This is the property value";
}
set
{
// do whatever needs to be done to set the property
}
}

Có sự hạn chế thông thường ở đây là: Thủ tục get không có tham số và phải trả về cùng kiểu với thuộc
tính đã được khai báo. Bạn không nên khai báo tường minh các tham số trong thủ tục set, mà trình biên
dịch sẽ tự động biết là có một tham số cùng kiểu trỏ đến giá trị. Cho một ví dụ, đoạn mã sau chứa một
thuộc tính gọi là ForeName, nó sẽ cài một trường foreName có chiều dài giới hạn:

private string foreName;

public string ForeName


{
get
{
return foreName;
}
set
{
if (value.Length > 20)
// code here to take error recovery action
// (eg. throw an exception)
else
foreName = value;
}
}

Khác với VB các thủ tục get và set được định nghĩa như là những hàm riêng biệt, trong C# chúng được
khai báo cùng nhau trong một khai báo thuộc tính đơn.Trong VB bạn khai báo tường minh tham số cho
thủ tục set và có thể chọn tên của nó, nhưng ngược lại trong C# tham số này hoàn toàn giả lập và luôn
mang tên là value.

Thuộc tính chỉ đọc và chỉ viết:


Bạn có thể tạo ra thuộc tính chỉ đọc bằng cách bỏ thủ tục set trong khai báo và tạo ra thuộc tính chỉ ghi
bằng cách bỏ thủ tục get trong khai bao thuộc tính đó.
Ví dụ để định nghĩa thuộc tính Forename là chỉ đọc:

private string foreName;

public string ForeName


{
get
{
return foreName;
}
}

Bổ từ truy cập:
C# không cho phép cài đặt những bổ từ khác nhau cho thủ tục set và get. Nếu bạn muốn tạo ra một
thuộc tính có public để đọc nhưng lại muốn hạn chế protected trong gán thì đầu tiên bạn phải tạo thuộc
tính chỉ đọc với bổ từ public sau đó tạo một hàm set() với bổ từ protected ở bên ngoài thuộc tính đó.

public string ForeName


{
get
{
return foreName;
}
}

protected void SetForeName(string value)


{
if (value.Length > 20)
// code here to take error recovery action
// (eg. throw an exception)
else
foreName = value;
}

Thuộc tính Virtual và Abstract:


C# cho phép bạn tạo một thuộc tính virtual hay abstract. Để khai báo một thuộc tính virtual, overriden hay
abstract bạn chỉ cần thêm từ khoá đó trong lúc định nghĩa thuộc tính. Ví dụ để tạo một thuộc tính abstract
thì cú pháp như sau:

public abstract string ForeName


{
get;
set;
}

Giao diện :
C# hỗ trợ giao diện (Interfaces). Khi thừa kế một giao diện, một lớp đang khai báo sẽ thực thi những hàm nào đó. Chúng ta sẽ
minh họa về giao diện thông qua việc giới thiệu một giao diện đã được Microsoft định nghĩa, System.IDisposable. IDisposable
chứa một phương thức Dispose() dùng để xoá mã.

public interface IDisposable


{
void Dispose();
}

Trên ví dụ trên ta thấy việc khai báo một giao diện làm việc giống như việc khai báo một lớp Abstract, nhưng nó không cho phép
thực thi bất kỳ một thành phần nào của giao diện. Một giao diện chỉ có thể chứa những khai báo của phương thức, thuộc tính, bộ
phận lập mục lục, và sự kiện.
Bạn không thể khởi tạo một giao diện thực sự mà nó chỉ chứa những thành phần bên trong nó. Một giao diện thì không có
Constructor hay các trường. Một giao diện thì không cho phép chứa các phương thức nạp chồng.

Nó cũng không cho phép khai báo những bổ từ trên các thành phần trong khi định nghĩa một giao diện. Các thành phần bên trong
một giao diện luôn luôn là public và không thể khai báo virtual hay static.

Định nghĩa và thi hành một giao diện:


Chúng ta minh họa cách để định nghĩa và sử dụng giao diện bằng việc phát triển những ví dụ ngắn trình bày mẫu thừa kế giao
diện. Ví dụ về những account ngân hàng. Chúng ta sẽ viết mã để cho phép các máy điện toán chuyển khoảng qua lại giữa các
account. Có nhiều công ty thực thi account và đều đồng ý với nhau là phải thi hành một giao diện IBankaccount có các phương
thức nạp hay rút tiền và thuộc tính trả về số tài khoản.

Để bắt đầu, ta định nghĩa giao diện IBank:

namespace Wrox.ProCSharp.OOCSharp.BankProtocols
{
public interface IBankAccount
{
void PayIn(decimal amount);
bool Withdraw(decimal amount);

decimal Balance
{
get;
}
}
}

Chú ý: Tên của giao diện thường phải có ký tự I đứng đầu để nhận biết đó là một giao diện.

Có ý kiến là chúng ta có thể viết các lớp mô tả Account. Những lớp này có thể khác nhau hoàn toàn. Nhưng trên thực tế chúng
đều thực thi giao diện IBankAccount.

Hãy bắt đầu với lớp đầu tiên :

namespace Wrox.ProCSharp.OOCSharp.VenusBank
{
public class SaverAccount : IBankAccount
{
private decimal balance;

public void PayIn(decimal amount)


{
balance += amount;
}

public bool Withdraw(decimal amount)


{
if (balance >= amount)
{
balance -= amount;
return true;
}
Console.WriteLine("Withdrawal attempt failed.");
return false;
}

public decimal Balance


{
get
{
return balance;
}
}
public override string ToString()
{
return String.Format("Venus Bank Saver: Balance = {0,6:C}", balance);
}
}
}

Trong ví dụ trên chúng ta duy trì một trường private balance và điều chỉnh số lượng này khi tiền được nạp hay rút. Chú ý chúng
ta xuất ra một thông báo lỗi khi thao tác rút tiền không thành công vì thiếu số tiền trong tài khoản.

Xét dòng lệnh sau:

public class SaverAccount : IBankAccount

Chúng ta khai báo lớp SaverAccount thừa kế giao diện IBankAccount. Tuy trên ví dụ trên ta không nêu rõ ràng lớp SaverAccount
thừa kế từ một lớp nào khác nhưng trên thực tế ta có thể khai báo một lớp thừa kế từ một lớp khác và nhiều giao diện như sau:

public class MyDerivedClass : MyBaseClass, IInterface1, IInterface2

Thừa kế từ IBankAccount có nghĩa là SaverAccount lấy tất cả các thành phần của IBankAccount nhưng nó không thể sử dụng các
phương thức đó nếu nó không định nghĩa lại các hành động của từng phương thức. Nếu bỏ quên một phương thức nào thì trình
biên dịch sẽ báo lỗi.

Để minh họa cho các lớp khác nhau có thể thực thi cùng một giao diện ta xét ví dụ về một lớp khác là GoldAccount.

namespace Wrox.ProCSharp.OOCSharp.JupiterBank
{
public class GoldAccount : IBankAccount
{
// etc
}
}

Chúng ta không mô tả chi tiết lơp GoldAccount bởi vì về cơ bản nó giống hệt lớp SaverAccount. Điểm nhấn mạnh ở đây là
GoldAccount không liên kết với VenusAccount và chúng cùng thực thi một Interface

Bây giờ ta có những lớp và chúng ta kiểm tra chúng:

using Wrox.ProCSharp.OOCSharp.BankProtocols;
using Wrox.ProCSharp.OOCSharp.VenusBank;
using Wrox.ProCSharp.OOCSharp.JupiterBank;
Chúng ta có phương thức main():

namespace Wrox.ProCSharp.OOCSharp
{
class MainEntryPoint
{
static void Main()
{
IBankAccount venusAccount = new SaverAccount();
IBankAccount jupiterAccount = new GoldAccount();
venusAccount.PayIn(200);
venusAccount.Withdraw(100);
Console.WriteLine(venusAccount.ToString());
jupiterAccount.PayIn(500);
jupiterAccount.Withdraw(600);
jupiterAccount.Withdraw(100);
Console.WriteLine(jupiterAccount.ToString());
}
}
}

Kết quả xuất ra là:

Venus Bank Saver: Balance = £100.00


Withdrawal attempt failed.
Jupiter Bank Saver: Balance = £400.00
Xem toàn bộ chương trình trên Thực thi chương trình

Chúng ta có thể trỏ đến bất kỳ thể hiện của bất kỳ lớp nào thực thi cùng một giao diện. Nhưng chúng ta chỉ được gọi những
phương thức là thành phần của giao diện thông qua sự tham khảo đến giao diện này. Nếu muốn gọi những phương thức mà
không là thành phần trong giao diện thì ta phải tham khảo đến những kiểu thích hợp. Như ví dụ trên ta có thể thực thi phương
thức ToString() mặc dù nó không là thành phần được khai báo trong giao diện IBankAccount bởi vì nó là thành phần của
System.Object.

Một giao diện có thể tham khảo đến bất kỳ lớp nào thực thi giao diện đó.

Ví dụ ta có một mảng kiểu một giao diện nào đó thì các phần tử của mảng có thể tham khảo đến bất kỳ lớp nào thực thi giao diện
đó:

IBankAccount[] accounts = new IBankAccount[2];


accounts[0] = new SaverAccount();
accounts[1] = new GoldAccount();

Thừa kế giao diện :


C# cho phép những giao diện có thể thừa kế các giao diện khác. Khi một giao diện thừa kế một giao diện khác thì nó có thể thi
hành tất cả các phương thức định nghĩa trong giao diện đó và những phương thức của nó định nghĩa. Ví dụ tao tạo ra một giao
diện mới thừa kế giao diện IBanKAccount :

namespace Wrox.ProCSharp.OOCSharp.BankProtocols
{
public interface ITransferBankAccount : IBankAccount
{
bool TransferTo(IBankAccount destination, decimal amount);
}
}

Như vậy giao diện ITransferBankAccount phải thi hành tất cả các phương thức trong giao diện IBankAccount và phương thức
TransferTo.

Chúng ta sẽ minh hoạ ITransferBankAccount bằng một ví dụ bên dưới về một current account. Lớp CurrentAccount được định
nghĩa gần giống hệt với các lớp SaverAccount và GoldAccount, vì thế trong đoạn mã bên dưới chúng tôi đã tô màu các dòng khác
nhau:

public class CurrentAccount : ITransferBankAccount


{
private decimal balance;

public void PayIn(decimal amount)


{
balance += amount;
}

public bool Withdraw(decimal amount)


{
if (balance >= amount)
{
balance -= amount;
return true;
}
Console.WriteLine("Withdrawal attempt failed.");
return false;
}

public decimal Balance


{
get
{
return balance;
}
}

public bool TransferTo(IBankAccount destination, decimal amount)


{
bool result;
if ((result = Withdraw(amount)) == true)
destination.PayIn(amount);
return result;
}

public override string ToString()


{
return String.Format("Jupiter Bank Current Account: Balance = {0,6:C}",
balance);
}
}

We can demonstrate the class with this code:

static void Main()


{
IBankAccount venusAccount = new SaverAccount();
ITransferBankAccount jupiterAccount = new CurrentAccount();
venusAccount.PayIn(200);
jupiterAccount.PayIn(500);
jupiterAccount.TransferTo(venusAccount, 100);
Console.WriteLine(venusAccount.ToString());
Console.WriteLine(jupiterAccount.ToString());
}

Khi thực thi đoạn mã trên bạn sẽ thấy kết quả như sau:

CurrentAccount
Venus Bank Saver: Balance = £300.00
Jupiter Bank Current Account: Balance = £400.00
Thực thi chương trình trên

Construction and Disposal


Constructor :
Cú pháp khai báo một Constructor là : chúng ta khai báo một phương thức mà cùng tên với lớp và không có kiểu trả về.

public class MyClass


{
public MyClass()
{
}
// rest of class definition

Như trong c++ và java, bạn có thể không cần định nghĩa constructor trong lớp của bạn nếu không cần thiết. Nếu bạn không định
nghĩa một constructor nào trong lớp của bạn thì trình biên dịch tạo một constructor mặc định để khởi tạo một số giá trị mặc định
như: gán chuỗi rỗng cho chuỗi, gán 0 cho kiểu số, false cho kiểu bool.
Các contructor theo cùng luật overloading như các phương thức khác. Bạn cũng có thể tạo nhiều constructor cùng tên và khác
tham số giống như các phương thức nạp chồng :

public MyClass() // zero-parameter constructor


{
// construction code
}
public MyClass(int number) // another overload
{
// construction code
}

Chú ý : khi bạn đã định nghĩa một constructor trong lớp của bạn thì trình biên dịch sẽ không tự động tạo ra constructor mặc định.

Chúng ta có thể định nghĩa các constructor với các bổ từ private và protected để chúng không thể được nhìn thấy trong các lớp
không có quan hệ:

public class MyNumber


{
private int number;
private MyNumber(int number) // another overload
{
this.number = number;
}
}

Chú ý: Nếu bạn định nghĩa một hay nhiều constructor private thì những lớp thừa kế lớp của bạn sẽ không thể khởi tạo được. Do
đó chúng ta phải cân nhắc kỹ lưỡng khi định nghĩa bổ từ của một constructor.

Constructor tĩnh(static):
Chúng ta định nghĩa một constructor tĩnh để khởi tạo giá trị cho các biến tĩnh.

class MyClass
{
static MyClass()
{
// initialization code
}
// rest of class definition
}

Một nguyên nhân để viết một constructor tĩnh là: Nếu lớp của bạn có một số trường hay thuộc tính cần được khởi tạo từ bên
ngoài trước khi lớp được sử dụng lần đầu tiên.

Chúng ta không thể biết chắc được khi nào một constructor tĩnh sẽ được thực hiện. Và chúng ta cũng không biết trước các
constructor tĩnh của các lớp khác nhau sẽ thực hiện những gì. Nhưng chúng ta có thể chắc chắn rằng constructor tĩnh chỉ chạy
một lần và nó sẽ được gọi trước khi đoạn mã của bạn tham khảo đến lớp đó. Trong C#, Constructor tĩnh thường được thực hiện
ngay trước lần gọi đầu tiên của một thành viên trong lớp đó.

Constructor tĩnh không có bổ từ truy cập, không có bất kỳ một tham số nào và chỉ có duy nhất một constructor tĩnh trong một
lớp.

Chúng ta có thể định nghĩa một constructor tĩnh và một constructor thực thể không có tham số trong cùng một lớp. Nó không gây
ra bất kỳ một sự xung đột nào bởi vì constructor tĩnh được thực hiện khi lớp được khởi tạo còn constructor thực thể được thực
hiện khi một thực thể được tạo ra.

Gọi các constructor từ những constructor khác:


Xét ví dụ như sau:

class Car
{
private string description;
private uint nWheels;
public Car(string model, uint nWheels)
{
this.description = description;
this.nWheels = nWheels;
}

public Car(string model)


{
this.description = description;
this.nWheels = 4;
}
// etc.

Ta thấy cả hai constructor đều khởi tạo cùng các trường, và nó sẽ ngắn gọn hơn nếu ta chỉ cần viết đoạn mã ở một constructor.
C# cho phép ta làm đều đó như sau:

class Car
{
private string description;
private uint nWheels;

public Car(string model, uint nWheels)


{
this.description = description;
this.nWheels = nWheels;
}

public Car(string model) : this(model, 4)


{
}
// etc

Khi ta khởi tạo một biến như sau:

Car myCar = new Car("Proton Persona");

Thì constructor 2 tham số sẽ được thực thi trước bất kỳ đoạn mã nào trong constructor 1 biến.

Constructor của các lớp thừa hưởng:


Khi chúng ta tạo ra một thể hiện của một lớp thừa hưởng thì không phải chỉ những constructor của lớp thừa hưởng đó được thực
hiện mà cả những constructor của lớp cơ sở cũng được gọi. Và các constructor của lớp cơ sở sẽ được thực hiện trước khi các
constructor của lớp thừa hưởng.

Chúng ta xét ví dụ sau:

abstract class GenericCustomer


{
private string name;
// lots of other methods etc.
}

class Nevermore60Customer : GenericCustomer


{
private uint highCostMinutesUsed;
// other methods etc.
}
Đều chúng ta cần ở ví dụ trên là khi một thể hiện của lớp Nevermore60Customer được tạo ra thì thuộc tính name phải được khởi
tạo giá trị null và thuộc tính highCostMinutesUsed được khởi tạo là 0.

GenericCustomer arabel = new Nevermore60Customer();

Đối với thuộc tính highCostMinutesUsed thì không có vấn đề gì, nó sẽ được constructor mặc định khởi tạo giá trị 0. Còn thuộc
tính name thì sao? Lớp con không thể truy cập vào thuộc tính này bởi vì nó được khai báo private. Nhưng trên thực tế thì thuộc
tính này luôn được khởi tạo giá trị null vì khi này constructor của lớp cơ sở cũng được gọi và nó thực hiện trước khởi tạo giá trị
null cho thuộc tính name.

Thêm một constructor không tham số trong một quan hệ thừa kế:
Chúng ta sẽ xem xét chuyện gì sẽ xảy ra nếu ta thay thế constructor mặc định bằng một constructor khác không có tham số. Xét
ví dụ ở trên, bây giờ ta muốn khởi tạo name bằng giá trị <noname> ta làm như sau:

public abstract class GenericCustomer


{
private string name;

public GenericCustomer()
: base() // chúng ta có thể xoá bỏ dòng này mà không có ảnh hưởng gì khi biên dịch
{
name = "<no name>";
}

Điểm chú ý ở đây là chúng ta thêm lời gọi tường minh đến constructor của lớp cơ sở trước khi constructor của lớp
GenericCustomer được thực hiện và chúng ta sử dụng từ khoá base để gọi các constructor ở lớp cơ sở.

Trên thực tế chúng ta có thể viết như sau:

public GenericCustomer()
{
name = "<no name>";
}

Nếu trình biên dịch không thấy bất kỳ một sự tham khảo nào đến các constructor khác thì nó sẽ nghĩ là chúng ta muốn gọi
constructor mặc định của lớp cơ sở.

Chú ý: Từ khoá base và this chỉ cho phép dùng để gọi một constructor khác, nếu không nó sẽ báo lỗi.

Nếu chúng ta khai báo như sau:

private GenericCustomer()
{
name = "<no name>";
}

Thì khi khởi tạo một thể hiện của lớp thừa hưởng neverMore60Customer trình biên dịch sẽ báo lỗi :

'Wrox.ProCSharp.OOCSharp.GenericCustomer.GenericCustomer()' is inaccessible due to its


protection level

Bởi vì bạn đã khai báo private nên lớp con sẽ không nhìn thấy constructor này nên sẽ báo lỗi.

Thêm các constructor có tham số trong một quan hệ thừa kế.


Cũng ví dụ như ở trên nhưng bây giờ chúng ta yêu cầu thuộc tính name phải được khởi tạo một giá trị xát định. Tức là ta phải tạo
một constructor một tham số ở lớp GenericCustomer:

abstract class GenericCustomer


{
private string name;

public GenericCustomer(string name)


{
this.name = name;
}

Khi đó nếu ta không sửa constructor ở lớp thừa hưởng thì trình biên dịch sẽ báo lỗi vì nó không tìm thấy một constructor không
tham số nào trong lớp cơ sở. Vì thế ta phải sửa như sau:

class Nevermore60Customer : GenericCustomer


{
private uint highCostMinutesUsed;
public Nevermore60Customer(string name)
: base(name)
{
}

Xét constructor một tham số ta thấy mặc dù không có quyền truy cập đến thuộc tính name của lớp cơ sở nhưng nó vẫn khởi tạo
được thuộc tính name bởi vì constructor của lớp cơ sở đã được gọi thông qua từ khóa base.

Bây giờ ta xét một trường hợp phức tạp hơn:

The Nevermore60Customer definition will look like this at this stage:

class Nevermore60Customer : GenericCustomer


{
public Nevermore60Customer(string name, string referrerName)
: base(name)
{
this.referrerName = referrerName;
}

private string referrerName;


private uint highCostMinutesUsed;
public Nevermore60Customer(string name)
: this(name, "<None>")
{
}

Bây giờ ta khởi tạo một thể hiện như sau:

GenericCustomer arabel = new Nevermore60Customer("Arabel Jones");

Ta thấy trình biên dịch sẽ cần một constructor một tham số để lấy một chuỗi và nó sẽ nhận ra constructor:

public Nevermore60Customer(string name)


: this(name, "<None>")
{
}
Khi ta khởi tạo thể hiện arabel thì constructor của nó sẽ được gọi. Ngay lập tức nó chuyến quyền điều khiển cho constructor 2
tham số của lớp Nervemore60customer sẽ gán hai giá trị Arabel Jone và <none>. Sau đó chuyển quyền điều khiển cho
constructor 1 tham số của lớp GenericCustomer với chuỗi "Arabel Jone". Và tiếp tục chuyển quyền điều khiển cho constructor
system.object thực hiện gán chuỗi "Arable Jone" cho thuộc tính name. Sau đó constructor 2 tham số của lớp
Nervemore60customer lấy lại quyền điều khiển và khởi tạo referrerName bằng <none>. Và cuối cùng constructor 1 tham số của
lớp Nervemore60customer lấy lại quyền điều khiển và nó không làm gì hết.

Như vậy ta đã hiểu rõ về constructor và cách thức mà chúng hoạt động để biết cách sử dụng đúng trong thực tiễn.

Destructors và phương thức Dispose()


C# cũng hỗ trợ Destructor, nhưng chúng không được dùng thường xuyên như trong C++ và cách chúng hoạt động rất khác nhau.
Bởi vì các đối tượng trong .NET và C# thì bị xoá bởi bộ thu gom rác (garbage collection). Trong C#, mẫu destruction làm việc
theo hai giai đoạn:

1.Lớp sẽ thực thi giao diện System.IDisposable, tức là thực thi phương thức IDisposable.Dispose(). Phương thức này được gọi
tường minh khi trong đoạn mã khi một đối tượng không cần nữa.
2. Một Destructor có thể được định nghĩa và nó được tự động gọi khi đối tượng bị thu gom rác. Destructor chỉ đóng vai trò như
một máy rà soát lại trong một số trường hợp xấu client không gọi phương thức Dispose().

Nhìn chung các đối tượng của một vài lớp có thể chứa sự tham khảo đến các đối tượng quản lý khác.Các đối tượng này rất lớn và
nên được xoá càng sớm càng tốt, sau đó lớp đó nên thực thi phương thức Dispose(). Nếu một lớp nắm những tài nguyên không
quản lý thì nó nên thực thi cả hai phương thức Dispose() và một Destructor.

Cú pháp để định nghĩa Destructor và Dispose():

class MyClass : IDisposable


{
public void Dispose()
{
// implementation
}

~MyClass() // destructor. Only implement if MyClass directly holds


// unmanaged resources.
{
// implementation
}
// etc.

Phương thức Dispose() giống như một phương thức bình thường, nó không có kiểu trả về và không có tham số truyền.

Cú pháp của một Destructor giống như một phương thức nhưng có cùng tên với lớp, có tiền tố là một dấu sóng(~), không có kiểu
trả về, không có tham số truyền và không có bổ từ.

Thực thi phương thức Dispose() và một Destructor:


Destrutor được gọi khi một đối tượng bị huỹ. Có một vài điểm ta phải nhớ như sau:

1. Chúng ta không thể biết trước khi nào một thể hiện bị huỹ, tức là ta không biết trước khi nào một Destructor được gọi.

2. Bạn có thể tác động đến bộ thu gom rác để chạy tại một thời điểm trong đoạn mã của bạn bằng cách gọi phương thức
System.GC.Collect(). System.GC là một lớp cơ sở .NET mô tả bộ thu gom rác và phương thức Collect() dùng để gọi bộ thu gom
rác.

3. Có một lời khuyên là chúng ta không nên thực thi một Destructor nếu như lớp của bạn không thực sự cần đến nó. Nếu một đối
tượng thực thi một Destructor, thì nó đặt một đặc tính quan trọng vào quá trình thu gom rác của đối tượng đó. Nó sẽ trì hoãn việc
di chuyển cuối cùng của đối tượng từ bộ nhớ. Những đối tượng không có một Destructor thì bị xoá khỏi bộ nhớ trong một lần
hoạt động của bộ thu gom rác. Còn đối tượng có Destructor thì nó sẽ qua hai bước : lần đầu nó gọi Destructor mà không xoá đối
tượng, lần thứ hai mới thực sự xoá đối tượng.

Chú ý : Không có bất kỳ một tham số nào trong một Destructor, không kiểu trả về và không có bổ từ. Không cần thiết phải gọi
tường minh một Destructor của lớp cơ sở mà trình biên dịch sẽ tự động sắp xếp tất cả Destructor được định nghĩa trong các lớp
thừa kế đều được gọi.

Close() vs. Dispose()


Sự khác nhau giữa hai phương thức Close() và Dispose() là rất lớn. Phương thức Close() sẽ đóng tài nguyên và có thể được gọi
lại sau này. Còn khi gọi Dispose() tức là client đã kết thúc. Bạn có thể thực thi một hoặc cả hai phương thức trên, tuy nhiên để
tránh một số rắc rối bạn nên căn nhắc trước khi thực thi chúng. và bạn nên sử dụng Dispose() nếu bạn muốn lấy một số lợi ích từ
cấu trúc của giao diện IDisposable.

Sử dụng giao diện IDisposable:


C# đưa ra cú pháp để chắc chắn rằng phương thức Dispose() phải tự động được gọi khi một đối tượng tham khảo ra ngoài phạm
vi. Ví dụ ta có một lớp ResourceGobbler sử dụng một số tài nguyên bên ngoài và chúng ta cần khởi tạo một thể hiện của lớp này:

{
ResourceGobbler theInstance = new ResourceGobbler();

// do your processing
theInstance.Dispose();
}

Theo đoạn mã trên thì phương thức Dispose() sẽ được gọi vào cuối khối mã khi đó thể hiện theInstance sẽ bị huỹ. Và chúng ta
còn một cách khác như sau:

class ResourceGobbler : IDisposable


{
// etc.

public void Dispose()


{
// etc.
}
}

Ta thấy đoạn mã trên lớp ResourceGobbler thừa kế giao diện IDisposable, việc thừa kế một giao diện khác với việc thừa kế một
lớp. Nó sẽ bắt buột lớp ResourceGobbler phải thực thi phương thức Dispose(). Bạn sẽ bị báo lỗi nếu bạn thừa kế từ giao diện
IDisposable và không thực thi phương thức Dispose(). Chính vì thế trình biên dịch có thể kiểm tra xem một đối tượng được định
nghĩa có phương thức Dispose() thì nó phải tự động được gọi.

Sự thực thi của các Destructor và phương thức Dispose():


Xét ví dụ một lớp chứa cả tài nguyên không quản lý và tài nguyên quản lý:

public class ResourceGobbler : IDisposable


{
private StreamReader sr;
private int connection;

public void Dispose()


{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)


{
if (disposing)
{
if (sr != null)
{
sr.Close();
sr = null;
}
}
CloseConnection();
}

~ResourceGobbler()
{
Dispose (false);
}

void CloseConnection()
{
// code here will close connection
}
}
Lớp ResourceGobbler có một sự tham khảo đến một đối tượng StreamReader. Đây là một lớp được định nghĩa trong System.IO
để đọc dữ liệu như tập tin.Tuy nhiên có một sự kết nối gọi bên trong để mô tả một số đối tượng không quản lý. Tức là chúng ta
cần thực thi một Destructor. Trong đoạn mã trên thì phương thức CloseConnection() dùng để đóng tài nguyên ngoài.

Struct
Cú pháp để định nghĩa một struct được mô tả trong ví dụ sau:

struct Dimensions
{
public double Length;
public double Width;
}

Ta thấy Dimensions được định nghĩa như trên gọi là một struct. Một struct dùng để nhóm một số dữ liệu lại với nhau. Trong C#,
một struct được định nghĩa gần giống như một lớp chỉ khác từ khoá và một vài điểm như sau:

1.Struct là một kiểu giá trị, không phải là kiểu tham khảo.

2. Struct không hổ trợ thừa kế.

3. Có vài sự khác nhau trong cách làm việc của các constructor đối với struct. Trình biên dịch luôn luôn cung cấp một constructor
không tham số mặc định, và không được cho phép thay thế.

4. Với một struct, bạn có thể chỉ rỏ cách mà các trường được đặt ngoài bộ nhớ.

Struct là các kiểu giá trị:


Mặc dù struct là kiểu giá trị nhưng cú pháp để sử dụng nó giống như sử dụng lớp. Ví dụ như bạn khai báo như trên thì bạn có thể
viết như sau:

Dimensions point = new Dimensions();


point.Length = 3;
point.Width = 6;

Chú ý rằng struct là các kiểu giá trị, do đó thao tác new sẽ không làm việc theo cách của lớp hay những kiểu tham khảo khác. Nó
chỉ đơn giản định vị trên bộ nhớ và gọi constructor thích hợp để khởi tạo các trường. Do đó bạn hoàn toàn có thể viết:

Dimensions point;
point.Length = 3;
point.Width = 6;

Nếu Dimensions là một lớp thì đoạn mã trên sẽ báo lỗi nhưng là một struct thì hoàn toàn hợp lý. Bởi vì là một struct rất dể để gán
giá trị. Nhưng trình biên dịch sẽ báo lỗi nếu bạn viết mã như sau:

Dimensions point;
Double D = point.Length;

Trình biên dịch sẽ báo bạn đã sử dụng một biến chưa khởi tạo. Và khi sử dụng struct bạn phải tuân thủ một số quy định sau cho
mọi kiểu dữ liệu :

Mọi thứ đều phải được khởi tạo trước khi sử dụng. Một struct được xem như được khởi tạo đầy đủ khi thao tác new được gọi hay
khi tất cả các trường đều được gán giá trị. Một struct được định nghĩa là một trường thành viên của một lớp thì nó sẽ được tự
động khởi tạo khi đối tượng khởi tạo.

Struct và thừa kế:


Struct không hổ trợ thừa kế, tức là bạn không thể thừa kế từ một struct khác, hay từ bất kỳ lớp nào lớp. Nhưng nó cũng như các
kiểu dữ liệu khác đều thừa kế từ lớp System.object. Và chúng ta có thể override trong một struct. Ví dụ ta override phương thức
ToString() trong struct sau:
struct Dimensions
{
public double Length;
public double Width;
Dimensions(double length, double width)
{ Length=length; Width=width; }

public override string ToString()


{
return "( " + Length.ToString() + " , " + Width.ToString() + " )";
}
}

Chúng ta khai báo phương thức như làm trong một lớp. Nhưng chú ý rằng không được khai báo virtual, abstract, hay sealed trong
bất kỳ thành viên nào của struct. Một lớp thì có thể thừa kế từ một struct nhưng làm ngược lại thì không cho phép.

Constructor cho struct :


Bạn có thể định nghĩa constructor cho các struct như làm với lớp. Nhưng bạn không được phép định nghĩa constructor không có
tham số. Có một số trường hợp hiếm thấy trong thời gian chạy của .NET sẽ không thể gọi một constructor không tham số mà bạn
cung cấp. Chính vì thế microsoft đã cấm định nghĩa constructor không tham số cho struct trong C#.

Bạn có thể cung cấp một phương thức Close() hay Dispose() cho một struct nhưng bạn không được phép định nghĩa Destructor.

Nạp chồng toán hạng :


Điểm nổi bật của nạp chồng toán hạng là không phải lúc nào bạn cũng muốn gọi các phương thức hay thuộc tính trên các thể hiện
lớp. Chúng ta thường cần làm một số công việc như cộng các số lượng với nhau, nhân chúng hay thực hiện một số toán hạn logic
như so sánh các đối tượng. Ví dụ ta định nghĩa một lớp mô tả ma trận toán học. Các ma trận thì có thể cộng, nhân với nhau như
các số, nên ta có thể viết đoạn mã như sau:

Matrix a, b, c;
// assume a, b and c have been initialized
Matrix d = c * (a + b);

Bằng nạp chồng các toán hạng ta có thể làm cho trình biên dịch biết những gì mà + và * làm đối với một ma trận, và bạn có thể
viết đoạn mã như trên. Nếu như không sử dụng toán hạng nạp chồng như trên, ta cũng có thể định nghĩa các phương thức để thực
hiện các toán hạng trên nhưng nó sẽ có rất nhiều hỗn độn:

Matrix d = c.Multiply(a.Add(b));

Các toán hạng như + và * rất khắc khe với các kiểu dữ liệu định nghĩa trước, và do đó trình biên dịch sẽ tự động biết ý nghĩa của
các toán hạng dựa trên các kiểu dữ liệu đó. Ví dụ như nó biết cách để cộng hai số kiểu long, hay cách để chia một số kiểu double
cho một số kiểu double. Khi chúng ta định nghĩa lớp hay struct chúng ta phải nói với trình biên dịch mọi thứ như: những phương
thức nào có thể được gọi, những trường nào được lưu trữ với mọi thực thể và vân vân. Nếu chúng ta sử dụng các toán hạng như
+, * trong lớp của chúng ta. Chúng ta phải nói với trình biên dịch biết ý nghĩa của những toán hạng có liên quan trong ngữ cảnh
của lớp đó. Và cách chúng ta làm là định nghĩa nạp chồng cho các toán hạng.

Một số trường hợp chúng ta nên viết các toán hạng nạp chồng:

1. Trong thế giới toán học, mọi đối tượng toán học như: tọa độ, vector, ma trận, hàm số và vân vân. Nếu bạn viết chương trình
làm những mô hình toán học hay vật lý, bạn nhất định sẽ mô tả những đối tượng này.

2.Những chương trình đồ hoạ sẽ sử dụng các đối tượng toán học và toạ độ khi tính toán vị trí của trên màn hình.

3. Một lớp mô tả số lượng tiền.

4. Việc sử lý từ hay chương trình phân tích văn bản có lớp để mô tả các câu văn, mệnh đề và bạn phải sử dụng các toán hạng để
liên kết các câu lại với nhau.

Cách hoạt động của các toán hạng :


Để hiểu cách nạp chồng toán hạng, chúng ta phải nghĩ về những gì xảy ra khi trình biên dịch gặp một toán hạng - :

int a = 3;
uint b = 2;
double d = 4.0;
long l = a + b;
double x = d + a;

Xem dòng lênh:

long l = a + b;

Việc thực hiện a+b như trên là rất trực quan, đó là một cú pháp tiện lợi để nói rằng chúng ta đang gọi phương thức cộng hai số.

Trình biên dịch sẽ thấy nó cần thiết để cộng hai số nguyên và trả về số kiểu long. Ta thấy đây là phép cộng hai số kiểu integer và
kết quả cũng là một số integer nhưng nó ép kiểu sang kiểu long và điều này thì cho phép trong C#.

Xét dòng lệnh:

double x = d + a;

Ta thấy trong nạp chồng này có số kiểu double và kiểu integer, cộng chúng lại và trả về kiểu doube. Chúng ta cần phải đổi kiểu
int sang kiểu double sau đó cộng hai số đó lại với nhau. Và chúng ta nhận ra sự nạp chồng của toán tử cộng ở đây như là một
phiên bản của toán tử nhận hai số double như hai tham số. Và trình biên dịch phải chắc là nó có thể ép kiểu kết quả về một kiểu
thích hợp nếu cần.

Xét đoạn mã sau:

Vector vect1, vect2, vect3;


// initialise vect1 and vect2
vect3 = vect1 + vect2;
vect1 = vect1*2;

Ở đây vector là một struct, trình biên dịch cần phải cộng hai vector vect1 và vect2 với nhau. Và nó sẽ tìm một nạp chồng của toán
hạng + lấy hai vector như tham số của nó. Và toán hạng này trả về một vector khác. Bởi vậy trình biên dịch cần tìm một định
nghĩa của toán hạng có dạng như sau:

public static Vector operator + (Vector lhs, Vector rhs)

Nếu tìm ra nó sẽ thực thi toàn hạng đó. Nếu không nó sẽ sử dụng bất kỳ nạp chồng của toán hạng + nào có hai tham số kiểu dữ
liệu khác và có thể chuyển sang thực thể vector. Nếu không tìm được cái nào thích hợp thì nó sẽ báo lỗi.

Ví dụ về nạp chồng toán hạng : struct Vector


Chúng ta sẽ định nghĩa một struct Vector, nó mô tả một vector ba chiều.

Một vector ba chiều là một tập hợp ba con số kiểu double. Các biến mô tả các con số được gọi là x, y, z. Liên kết ba con số lại
với nhau và để chúng tạo thành một vector toán học.

Sau đây là định nghĩa cho Vector- chứa các trường thành viên, contructor, và một phương thức ToString() overriden, vì thế
chúng ta có thể dễ dàng thấy nội dung của một vector và cuối cùng là nạp chồng toán hạn:

namespace Wrox.ProCSharp.OOCSharp
{
struct Vector
{
public double x, y, z;

public Vector(double x, double y, double z)


{
this.x = x;
this.y = y;
this.z = z;
}

public Vector(Vector rhs)


{
x = rhs.x;
y = rhs.y;
z = rhs.z;
}

public override string ToString()


{
return "( " + x + " , " + y + " , " + z + " )";
}

Chú ý rằng để làm cho mọi thứ đơn giản thì mọi trường nên được khai báo public. Nên nhớ các struct không cho phép để
contructor mặc định. Do đó, tôi đã tạo hai constructor để khởi tạo giá trị cho vector bằng cách truyền giá trị cho mọi phần tử hay
truyền một vector khác để sao chép các giá trị của vector này.

Bây giờ hãy xem qua một nạp chồng toán hạng:

public static Vector operator + (Vector lhs, Vector rhs)


{
Vector result = new Vector(lhs);
result.x += rhs.x;
result.y += rhs.y;
result.z += rhs.z;
return result;
}
}
}

Nó làm việc như thế nào? Cú pháp quan trọng là trong khai báo của toán hạng. Nó được khai báo như cách khai báo một phương
thức, ngoại trừ từ khoá operation sẽ nói với trình biên dịch là nó là một nạp chồng toán hạng. Toán hạng được đại diện bởi ký
kiệu thực tế cho phù hợp. Kiểu trả về là kiểu mà bạn nhận được khi sử dụng toán hạng này. Trong trường hợp của chúng ta, cộng
hai vector sẽ cho chúng ta một vector khác, vì thế kiểu trả về là một vector. Bạn có thể override toán hạng + này, bằng cách định
nghĩa toán hạng cùng tên nhưng khác kiểu trả về. Hai tham số ở đây để chỉ hai phần tử bạn đang muốn thực hiện toán hạng giữa
chúng. Ví dụ như một toán hạng có hai tham số giống như "+" ở trên, tham số đầu là một đối tượng hay giá trị nằm ở phía bên
trái dấu "+", và tham số thứ hai là đối tượng hay giá trị nằm bên phải dấu "+".

Cuối cùng, chú ý toán hạng được khai báo static, với ý nghĩa là nó được kết nối với struct hoặc class, không phải với bất kỳ đối
tượng nào, và vì thế không truy cập đến một con trỏ this được.

Bây giờ chúng ta đề cập đến cú pháp cho việc khai báo toán hạng +, chúng ta có thể quan sát những gì xảy ra bên trong toán hạng

{
Vector result = new Vector(lhs);
result.x += rhs.x;
result.y += rhs.y;
result.z += rhs.z;
return result;
}

Phần mã này rõ ràng giống như nếu chúng ta khai báo một phương thức, và bạn có thể tin tưởng rằng nó sẽ trả về một vector
chứa tổng của lhs và rhs như định nghĩa ở trên. Chúng ta chỉ đơn giản cộng các số x, y, và z một cách riêng lẽ.

Bây giờ chúng ta cần kiểm tra struct của chúng ta bằng đoạn mã sau:

static void Main()


{
Vector vect1, vect2, vect3;
vect1 = new Vector(3.0, 3.0, 1.0);
vect2 = new Vector(2.0, -4.0, -4.0);
vect3 = vect1 + vect2;
Console.WriteLine("vect1 = " + vect1.ToString());
Console.WriteLine("vect2 = " + vect2.ToString());
Console.WriteLine("vect3 = " + vect3.ToString());
}

Thực thi ta được kết quả sau:

Vectors
vect1 = ( 3 , 3 , 1 )
vect2 = ( 2 , -4 , -4 )
vect3 = ( 5 , -1 , -3 )
Thực thi chương trình trên

Thêm nhiều sự nạp chồng:


Mặc dù chúng ta hiểu được cách để nạp chồng một toán hạng nhưng trong thực tế ta có rất nhiều toán hạng. Ví dụ như một
vector bạn có thể thực hiện cộng hai vector, nhân hai vector, trừ hai vector hoặc so sánh giá trị của nó. Ta sẽ xét ví dụ nạp chồng
phép nhân vector bằng cách nhân vô hướng hai vector với nhau.

public static Vector operator * (double lhs, Vector rhs)


{
return new Vector(lhs * rhs.x, lhs * rhs.y, lhs * rhs.z);
}

Nếu a và b được khai báo kiểu vector thì nó cho phép ta viết mã như sau:

b=2*a;

nhưng nó không cho phép viết như sau:

b=a*2;

Bởi vì lúc này trình biên dich xem như toán hạng nạp chồng giống như phương thức nạp chồng. Nó kiểm tra tất cả các nạp chồng
khả thể của toán hạng để tìm được sự ăn khớp tốt nhất. Như ở trên thì nó yêu cầu có tham số đầu tiên là một vector, tham số thứ
hai là một số nguyên. Và tìm không thấy nó sẽ báo lỗi. Có 2 cách để xử lý trường hợp trên bằng cách nạp chồng các toán hạng *
khác:

public static Vector operator * (Vector lhs, double rhs)


{
return new Vector(rhs * lhs.x, rhs * lhs.y, rhs *l hs.z);
}

Hoặc:

public static Vector operator * (Vector lhs, double rhs)


{
return rhs * lhs;
}

Nạp chồng các toán hạng so sánh:


Có sáu toán hạng trong C#, và chúng ta xét từng cặp:

• = = and !=
• > and <

• >= and <=


Ý nghĩa của từng cặp này là gấp đôi. Giữa các cặp này luôn luôn có kết quả đối nghịch nhau: Nếu toán hạng đầu trả về giá trị true
thì toán hạng kia trả về giá trị false. C# luôn luôn yêu cầu bạn nạp chồng cả hai toán tử đó. Nếu bạn nạp chồng toán tử "= =" thì
phải nạp chồng toán tư"!=" nếu không trình biên dịch sẽ báo lỗi.

Có một hạn chế là toán hạng so sánh phải trả về kiểu bool. và đó cũng là điểm khác nhau giữa các toán hạng này và toán hạng số
học.

Xét ví dụ ta override toán hạng = = và != cho lớp vector:

public static bool operator = = (Vector lhs, Vector rhs)


{
if (lhs.x = = rhs.x && lhs.y = = rhs.y && lhs.z = = rhs.z)
return true;
else
return false;
}

Các vector so sánh được xét bằng nhau trên các giá trị của các thành phần. Đều cần chú ý là bạn đang xét sự bằng nhau giữa hai
vector theo sự tham khảo hay theo giá trị của nó.

Và chúng ta cũng làm tương tự cho toán hạng !=:

public static bool operator != (Vector lhs, Vector rhs)


{
return ! (lhs = = rhs);
}

Ta có đoạn chương trình sau:

static void Main()


{
Vector vect1, vect2, vect3;
vect1 = new Vector(3.0, 3.0, -10.0);
vect2 = new Vector(3.0, 3.0, -10.0);
vect3 = new Vector(2.0, 3.0, 6.0);
Console.WriteLine("vect1= =vect2 returns " + (vect1= =vect2));
Console.WriteLine("vect1= =vect3 returns " + (vect1= =vect3));
Console.WriteLine("vect2= =vect3 returns " + (vect2= =vect3));
Console.WriteLine();
Console.WriteLine("vect1!=vect2 returns " + (vect1!=vect2));
Console.WriteLine("vect1!=vect3 returns " + (vect1!=vect3));
Console.WriteLine("vect2!=vect3 returns " + (vect2!=vect3));
}

Khi chạy và biên dịch, trình biên dịch sẽ cảnh báo bạn không override phương thức Equals() cho vector. Nhưng với mục đích của
chúng ta thì nó không có ý nghĩa gì:

Vectors3
vect1= =vect2 returns True
vect1= =vect3 returns False
vect2= =vect3 returns False

vect1!=vect2 returns False


vect1!=vect3 returns True
vect2!=vect3 returns True
Thực thi chương trình trên

Những toán tử nào bạn có thể nạp chồng:


Phạm trù Toán hạng Hạn chế

Nhị phân toán học +, *, /, -, % Không

Thập phân toán học +, -, ++, -- Không

Nhị phân bit &, |, ^, <<, >> Không

Thập phân bit !, ~, true, false Không

So sánh ==, !=, >=, <, <=, > Phải nạp chồng theo từng cặp.

Tóm tắt :
Trong chương này chúng ta đã kiểm tra một số tính năng của C# để viết mã và các lớp dễ dàng hơn, cho phép chúng có nhiều cú
pháp trực quan hơn. Chúng ta thấy được cách mà C# quản lý bộ nhớ của nó. Nó sẽ rất có lợi khi bạn viết các chương trình lớn.
Chúng ta kiểm tra cách mà các construcotr và Destructor khởi tạo cho các đối tượng, cách chúng hoạt động và bị xoá tài nguyên
khi đối tượng bị huỹ. Chúng ta thấy được sự cải tiến khi định nghĩa struct thay cho lớp. Và chúng ta biết được một vài cú pháp
tiện lợi khi nó cung cấp những toán hạng nạp chồng, indexer, và phương thức nạp chồng.

Chương 4: Những chủ đề tiến bộ trong C#


Tổng quan
Đến thời điểm này,ta đã xem xét tất cả các cú pháp cơ bản trong C# cũng như lập trình hướng đối tượng trong C#.Bằng những
kiến thức này ta đã có thể dùng C# để thiết kế tốt các chương trình hướng đối tượng.Tuy nhiên,ta vẫn chưa hoàn chỉnh hết ngôn
ngữ C#,bởi vì C# còn có một số đặc tính tiến bộ mà có thể làm tăng tính hữu ích trong 1 số tình huống.Những đặc tính này sẽ
được xem xét trong hai chương kế tiếp

Trong chương này ta sẽ tập trung vào những đặc tính tiến bộ của ngôn ngữ C#.

Những chủ đề mà ta sẽ tìm hiểu trong chương này là :

• Xử lí lỗi và biệt lệ - Cơ chế của C# trong việc xử lí các trạng thái lỗi mà cho phép ta có thể tùy biến trong việc chọn
cách xử lí cho mỗi trạng thái lỗi khác nhau ,và cũng tách biệt rõ ràng hơn những đoạn mã có khả năng gây ra lỗi để ta
có thể xử lí chúng.
• Ép kiểu do người dùng định nghĩa - Định nghĩa các kiểu ép kiểu giữa các lớp riêng của ta

• Delegates - Cách mà C# dùng để gọi phương thức như thông số gần giống với con trỏ hàm trong C++

• Các sự kiện - thông báo khi 1 hành động cụ thể được sinh ra , ví dụ khi người dùng nhấn nút chuột

• Các chỉ thị tiền xử lí trong C# - Giới thiệu các tiến bộ trong tiền xử lí trước khi biên dịch

• Các Attribute -1 kĩ thuật trong việc đánh dấu các mục trong mã mà ta quan tâm theo một cách nào đó

• Quản lí bộ nhớ - ta sẽ tìm hiểu về heap và stack và cách mà các biến tham trị và tham chiếu được lưu và thi hành

• Mã không an toàn - khai báo các khối mã ' không an toàn' để truy xuất bộ nhớ trực tiếp

Các ép kiểu do người dùng định nghĩa


Trong các chương trước ta được học về cách chuyển đổi giá trị giữa những kiểu dữ liệu cơ bản. chúng ta cũng đã học hai cách ép
kiểu là :
- Không tường minh (Implicit)
- Tường minh (Explicit)
Vì c# cho phép ta định nghĩa những lớp và cấu trúc riêng,do đó ta cũng muốn có những cách thức mà cho phép ta chuyển đổi
giữa những loại dữ liệu của riêng ta. C# cho phép làm điều đó.cơ chế của nó là ta có thể định nghĩa một ép kiểu như là một thao
tác thành viên của một trong những lớp thích hợp. việc ép kiểu phải được đánh dấu là implicit hoặc explicit để chỉ định cách mà
bạn muốn sử dụng với nó. cũng giống như việc ép kiểu cơ bản : nếu bạn biết việc ép kiểu là an toàn ,dù là bất cứ giá trị nào đựợc
giữ bởi biến nguồn, thì bạn định nghĩa nó như là implicit.ngược lại nếu bạn biết việc ép kiểu có thể đi đến sự liều lĩnh - mất dữ
liệu hay một biệt lệ sẽ bị tung ra - bạn nên định nghĩa ép kiểu như là explicit.
Bạn nên định nghĩa bất kỳ kiểu ép kiểu mà bạn viết là tường minh nếu có bất kì giá trị dữ liệu nguồn nào mà việc ép kiểu có khả
năng thất bại, hoặc nếu có sự mạo hiểm do một biệt lệ được tung ra.
Cú pháp của việc định nghĩa ép kiểu cũng giống như việc overload thao tác . không phải ngẫu nhiên mà ta nói thế , bởi vì theo
cách mà ép kiểu được xem như là thao tác là tác động của nó là chuyển từ kiểu dữ liệu nguồn sang kiểu dữ liệu đích. để minh hoạ
cho cú pháp này, cú pháp sau được lấy từ ví dụ mà sẽ được giới thiệu sau đây trong phần này:
public static implicit operator float (Currency value)
{
// xử lí
}
Đoạn mã này là một phần của cấu trúc - currency - được dùng để lưu trữ tiền.ép kiểu được định nghĩa ở đây cho phép chúng ta
chuyển đổi 1 cách ẩn dụ giá trị của 1 kiểu tiền tệ sang 1 số thực ( float). chú ý rằng nếu việc chuyển được khai báo như là
implicit, thì trình biên dịch cho phép nó sử dụng cả implicit và explicit. nếu nó được khai báo như là explicit , thì trình biên dịch
chỉ cho phép nó sử dụng như là explicit.
Trong khai báo này việc ép kiểu được khai báo là static. giống như các thao tác được overload , C# đòi hỏi việc ép kiểu là static.
điều này có nghĩa là mỗi ép kiểu cũng lấy một thông số , mà là kiểu dữ liệu trong nguồn
Thực hành ép kiểu dữ liệu do người sử dụng định nghĩa.
Trong phần này, chúng ta sẽ xem xét việc ép kiểu implicit và explicit của kiểu dữ liệu này trong ví dụ Simplecurrency. trong ví
dụ này chúng ta định nghĩa 1 cấu trúc struct, currency, mà giữ tiền USA. thông thường, C# cung cấp kiểu thập phân ( decimal)
cho mục đích này, nhưng bạn vẫn có thể viết riêng 1 cấu trúc struct hay một lớp để trình bày giá trị tiền nếu bạn muốn biểu diễn
quy trình tài chính phức tạp và do đó muốn có một phương thức cụ thể để thực thi như là một lớp.
cấu trúc của ép kiểu là giống nhau cho struct hay lớp . trong ví dụ này là struct, nhưng nó cũng làm việc tốt nếu bạn khai báo
currency như là một lớp.
khởi đầu , định nghĩa cấu trúc currency như sau:
struct Currency
{
public uint Dollars;
public ushort Cents;

public Currency(uint dollars, ushort cents)


{
this.Dollars = dollars;
this.Cents = cents;
}
Việc dùng kiểu dữ liệu không dấu cho trường Dollar và cent bảo đảm rằng một thể hiện của currency chỉ giữ 1 số dương.chúng ta
giới hạn nó bằng cách này để có thể minh hoạ một số điểm về tường minh sau này.để giữ cho lớp đơn giản,ta chọn các trường là
public, nhưng nói chung bạn sẽ phải định nghĩa chúng private, và định nghĩa những thuộc tính đáp ứng cho dollar và cent
Chúng ta hãy bắt đầu bằng cách giả sử như là bạn muốn chuyển giá trị từ currency sang float, mà phần nguyên của kiểu float sẽ
trình bày dollar:
Currency balance = new Currency(10,50);
float f = balance; // ta muốn f được đặt là 10.5
Để cho phép làm điều này , cần định nghĩa 1 ép kiểu. từ đây ta thêm vào trong cấu trúc currency:
public static implicit operator float (Currency value)
{
return value.Dollars + (value.Cents/100.0f);
}
Ép kiểu này là implicit, Đây là sự chọn lựa dễ nhận thấy , bởi vì , nó nên rõ ràng từ định nghĩa trong currency, bất kì giá trị nào
lưu trữ trong currency cũng có thể lưu trong kiểu float.
Nếu chuyển ngược thì sao? từ một số float sang currency .trong trường hợp này việc chuyển đổi có thể không làm việc ,nếu float
lưu trữ số âm,còn currency thì không , và số này sẽ lưu trữ phần làm tròn vào trong trường dollar của currency.nếu float chứa
đựng một giá trị không thích hợp việc chuyển nó sẽ gây ra một kết quả không dự đoán truớc. do đó việc chuyển đổi này nên được
khai báo là explicit. sau đây là đoạn mã thử đầu tiên , tuy nhiên nó không gửi kết quả hoàn toàn đúng:
public static explicit operator Currency (float value)
{
uint dollars = (uint)value;
ushort cents = (ushort)((value-dollars)*100);
return new Currency(dollars, cents);
}
Đoạn mã sau sẽ dịch đúng :
float amount = 45.63f;
Currency amount2 = (Currency)amount;
Tuy nhiên đoạn mã sau sẽ báo lỗi bởi vì nó sử dụng một ép kiểu tường mình một cách không rõ ràng :
float amount = 45.63f;
Currency amount2 = amount; // sai
Sau đây là phương thức main() mà khởi tạo một struct Currency, và thực hiện một vài việc chuyển đổi. vào đầu đoạn mã, chúng
ta viết giá trị của biến balance theo 2 cách ( để minh họa 1 số điều cho phần sau)

static void Main()


{
try
{
Currency balance = new Currency(50,35);
Console.WriteLine(balance);
Console.WriteLine("balance is " + balance);
Console.WriteLine("balance is (using ToString()) " +
balance.ToString());
float balance2= balance;
Console.WriteLine("After converting to float, = " + balance2);
balance = (Currency) balance2;
Console.WriteLine("After converting back to Currency, = " + balance);

Console.WriteLine("Now attempt to convert out of range value of " +


"-$100.00 to a Currency:");
checked
{
balance = (Currency) (-50.5);
Console.WriteLine("Result is " + balance.ToString());
}
}
catch(Exception e)
{
Console.WriteLine("Exception occurred: " + e.Message);
}
}

Chú ý rằng ta đặt toàn bộ đoạn mã trong khối try để bắt bất cứ biệt lệ nào xảy ra trong quá trình ép kiểu. Sau khi chạy ta có kết
quả sau :
SimpleCurrency
50.35
Balance is $50.35
Balance is (using ToString()) $50.35
After converting to float, = 50.35
After converting back to Currency, = $50.34
Now attempt to convert out of range value of -$100.00 to a Currency:
Result is $4294967246.60486

Kết quả cho thấy đoạn mã không làm việc như mong đợi. ở phần đầu việc chuyển cho kết quả sai là 50.34 thay vì 50.35. trong
phần hai, không có biệt lệ nào được sinh ra khi ta cố chuyển một giá trị nằm ngoài vùng.
Lỗi dầu tiên là do làm tròn.. nếu ép kiểu từ float sang uint , máy tính sẽ cắt bỏ số hơn là làm tròn nó.máy tính lưu trữ số dạng nhị
phân hơn là thập phân.và phần dư 0.35 không thể được trình bày một cách chính xác như là phần dư dạng nhị phân.do đó máy
tính lưu trữ một số nhỏ hơn 0.35, mà có thể trình bày chính xác trong dạng nhị phân.nhân cho 100 và lấy phần dư nhỏ hơn 35 cắt
thành 34 cent.rõ ràng trong hoàn cảnh này, lỗi cắt bỏ là nghiêm trọng.để tránh chúng thì chắc rằng một sự làm tròn thông minh
phải được thi hành trong việc chuyển đổi số.thật may mắn Microsoft đã viết một lớp để làm điều đó : System.Convert .
System.Convert chứa đựng một số lượng lớn những phương thức static biểu diễn việc chuyển đổi số, và Phương thức mà chúng
ta muốn là System.convert.Uint6().
Bây giờ chúng ta sẽ kiểm tra xem tại sao biệt lệ tràn đã không xuất hiện . vấn đề ở đây là : nơi mà việc tràn xuất hiện không thực
sự nằm trong hàm main()- nó ở bên trong mã của thao tác ép kiểu. mà được gọi từ phương thức main() . và chúng ta đã không
kiểm tra đoạn mã đó.
Giải pháp ở đây là cho phép kiểm tra ngay trong hàm ép kiểu
public static explicit operator Currency (float value)
{
checked
{
uint dollars = (uint)value;
ushort cents = Convert.ToUInt16((value-dollars)*100);
return new Currency(dollars, cents);
}
}
Chú ý rằng ta sử dụng convert.uint16() để tính phần xu thay cho đoạn mã trên.ta không cần dùng cách này để tính phần dollar vì
việc cắt bỏ trong giá trị float đã đưa ra kết quả ta cần
Ép kiểu giữa những lớp
Ví dụ trên cho ta thấy việc ép kiểu giữa 2 kiểu dữ liệu đã được định nghĩa trước.tuy nhiên ta cũng có thể ép kiểu giữa 2 cấu trúc
hoặc lớp mà ta định nghĩa. có 2 hạn chế cần quan tâm :
- Ta không thể định nghĩa một ép kiểu nếu một trong những lớp được dẫn xuất từ 1 lớp khác.
- Ép kiểu phải được định nghĩa bên trong việc định kiểu dữ liệu nguồn hay đích.
Để minh hoạ những yêu cầu này , giả sử rằng ta có biểu đồ lớp sau:
Nói cách khác,lớp c và d được dẫn xuất gián tiếp từ lớp a.trong trường hợp này,chỉ có những ép kiểu riêng giữa a,b,c,d mà hợp
pháp sẽ được chuyển là những lớp c và d bởi vì những những lớp này không được dẫn xuất từ mỗi lớp khác.mã có thể như sau :
public static explicit operator D(C value)
{
// and so on
}
public static explicit operator C(D value)
{
// and so on
}

Cho mỗi kiểu ép kiểu này , ta có quyền chọn nơi mà ta đặt định nghĩa- bên trong lớp định nghĩa C hoặc bên trong lớp định nghĩa
D, nhưng không nằm ở bất cứ chổ nào khác.C# đòi hỏi bạn đặt định nghĩa của 1 ép kiểu bên trong lớp ( hoặc cấu trúc)nguồn hoặc
bên trong lớp ( hoặc cấu trúc) đích
Mỗi lần bạn định nghĩa một ép kiểu bên trong 1 lớp , bạn không thể định nghĩa giống như vậy bên trong những lớp khác.rõ ràng,
chỉ nên có 1 hàm ép kiểu cho mỗi chuyển đổi. nếu không trình biên dịch không biết sử dụng cái nào.
Ép kiểu giữa lớp dẫn xuất và lớp cơ sở
Để xem làm thế nào việc ép kiểu này làm, ta xem xét 2 lớp Mybase và Myderived , trong đó Mydrived được dẫn xuất trực tiếp
hoặc gián tiếp từ lớp cơ sở
đầu tiên từ lớp Myderived đến Mybase ; luôn luôn ( giả sử hàm dựng có giá trị)có thể viết :
MyDerived derivedObject = new MyDerived();
MyBase baseCopy = derivedObject;

Trong trường hợp này,chúng ta ép kiểu không tường minh từ myderived đến mybase. điều này làm việc bởi vì luật là bất kì tham
chiếu đến 1 kiểu mybase được cho phép để chuyển thành đối tượng của lớp mybase hoặc đến đối tượng bất kì được dẫn xuất từ
lớp mybase.trong ngôn ngữ lập trình hướng đối tượng, thể hiện của lớp dẫn xuất là thể hiện của một lớp cơ sở cộng thêm với một
thứ gì đó thêm. tất cả chức năng và thuộc tính được định nghĩa trong lớp cơ sở cũng được định nghĩa trong lớp dẫn xuất .
Bây giờ ta có thể viết
MyBase derivedObject = new MyDerived();
MyBase baseObject = new MyBase();
MyDerived derivedCopy1 = (MyDerived) derivedObject; // OK
MyDerived derivedCopy2 = (MyDerived) baseObject;
Tất cả các câu lệnh trên là hợp lệ trong C# và minh họa việc ép kiểu từ lớp cơ sở sang lớp dẫn xuất. tuy nhiên câu lệnh cuối sẽ
tung ra biệt lệ khi thực thi.
Chú ý rằng những lệnh ép kiểu mà trình biên dịch cung cấp , mà chuyển giữa lớp cơ sở và lớp dẫn xuất thì không thực sự chuyển
bất cứ dữ liệu nào trên các đối tượng.tất cả chúng làm là thiết lập một sự tham chiếu mới để quy cho một đối tượng nếu nó hợp lệ
cho việc chuyển đổi .những lệnh ép kiểu này thì rất khác trong tự nhiên từ những cái mà ta thường xuyên tự định nghĩa.ví dụ,
trong ví dụ Simplecurrency chúng ta định nghĩa việc ép kiểu là chuyển giữa 1 kiểu tiền tệ sang kiểu số thực. trong ép kiểu thực-
thành-currency, chúng ta thực sự tạo một cấu trúc currency mới và khởi tạo nó với giá trị được yêu cầu .những lệnh ép kiểu tiền
định nghĩa giữa những lớp cơ s ở và lớp dẫn xuất không làm điều này.nếu ta thực sự chuyển 1 thể hiện Mybase thành một đối
tượng Myderived thực với giá trị dựa trên nội dung của thể hiện Mybase, ta sẽ không thể sử dụng cú pháp ép kiểu để làm điều
này.tuỳ chọn hợp lí nhất là thường xuyên định nghĩa 1 hàm dựng của lớp dẫn xuất mà lấy thể hiện của lớp cơ sở như là 1 thông
số và có hàm dựng này biểu diễn việc khởi tạo chính xác:
class DerivedClass : BaseClass
{
public DerivedClass(BaseClass rhs)
{
// khởi tạo đối tượng từ thể hiện Base
}
// etc.

Ép kiểu boxing và unboxing


Ví dụ với cấu trúc currency:
Currency balance = new Currency(40,0);
object baseCopy = balance;
Khi ép kiểu không tường minh trên được thực hiện nội dung của balance được sao chép vào heap trong đối tượng box và đối
tượng basecopy tham khảo đến đối tượng này.khi chúng ta định nghĩa cấu trúc currency , .net framework cung cấp không tường
minh một lớp ( ẩn) khác , một lớp currency boxed, mà chứa đựng tất cả các trường như là cấu trúc currency nhưng là kiểu tham
chiếu lưu trong heap.điều này xảy ra bất cứ khi nào chúng ta định nghĩa một kiểu dữ liệu- dù đó là struct hay kiểu liệt kê
( enum) ,và kiểu tham khảo boxed tồn tại đáp ứng đến tất cả kiểu dữ liệu nguyên thuỷ int,double,uint, và ...ta không thể truy nhập
vào các lớp này nhưng chúng sẽ làm việc bất cứ khi nào có việc ép kiểu thành đối tượng khi chúng ta ép kiểu currency thành đối
tượng một thể hiện currency boxed tạo ra và khởi tạo với tất cả các giá trị từ cấu trúc currency. trong ví dụ trên basecopy sẽ tham
khảo đến lớp currency boxed.
Ép kiểu còn được biết đến như là unboxing,dùng cho việc ép kiểu giữa những lớp kiểu tham chiếu cơ sở và kiểu tham chiếu dẫn
xuất.đó là ép kiểu tường minh, bởi vì 1 biệt lệ sẽ được tung ra nếu đối tượng được ép kiểu không ép đúng.
object derivedObject = new Currency(40,0);
object baseObject = new object();
Currency derivedCopy1 = (Currency)derivedObject; // OK
Currency derivedCopy2 = (Currency)baseObject; // Exception thrown

Khi sử dụng boxing và unboxing điều quan trọng để hiểu là cả hai tiến trình này thực sự sao chép dữ liệu vào một đối tượng
boxed hay unboxed. chính vì lí do đó, thao tác trên đối tượng hộp sẽ không tácđộng đến nội dung của kiểu dữ liệu nguyên thuỷ.
Multiple casting
Ví dụ với cấu trúc currency , giả sử trình biên dịch chạm trán với các dòng mã sau:
Currency balance = new Currency(10,50);
long amount = (long)balance;
double amountD = balance;

Đầu tiên chúng ta khởi tạo 1 thể hiện currency ,sau đó ép nó thành kiểu long.vấn đề là ta chưa định nghĩa ép kiểu cho việc này.
tuy nhiên đoạn mã nay vẫn biên dịch thành công bởi vì trình biên dịch nhận ra rằng ta đã định nghĩa ép kiểu không tường minh
để chuyển currency thành float.và nó biết cách chuyển tường minh từ float sang long. ví lí do đó, nó sẽ biên dịch đầu tiên là
chuyển balance sang float rồi từ float sang long.tương tự cho kiểu double tuy nhiên do chuyển tử float sang double không tường
minh , do đó chúng ta có thể viết lại tường minh :
Currency balance = new Currency(10,50);
long amount = (long)(float)balance;
double amountD = (double)(float)balance;

Đoạn mã sau gây ra lỗi :


Currency balance = new Currency(10,50);
long amount = balance;

Do việc chuyển từ float sang long cần tường minh.

Nếu ta không cẩn thận khi nào ta sẽ định nghĩa ép kiểu, thì trình biên dịch có thể sẽ dẫn đến một kết quả không mong đợi..Ví dụ,
giả sử một ai khác trong nhóm đang viết cấu trúc Currency,mà sẽ hửu ích nếu có khả năng chuyển 1 số uint chứa tổng số Cent
thành 1 kiểu Currency ( Cent chứ không phải Dollar bởi vì nó sẽ không làm mất đi phần thập phân của Dollar ) .vì thế việc ép
kiểu này có thể được viết như sau :

public static implicit operator Currency (uint value)


{
return new Currency(value/100u, (ushort)(value%100));
} // Don't do this!

Lưu ý chữ u sau số 100 đảm bảo rằng value/100u sẽ đuợc phiên dịch thành 1 số uint. nếu ta viết value/100 thì trình biên dịch sẽ
phiên dịch sẽ phiên dịch nó là số int chứ không phải uint.

Lý do ta không nên viết mã kiểu này là vì : tất cả ta làm trong đó là chuyển 1 uint chứa 350 thành 1 kiểu Currency và ngược trở
lại. Ta sẽ có gì sau khi thi hành mã này :

uint bal = 350;


Currency balance = bal;
uint bal2 = (uint)balance;

Câu trả lời không phải là 350 mà là 3. Ta chuyển 350 thành 1 Currency 1 cách không tường minh, trả về kết quả
Balance.Dollars=3 , Balance.Cents=50. Sau đó trình biên dịch tính toán hướng tốt nhất để chuyển trở lại.Balance được chuyển
không tường minh thành 1 kiểu float ( giá trị 3.5) và số này được chuyển tường minh thành 1 số uint với giá trị 3

Vấn đề là có 1 sự xung đột giữa cách các ép kiểu của ta dịch các số nguyên integer.các ép kiểu giữa Currency và float dịch 1 số
nguyên giá trị 1 thành 1 doolar, nhưng cách ép kiểu uint-to-Currency cuối nhất sẽ dịch giá trị này là 1 cent.Nếu ta muốn lớp của
ta dễ dàng để dùng thì ta nên chắc rằng tất cả các ép kiểu của ta cư xử hợp với nhau,theo hướng cho ra cùng 1 kết quả. Trong
trường hợp này ,giải pháp là viết lại hàm ép kiểu uint-to-Balance để nó phiên dịch 1 số nguyên integer giá trị 1 thành 1 dollar.

public static implicit operator Currency (uint value)


{
return new Currency(value, 0);
}

1 cách kiểm tra tốt là xét xem 1 chuyển đổi có cho ra cùng kết quả hay không.Lớp Currency đưa ra 1 ví dụ tốt cho kiểm tra này :

Currency balance = new Currency(50, 35);


ulong bal = (ulong) balance;
Hiện tại chỉ có 1 cách mà trình biên dịch có thể thực hiện việc chuyển đổi này: bằng cách chuyển Currency thành 1 float 1 cách
không tường minh.việc chuyển float thành ulong đòi hỏi 1 sự tường minh.

Giả sử ta thêm 1 cách ép kiểu khác,để chuyển 1 cách không tường minh từ Currency thành uint.Ta sẽ làm điều này bằng việc cập
nhật lại cấu trúc Currency bằng cách thêm vào các ép kiểu thành và từ kiểu uint. Ta xem đây là ví dụ SimpleCurrency2:

public static implicit operator Currency (uint value)


{
return new Currency(value, 0);
}

public static implicit operator uint (Currency value)


{
return value.Dollars;
}

Bây giờ trình biên dịch có 1 cách khác để chuyển từ Currency thành ulong: là chuyển từ Currency thành uint 1 cách tường minh
sau đó thành ulong 1 cách không tường minh.

để kiểm tra ví dụ SimpleCurrency2, ta sẽ thêm đoạn mã này vào phần kiểm tra trong SimpleCurrency:

try
{
Currency balance = new Currency(50,35);
Console.WriteLine(balance);
Console.WriteLine("balance is " + balance);
Console.WriteLine("balance is (using ToString()) " + balance.ToString());

uint balance3 = (uint) balance;


Console.WriteLine("Converting to uint gives " + balance3);
Chạy ví dụ ta có kết quả
SimpleCurrency2
50
balance is $50.35
balance is (using ToString()) $50.35
Converting to uint gives 50
After converting to float, = 50.35
After converting back to Currency, = $50.34
Now attempt to convert out of range value of -$100.00 to a Currency:
Exception occurred: Arithmetic operation resulted in an overflow.

Kết quả chỉ ra việc chuyển đổi thành uint thành công,ta đã mất phần cent của Currency trong việc chuyển này. Ép kểu 1 số float
âm thành Currency đã gây ra 1 biệt lệ.

Tuy nhiên kết quả cũng giải thích 1 vấn đề nữa mà ta cần nhận thức khi làm việc với ép kiểu.dòng đầu tiên của kết quả không
trình bày balance đúng,trình bày 50 thay vì $50.35.

Console.WriteLine(balance);
Console.WriteLine("balance is " + balance);
Console.WriteLine("balance is (using ToString()) " + balance.ToString());

Chỉ có 2 dòng cuối trình bày đúng Currency thành chuỗi.Vấn để ở đây là khi ta kết hợp ép kiểu với phương thức overload,ta lấy 1
nguồn khác không dự đoán trước được.Ta sẽ xem kĩ hơn vấn đề này sau.

Câu lệnh thứ 3 Console.WriteLine() gọi tường minh phương thức Currency.ToString() đảm bảo Currency được trình bày là
chuỗi.Cái thứ 2 không làm như vậy. tuy nhiên ,chuỗi " balance is" được truyền đến Console.WriteLine làm rõ hơn rằng thông số
được phiên dịch như chuỗi. Chính vì vậy Currency,ToString() sẽ được gọi không tường minh.

Phương thức Console.WriteLine đầu tiên đơn giản truyền1 cấu trúc Currency đến Console.WriteLine.Console.WriteLine có
nhiều hàm overload,nhưng không có cái nào trong chúng lấy cấu trúc Currency .Vì vậy trình biên dịch sẽ bắt đầu tìm xem nó có
thể ép Currency thành kiểu nào để làm cho nó phù hợp với overload của Console.WriteLine. khi nó xảy ra , một trong những
overload Console.WriteLine() được thiết kế để trình bày uint 1 cách nhanh chóng và hiệu quả, và nó lấy 1 uint như là 1 thông số,
và ta vừa cung cấp 1 ép kiểu chuyển Currency thành uint không tường minh.Kết quả là nó có thể trình bày.

Quả thực Console.WriteLine có overload khác lấy 1 double làm thông số và trình bày giá trị của double.nếu ta xem kĩ kết quả từ
ví dụ SimpleCurrency đầu ta sẽ sẽ thấy dòng kết quả đầu tiên sẽ trình bày Currency như là 1 số double.trong ví dụ đó không có
việc ép kiểu trực tiếp từ Currency thàng uint ,vì vậy trình biên dịch sẽ lấy Currency-to-float-to-double để làm.

Code for Download:


SimpleCurrency

SimpleCurrency2

Delegate
Delegate có thể được xem như là kiểu đối tượng mới trong C#, mà có môt số điểm quen thuộc với lớp.chúng tồn tại trong tình
huống mà ta muốn truyền phương thức xung quanh những phương thức khác.để minh hoạ ta xem dòng mã sau:
int i = int.Parse("99");
Chúng ta quen với việc truyền dữ liệu đến một phương thức như là thông số,vì vậy ý tường truyền phương thức như là thông số
nghe có vẻ hơi lạ đối với chúng ta.tuy nhiên có trường hợp mà ta có 1 phương thức mà làm 1 điều gì đó, nhiều hơn là xử lí dữ
liệu, phương thức đó có thể cần làm điều gì đó mà liên quan đến việc thực thi phương thức khác.phức tạp hơn, bạn không biết
vào lúc nào thì phương thức thứ hai sẽ được biên dịch. thông tin đó chỉ biết vào lúc chạy , và chính vì lí do đó mà phương thức 2
sẽ cần truyền vào như là thông số cho phương thức đầu tiên.điều này nghe có vẻ hơi khó hiểu,nhưng nó sẽ được làm rõ hơn trong
1 vài ví dụ sau:
Luồng bắt đầu: C# có thể bảo máy tính bắt đầu một chuỗi thực thi mới song song với việc thực thi đương thời.1 chuỗi liên tiếp
này gọi là luồng,và việc bắt đầu này được làm bằng cách dùng phương thức, Start() trên 1 thể hiện của lớp cơ
sở.System.Threading.Thread. ( chi tiết hơn về luồng ở chương 5).khi chương trình bắt đầu chạy,nơi nó bắt đầu là main(). tương
tự như vậy khi bạn muốn máy tính chạy một chuỗi thực thi thì bạn phải báo cho máy tính biết bắt đầu chạy là ở đâu. bạn phải
cung cấp cho nó chi tiết của phương thức mà việc thực thi có thể bắt đầu.-nói cách khác , phương thức Thread.Start() phải lấy 1
thông số mà định nghĩa phương thức được thi hành bởi luồng.
Lớp thư viện chung . khi 1 nhiệm vụ chứa đựng nhiệm vụ con mà mã của các nhiệm vụ con này được viết trong các thư viện chỉ
có sử dụng thư viện mới biết nó làm gì.ví dụ , chúng ta muốn viết một lớp chứa một mảng đối tuợng và sắp nó tăng dần. 1 phần
công việc được lặp lại là lấy 2 đối tượng trong lớp so sánh với nhau để xem đối tượng nào đứng truớc.nếu ta muốn lớp có khả
năng sắp xếp bất kì đối tượng nào, không có cách nào có thể làm được việc so sánh trên .mã client dùng mảng đối tượng của ta sẽ
bảo cho ta biết cách so sánh cụ thể đối tượng mà nó muốn sắp xếp.nói cách khác , mã client sẽ phải truyền cho lớp của ta phương
thức thích hợp mà có thể được gọi, để làm việc so sánh.
Nguyên tắc chung là: mã của ta sẽ cần thông báo cho thời gian chạy .NET biết phương thức nào xử lí tình huống nào.
Vì thế chúng ta phải thiết lập những nguyên tắc mà đôi lúc , những phương thức cần lấy chi tiết của phương thức khác như là
thông số.kế tiếp chúng ta sẽ minh họa cách làm điều đó.cách đơn giản nhất là truyền tên của phương thức như là thông số.giả sử
chúng ta muốn bắt đầu một luồng mới, và chúng ta có phương thức được gọi là entrypoint(), mà ta muốn luồng bắt đầu chạy từ
đó:
void EntryPoint()
{
// làm những gì luồng mới cần làm
}
Có thể chúng ta bắt đầu luồng mới với một đoạn mã :
Thread NewThread = new Thread();
Thread.Start(EntryPoint); // sai
Thật sự đây là cách đơn giản nhất. trong một vài ngôn ngữ dùng cách này như c và c++ ( trong c và c++ thông số entrypoint là
con trỏ hàm)
Không may, cách thực thi trực tiếp này gây ra một số vấn đề về an toàn kiểu.nhớ rằng ta đang lập trình hướng đôí tượng, phương
thức hiếm khi nào tồn tại độc lập , mà thường là phải kết hợp với phương thức khác trưóc khi được gọi.vì vậy .NET không cho
làm điều này.thay vào đó nếu ta muốn truyền phương thức ta phải gói chi tiết của phương thức trong một loại đối tượng mới là 1
delegate. delegate đơn giản là một kiểu đối tượng đặc biệt- đặc biệt ở chổ ,trong khi tất cả đối tượng chúng ta định nghĩa trước
đây chứa đựng dữ liệu , thì delegate chứa đựng chi tiết của phương thức.
Dùng delegate trong C#
Đầu tiên ta phải định nghĩa delegate mà ta muốn dùng ,nghĩa là bảo cho trình biên dịch biết loại phương thức mà delegate sẽ trình
bày.sau đó ta tạo ra các thể hiện của delegate.
Cú pháp
delegate void VoidOperation(uint x);
Ta chỉ định mỗi thể hiện của delegate có thể giữ một tham chiếu đến 1 phương thức mà chứa một thông số uint và trả vầ kiểu
void.
Ví dụ khác : nếu bạn muốn định nghĩa 1 delegate gọi là twolongsOp mà trình bày 1 hàm có 2 thông số kiểu long và trả về kiểu
double. ta có thể viết :
delegate double TwoLongsOp(long first, long second);
Hay 1 delegate trình bày phương thức không nhận thông số và trả về kiểu string
delegate string GetAString();
Cú pháp cũng giống như phương thức , ngoại trừ việc không có phần thân của phương thức,và bắt đầu với delegate.ta cũng có thể
áp dụng các cách thức truy nhập thông thường trên một định nghĩa delegate - public,private,protected ...
public delegate string GetAString();
Mỗi lần ta định nghĩa một delegate chúng ta có thể tạo ra một thể hiện của nó mà ta có thể dùng đề lưu trữ các chi tiết của 1
phưong thức cụ thể.
Lưu ý : với lớp ta có 2 thuật ngữ riêng biệt : lớp để chỉ định nghĩa chung , đối tượng để chỉ một thể hiện của 1 lớp, tuy nhiên đối
với delegate ta chỉ có một thuật ngữ là '1 delegate' khi tạo ra một thể hiện của delegate ta cũng gọi nó là delegate. vì vậy cần xem
xét ngữ cảnh để phân biệt.
Đoạn mã sau minh hoạ cho 1 delegate:
private delegate string GetAString();

static void Main(string[] args)


{
int x = 40;
GetAString firstStringMethod = new GetAString(x.ToString);
Console.WriteLine("String is" + firstStringMethod());
// With firstStringMethod initialized to x.ToString(),
// the above statement is equivalent to saying
// Console.WriteLine("String is" + x.ToString());

}
Trong mã này , ta tạo ra delegate GetAString, và khởi tạo nó để nó tham khảo đến phương thức ToString() của một biến nguyên
x .chúng ta sẽ biên dịch lỗi nếu cố gắng khởi tạo FirstStringMethod với bất kì phương thức nào có thông số vào và kiểu trả về là
chuỗi.
1 đặc tính của delegate là an toàn- kiểu ( type-safe) để thấy rằng chúng phải đảm bảo dấu ấn ( signature) của phương thức được
gọi là đúng.tuy nhiên 1 điều thú vị là, chúng không quan tâm kiểu của đối tượng phương thức là gì khi gọi hoặc thậm chí liệu
rằng phương thức đó là static hay là một phưong thức thể hiện.
Để thấy điều này ta mở rộng đoạn mã trên, dùng delegate FirstStringMethod để gọi các phương thức khác trên những đối tượng
khác - 1 phương thức thể hiện và 1 phương thức tĩnh .ta cũng dùng lại cấu trúc currency, và cấu trúc currency đã có overload
riêng của nó cho phương thức ToString().để xem xét delegate với phương thức tĩnh ta thêm 1 phương thức tĩnh với cùng dấu ấn
như currency:
struct Currency
{
public static string GetCurrencyUnit()
{
return "Dollar";
}
Bây giờ ta sử dụng thể hiện GetAString như sau:
private delegate string GetAString();

static void Main(string[] args)


{
int x = 40;
GetAString firstStringMethod = new GetAString(x.ToString);
Console.WriteLine("String is " + firstStringMethod());
Currency balance = new Currency(34, 50);
firstStringMethod = new GetAString(balance.ToString);
Console.WriteLine("String is " + firstStringMethod());
firstStringMethod = new GetAString(Currency.GetCurrencyUnit);
Console.WriteLine("String is " + firstStringMethod());
Đoạn mã này chỉ cho ta biết làm thế nào để gọi 1 phương thức qua trung gian là delegate,đăng kí lại delegate để tham chiếu đến
một phương thức khác trên 1 thể hiện khác của lớp.
Tuy nhiên ta vẫn chưa nắm rõ được quy trình truyền 1 delegate đến 1 phương thức khác, cũng như chưa thấy được lợi ích của
delegate qua ví dụ trên. như ta có thể gọi trực tiếp ToString() từ int hay currency mà không cần delegate.ta cần những ví dụ phức
tạp hơn để hiểu rõ delegate. ta sẽ trình bày 2 ví dụ : ví dụ 1 đơn giản sử dụng delegate để gọi vào thao tác khác..nó chỉ rõ làm thế
nào để truyền delegate đến phương thức và cách sử dụng mảng trong delegate . ví dụ 2 phức tạp hơn là lớp BubbleSorter, mà
thực thi 1 phương thức sắp xếp mảng đối tượng tăng dần. lớp này sẽ rất khó viết nếu không có delegate.
Ví dụ SimpleDelegate
Trong ví dụ này ta sẽ tạo lớp MathOperations mà có vài phương thức static để thực thi 2 thao tác trên kiểu double, sau đó ta dùng
delegate để gọi những phương thức này.lớp như sau:
class MathsOperations
{
public static double MultiplyByTwo(double value)
{
return value*2;
}

public static double Square(double value)


{
return value*value;
}
}

Sau đó ta gọi phương thức này như sau:

using System;

namespace Wrox.ProCSharp.AdvancedCSharp
{
delegate double DoubleOp(double x);

class MainEntryPoint
{
static void Main()
{
DoubleOp [] operations =
{
new DoubleOp(MathsOperations.MultiplyByTwo),
new DoubleOp(MathsOperations.Square)
};

for (int i=0 ; i<operations.Length ; i++)


{
Console.WriteLine("Using operations[{0}]:", i);
ProcessAndDisplayNumber(operations[i], 2.0);
ProcessAndDisplayNumber(operations[i], 7.94);
ProcessAndDisplayNumber(operations[i], 1.414);
Console.WriteLine();
}
}

static void ProcessAndDisplayNumber(DoubleOp action, double value)


{
double result = action(value);
Console.WriteLine(
"Value is {0}, result of operation is {1}", value, result);
}

Trong đoạn mã này ta khởi tạo 1 mảng delegate doubleOp.mỗi phần tử của mảng được khởi động để tham chiếu đến 1 thao tác
khác được thực thi bởi lớp MathOperations.sau đó , nó lặp xuyên suốt mảng,ứng dụng mỗi thao tác đến 3 kiểu giá trị khác
nhau.điều này minh họa cách sử dụng delegate- là có thể nhóm những phương thức lại với nhau thành mảng để sử dụng, để ta có
thể gọi một vài phương thức trong vòng lặp.
Chỗ quan trọng trong đoạn mã là chỗ ta truyền 1 delegate vào phương thức ProcessAndDisplayNumber(), ví dụ:

ProcessAndDisplayNumber(operations[i], 2.0);
Ở đây ta truyền tên của delegate,nhưng không có thông số nào.cho rằng operation[i] là 1 delegate :
operation[i] nghĩa là 'delegate',nói cách khác là phương thức đại diện cho delegate
operation[i](2.0) nghĩa là ' gọi thực sự phương thức này, truyền giá trị vào trong ngoặc'.
Phương thức ProcessAndDisplayNumber() được định nghĩa để lấy 1 delegate như là thông số đầu tiên của nó :
static void ProcessAndDisplayNumber(DoubleOp action, double value)
Sau đó khi ở trong phương thức này , ta gọi:
double result = action(value);
Thể hiện delegate action được gọi và kết quả trả về được lưu trữ trong result
chạy ví dụ ta có:
SimpleDelegate
Using operations[0]:
Value is 2, result of operation is 4
Value is 7.94, result of operation is 15.88
Value is 1.414, result of operation is 2.828

Using operations[1]:
Value is 2, result of operation is 4
Value is 7.94, result of operation is 63.0436
Value is 1.414, result of operation is 1.999396
Ví dụ BubleSorter
Sau đây ta sẽ xem 1 ví dụ cho thấy sự hữu ích của delegate. ta sẽ tạo lớp bublesorter. lớp này thực thi 1 phương thức tĩnh,Sort(),
lấy thông số đầu là 1 mảng đối tượng, và sắp xếp lại chúng tăng dần.ví dụ để sắp xếp 1 mảng số nguyên bằng thuật toán Bubble
sort :
///đây không phải là 1 phần của ví dụ
for (int i = 0; i < sortArray.Length; i++)
{
for (int j = i + 1; j < sortArray.Length; j++)
{
if (sortArray[j] < sortArray[i]) // problem with this test
{
int temp = sortArray[i]; // swap ith and jth entries
sortArray[i] = sortArray[j];
sortArray[j] = temp;
}
}
}

Thuật toán này tốt cho số nguyên, nhưng ta muốn phương thức sort() sắp xếp cho mọi đối tượng,ta thấy vấn đề nằm ở dòng
if(sortArray[j] < sortArray[i]) trong đoạn mã trên.bởi ta muốn so sánh 2 đối tượng trên mảng mà cái nào là lớn hơn.chúng ta có
thể sắp xếp kiểu int, nhưng làm thế nào để sắp xếp những lớp chưa biết hoặc không xác định cho đến lúc chạy.câu trả lời là mã
client, mà biết về lớp muốn sắp xếp, phải truyền 1 delegate gói trong một phương thức sẽ làm công việc so sánh
Định nghĩa delegate như sau:
delegate bool CompareOp(object lhs, object rhs);
Và xây dựng phương thức sort() là :
static public void Sort(object [] sortArray, CompareOp gtMethod)
Phần hướng dẫn cho phương thức này sẽ nói rõ rằng gtmethod phải tham chiếu đến 1 phương thức static có 2 đối số,và trả về true
nếu giá trị của đối số thứ 2 là 'lớn hơn' ( nghĩa là năm sau trong mảng) đối số thứ nhất.
mặc dù ta có thể sử dụng delegate ở đây,nhưng cũng có thể giải quyết vấn đề bằng cách sử dụng interface. .NET xây dựng 1
interface IComparer cho mục đích này. tuy nhiên , ta sử dụng delegate vì loại vấn đề này thì thường có khuynh hướng dùng
delegate.
Sau đây là lớp bublesorter :
class BubbleSorter
{
static public void Sort(object [] sortArray, CompareOp gtMethod)
{
for (int i=0 ; i<sortArray.Length ; i++)
{
for (int j=i+1 ; j<sortArray.Length ; j++)
{
if (gtMethod(sortArray[j], sortArray[i]))
{
object temp = sortArray[i];
sortArray[i] = sortArray[j];
sortArray[j] = temp;
}
}
}
}
}

Để dùng lớp này ta cần định nghĩa 1 số lớp khác mà có thể dùng thiết lập mảng cần sắp xếp.ví dụ , công ty điện thoại có danh
sách tên khách hàng, và muốn sắp danh sách theo lương.mỗi nhân viên trình bày bởi thể hiện của một lớp , Employee:
class Employee
{
private string name;
private decimal salary;

public Employee(string name, decimal salary)


{
this.name = name;
this.salary = salary;
}

public override string ToString()


{
return string.Format(name + ", {0:C}", salary);
}

public static bool RhsIsGreater(object lhs, object rhs)


{
Employee empLhs = (Employee) lhs;
Employee empRhs = (Employee) rhs;
return (empRhs.salary > empLhs.salary) ? true : false;
}
}

Lưu ý để phù hợp với dấu ấn của delegate CompareOp, chúng ta phải định nghĩa RhsIsGreater trong lớp này lấy 2 đối tượng để
tham khảo,hơn là tham khảo employee như là thông số.điều này có nghĩa là ta phải ép kiểu những thông số vào trong tham khảo
employee để thực thi việc so sánh.
Bây giờ ta viết mã yêu cầu sắp xếp :

using System;

namespace Wrox.ProCSharp.AdvancedCSharp
{
delegate bool CompareOp(object lhs, object rhs);

class MainEntryPoint
{
static void Main()
{
Employee [] employees =
{
new Employee("Karli Watson", 20000),
new Employee("Bill Gates", 10000),
new Employee("Simon Robinson", 25000),
new Employee("Mortimer", (decimal)1000000.38),
new Employee("Arabel Jones", 23000),
new Employee("Avon from 'Blake's 7'", 50000)};
CompareOp employeeCompareOp = new CompareOp(Employee.RhsIsGreater);
BubbleSorter.Sort(employees, employeeCompareOp);

for (int i=0 ; i<employees.Length ; i++)


Console.WriteLine(employees[i].ToString());
}
}

Chạy mã này sẽ thấy employees được sắp xếp theo lương


BubbleSorter
Bill Gates, £10,000.00
Karli Watson, £20,000.00
Arabel Jones, £23,000.00
Simon Robinson, £25,000.00
Avon from 'Blake's 7', £50,000.00
Mortimer, £1,000,000.38

Multicast delegate
Đến lúc này mỗi delegate mà chúng ta sử dụng chỉ gói ghém trong 1 phương thức đơn gọi.gọi delegate nào thì dùng phương thức
đó.nếu ta muốn gọi nhiều hơn 1 phương thức, ta cần tạo một lời gọi tường minh xuyên suốt delegate nhiều hơn một lần.tuy nhiên,
1 delegate có thể gói ghém nhiều hơn 1 phương thức. 1 delegate như vậy gọi là multicast delegate. nếu 1 multicast delegate được
gọi, nó sẽ gọi liên tiếp những phương thức theo thứ tự.để làm điều này, delegate phải trả về là void.nếu ta dùng một delegate có
kiểu trả về là void , trình biên dịch sẽ coi như đây là một multicast delegate.xem ví dụ sau , dù cú pháp giống như trước đây
nhưng nó thực sự là một multicast delegate, operations,
delegate void DoubleOp(double value);
// delegate double DoubleOp(double value); // can't do this now

class MainEntryPoint
{
static void Main()
{
DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo);
operations += new DoubleOp(MathOperations.Square);
Trong ví dụ trên muốn tham khảo đến 2 phương thức ta dùng mảng delegate. ở đây , đơn giản ta chỉ thêm 2 thao tác này vào
trong cùng một multicast delegate.multiccast delegate nhận toán tử + và +=. nếu ta muốn , ta có thể mở rộng 2 dòng mã trên , có
cùng cách tác động :
DoubleOp operation1 = new DoubleOp(MathOperations.MultiplyByTwo);
DoubleOp operation2 = new DoubleOp(MathOperations.Square);
DoubleOp operations = operation1 + operation2;
multicast delegate cũng biết toán tử - và -= để bỏ đi phương thức được gọi từ delegate.
một muticast delegate là một lớp được dẫn xuất từ System.MulticastDelegate mà lại được dẫn xuất từ System.Delegate.
System.MulticastDelegate có thêm những thành phần để cho phép nối những phương thức gọi cùng với nhau vào một danh sách.
Minh hoạ cho sử dụng multicast delegate ta sử dụng lại ví dụ simpleDelegate biến nó thành một ví dụ mới MulticastDelegate. bởi
vì ta cần delegate trả về kiểu void , ta phải viết lại những phương thức trong lớp Mathoperations ,chúng sẽ trình bày kết quả thay
vì trả về :
class MathOperations
{
public static void MultiplyByTwo(double value)
{
double result = value*2;
Console.WriteLine(
"Multiplying by 2: {0} gives {1}", value, result);
}

public static void Square(double value)


{
double result = value*value;
Console.WriteLine("Squaring: {0} gives {1}", value, result);
}
}

Để dàn xếp sự thay đổi này , ta viết lại ProcessAndDisplayNumber:

static void ProcessAndDisplayNumber(DoubleOp action, double value)


{
Console.WriteLine("\nProcessAndDisplayNumber called with value = " +
value);
action(value);
}

Bây giờ thử multicast delegate ta vừa tạo :


static void Main()
{
DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo);
operations += new DoubleOp(MathOperations.Square);

ProcessAndDisplayNumber(operations, 2.0);
ProcessAndDisplayNumber(operations, 7.94);
ProcessAndDisplayNumber(operations, 1.414);
Console.WriteLine();
}

Bây giờ mỗi lần ProcessAndDisplayNumber được gọi ,nó sẽ trình bày 1 thông điệp để báo rằng nó được gọi câu lệnh
sau:action(value);
Sẽ làm cho mỗi phương thức gọi trong thể hiện delegate action được gọi liên tiếp nhau.
Kết quả :
MulticastDelegate

ProcessAndDisplayNumber called with value = 2


Multiplying by 2: 2 gives 4
Squaring: 2 gives 4

ProcessAndDisplayNumber called with value = 7.94


Multiplying by 2: 7.94 gives 15.88
Squaring: 7.94 gives 63.0436

ProcessAndDisplayNumber called with value = 1.414


Multiplying by 2: 1.414 gives 2.828
Squaring: 1.414 gives 1.999396

Nếu dùng multicast delegate , ta nên nhận thức đến thứ tự phương thức được nối với nhau trong cùng một delegate sẽ được gọi là
không xác định. do đó ta nên tránh viết mã mà những phương thức được gọi liên hệ với nhau theo một thứ tự cụ thể.
Code for Download:

SimpleDelegate

BubleSorter

MultiCastDelegate

Các chỉ thị tiền xử lí trong C#


Bên cạnh các từ khoá thường dùng, C# có 1 số lệnh tiền xử lí .những lệnh này không bao giờ được biên dịch thành bất kì dòng
lệnh nào trong mã thực thi. thay vào đó nó có ảnh hưởng đến các khía cạnh của quy trình biên dịch. ví dụ , ta có thể dùng chỉ dẫn
tiền xử lí để ngăn trình biên dịch biên dịch một phần đoạn mã nào đó .
Chỉ thị tiền xử lí được phân biệt bằng cách bắt đầu với dấu # .
#define và #undef
#define được dùng ví dụ như :
#define DEBUG
Cho trình biên dịch biết biểu tượng với tên được đặt ( DEBUG) tồn tại. nó hơi giống như khai báo biến nhưng nó không có giá trị
- mà chỉ tồn tại.
Trái ngược với #define là #undef : bỏ định nghĩa biểu tượng : #undef DEBUG
Ta cần đặt các chỉ thị #define và #undef vào đầu tập tin nguồn trước bất kì khai báo đối tượng được biên dịch.
lưu ý : các chỉ thị tiền xử lí không có dấu ' ; ' ở cuối câu lệnh thường thì các lệnh chỉ trên 1 dòng . nếu nó thấy 1 chỉ thị tiền xử lí,
nó xem lệnh kết tiếp sẽ nằm ở dòng kế tiếp.
#if,#elif,#else,#endif
các chỉ thị này thông báo cho trình biên dịch biết liệu có biên dịch đoạn mã hay không, ví dụ :
int DoSomeWork(double x)
{
// do something
#if DEBUG
Console.WriteLine("x is " + x);
#endif
}

Dòng lệnh Console.Writeline chỉ được thực hiện khi DEBUG được định nghĩa.( bằng chỉ thị #define). khi thấy #if nó kiểm tra
xem biểu tượng có tồn tại hay không nếu có thì biên dịch đoạn mã bên trong ngược lại bỏ qua đoạn mã bên trong giữa #if và
#endif. ta dùng cách này khi muốn vá lỗi, mà lỗi liên quan đến đoạn mã trong #if ,khi muốn đoạn mã này chạy ta định nghĩa
DEBUG, khi muốn nó không chạy ta #undef DEBUG,kích cỡ cũa tập tin thực thi sẽ nhỏ hơn.
Các chỉ thị #elif (= else if ) và # else được dùng trong khối #if .và có thể lồng khối #if

#define ENTERPRISE
#define W2K

// further on in the file

#if ENTERPRISE
// do something
#if W2K
// some code that is only relevant to enterprise
// edition running on W2K
#endif
#elif PROFESSIONAL
// do something else
#else
// code for the leaner version
#endif

#if và #elif cũng hổ trợ 1 số tác tử luận lý , dùng !,==,!=,||. 1 biểu tượng là true nếu nó tồn tại và ngược lại là false. ví dụ :
#if W2K && (ENTERPRISE==false) // nếu W2K đưọc định nghĩa còn ENTERPRISE thì không
#warning và #error
Nếu trình biên dịch thấy #warning nó sẽ trình bày chuỗi xuất iện phía sau nó đến người dùng,sau đó biên dịch tiếp. nếu thấy chỉ
thi #error ,nó sẽ trình bày chuỗi con đến người dùng nếu đó là 1 thông báo lỗi biên dịch , sau đó ngừng biên dịch
#if DEBUG && RELEASE
#error "You've defined DEBUG and RELEASE simultaneously! "
#endif

#warning "Don't forget to remove this line before the boss tests the code! "
Console.WriteLine("*I hate this job*");

#region và #endregion
Các chỉ thị #region và #endregion đưọc dùng để đánh dấu khối mã được xem như là khối đơn với tên được đặt như :
#region Member Field Declarations
int x;
double d;
Currency balance;
#endregion
chỉ thị này không có ảnh hưởng nào đến trình biên dịch nhưng nó hữu ích cho 1 số trình biên tập bao gồm cả trình biên tập của
VS.NET. trình biên tập có thể dùng chỉ thị naỳ để gói gọn mã của ta trên màn hình.
# line
chỉ thị #line có thể được dùng thay cho tên tập tin và thông tin số dòng mà xuất bởi trình biên dịch trong các thông báo cảnh báo
hay lỗi .nó thường dùng khi bạn viết mã kết hợp với 1 số gói khác mà sau đó sẽ thay đoạn mã bạn gõ vào trước khi biên dịch
,trình biên dịch sẽ thông báo số dòng và tên tập tin không phù hợp với số dòng trong tập tin mà bạn đang chỉnh sửa .chỉ thị #line
có thể được dùng để lưu sự phù hợp. ta cũng có thể dùng cú pháp #line default để lưu dòng thành số dòng mặc định :
#line 164 "Core.cs" // we happen to know this is line 164 in the file
// Core.cs, before the intermediate
// package mangles it.

// later on

#line default // restores default line numbering

Attribute
1 attribute là 1 thứ dùng để đánh dấu (marker) mà được ứng dụng đến 1 phương thức hay 1 lớp thậm chí là 1 đối số riêng trong 1
phưong thức ,cung cấp thông tin thêm về mục đó. ví dụ attribute Conditional có thể được dùng để đánh dấu 1 phương thức như là
1 phương thức debug như sau :
[Conditional("DEBUG")]
public void DoSomeDebugStuff()
{
// do something
}

Để áp dụng 1 attribute đến 1 mục ta cung cấp tên của attribute trong ngoặc vuông ngay trước định nghĩa mục. các attribute lấy
vài thông số - những thông số này cung cấp bên trong dấu ngoặc theo sau tên attribute
Ở cấp độ cơ bản nhất của nó , ứng dụng 1 attribute vào 1 mục có thể đơn giản chỉ là thông tin thêm về mục đó đựợc bỏ trong
assembly đuợc biên dịch dùng cho mục đích hướng dẫn thêm. 1 số attribute được định nghĩa trong các lớp cơ sở đuợc nhận ra bởi
trình biên dịch C#.mà từ đó trình biên dịch có thể gây ra những tác động trên mã . ví dụ trên trình biên dịch sẽ biên dịch mã của
mục Conditional chỉ nếu biểu tượng DEBUG đã được định nghĩa trước.
Trong phần này ta sẽ xem xét 3 attribute sau :
- Conditional : có thể đánh dấu bất kì phương thức với attribute Conditional . sẽ ngăn ngừa trình biên dịch từ việc biên dịch
phương thức đó hay bất kì câu lệnh mà tham khảo đến nó nếu tên biểu tượng không được định nghĩa. điều này có thể được dùng
cho điều kiện biên dịch .
- Dllimport - đôi khi ta cần truy nhập đến các Window API cơ bản hoặc các hàm kiểu cũ khác mà được thực thi trong các
window DLLs.attribute Dllimport dùng cho mục đích này . nó được dùng đánh dấu 1 phương thức được định nfghĩa trong Dll
bên ngoài.
- Obsolete - attribute này được dùng để đánh dấu 1 phương thức được xem như obsolete ( không dùng nữa) . tuỳ thuộc vào việc
thiết lập trên attribute này , trình biên dịch sẽ sinh ra 1 cảnh báo hay lỗi nếu nó gặp bất kì mã nào cố gắng dùng phương thức này.
Ta xem xét các attribute này thông qua ví dụ Attributes . trình bày 1 message box dùng hàm Window API ,MessageBox , và cũng
trình bày 2 thông điệp vá lỗi. 1 trong những thông điệp này được gọi trung gian qua 1 phương thức obsolete

#define DEBUG // comment out this line if doing a release build

using System;
using System.Runtime.InteropServices;
using System.Diagnostics;

namespace Wrox.ProCSharp.AdvancedCSharp
{

class MainEntryPoint
{
[DllImport("User32.dll")]
public static extern int MessageBox(int hParent, string Message,
string Caption, int Type);

static void Main()


{
DisplayRunningMessage();
DisplayDebugMessage();
MessageBox(0, "Hello", "Message", 0);
}

[Conditional("DEBUG")]
private static void DisplayRunningMessage()
{
Console.WriteLine("Starting Main routine. Current time is " +
DateTime.Now);
}

[Conditional("DEBUG")]
[Obsolete()]
private static void DisplayDebugMessage()
{
Console.WriteLine("Starting Main routine");
}
}
}

Đầu tiên ta chỉ định một số namespace


- DllImport nằm trong namespace System.Runtime.InteropServices
- Conditional nằm trong namespace System.Diagnostics .
Bên trong phương thức ta định nghĩa phương thức mà ta gọi từ dll bên ngoài .hàm API MessageBox() đuợc định nghĩa trong
User32.dll, vì thế ta truyền tên tập tin vào attribute DllImport . ta khai báo hàm giống như trong C.
Tiếp theo trong phương thức DisplayDebugMessage() , ta chỉ muốn phương thức này chạy nếu ta đang làm 1 xây dựng vá lỗi, vì
thế ta đánh dấu nó như là conditional trên biểu tượng DEBUG .ta cũng xem phương thức này là 1 obsoblete , bởi vì ta đã viết một
phiên bản tốt hơn, DisplayRunningMessage(), mà trình bày thêm thời gian và ngày tháng.ta không khuyến khích mọi người dùng
DisplayDebugMessage() thêm nữa, vì thế ta đánh dấu nó với attribute Obsolete để trình biên dịch sẽ trình bày 1 cảnh báo nếu
phương thức này được dùng. có 1 vài kiểu overload của hàm này :
[Obsolete("The DisplayDebugMessage is obsolete. Use " +
"DisplayRunningMessage instead.")]
Hàm này sẽ sinh ra một cảnh báo nếu phương thức này được dùng, trong khi :
[Obsolete("The DisplayDebugMessage is obsolete. Use " +
"DisplayRunningMessage instead.", true)]
sẽ gây ra lỗi biên dịch thay vì là 1 cảnh báo nếu phương thức này được dùng. thông số thứ hai quy định đây là lỗi hay là cảnh báo
nếu obsolete được dùng .
Về attribute Conditional , đây là 1 attribute phức tạp , bởi vì nếu điều kiện không hợp, trình biên dịch không chỉ không biên dịch
đoạn mã trong phương thức mà còn tự động bỏ qua bất kì dòng nào mã khác trong tập tin nguồn mà gọi phương thức này . để
trình biên dịch có khả năng làm điều này phương thức phải trả về void . attribute Conditional chỉ có thể áp dụng đến phương thức
hoàn chỉnh như là 1 đơn vị biên dịch
Chạy ví dụ trên với biểu tượng DEBUG đưọc định nghĩa cho ra kết quả . lưu ý cảnh báo được đưa ra :
csc Attributes.cs
Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

Attributes.cs(19,10): warning CS0612:


'Wrox.ProCSharp.AdvancedCSharp.MainEntryPoint.DisplayDebugMessage()'
is obsolete

Attributes
Starting Main routine. Current time is 12/02/2002 18:47:32
Starting Main routine

Code for Download:

Attributes

Các mã không an toàn


Có những trường hợp ta cần truy xuất bộ nhớ trực tiếp khi ta muốn truy xuất vào các hàm bên ngoài ( không thuộc .NET) mà đòi
hỏi con trỏ được truyền vào như tham số( ví dụ như các hàm API ).hoặc là vì ta muốn truy nhập vào nội dung bộ nhớ để sửa
lỗi....Trong phần này ta sẽ xem xét cách C# đáp ứng những điều này như thế nào.

Con trỏ
( trình bày vắng tắt )

Con trỏ đơn giản là 1 biến lưu địa chỉ của một thứ khác theo cùng 1 cách như là 1 tham chiếu. sự khác biệt là cú pháp C# trong
tham chiếu không cho phép ta truy xuất vào địa chỉ bộ nhớ.

3 ưu điểm của con trỏ :

• Cải thiện sự thực thi : cho ta biết những gì ta đang làm,đảm bảo rằng dữ liệu được truy xuất hay thao tác theo cách hiệu
quả nhất - đó là lí do mà C và C++ cho phép dung con trỏ trong ngôn ngữ của mình.
• Khả năng tích hợp với các phần trước ( Backward compatibility ) - đôi khi ta phải sử dụng lại các hàm API cho mục
đích của ta.Mà các hàm API được viết bằng C,ngôn ngữ dùng con trỏ rất nhiều, nghĩa là nhiều hàm lấy con trỏ như
tham số.Hoặc là các DLL do 1 hãng nào đó cung cấp chứa các hàm lấy con trỏ làm tham số . Trong nhiều trường hợp ta
có thể viết các khai báo DLlImport theo cách tránh sử dụng con trỏ , ví dụ như dùng lớp System.IntPtr.
• Ta có thể cần tạo ra các địa chỉ vùng nhớ có giá trị cho người dùng - ví dụ nếu ta muốn phát triển 1 ứng dụng mà cho
phép người dùng tương tác trực tiếp đến bộ nhớ, như là 1 debugger.

Nhược điểm :

• Cú pháp để lấy các hàm phức tạp hơn


• Con trỏ khó sử dụng

• Nếu không cẩn thận ta có thể viết lên các biến khác ,làm tràn stack, mất thông tin, đụng độ ...

• C# có thể từ chối thi hành những đoạn mã không an toàn này (đoạn mã có sử dụng con trỏ)

Ta có thể đánh dấu đoạn mã có sử dụng con trỏ bằng cách dùng từ khoá unsafe
Ví dụ : dùng cho hàm

unsafe int GetSomeNumber()


{
// code that can use pointers
}
Dùng cho lớp hay struct
unsafe class MyClass
{
// any method in this class can now use pointers
}
Dùng cho 1 trường
class MyClass
{
unsafe int *pX; // declaration of a pointer field in a class
}

Hoặc một khối mã

void MyMethod()
{
// code that doesn't use pointers
unsafe
{
// unsafe code that uses pointers here
}
// more 'safe' code that doesn't use pointers
}
Tuy nhiên ta không thể đánh dấu 1 biến cục bộ là unsafe
int MyMethod()
{
unsafe int *pX; // WRONG
}

Để biên dịch các mã chứa khối unsafe ta dùng lệnh sau :

csc /unsafe MySource.cs


hay
csc -unsafe MySource.cs
Cú pháp con trỏ
int * pWidth, pHeight;
double *pResult;
Lưu ý khác với C++ ,kí tự * kết hợp với kiểu hơn là kết hợp với biến - nghĩa là khi ta khai báo
như ở trên thì pWidth và pHeight đều là con trỏ do có * sau kiểu int, khác với C++ ta phải khai
báo * cho cả hai biến trên thì cả hai mới là con trỏ.
Cách dùng * và & giống như trong C++ :
& : lấy địa chỉ
* : lấy nội dung của địa chỉ
Ép kiểu con trỏ thành kiểu Int
Vì con trỏ là 1 số int lưu địa chỉ nên ta có thể chuyển tường minh con trỏ thành kiểu int hay ngược lại.Ví dụ:

int x = 10;
int *pX, pY;
pX = &x;
pY = pX;
*pY = 20;
uint y = (uint)pX;
int *pD = (int*)y;

y là uint.sau đó ta chuyển ngược lại thành biến con trỏ pD

1 lý do để ta phải ép kiểu là Console.WriteLine không có overload nào nhận thông số là con trỏ do đó ta phải ép nó sang kiểu số
nguyên int
Console.WriteLine("Address is" + pX); // wrong - will give a
// compilation error
Console.WriteLine("Address is" + (uint) pX); // OK
Ép kiểu giữa những kiểu con trỏ
Ta cũng có thể chuyển đổi tường minh giữa các con trỏ trỏ đến1 kiểu khác ví dụ :

byte aByte = 8;
byte *pByte= &aByte;
double *pDouble = (double*)pByte;

void Pointers
Nếu ta muốn giữ 1 con trỏ , nhưng không muốn đặc tả kiểu cho con trỏ ta có thể khai báo co ntrỏ là void:

void *pointerToVoid;
pointerToVoid = (void*)pointerToInt; // pointerToInt declared as int*

mục đích là khi ta cần gọi các hàm API mà đòi hỏi thông số void*.

Toán tử sizeof
Lấy thông số là tên của kiểu và trả về số byte của kiểu đó ví dụ :

int x = sizeof(double);

x có giá trị là 8

Bảng kích thước kiểu :

sizeof(sbyte) = 1; sizeof(byte) = 1;

sizeof(short) = 2; sizeof(ushort) = 2;

sizeof(int) = 4; sizeof(uint) = 4;

sizeof(long) = 8; sizeof(ulong) = 8;

sizeof(char) = 2; sizeof(float) = 4;

sizeof(double) = 8; sizeof(bool) = 1;

Ta cũng có thể dùng sizeof cho struct nhưng không dùng được cho lớp.

Ví dụ PointerPlayaround
Ví dụ sau trình bày cách thao tác trên con trỏ và trình bày kết quả, cho phép ta thấy những gì xảy ra trong bộ nhớ và nơi biến
được lưu trữ:

using System;

namespace Wrox.ProCSharp.AdvancedCSharp
{
class MainEntryPoint
{
static unsafe void Main()
{
int x=10;
short y = -1;
byte y2 = 4;
double z = 1.5;
int *pX = &x;
short *pY = &y;
double *pZ = &z;

Console.WriteLine(
"Address of x is 0x{0:X}, size is {1}, value is {2}",
(uint)&x, sizeof(int), x);
Console.WriteLine(
"Address of y is 0x{0:X}, size is {1}, value is {2}",
(uint)&y, sizeof(short), y);
Console.WriteLine(
"Address of y2 is 0x{0:X}, size is {1}, value is {2}",
(uint)&y2, sizeof(byte), y2);
Console.WriteLine(
"Address of z is 0x{0:X}, size is {1}, value is {2}",
(uint)&z, sizeof(double), z);
Console.WriteLine(
"Address of pX=&x is 0x{0:X}, size is {1}, value is 0x{2:X}",
(uint)&pX, sizeof(int*), (uint)pX);
Console.WriteLine(
"Address of pY=&y is 0x{0:X}, size is {1}, value is 0x{2:X}",
(uint)&pY, sizeof(short*), (uint)pY);
Console.WriteLine(
"Address of pZ=&z is 0x{0:X}, size is {1}, value is 0x{2:X}",
(uint)&pZ, sizeof(double*), (uint)pZ);

*pX = 20;
Console.WriteLine("After setting *pX, x = {0}", x);
Console.WriteLine("*pX = {0}", *pX);

pZ = (double*)pX;
Console.WriteLine("x treated as a double = {0}", *pZ);

Console.ReadLine();
}
}
}
Mã gồm 3 biến

• int x
• short y

• double z

Cùng với các con trỏ trỏ đến các giá trị này.sau đó ta trình bày giá trị của các biến và kích thước,địa chỉ của nó.Ta dùng đặc tả
{0:X} trong Console.WriteLine để địa chỉ bộ nhớ được trình bày theo định dạng số bát phân.

Cuối cùng ta dùng con trỏ pX thay đổi giá trị của x thành 20,và thử ép kiểu biến x thành 1 double để xem điều gì sẻ xảy ra

Biên dịch mã ,ta có kết quả sau :


csc PointerPlayaround.cs
Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

PointerPlayaround.cs(7,26): error CS0227: Unsafe code may only appear if


compiling with /unsafe

csc /unsafe PointerPlayaround.cs


Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

PointerPlayaround
Address of x is 0x12F8C4, size is 4, value is 10
Address of y is 0x12F8C0, size is 2, value is -1
Address of y2 is 0x12F8BC, size is 1, value is 4
Address of z is 0x12F8B4, size is 8, value is 1.5
Address of pX=&x is 0x12F8B0, size is 4, value is 0x12F8C4
Address of pY=&y is 0x12F8AC, size is 4, value is 0x12F8C0
Address of pZ=&z is 0x12F8A8, size is 4, value is 0x12F8B4
After setting *pX, x = 20
*pX = 20
x treated as a double = 2.63837073472194E-308
Pointer Arithmetic
Ta có thể cộng hay trừ số nguyên trên con trỏ.Ví dụ , giả sử ta có 1 con trỏ trỏ đến số nguyên,và ta thử cộng 1 vào giá trị của nó
.trình biên dịch sẽ biết và tăng vùng nhớ lên 4 byte ( do kiểu int có kích thước 4 byte).nếu là kiểu double thì khi cộng 1 sẽ tăng
giá trị của con trỏ lên 8 byte.

ta có thể dùng toán tử +, -, +=, -=, ++,và -- với biến bên phía phải của toán tử này là long hay ulong

Ví dụ

uint u = 3;
byte b = 8;
double d = 10.0;
uint *pUint= &u; // size of a uint is 4
byte *pByte = &b; // size of a byte is 1
double *pDouble = &d; // size of a double is 8

Giả sử địa chỉ của những con trỏ này trỏ đến là :

• pUint: 1243332
• pByte: 1243328

• pDouble: 1243320

sau khi thi hành ta có :


++pUint; // adds 1= 4 bytes to pUint
pByte -= 3; // subtracts 3=3bytes from pByte
double *pDouble2 = pDouble - 4; // pDouble2 = pDouble - 32 bytes (4*8 bytes)

Con trỏ sẽ có giá trị:

• pUint: 1243336
• pByte: 1243321

• pDouble2: 1243328

Ta cũng có thể trừ 2 con trỏ với nhau .giá trị kết quả là kiểu long bằng giá trị con trỏ chia cho kích thước của kiểu mà nó đại
diện .Ví dụ :

double *pD1 = (double*)1243324; // note that it is perfectly valid to


// initialize a pointer like this.
double *pD2 = (double*)1243300;
long L = pD1-pD2; // gives the result 3 (=24/sizeof(double))
Con trỏ đến Struct - Toán tử truy xuất các thành viên con trỏ
Cũng giống như con trỏ trong các kiểu dữ liệu có sẵn. tuy nhiên thêm 1 điều kiện là - Struct không chứa bất kì kiểu tham chiếu
nào.Do con trỏ không thể trỏ đến bất kì kiểu tham chiếu nào. để tránh điều này , trình biên dịch sẽ phất cờ lỗi nếu ta tạo ra một
con trỏ đến bất kì Struct nào chứa kiểu tham chiếu .

Giả sử ta có struct như sau :

struct MyGroovyStruct
{
public long X;
public float F;
}

Sau đó ta định nghĩa con trỏ cho nó :

MyGroovyStruct *pStruct;
Khởi tạo nó :

MyGroovyStruct Struct = new MyGroovyStruct();


pStruct = &Struct;

Cũng có thể truy xuất các giá trị thành viên của 1 struct bằng con trỏ :

(*pStruct).X = 4;
(*pStruct).F = 3.4f;

Tuy nhiên cú pháp này hơi phức tạp. C# định nghĩa 1 toán tử khác cho phép ta truy xuất các thành viên của Struct bằng con trỏ
đơn giản hơn , gọi là toán tử truy xuất thành viên con trỏ ,kí hiệu là ->

Cách dùng :

pStruct->X = 4;
pStruct->F = 3.4f;

Ta cũng có thể thiết đặt trực tiếp con trỏ của kiểu tương đương để trỏ đến các trường trong Struct

long *pL = &(Struct.X);


float *pF = &(Struct.F);
hay :
long *pL = &(pStruct->X);
float *pF = &(pStruct->F);
Con trỏ đến các thành viên của lớp
Ta đã nói rằng không thể tạo ra con trỏ đến lớp.vì việc tạo có thể làm cho bộ gom rác hoạt động không đúng.

tuy nhiên ta có thể tạo các con trỏ đến các thành viên của lớp .Ta sẽ viết lại struct của ví dụ trước như là lớp :

class MyGroovyClass
{
public long X;
public float F;
}

sau đó ta có thể tạo 1 con trỏ đến các trường của nó ,X và F.tuy nhiên làm như vậy sẽ gây ra lỗi :

MyGroovyClass myGroovyObject = new MyGroovyClass();


long *pL = &( myGroovyObject.X); // wrong
float *pF = &( myGroovyObject.F); // wrong

Do X và F nằm trong 1 lớp , mà được đặt trong heap.nghĩa là chúng vẫn gián tiếp chịu sự quản lý của bộ gom rác.cụ thể bộ gom
rác có thể quyết định di chuyển MyGroovyClass đến 1 vị trí mới trong bộ nhớ để dọn dẹp heap.Nếu làm điều này thì bộ gom rác
tất nhiên sẽ cập nhật tất cả các tham chiếu đến đối tượng ,giả sử như biến myGrooveObject vẫn sẽ trỏ đến đúng vị trí.Tuy nhiên
bộ gom rác không biết gì về con trỏ cả. vì thế nếu di chuyển các đối tượng tham chiếu bởi myGrooveObject,pL và pF sẽ vẫn
không thay đôỉ và kết cuộc là trỏ đến sai vị trí vùng nhớ.

Để giải quyết vấn đề này ta dùng từ khóa fixed , mà cho bộ gom rác biết rằng có thể có con trỏ trỏ đến các thành viên của các thể
hiện lớp,vì thế các thể hiện lớp này sẽ không được di chuyển.cú pháp như sau nếu ta chỉ muốn khai báo 1 con trỏ :

MyGroovyClass myGroovyObject = new MyGroovyClass();


// do whatever
fixed (long *pObject = &( myGroovyObject.X))
{
// do something
}

nếu ta muốn khai báo nhiều hơn 1 con trỏ ta có thể đặt nhiều câu lệnh fixed trước khối mã giống nhau :

MyGroovyClass myGroovyObject = new MyGroovyClass();


fixed (long *pX = &( myGroovyObject.X))
fixed (float *pF = &( myGroovyObject.F))
{
// do something
}
Ta có thể lồng các khối fixed nếu ta muốn fix các con trỏ trong các thời điểm khác nhau

MyGroovyClass myGroovyObject = new MyGroovyClass();


fixed (long *pX = &( myGroovyObject.X))
{
// do something with pX
fixed (float *pF = &( myGroovyObject.F))
{
// do something else with pF
}
}

Ta cũng có thể khởi tạo vài biến trong cùng 1 khối fixed :

MyGroovyClass myGroovyObject = new MyGroovyClass();


MyGroovyClass myGroovyObject2 = new MyGroovyClass();
fixed (long *pX = &( myGroovyObject.X), pX2 = &( myGroovyObject2.X))
{
// etc.
Thêm các lớp và Struct đến ví dụ
Trong phần này ta sẽ minh họa việc tính toán trên con trỏ và các con trỏ đến struct và lớp .Ta dùng ví dụ 2, PointerPlayaround2:

struct CurrencyStruct
{
public long Dollars;
public byte Cents;

public override string ToString()


{
return "$" + Dollars + "." + Cents;
}
}

class CurrencyClass
{
public long Dollars;
public byte Cents;

public override string ToString()


{
return "$" + Dollars + "." + Cents;
}
}

Bây giờ ta có thể áp dụng con trỏ cho các struct và lớp của ta .ta bắt đầu bằng việc trình bày kích thước của stuct , tạo ra 1 vài thể
hiện của nó cùng với con trỏ.ta dùng những con trỏ này để khởi tạo 1 trong những struct Currency ,amount1. và trình bày các địa
chỉ của các biến :

public static unsafe void Main()


{
Console.WriteLine(
"Size of Currency struct is " + sizeof(CurrencyStruct));
CurrencyStruct amount1, amount2;
CurrencyStruct *pAmount = &amount1;
long *pDollars = &(pAmount->Dollars);
byte *pCents = &(pAmount->Cents);

Console.WriteLine("Address of amount1 is 0x{0:X}", (uint)&amount1);


Console.WriteLine("Address of amount2 is 0x{0:X}", (uint)&amount2);
Console.WriteLine("Address of pAmt is 0x{0:X}", (uint)&pAmount);
Console.WriteLine("Address of pDollars is 0x{0:X}", (uint)&pDollars);
Console.WriteLine("Address of pCents is 0x{0:X}", (uint)&pCents);
pAmount->Dollars = 20;
*pCents = 50;
Console.WriteLine("amount1 contains " + amount1);

Ta biết rằng amount2 sẽ được lưu trữ ở 1 địa chỉ ngay sau amount1, sizeof ( CurrencyStru) trả về 16, vì vậy CurrencyStruct sẽ
nằm ở địa chỉ là bội số của 4 byte.do đó sau khi giảm con trỏ currency , nó sẽ trỏ đến amount2:
--pAmount; // this should get it to point to amount2
Console.WriteLine("amount2 has address 0x{0:X} and contains {1}",
(uint)pAmount, *pAmount);

Ta trình bày nội dụng của amount2 nhưng chưa khởi tạo nó .Dù trình biên dịch C# ngăn không cho chúng ta dùng các giá trị chưa
được khởi tạo nhưng khi dùng con trỏ thì điều này không còn đúng nửa.trình biên dịch không cách nào biết nội của amount2 mà
ta trình bày, chỉ có ta biết.

kết tiếp ta sẽ tính toán trên con trỏ pCents,pCents hiện thời trỏ đến amount1.Cents , nhưng mục đích của ta là làm cho nó trỏ đến
amount2.Cents .Làm điều này ta cần giảm địa chỉ của nó.ta cần làm một vài ép kiểu :

// do some clever casting to get pCents to point to cents


// inside amount2
CurrencyStruct *pTempCurrency = (CurrencyStruct*)pCents;
pCents = (byte*) ( --pTempCurrency );
Console.WriteLine("Address of pCents is now 0x{0:X}", (uint)&pCents);

Cuối cùng ta dùng vài từ khoá fixed để tạo ra một vài con trỏ mà trỏ đến các trường trong thể hiện lớp,và dùng những con trỏ này
để thiết đặt giá trị của thể hiện này.Lưu ý rằng điều này cũng là lần đầu tiên ta thấy địa chỉ của một mục được lưu trữ trên heap
hơn là trên stack:

Console.WriteLine("\nNow with classes");


// now try it out with classes
CurrencyClass amount3 = new CurrencyClass();

fixed(long *pDollars2 = &(amount3.Dollars))


fixed(byte *pCents2 = &(amount3.Cents))
{
Console.WriteLine(
"amount3.Dollars has address 0x{0:X}", (uint)pDollars2);
Console.WriteLine(
"amount3.Cents has address 0x{0:X}", (uint) pCents2);
*pDollars2 = -100;
Console.WriteLine("amount3 contains " + amount3);
}
chạy chương trình ta có :
csc /unsafe PointerPlayaround2.cs
Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

PointerPlayaround2
Size of Currency struct is 16
Address of amount1 is 0x12F8A8
Address of amount2 is 0x12F898
Address of pAmt is 0x12F894
Address of pDollars is 0x12F890
Address of pCents is 0x12F88C
amount1 contains $20.50
amount2 has address 0x12F898 and contains $5340121818976080.102
Address of pCents is now 0x12F88C

Now with classes


amount3.Dollars has address 0xBA4960
amount3.Cents has address 0xBA4968
amount3 contains $-100.0

Dùng con trỏ để tối ưu hoá thực thi


Sau đây ta sẽ áp dụng những hiểu biết về con trỏ và minh họa 1 ví dụ mà ta thấy rõ lợi ích của việc dùng con trỏ trong thực thi

Tạo ra mảng có nền là Stack


Để tạo ra mảng này ta cần từ khoá stackalloc. lệnh stackalloc chỉ dẫn thời gian chạy .NET để cấp phát 1 số vùng nhớ trên stack
khi ta gọi nó ,ta cần cung cấp cho nó 2 thông tin

• Kiểu của biến mà ta muốn lưu trữ


• Ta cần lưu bao nhiêu biến

trong ví dụ , để cấp phát đủ vùng nhớ lưu trữ 10 số thập phân decimal , ta viết :

decimal *pDecimals = stackalloc decimal [10];

lệnh này chỉ đơn giản cấp phát vùng nhớ. không khởi tạo bất kì giá trị nào.

Để lưu 20 số double ta viết :

double *pDoubles = stackalloc double [20];

mặc dù dòng mã này đặc tả số biến được lưu là hằng, điều này có thể là 1 định giá số lượng vào lúc chạy. vì thế ta có thể viết
tương đương với ví dụ trên như sau :

int size;
size = 20; // or some other value calculated at run-time
double *pDoubles = stackalloc double [size];

Kiểu mảng cơ bản nhất mà có thể có là 1 khối bộ nhớ lưu các phần tử như sau :

Câu hỏi được đặt ra là làm thế nào ta sử dụng vùng nhớ mà ta vừa tạo.trở lại ví dụ ta vừa nói rằng giá trị trả về từ stackalloc trỏ
đến bắt đầu của vùng nhớ.do đó cho phép ta có thể lấy vị trí đầu tiên của vùng nhớ được cấp phát.ví dụ để cấp phát các số double
và thiết lập phần tử đầu tiên ( phàn tử 0 của mảng) giá trị 3.0 tacó thể viết :

double *pDoubles = stackalloc double [20];


*pDoubles = 3.0;

Ta có thể thiết lập phần tử thứ 2 của mảng bằng cách dùng cách tính toán trên con trỏ mà ta đã biết .Ví dụ nếu ta muốn đặt giá trị
của phần tử thứ hai ta làm như sau :

double *pDoubles = stackalloc double [20];


*pDoubles = 3.0;
*(pDoubles+1) = 8.4;

Nó chung ta có thể lấy phần tử thứ X của mảng với biểu thức *(pDoubles+X)

Bên cạnh đó C# cũng định nghĩa 1 cú pháp thay thế .Nếu p là con trỏ và X là kiểu số thì biểu thức p[X] tương đương với *(p+X).

double *pDoubles = stackalloc double [20];


pDoubles[0] = 3.0; // pDoubles[0] is the same as *pDoubles
pDoubles[1] = 8.4; // pDoubles[1] is the same as *(pDoubles+1)

Mặc dù mảng của ta có thể được truy xuất theo cùng cách như mảng bình thường, ta cần quan tâm đến cảnh báo sau . đoạn mã
sau đây sẽ gây ra 1 biệt lệ:

double [] myDoubleArray = new double [20];


myDoubleArray[50] = 3.0;

Biệt lệ xuất hiện vì ta cố truy xuất vào mảng dùng chỉ mục vượt quá mảng ( chỉ mục là 50 , nhưng giá trị lớn nhất cho phép là
19).Tuy nhiên ,nếu ta khai báo 1 mảng dùng stackalloc , điều đó sẽ không gây ra biệt lệ :

double *pDoubles = stackalloc double [20];


pDoubles[50] = 3.0;

Ví dụ QuickArray
Ta sẽ thảo luận về con trỏ với stackalloc trong ví dụ QuickArray. ví dụ hỏi người dùng bao nhiêu phần tử họ muốn cấp phát cho
mảng. sau đó ta dùng stackalloc để cấp phát mảng với độ dài đó.các phần tử của mảng này được gán giá trị là bình phương của
chỉ mục của nó .kết quả trình bày bên dưới :

using System;

namespace Wrox.ProCSharp.AdvancedCSharp
{
class MainEntryPoint
{
static unsafe void Main()
{
Console.Write("How big an array do you want? \n> ");
string userInput = Console.ReadLine();
uint size = uint.Parse(userInput);

long *pArray = stackalloc long [(int)size];


for (int i=0 ; i<size ; i++)
pArray[i] = i*i;

for (int i=0 ; i<size ; i++)


Console.WriteLine("Element {0} = {1}", i, *(pArray+i));
}
}
}
QuickArray
How big an array do you want?
> 15
Element 0 = 0
Element 1 = 1
Element 2 = 4
Element 3 = 9
Element 4 = 16
Element 5 = 25
Element 6 = 36
Element 7 = 49
Element 8 = 64
Element 9 = 81
Element 10 = 100
Element 11 = 121
Element 12 = 144
Element 13 = 169
Element 14 = 196

Code for Download :

PointerPlayrouund

PointerPlayrouund2

QuickArray

Tóm tắt

Chương này bao gồm một số chủ đề riêng biệt trong ngôn ngữ C#.Ta đã biết cơ chế của C# trong việc xử lí các trạng thái lỗi và
các biệt lệ,khả năng ép kiểu giữa các lớp và struct,truyền các phương thức như là thông số qua delegate,và thông báo các thông
điệp giữa các ứng dụng khi có điều gì đó xảy ra bằng các sự kiện ( event).Ta đã tìm hiểu 2 cách thức chính của C# để điều khiển
cách biên dịch , là các chỉ thị tiền xử lí và attribute ,trước khi hoàn tất chương này ta xem xét những gì xảy ra trong bộ nhớ bên
dưới C# và cách dùng con trỏ để tăng tốc sự thực thi khi ta thực sự cần.

Trong chương kết tiếp ta sẽ tìm hiểu cách C# tương tác với các lớp cơ sở để ta truy xuất đến các kĩ thật lập trình mạnh trong một
số lĩnh vực khác,bao gồm xử lí chuỗi,các attribute tùy biến và luồng.Ta sẽ xem xét kĩ thuật reflection cho phép 1 chương trình C#
truy xuất vào các lớp assembly mà mô tả các đối tượng riêng của nó và củ các ứng dụng khác.
System.object
Trong chương 3 , chúng ta đã xem xét về system.object. chúng ta biết rằng đó là lớp cơ sở chung mà mọi đối tượng khác được
thừa kế và ta cũng xem xét về các phương thức thành viên chính của nó.tuy nhiên,trong chương trước ta chưa tìm hiểu kỉ về khả
năng của tất cả các phương thức,(chúng ta chỉ mới tìm hiểu chi tiết những phương thức tostring() và finalize() ).trong chương này
ta sẽ tìm hiểu các phương thức còn lại của system.object .đầu tiên ta sẽ tìm hiểu tóm tắt của từng phương thức :

Phương thức Truy xuất Mục đích


string ToString() public virtual Trả về 1 chuỗi đại diện cho đối tượng
trả về mã băm của đối tượng được thiết kế cho phép ta tìm kiếm 1 cách hiệu
int GetHashCode() public virtual
quả các thể hiện của đối tượng trong từ điền
bool Equals(object obj) public virtual so sánh đối tượng này với 1 đối tượng khác
bool Equals(object objA, object objB)
public static so sánh 2 đối tượng

bool ReferenceEquals(object objA,


public static so sánh các tham chiếu đối tượng để xem chúng có chỉ đến cùng đối tượng
object objB)
Type GetType() public trả về 1 đối tượng dẫn xuất từ System.Type mà đưa ra chi tiết kiểu dữ liệu
Makes a shallow copy of the object (in other words, copies data in the object
object MemberwiseClone() protected
but not other objects any fields refer to)
protected
void Finalize() Hàm hủy ( Destructor)
virtual

4 trong những phương thức này khai báo là ảo ( virtual) vì vậy ta có thể overload chúng .
Các thành viên của system.object:
ToString() - đây là cách trình bày chuỗi dễ dàng nhanh chóng và cơ bản. được dùng trong tình huống khi bạn muốn phác hoạ
nhanh nội dung của một đối tượng,chẳng hạn để vá lỗi .nếu bạn muốn trình bày chuỗi phức tạp hơn bạn có thể dùng interface
IFormatable.
GetHashcode() - hữu ích nếu đối tượng được đặt trong cấu trúc dữ liệu map ( hay còn gọi là bảng băm hoặc từ điển).Nó được
dùng bởi những lớp mà thao tác những cấu trúc này để quyết định nơi đặt đối tượng trong cấu trúc .nếu bạn định lớp của bạn
được sử dụng như là khoá trong từ điển, thì bạn sẽ cần overload GetHashcode().
Equals() ( cả 2 phiên bản) và ReferenceEquals()- có những khác biệt tinh vi giữa 3 kiểu phương thức này, đi cùng với toán tử
so sánh ,==.ta sẽ xem xét sau.
Finalize() - đây là 1 destructor và được gọi khi một đối tượng tham chiếu là rác được thu nhặt để dọn dẹp tài nguyên. thực sự thì
finalize() không làm gì cả , bị bỏ qua bởi bộ gom rác, nhưng điều này không đúng với các overload . ta nên overload chỉ khi cần
thiết.overload Finalize() trong C# không được làm tường minh ( điều này hay gây ra lỗi biên dịch) bằng cách cung cấp destructor.
destructor đưọc chuyển bởi trình biên dịch thành phươngg thức Finalize().
GetType() - phương thức này trả về thể hiện của một lớp được dẫn xuất từ System.type. đối tượng này có thể cung cấp một số
thông tin mở rộng về lớp mà đối tượng là thành viên, bao gồm kiểu cơ sở, phương thức , thuộc tính ...
MemberWiseClone() - đây chỉ là thành viên của System.Object mà ta không đi sâu trong cuốn sách này. nó đơn giản tạo một
bản sao của đối tượng và trả về 1 tham khảo đến bản sao.lưu ý rằng bản sao được tạo như là bản sao bóng- nghĩa là nó sao chép
tất cả các kiểu giá trị trong lớp.nếu lớp chứa đựng những tham khảo kèm theo thì chỉ có tham khảo được sao chép ,không phải là
đối tượng.
So sánh các đối tượng tham chiếu tương đương
Ta thấy rằng trong C# có đến 4 kiểu so sánh tương đương ( gồm 3 phương thức so sánh và 1 toán tử = ). ta sẽ tìm hiểu sự khác
nhau giữa các loại so sánh này .

ReferenceEquals()
Tên của phương thức chính là ý nghĩa của nó . nó kiểm tra 2 tham chiếu liệu có quy vào cùng một thể hiện của một lớp:rằng 2
tham chiếu này chứa đựng cùng một địa chỉ trong bộ nhớ hay không.ReferenceEquals() luôn trả về true nếu được cung cấp 2
tham tham chiếu cùng quy vào một thể hiện đối tượng và sai nếu khác. tuy nhiên nó xem null là bằng null.
SomeClass x, y;
x = new SomeClass();
y = new SomeClass();
bool B1 = ReferenceEquals(null, null); // trả về true
bool B2 = ReferenceEquals(null,x); // trả về false
bool B3 = ReferenceEquals(x, y); // trả về false bởi x và y trỏ đến 2 đối tượng khác nhau.
Phương thức ảo Equals()
Ảo, một phiên bản thể hiện của Equals() có thể được xem như là đối nghịch với ReferenceEquals() .Đúng là phương thức
Equals() làm việc bằng cách so sánh những tham chiếu . phương thức này được cung cấp trong trường hợp ta muốn nạp chồng nó
để so sánh giá trị của những thể hiện đối tượng.cụ thể ,nếu ta dự định thể hiện của lớp được sử dụng như khoá trong từ điển, ta
cần nạp chồng phương thức để so sánh giá trị. nếu không thì tuỳ thuộc vào cách ta nạp chồng GetHashCode(), lớp từ điển chứa
đựng những đối tượng của ta cũng không làm việc hoàn toàn, hoặc là làm việc thiếu khả năng.1 điểm mà ta chú ý khi nạp chồng
Equals() là việc nạp chồng của ta sẽ không bao giờ tung ra biệt lệ.
Phương thức static Equals()
Phương thức static Equals() thực sự thì cũng giống như phiên bản phương thức ảo Equals().điểm khác là phương thức static có
thể đối phó khi đối tượng là null, và do đó cung cấp 1 kiểu bảo vệ an toàn chống lại việc tung ra biệt lệ nếu đối tượng có thể là
null.phương thức này đầu tiên sẽ kiểm tra khi overload thì tham chiếu được truyền có phải là null không.nếu cả hai là null, thì nó
trả về true, ( vì null được xem là bằng null). nếu chỉ có một cái là null. nó trả về false.nếu cả hai tham chiếu thực sư quy đến một
thứ,thì nó gọi phiên bản thể hiện ảo của Equals() nghĩa là khi bạn nạp chồng bản thể hiện của equals() ,kết quả cũng như bạn nạp
chồng lên bản static.
Toán tử so sánh (==)
Toán tử so sánh có thể được xem như là bản trung gian giữa việc so sánh giá trị và việc so sánh tham chiếu. thường viết :
bool b = (x == y); // x, y là đối tượng tham chiếu
nghĩa là ta đang so sánh 2 tham chiếu. tuy nhiên,chấp nhận rằng có 1 vài lớp mà ý nghĩa của nó trực quan hơn nếu xem nó như là
một giá trị.trong trường hợp đó, tốt hơn hết là nạp chồng toán tử so sánh thành so sánh giá trị. ví dụ như là những chuỗi mà
microsoft nạp chồng toán tử này, bởi vì khi các nhà phát triển nghĩ đến việc so sánh chuỗi, họ luôn nghĩ rằng phải so sánh nội
dung của chuỗi hơn là so sánh tham chiếu.
Nếu ta muốn nạp chồng những hàm so sánh và Object.GetHashCode() cần lưu ý những hướng dẫn sau:
không nên nạp chồng chỉ 1 Object.Equals() hay Object.GetHashCode() . nếu ta nạp chồng cái naỳ thì cũng phải nạp chồng cái
kia.bởi vì việc thực thi từ điển đòi hỏi cả 2 phương thức hoạt động đồng nhất .( nếu Obkect.Equals() làm việc so sánh giá trị,thì
GetHashCode() cũng nên xây dựng mã dựa trên giá trị.)
Nếu ta nạp chồng toán tử == , thì phải nạp chồng Object.Equals() ( và vì vậy cũng nạp chồng object.GetHashCode() ) bởi vì nếu
== so sánh giá trị , thì object .Equals() cũng so sánh giá trị.
So sánh các kiểu giá trị trong equal
Khi so sánh kiểu giá trị trong equal , nguyên tắc cũng giống như kiểu tham chiếu :ReferenceEquals() được dùng để so sánh tham
chiếu, Equals() được dùng cho so sánh giá trị, và toán tử so sánh được xem như trường hợp trung gian.tuy nhiên sự khác biệt ở
đây là kiểu giá trị cần được bỏ vào hộp ( boxed ) để chuyển thành kiểu tham chiếu.do đó microdoft nạp chồng phương thức
Equals() vào lớp System.ValueType để cung cấp ý nghĩa tương đương đến kiểu giá trị.nếu ta gọi sA.Equals(sB) ,sA,sB là thể
hiện của một vài Struct, thì giá trị trả về sẽ là true hoặc false tuỳ theo liệu sA và sB chứa đựng cùng giá trị trong tất cà các
trường. mặt khác, không có việc nạp chồng == bằng mặc định trong stuct riêng của ta.Viết ( sA==sB) trong bất kì biểu thức nào
thì đều bị lỗi trừ khi ta cung cấp 1 overload == vào mã trong Struct.
1 điểm khác là ReferenceEquals() luôn trả về false khi ứng dụng vào kiểu giá trị, bởi vì để gọi phương thức, kiểu giá trị sẽ cần bỏ
vào hộp thành đối tượng . thậm chí nếu ta viết :
bool b = ReferenceEquals(v,v); // v là biến của một vài kiểu giá trị
Ta sẽ có câu trả lời là false bởi vì v sẽ được đóng hộp riêng rẽ khi chuyển mỗi thông số,nghĩa là ta sẽ có sự tham chiếu khác
nhau.Gọi referenceEquals() để so sánh kiểu giá trị không phải là 1 sự chọn lựa hay.
Mặc dù nạp chồng mặc định của Equals() cung cấp bởi System.ValueType hầu như chắc chắn tương thích với số lượng lớn cấu
trúc mà ta định nghĩa ,ta có thể muốn nạp chồng nó lần nữa theo ý riêng của ta để cải thiện việc thực thi.nếu 1 kiểu giá trị chứa
kiểu tham chiếu như 1 trường,ta có thể muốn nạp chồng Equals() để cung cấp ngữ nghĩa tương đương cho những trường này,
mặc định thì Equals() đơn giản sẽ so sánh điạ chỉ của chúng.

Xử lý chuỗi
Qua chương 2, ta đã xem xét về chuỗi và thấy rằng từ khoá String trong C# thực sự tham khảo lớp cơ sở system.String.
System.string là lớp rất linh hoạt và mạnh , không phải chỉ là lớp có liên quan đến chuỗi trong .NET. trong phần này ta sẽ xem lại
những đặc tính của System.String, sau đó sử dụng chuỗi để ứng dụng trong môt số lớp .NET - cụ thể là lớp System.Text và
namespace System.Text.RegularExpressions .

• Xây dựng chuỗi - nếu ta hay lặp lại việc thay đổi trên 1 chuỗi , ví dụ để định 1 độ dài cho chuỗi trước khi trình bày nó
hoặc truyền nó đến vài phương thức hoặc phần mềm,lớp chuỗi có thể không đủ khả năng để làm.trong tình huống này,
1 lớp khác , System.Text.StringBuilder thích hợp hơn, bởi vì nó được thiết kế để làm trong các tình huống này.
• Các biểu thức định dạng - ta sẽ xem xét kĩ hơn những biểu thức định dạng sử dụng Console.Writeline(). những biểu
thức định dạng này sử dụng vài interface. IFormatProvider và IFormattable,bằng việc sử dụng các interface này trong
lớp riêng , ta có thể định nghĩa những chuỗi định dạng riêng để console.Writeline() và những lớp quen thuộc sẽ trình
bày giá trị trong lớp của ta theo bất cứ cách nào mà ta chỉ định.
• Biểu thức chính quy ( regular expressions )- .NET cũng đưa ra một số lớp phức tạp mà đưọc dùng khi ta cần xác
định hoặc trích ra chuỗi con thoả mãn 1 điều kiện phức tạp từ 1 chuỗi dài.ví dụ như cần tìm tất cả các lần xuất hiện của
1 kí tự hay 1 tập kí tự được lặp lại.hoặc cần tìm tất cả các từ bắt đầu với 's' và chứa ít nhất 1 kí tự 'n'.mặc dù ta có thể
viết phương thức để làm điều này chỉ bằng việc dùng lớp chuỗi ,nhựng nó rất cồng kềnh. thay vào đó , ta có thể dùng 1
vài lớp trong System.Text.RegularExpressions mà đưọc thiết kế để thực thi các quy trình này.

System.String
Trước khi kiểm tra các lớp chuỗi khác, ta sẽ xem lại nhanh những phương thức trong lớp chuỗi.
System.String là lớp được thiết kế để lưu trữ chuỗi, bao gồm 1 số lớn các thao tác trên chuỗi.không chỉ thế mà còn bởi vì tầm
quan trọng của kiểu dữ liệu này , C# có từ khoá riêng cho nó và kết hợp với cú pháp để tạo nên cách dễ dàng trong thao tác chuỗi.
Ta có thể nối chuỗi :
string message1 = "Hello";
message1 += ", There";
string message2 = message1 + "!";
Trích 1 phần chuỗi dùng chỉ mục :
char char4 = message[4]; // trả về 'a'. lưu ý rằng kí tự bắt đầu tính từ chỉ mục 0
các phương thức khác ( sơ lược) :

Phương thức Mục đích


Compare so sánh nội dung của 2 chuỗi
giống compare nhưng không kể đến ngôn ngữ bản địa hoặc văn hoá (as compare but doesn't take culture into
CompareOrdinal account)

Format định dạng một chuỗi chứa 1 giá trị khác và chỉ định cách mỗi giá trị nên được định dạng.
IndexOf vị trí xuất hiện đầu tiên của 1 chuỗi con hoặc kí tự trong chuỗi
IndexOfAny vị trí xuất hiện đầu tiên của bất kì 1 hoặc 1 tập kí tự trong chuỗi
LastIndexOf giống indexof , nhưng tìm lần xuất hiện cuối cùng
LastIndexOfAny giống indexofAny , nhưng tìm lần xuất hiện cuối cùng
canh phải chuỗi
PadLeft
điền chuỗi bằng cách thêm 1 kí tự được chỉ định lặp lại vào đầu chuỗi
canh trái chuỗi
PadRigth
điền chuỗi bằng cách thêm 1 kí tự được chỉ định lặp lại vào cuối chuỗi
Replace thay thế kí tự hay chuỗi con trong chuỗi với 1 kí tự hoặc chuỗi con khác
Split chia chuỗi thành 2 mảng chuỗi con ,ngắt bởi sự xuất hiện của một kí tự nào đó
Substring trả về chuỗi con bắt đầu ở một vị trí chỉ định trong chuỗi.
ToLower chuyển chuỗi thành chữ thuờng
ToUpper chuyển chuỗi thành chữ in
Trim bỏ khoảng trắng ở đầu và cuối chuỗi

Xây dựng chuỗi


Chuỗi là 1 lớp mạnh với nhiều phương thức hữu ích , tuy nhiên chuỗi gặp khó khăn trong việc lặp lại sự thay đổi đến chuỗi ban
đầu.nó thực sự là kiểu dữ liệu không biến đổi, nghĩa là mỗi lần ta khởi động 1 đối tượng chuỗi, thì đối tượng chuỗi đó không bao
giờ được thay đổi.những phương thức hoặc toán tử mà cập nhật nội dung của chuỗi thực sự là tạo ra một chuỗi mới , sao chép
chuỗi cũ vào nếu cần thiết. ví dụ :
string greetingText = "Hello from all the guys at Wrox Press. ";
greetingText += "We do hope you enjoy this book as much as we enjoyed
writing it.";

Đầu tiên lớp system.String được tạo và khởi tạo giá trị "Hello from all the people at Wrox Press. "chú ý khoảng trắng sau sau dấu
chấm. khi điều này xảy ra ,thời gian chạy .NET sẽ định vị đủ bộ nhớ trong chuỗi để chứa đoạn kí tự này( 39 kí tự ).và tạo ra 1
biến greetingText để chuyển đến 1 thể hiện chuỗi.
Ở dòng tiếp theo , khi ta thêm kí tự vào .ta sẽ tạo ra một chuỗi mới với kích thước đủ để lưu trữ cả hai đoạn ( 103 kí tự ).đoạn gốc
"Hello from all the people at Wrox Press. ", sẽ được sao chép vào chuỗi mới với đoạn thêm "We do hope you enjoy this book as
much as we enjoyed writing it. " sau đó địa chỉ trong biến greetingText được cập nhật, vì vậy biến sẽ trỏ đúng đến đối tượng
chuỗi mới.chuỗi cũ không còn được tham chiếu - không có biến nào truy vào nó- và vì vậy nó sẽ được bộ thu gom rác gỡ bỏ.
Giả sử ta muốn mã hoá chuỗi bằng cách thay đổi từng kí tự với 1 kí tự ASCII. điều này sẽ trả vế chuỗi : "Ifmmp gspn bmm uif
hvst bu Xspy Qsftt. Xf ep ipqf zpv fokpz uijt cppl bt nvdi bt xf fokpzfe xsjujoh ju." có nhiều cách làm điều này nhưng cách đơn
giản nhất là dùng phương thức String.replace() mà thay thế chuỗi con này bằng chuỗi con khác, dùng Replace() ta viết đoạn mã
mã hoá như sau :
string greetingText = "Hello from all the guys at Wrox Press. ";
greetingText += "We do hope you enjoy this book as much as we enjoyed writing it.";

for(int i = (int)'z'; i>=(int)'a' ; i--)


{
char old = (char)i;
char new = (char)(i+1);
greetingText = greetingText.Replace(old, new);
}

for(int i = (int)'Z'; i>=(int)'A' ; i--)


{
char old = (char)i;
char new = (char)(i+1);
greetingText = greetingText.Replace(old, new);
}
Console.WriteLine("Encoded:\n" + greetingText);

Vậy cần bao nhiêu vùng nhớ làm điều này? Replace() làm việc theo cách thông minh, để mở rộng nó sẽ không tạo ra một chuỗi
mới trừ khi nó thực sự phải thay đổi chuỗi cũ.chuỗi gốc có 23 kí tự chữ thường khác nhau và 3 kí tự in khác nhau,Replace() sẽ
định vị 1 chuỗi mới tổng cộng 26 lần,mỗi chuỗi mới lưu trữ 103 kí tự.nghĩa là kết quả quy trình mã hoá sẽ là đối tượng chuỗi có
khả năng lưu trữ kết hợp tổng cộng 2678 kí tự bây giờ đang nằm trong heap để được thu dọn.Rõ ràng nếu ta sử dụng chuỗi để
làm điều này, ứng dụng của ta sẽ chạy không tốt.
Để giải quyết Microsoft cung cấp lớp System.Text.StringBuilder . lớp System.Text.StringBuilder không mạnh như lớp chuỗi tính
theo thuật ngữ là số phương thức nó có .tiến trình mà ta có thể làm trên Stringbuilder được giới hạn thành thay thế hoặc mở rộng
hoặc bỏ đoạn từ chuỗi .tuy nhiên nó làm việc theo cách hiệu quả hơn.
bất cứ nơi nào ta xây dựng chuỗi , chỉ đủ vùng nhớ được định vị để giữ chuỗi,Stringbuidler sẽ định vị vùng nhớ nhiều hơn cần.ta
có thể chỉ định vùng nhớ được định vị , nếu không , thì số mặc định tuy thuộc vào kích cỡ của chuỗi mà StringBuilder được khởi
động cùng .nó có 2 thuộc tính chính:
Length - độ dài của chuỗi thực sự chứa
Capacity - độ daì chuỗi mà nó chỉ định đủ vùng nhớ để lưu trữ.
Bất kì cập nhật nào đến chuỗi sẽ được làm trong khối vùng nhớ này,như là viết thêm chuỗi con hoặc thay thế các kí tự riêng trong
chuỗi rất tốt.Việc gỡ bỏ hoặc chèn chuỗi con vẫn không tốt, bởi vì những phần theo sau chuỗi phải bị di chuyển.chỉ nếu ta thực
thi vài thao tác mà tăng dung lượng chuỗi sẽ tạo ra vùng nhớ mới cần được định vị và toàn bộ chuỗi được chứa có thể được di
chuyển.vào lúc viết ,Microsoft không cho biết làm cách nào để dung lượng thêm được thêm vào, nhưng từ kinh nghiệm cho thấy
StringBuilder xuất hiện với dung lượng gấp đôi của nó nếu nó thăm dò thấy dung lương bị vượt quá và không có giá trị mới của
dung lương được thiết đặt rõ ràng.
Ví dụ , nếu ta sử dụng đối tượng StringBuilder để tạo ra chuỗi chào đón gốc, ta viết :
StringBuilder greetingBuilder =
new StringBuilder("Hello from all the guys at Wrox Press. ", 150);
greetingBuilder.Append("We do hope you enjoy this book as much as we enjoyed
writing it");

Trong mã này . ta thiết lập dung lương khởi tạo là 150 cho StringBuilder. nó luôn là ý tưởng hay để thiết lập dung lượng mà bao
phủ độ dài lớn nhất của chuỗi , để bảo đảm String builder không cần tái định vị bởi vì dung lượng của nó lớn .vì vậy ta có thể
thiết lập 1 số int cho dung lượng, mặc dù nếu ta cố định vị vượt quá 2 tỷ ký tự hệ thống sẽ cảnh báo.
khi mã trên thực thi , đầu tiên ta tạo ra 1 đối tượng StringBuilder khởi tạo như sau:

Khi gọi phương thức append() ,đoạn còn lại đuợc đặt trong khoảng trống, không cần định vị thêm vùng nhớ.tuy nhiên khả năng
thực sự của StringBuilder chỉ thấy rõ khi lặp lại việc thay thế đoạn. ví dụ, nếu ta cố mã hoá đoạn văn bản theo cách trên ,ta có thể
mã hoá toàn bộ mà không cần định vị thêm bất kì vùng nhớ nào nữa :
StringBuilder greetingBuilder =
new StringBuilder("Hello from all the guys at Wrox Press. ", 150);
greetingBuilder.Append("We do hope you enjoy this book as much as we enjoyed
writing it");

for(int i = (int)'z'; i>=(int)'a' ; i--)


{
char old = (char)i;
char new = (char)(i+1);
greetingBuilder = greetingBuilder.Replace(old, new);
}

for(int i = (int)'Z'; i>=(int)'A' ; i--)


{
char old = (char)i;
char new = (char)(i+1);
greetingBuilder = greetingBuilder.Replace(old, new);
}
Console.WriteLine("Encoded:\n" + greetingBuilder.ToString());
Đoạn mã này dùng phương thức StringBuilder.Replace() mà giống như String.Replace() nhưng không có sao chép chuỗi trong
lúc làm.tổng vùng nhớ được định vị để giữ chuỗi trong đoạn mã trên là 150 cho người xây dựng, cũng như vùng nhớ chỉ định
trong suốt việc thao tác chuỗi thi hành bên trong trong câu lệnh cuối Console.Writeline()
thông thường , ta sử dụng StringBuilder để thi hành bất kì thao tác của chuỗi, và String để lưu trữ hoặc trình bày kết quả cuối
cùng.
Các thành viên của StringBuilder
Chúng ta đã minh hoạ 1 hàm dựng của StringBuilder, mà lấy thông số khởi tạo là chuỗi và dung lượng.cũng có một vài cái khác ,
trong số chúng, ta có thể cung cấp chỉ 1 chuỗi :
StringBuilder sb = new StringBuilder("Hello");
hoặc chỉ cung cấp dung luợng , chuỗi trống:
StringBuilder sb = new StringBuilder(20);

Ngoại trừ 2 thuộc tính trên ta còn có thuộc tính chỉ đọc MaxCapacity chỉ định giới hạn mà 1 thể hiện của StringBuilder cho
phép .mặc định , số này là int.MaxValue ( khoảng 2 tỷ).
// dung lượng khởi tạo là 100 nhưng lớn nhất là 500
// do đó StringBuilder không thể phát triển hơn 500 kí tự được.
// chúng sẽ tung biệt lệ nếu ta cố làm điều đó.

StringBuilder sb = new StringBuilder("Hello", 100, 500);


StringBuilder sb = new StringBuilder(100, 500);
Ta có thể thiết lập dung lượng ở bất cứ đâu .nếu dung lượng thiêt lập nhỏ hơn chuỗi hiện hành hoặc lớn hơn độ dài lớn nhất thì
biệt lệ sẽ được tung ra.
StringBuilder sb = new StringBuilder("Hello");
sb.Capacity = 100;

Phương thức chính StringBuilder bao gồm :


Append() : thêm 1 chuỗi vào chuỗi đương thời
AppendFormat() :thêm 1 chuỗi mà được trình bày từ các chỉ định định dạng
Insert() :chèn 1 chuỗi con vào chuỗi đương thời
Remove() :bỏ các kí tự từ chuỗi đương thời
Replace() :thay thế kí tự này bằng kí tự khác hoặc chuỗi con này bằng chuỗi con khác trong chuỗi đương thời
ToString() :trả về chuỗi đương thời ép thành 1 đối tượng System.Object ( nạp chồn từ System.Object)
Vào lúc viết , không thể ép kiểu ( tường minh hay không tưòng minh) từ StringBuilder sang String. nếu ta muốn xuất nội dung
của StringBuilder như là 1 String , cách duy nhất để làm là dùng phương thức Tostring()
Định dạng Chuỗi
Nếu ta muốn những lớp mà ta viết thân thiện với người sử dụng , thì chúng cần để trình bày chuỗi theo bất cứ cách nào mà người
sử dụng muốn dùng.Thời gian chạy .NET định nghĩa 1 cách chuẩn để làm : dùng 1 interface hoặc IFormatable.biểu diễn làm thế
nào để thêm những đặc tính quan trọng đến lớp của ta và những cấu trúc là chủ đề của phần này.
ta thường chỉ định định dạng của biến được trình bày khi gọi Console.Writeline. do đó ta sẽ lấy phương thức này làm ví dụ, mặc
dù hầu hết những điều ta sắp học có thể ứng dụng trong bất cứ tình huống nào mà ta muốn định dạng chuỗi
Ví dụ:
double d = 13.45;
int i = 45;
Console.WriteLine("The double is {0,10:E} and the int contains {1}", d, i);
Chuỗi định dạng tự nó bao gồm hầu hết văn bản được trình bày,nhưng bất cứ ở đâu có biến được định dạng , chỉ mục của nó
trong danh sách thông số trong dấu ngoặc.có thể là thông tin khác bên trong dấu ngoặc về việc định dạng của mục đó :
số kí tự được giữ bởi sự trình bày của mục có thể xuất hiện, thông tin này sẽ có dấu phảy đứng trước.một số âm chỉ định rằng
mục đó đưọc canh trái,trong khi 1 số dương chỉ định mục đó được canh phải. nếu mục đó giữ nhiều kí tự hơn được yêu cầu, nó
vẫn xuất hiện đầy đủ.
Một chỉ định định dạng cũng có thể xuất hiện.điều này sẽ được đặt trước bởi dấu hai chấm và chỉ định cách ta muốn mục được
định dạng. ví dụ ta muốn định dạng số như kiểu tiền tệ hoặc trình bày theo ký hiệu khoa học ?

Đặc tả Áp dụng đến Ý nghĩa Ví dụ

C numeric types locale-specific monetary value $4834.50 (USA)£4834.50 (UK)

D integer types only general integer 4834

E numeric types scientific notation 4.834E+003

F numeric types fixed point decimal 4384.50

G numeric types general number 4384.5

N numeric types usual locale specific format for 4,384.50 (UK/USA)4 384,50 (continental
numbers Europe)

P numeric types Percentage notation 432,000.00%

X integer types only hexadecimal format 1120 (NB. If you want to display 0x1120, you'd
need to write out the 0x separately)

Làm thế nào chuỗi được định dạng


Xem ví dụ sau:
Console.WriteLine("The double is {0,10:E} and the int contains {1}", d, i);
Thực vậy, Console.Writeline() chỉ việc kiểm soát toàn bộ tập thông số bằng cách chuyển đến phương thức static,
String.Format()- cũng phương thức này được gọi nếu ta muốn định dạng các giá trị trong chuỗi theo các mục đích khác , như là
trình bày trong textbox.Để làm rõ những gì mã nguồn thực sự thực thi phương thức này là gì ,ta sẽ xem xét việc thi hành phương
thức oveload với 3 thông số của phương thức Writeline() như sau :
:
// giống như thực thi Console.Writeline()

public void WriteLine(string format, object arg0, object arg1)


{
Console.WriteLine(string.Format(format, arg0, arg1));
}

Overload 1 thông số của phương thức này, đơn giản viết nội dung của chuỗi được trình bày,không làm bất cứ định dạng gì trên
nó.
String.format() cần xây dựng chuỗi cuối bằng cách thay thế mỗi phần đặc tả định dạng bằng việc trình bày chuỗi thích hợp của
đối tượng tương ứng.tuy nhiên như đã biết , chính xác trong tình huống này ta cần thể hiện Stringbuilder hơn là thể hiện string.
trong ví dụ ta đang xét , 1 thể hiện StringBuilder sẽ được tạo ra và khởi tạo với phần đầu là chuỗi " The double is" . phương thức
StringBuilder.AppendFormat() sẽ được gọi, truyền phần đặc tả định dạng dầu tiên , {0,10:E} ,và đối tượng kết hợp kiểu double
này sẽ được thêm vào chuỗi đang được xây dựng, quy trình này sẽ tiếp tục với việc gọi nhiều lần StringBuilder.Append() và
StringBuilder.AppendFormat() cho đến khi toàn bộ chuỗi được định dạng đã xong.
Bởi vì StringBuilder.AppendFormat() sẽ cần minh họa cách định dạng thực sự đối tượng. điều đầu tiên sẽ là thăm dò đối tượng
để xem liệu nó có thực thi 1 interface IFormatable ( trong namespace System ) hay chưa.ta có thể thử ép kiểu 1 đối tượng thành 1
interface và xem coi ép kiểu được không. nếu kiểm tra thất bại , thì AppendFormat() đơn giản gọi phương thức tostring() của đối
tượng,( do phương thức này được tất cả các đối tượng cùng thừa kế từ System.Object hoặc do nạp chồng).
IFormattable định nghĩa giống như một phương thức, mà cũng được goị ToString().tuy nhiên phương thức này lấy 2 thông số,
đối lập với phiên bản System.Object , mà không lấy bất kì thông số nào. đây là định nghĩa cho IFormattable:
interface IFormattable
{
string ToString(string format, IFormatProvider formatProvider);
}

Thông số đầu tiên mà hàm overload của Tostring() này lấy là chuỗi mà đặc tả định dạng được yêu cầu.nói cách khác nó chỉ định
phần chuỗi xuất hiện trong { } so với chuỗi gốc được truyền đến Console.WriteLine() hay String.Format(). ví dụ :
Console.WriteLine("The double is {0,10:E} and the int contains {1}", d, i);
Khi tính thông số đầu tiên , {0,10:E}, hàm sẽ gọi biến double , d, và thông số đầu tiên đưọc truyền đến nó sẽ là E. những gì
StringBuilder.AppendFormat() sẽ truyền ở đây là bất cứ đoạn văn bản nào xuất hiện sau dấu hai chấm trong phần đặc tả định
dạng từ chuỗi gốc.
Ta không quan tâm về thông số thứ 2 của Tostring().nó là 1 tham chiếu đến đối tượng mà thực thi interface
IFormatProvider.Interface này gửi thông tin mà Tostring() có thể cần khi định dạng đối tưọng.
Quay trở lại ví dụ trên, mục đầu tiên ta muốn định dạng là double với phần đặc tả định dạng là E.như đã đề cập , phương thức
StringBuilder.AppendFormat() sẽ thiết lập double thi hành IFormattable, và do đó sẽ gọi hàm overload Tostring() 2 thông
số,truyền vào nó chuỗi E cho thông số đầu tiên và null cho thông số thứ hai.bây giờ nó tuỳ thuộc vào sự thi hành của phương
thức này mà sẽ trả về 1 chuỗi trình bày kiểu double theo định dạng.
Nếu cần StringBuilder.AppendFormat() sẽ lựa chọn điền vào chuỗi trả về với khoảng trắng, để đủ 10 kí tự trong phần đặc tả định
dạng của chuỗi trong trường hợp này.
Đối tượng kế tiếp được định dạng là kiểu int, mà không yêu cầu định dạng cụ thể.vì vậy StringBuilder.AppendFormat() sẽ truyền
vào tham chiếu null cho định dạng chuỗi này.Toàn bộ quy trình có thể được tóm tắt như sau:

Ví dụ FormattableVector
Trong ví dụ này phần đặc tả định dạng mà ta sẽ hổ trợ là :
• N - được phiên dịch như là yêu cầu cung cấp 1 số được biết như là Norm của Vector.là tổng bình phương của các thành
phần của nó.
và luôn được trình bày giữa dấu || , ví dụ như || 34.5 ||
• VE- được phiên dịch như là yêu cầu trình bày mỗi thành phần trong đặc tả định dạng.như đặc tả E đối với 1 số double chỉ
định (2.3E+01, 4.5E+02, 1.0E+00).

• IJK - được phiên dịch như là yêu cầu trình bày vector dưới dạng 23i + 450i + 1k.
việc trình bày mặc định sẽ có dạng Vector( 23,450,1.0)
Để cho đơn giản ta sẽ không thi hành bất kì tuỳ chọn để trình bày Vector theo kết hợp giữa IJK và định dạng khoa học.tuy nhiên
ta sẽ có thể kiểm tra đặc tả theo cách không phân biệt chữ hoa và chữ thường , để cho phép ijk thay IJK. lưu ý rằng hoàn toàn tuỳ
thuộc vào chuỗi ta sử dụng để chỉ định đặc tả định dạng.
Đầu tiên ta sẽ khai báo Vector thi hành IFormatable:
struct Vector : IFormattable
{
public double x, y, z;
sao đó thêm vào phương thức overload tostring() 2 thông số :

public string ToString(string format, IFormatProvider formatProvider)


{
if (format == null)
return ToString();
string formatUpper = format.ToUpper();
switch (formatUpper)
{
case "N":
return "|| " + Norm().ToString() + " ||";
case "VE":
return String.Format("( {0:E}, {1:E}, {2:E} )", x, y, z);
case "IJK":
StringBuilder sb = new StringBuilder(x.ToString(), 30);
sb.Append(" i + ");
sb.Append(y.ToString());
sb.Append(" j + ");
sb.Append(z.ToString());
sb.Append(" k");
return sb.ToString();
default:
return ToString();
}
}

Đó là tất cả những gì ta phải làm. lưu ý cần kiểm tra định dạng null trước khi gọi bất cứ phương thức nào.trong ví dụ phần đặc tả
VE , ta cần mỗi thành phần đưọc định dạng theo cú pháp khoa học,vì thế ta dùng String.Format()để làm.tất cả các trường x,y,z là
double.Trong trường hợp định dạng IJK, có vài chuỗi con được thêm vào chuỗi , vì thế ta dùng đối tượng StringBuilder để làm.
để hoàn chỉnh ta sẽ xây dựng lại phương thức overload Tostring() không thông số mà ta đã phát triển ở chương 3 :
public override string ToString()
{
return "( " + x + " , " + y + " , " + z + " )";
}

cuối cùng ta thêm vào phương thức Norm() mà tính bình phương ( Norm) của Vector :
public double Norm()
{
return x*x + y*y + z*z;
}

Bây giờ ta sẽ thử đoạn mã trên theo vài cách :

static void Main()


{
Vector v1 = new Vector(1,32,5);
Vector v2 = new Vector(845.4, 54.3, -7.8);
Console.WriteLine("\nIn IJK format,\nv1 is {0,30:IJK}\nv2 is {1,30:IJK}",
v1, v2);
Console.WriteLine("\nIn default format,\nv1 is {0,30}\nv2 is {1,30}", v1,
v2);
Console.WriteLine("\nIn VE format\nv1 is {0,30:VE}\nv2 is {1,30:VE}", v1,
v2);
Console.WriteLine("\nNorms are:\nv1 is {0,20:N}\nv2 is {1,20:N}", v1,
v2);
}

Kết quả trả về là :


FormattableVector
In IJK format,
v1 is 1 i + 32 j + 5 k
v2 is 845.4 i + 54.3 j + -7.8 k

In default format,
v1 is ( 1 , 32 , 5 )
v2 is ( 845.4 , 54.3 , -7.8 )

In VE format
v1 is ( 1.000000E+000, 3.200000E+001, 5.000000E+000 )
v2 is ( 8.454000E+002, 5.430000E+001, -7.800000E+000 )

Norm là :
v1 is || 1050 ||
v2 is || 717710.49 ||

Code for Download:

FormattableVector

StringEncoder

Nhóm các đối tượng


Chúng ta đã khảo sát 1 số lớp cơ sở của .NET có cấu trúc dữ liệu trong đó một số đối tượng được nhóm với nhau.cấu trúc đơn
giản mà ta đã học là mảng, đây là 1 thể hiện của lớp System.Array . mảng có lợi điểm là ta có thể truy nhập từng phần tử thông
qua chỉ mục.tuy nhiên khuyết điểm của nó là ta phải khởi tạo kích thước của nó. không thể thêm ,chèn hoặc bỏ 1 phần tử sau
đó.và phải có một chỉ mục số để truy nhập vào 1 phần tử.điều này không tiện lắm ví dụ như khi ta làm việc với 1 bản ghi nhân
viên và muốn tìm bản ghi theo tên nhân viên.
.NET có một số cấu trúc dữ liệu khác hổ trợ cho công việc này.ngoài ra còn có 1 số inteface , mà các lớp có thể khai báo chúng
hổ trợ tất cả chức năng của một kiểu cụ thể cấu trúc dữ liệu. chúng ta sẽ xem xét 3 cấu trúc sau :
- Array lists
- Collection
- Dictionary ( hay maps)
Các lớp cấu trúc dữ liệu này nằm trong namespace System.Collection
Array lists
Array list giống như mảng, ngoại trừ nó có khả năng phát triển.được đại diện bởi lớp System.Collection.Arraylist
lớp Arraylist cũng có một một vài điểm tương tự với lớp StringBuilder mà ta tìm hiểu trưóc đây.như StringBuilder cấp phát đủ
chỗ trống trong vùng nhớ để lưu trữ 1 số kí tự, và cho phép ta thao tác các kí tự trong chỗ trống đó , the Arraylist cấp đủ vùng
nhớ để lưu trữ 1 số các tham chiếu đối tượng. ta có thể thao tác trên những tham chiếu đối tượng này.nếu ta thử thêm một đối
tượng đến Arraylist hơn dung lượng cho phép của nó, thì nó sẽ tự động tăng dung lượng bằng cách cấp phát thêm vùng nhớ mới
lớn đủ để giữ gấp 2 lần số phần tử của dung lượng hiện thời.
Ta có thể khởi tạo 1 danh sách bằng cách chỉ định dung lượng ta muốn .ví dụ , ta tạo ra một danh sách Vectors:
ArrayList vectors = new ArrayList(20);
Nếu ta không chỉ định kích cỡ ban đầu , mặc định sẽ là 16:
ArrayList vectors = new ArrayList(); // kích cỡ là 16
Ta có thể thêm phần tử bằng cách dùng phương thức Add():
vectors.Add(new Vector(2,2,2));
vectors.Add(new Vector(3,5,6));
Arraylist xem tất cả các phần tử của nó như là các tham chiếu đối tượng..nghĩa là ta có thể lưu trữ bất kì đối tượng nào mà ta
muốn trong 1 Arraylist. nhưng khi truy nhập đến đối tượng, ta sẽ cần ép kiểu chúng trở lại kiểu dữ liệu tương đương:
Vector element1 = (Vector)vectors[1];
Ví dụ này cũng chỉ ra Arraylist định nghĩa 1 indexer, để ta có thể truy nhập những phần tử của nó với cấu trúc như mảng. ta cũng
có thể chèn các phần tử vào array list:
vectors.Insert(1, new Vector(3,2,2)); // chèn vào vị trí 1
Đây là phương thức nạp chồng có ích khi ta muốn chèn tất cả các phần tử trong 1 collection vào arraylist
ta có thể bỏ 1 phần tử :
vectors.RemoveAt(1); // bỏ đối tượng ở vị trí 1
Ta cũng có thể cung cấp 1 đối tượng tham chiếu đến 1 phương thức khác, Remove().nhưng làm điều này sẽ mất nhiều thời gian
hơn vì arraylist phải quét qua toàn bộ mảng để tìm đối tượng
Lưu ý rằng việc thêm và bỏ 1 phần tử sẽ làm cho tất cả các phần tử theo sau phải bị thay đổi tương ứng trong bộ nhớ, thậm chí
nếu cần thì có thể tái định vị toàn bộ Arraylist
Ta có thể cập nhật hoặc đọc dung lượng qua thuộc tính :
vectors.Capacity = 30;
Tuy nhiên việc thay đổi dung lương đó sẽ làm cho toàn bộ Arraylist được tái định vị đến một khối bộ nhớ mới với dung lượng
đưọc yêu cầu.
Để biết số phần tử thực sự trong arraylist ta dùng thuộc tính Count :
int nVectors = vectors.Count;
1 arraylist có thể thực sự hữu ích nếu ta cần xây dựng 1 mảng đối tuợng mà ta không biết kích cỡ của mảng sẽ là bao nhiêu. trong
trường hợp đó, ta có thể xây dựng ' mảng' trong Arraylist, sau đó sao chép Arraylist trở lại mảng khi ta hoàn thành xong nếu ta
thực sự cần dữ liệu như là 1 mảng ( ví dụ nếu mảng được truyền đến 1 phương thức xem mảng là 1 thông số). mối quan hệ giữa
Arraylist và Array theo 1 cách nào đó giống như mối quan hệ giữa StringBUilder và String
không như lớp StringBuilder, không có phương thức đơn nào để làm việc chuyển đổi từ 1 arraylist sang array .ta phải dùng 1
vòng lặp để sao chép thủ công trở lại.tuy nhiên ta chỉ phải sao chép tham chiếu chứ không phải đối tượng:
// vectors is an ArrayList instance being used to store Vector instances
Vector [] vectorsArray = new Vector[vectors.Count];
for (int i=0 ; i< vectors.Count ; i++)
vectorsArray[i] = (Vector)vectors [i];

Collections
Ý tưởng của Collection là nó trình bày một tập các đối tượng mà ta có thể truy xuất bằng việc bước qua từng phần tử. cụ thể là 1
tập đối tượng mà ta có thể truy nhập sử dụng vòng lặp foreach. nói cách khác ,khi viết 1 thứ gì đó như :
foreach (string nextMessage in messageSet)
{
DoSomething(nextMessage);
}
Ta xem biến messageSet là 1 collection . khả năng để dùng vòng lặp foreach là mục đích chính của collection.
tiếp theo ta tìm hiểu chi tiết collection là gì và thi hành 1 collection riêng bằng việc chuyển ví dụ Vector mà ta đã phát triển
Collection là gì ?
1 đối tượng là 1 collection nếu nó có thể cung cấp 1 tham chiếu đến một đối tượng có liên quan, được biết đến như là enumarator,
mà có thể duyệt qua từng mục trong collection. đặc biệt hơn, 1 collection phải thi hành 1 interface
System.Collections.IEnumerable. IEnumerable định nghĩa chỉ một phương thức như sau:
interface IEnumerable
{
IEnumerator GetEnumerator();
}

Mục đích của GetEnumarator() là để trả về đối tuợng enumarator. khi ta tập họp những đoạn mã trên đối tượng enumarator được
mong đợi để thi hành 1 interface , System.Collections.IEnumerator.
Ngoài ra còn có một interface khác , Icollection , đưọc dẫn xuất từ IEnumerable. những collection phức tạp hơn sẽ thi hành
interface này.bên cạnh GetEnumerator(), nó thi hành một thuộc tính trả về trực tiếp số phần tử trong collection. nó cũng có đặc
tính hổ trợ việc sao chép collection đến 1 mảng và có thể cung cấp thông tin đặc tả nếu đó là một luồng an toàn.tuy nhiên trong
phần này ta chỉ xem xét interface IEnumerable.
IEnumarator có cấu trúc sau:
interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}

IEnumarator làm việc như sau : đối tuợng thực thi nên được kết hợp với 1 collection cụ thể. khi đối tượng này được khởi động
lần đầu tiên,nó chưa trỏ đến bất kì 1 phần tử nào trong collection, và ta phải gọi MoveNext(), mà sẽ di chuyển enumarator để nó
chuyển đến phần tử đầu tiên trong collection. ta có thể nhận phần tử này với thuộc tính Current.Current trả về 1 tham chiếu đối
tượng , vì thế ta sẽ ép kiểu nó về kiểu đối tượng mà ta muốn tìm trong Collection.ta có thể làm bất cứ điều gì ta muốn với đối
tượng đó sau đó di chuyển đến mục tiếp theo trong collection bằng cách gọi MoveNext() lần nữa.ta lập lại cho đến khi hết mục
trong collection- khi current trả về null.nếu muốn ta có thể quay trở về vị trí đầu trong collection bằng cách gọi Reset(). lưu ý
rằng Reset() thực sự trả về trước khi bắt đầu collection , vì thế nếu muốn di chuyển đến phần tử đầu tiên ta phải gọi MoveNext()
1 collection là 1 kiểu cơ bản của nhóm đối tượng.bởi vì nó không cho phép ta thêm hoặc bỏ mục trong nhóm.tất cả ta có thể làm
là nhận các mục theo 1 thứ tự được quyết định bởi collection.và kiểm tra chúng.thậm chí ta không thể thay thế hoặc cập nhật mục
vì thuộc tính current là chỉ đọc.hầu như cách dùng thường nhất của collection là cho ta sự thuận tiện trong cú pháp của lặp
foreach.
Mảng cũng là 1 collection,nhưng lệnh foreach làm việc tốt hơn mảng.
Ta có thể xem vòng lặp foreach trong C# là cú pháp ngắn trong việc viết:

{
IEnumerator enumerator = MessageSet.GetEnumerator();
string nextMessage;
enumerator.MoveNext();
while ( (nextMessage = enumerator.Current) != null)
{
DoSomething(nextMessage); // NB. We only have read access
// toNextMessage
enumerator.MoveNext();
}
}

1 khía cạnh quan trọng của collection là bộ đếm được trả về như là 1 đối tượng riêng biệt.lý do là để cho phép khả năng có nhiều
hơn 1 bộ đếm có thể áp dụng đồng thời trong cùng collection.
Thêm collection hổ trợ cấu trúc Vector
Trong lần cuối cùng ta nói về Vector , một thể hiện của Vector chứa đựng 3 phần, x,y,z và bởi vì ta đã định nghĩa 1 bộ chỉ mục ở
chương 3, nó có thể đuợc xem 1 thể hiện Vector là 1 mảng , để ta có thể truy nhập vào phần x bằng cách viết someVector[0],
phần y bằng cách viết someVecor[1] và z là someVector[2].
Bây giờ ta sẽ mở rộng cấu trúc vector, dự án VectorAsCollection mà cũng có thể quét qua các phần của 1 vector bằng cách viết :
foreach (double component in someVector)
Console.WriteLine("Component is " + component);
Nhiệm vụ đầu tiên của ta là biểu thị vector như là 1 collection bằng việc cho nó thực thi interface IEnumerable, ta bắt đầu bằng
việc cập nhật khai báo của cấu trúc vector:
struct Vector : IFormattable, IEnumerable
{
public double x, y, z;
Bây giờ ta thi hành interface IEnumerable :
public IEnumerator GetEnumerator()
{
return new VectorEnumerator(this);
}
Việc thi hành GetEnumerator() hầu như là đơn giản, nhưng nó tuỳ thuộc trên sự tồn tại của 1 lớp mới, VectorEnumerator,mà ta
cần định nghĩa. vì VectorEnumerator không phải là 1 lớp mà bất kì đoạn mã bên ngoài có thể thấy trực tiếp, ta khai báo nó là lớp
private bên trong cấu trúc Vector. việc định nghĩa nó như sau:

private class VectorEnumerator : IEnumerator


{
Vector theVector; // Vector object that this enumerato refers to
int location; // which element of theVector the enumerator is
// currently referring to

public VectorEnumerator(Vector theVector)


{
this.theVector = theVector;
location = -1;
}

public bool MoveNext()


{
++location;
return (location > 2) ? false : true;
}

public object Current


{
get
{
if (location < 0 || location > 2)
throw new InvalidOperationException(
"The enumerator is either before the first element or " +
"after the last element of the Vector");
return theVector[(uint)location];
}
}

public void Reset()


{
location = -1;
}
}

Khi được yêu cầu như 1 bộ đếm, VectorEnumerator thi hành interface IEnumerator. nó cũng chứa 2 trường thành viên,
theVector,1 tham chiếu đến Vector ( collection) mà bộ đếm kết hợp, location, 1 số nguyên mà chỉ định nơi trong collection mà bộ
đếm tham chiếu đến
Cách làm việc là xem location như là chỉ mục và thi hành enumerator để truy nhập Vector như mảng.khi truy nhập vector như
mảng giá trị chỉ mục là 0,1,2 - tamở rộng bằng cách dùng -1 như là giá trị chỉ định bộ đếm trước khi bắt đầu collection,và 3 để
chỉ nó đến cuối của collection. vì vậy , việc khởi tạo của trường nay là -1 trong hàm dựng VectorEnumerator :
public VectorEnumerator(Vector theVector)
{
this.theVector = theVector;
location = -1;
}
Lưu ý rằng hàm dựng cũng lấy 1 tham chiếu đến thể hiện của Vector mà chúng ta định đếm - điều này được cung cấp trong
phương thức Vector.GetEnumerator :
public IEnumerator GetEnumerator()
{
return new VectorEnumerator(this);
}
Dictionaries
Từ điển trình bày 1 cấu trúc dữ liệu rất phức tạp mà cho phép ta truy nhập vào các phần tử dựa trên 1 khoá nào đó, mà có thể là
kiểu dữ liệu bất kì.ta hay gọi là bảng ánh xạ hay bảng băm.Từ điển được dùng khi ta muốn lưu trữ dữ liệu như mảng nhưng muốn
dùng 1 kiểu dữ liệu nào đó thay cho kiểu dữ liệu số làm chỉ mục.nó cũng cho phép ta thêm hoặc bỏ các mục , hơi giống danh
sách mảng tuy nhiên nó không phải dịch chuyển các mục phía sau trong bộ nhớ.
Ta minh họa việc dùng từ điển trong ví dụ sau :MortimerPhonesEmployees.trong ví dụ này công ty điện thoại có vài phần mềm
xử lí chi tiết nhân viên .ta cần 1 cấu trúc dữ liệu -hơi giống mảng- mà chứa dữ liệu của nhân viên.ta giả sử rằng mỗi nhân viên
trong công ty được xác định bởi ID nhân viên,là tập kí tự như B342.. và được lưu trữ thành đối tượng EmployyeeID.chi tiết của
nhân viên được lưu trữ thành đối tượng EmployeeData, ví dụ chỉ chứa ID ,tên, lương của nhân viên.
giả sử ta có EmployeeID:
EmployeeID id = new EmployeeID("W435");
và ta có 1 biến gọi là employees, mà ta có thể xem như 1 mảng đối tượng EmployeeData.thực sự , nó không phải là mảng - nó là
từ điển và bởi vì nó là từ điển nên ta có thể lấy chi tiết của 1 nhân viên thông qua ID đuợc khai báo trên:
EmployeeData theEmployee = employees[id];
// lưu ý rằng ID không phải kiểu số- nó là 1 thể hiện của EmployeeID
Đó là sức mạnh của từ điển.Ta có thể dùng kiểu dữ liệu bất kì làm chỉ mục , lúc này ta gọi nó là khoá chứ không phải là chỉ mục
nữa.khi ta cung cấp 1 khoá truy nhập vào 1 phần tử ( như ID trên ), nó sẽ xử lí trên giá trị của khoá và trả về 1 số nguyên tuỳ
thuộc vào khoá, và được dùng để truy nhập vào 'mảng' để lấy dữ liệu.
Từ điển trong .NET
Trong .NET , từ điển cơ bản được trình bày qua lớp Hasthable, mà cách làm việc cũng giống như từ điển thực, ngoại trừ nó xem
khoá và mục có kiểu object.nghĩa là 1 bảng băm có thể lưu trữ bất kì cấu trúc dữ liệu nào ta muốn.
ta có thể tự định nghĩa 1 lớp từ điển riêng cụ thể hơn.Microsoft cung cấp 1 lớp cơ sở trừu tượng,DictionaryBase,cung cấp những
chức năng cơ bản của từ điển ,mà ta có thể dẫn xuất đến lớp mà ta muốn tạo.nếu khoá là chuỗi ta có thể dùng lớp
System.Collections.Specialized.StringDictionary thay cho Hasthable.
khi tạo một Hasthable ta có thể chỉ định kích thước khởi tạo của nó:
Hasthable employees = new Hasthable(53);
Ở đây ta chọn số 53 bởi vì thuật toán bên trong được dùng cho từ điển làm việc hiệu quả hơn nếu kích thước của nó là 1 số
nguyên tố.
Thêm đối tượng vào từ điển ta dùng phương thức Add(), có 2 thông số kiểu object : thông số đầu là khoá, thứ hai là 1 tham chiếu
đến dữ liệu. ví dụ:
EmployeeID id;
EmployeeData data;

// khởi tạo id và dữ liệu.


// giả sử employees là 1 thể hiện của bảng băm
//mà chứa đựng các tham chiếu EmployeeData

employees.Add(id, data);
để nhận dữ liệu ta cung cấp khoá cho nó:
EmployeeData data = employees[id];
để bỏ 1 mục ta cung cấp khoá và gọi :
employees.Remove(id);
Để đếm số mục trong từ điển ta dùng thuộc tính Count:
int nEmployees = employees.Count;
Việc lưu trữ trong từ điển không theo phải theo kiểu từ trên xuống, nghĩa là ta không thể tìm thấy 1 khối lớn dữ liệu ở phần đầu
của cấu trúc và 1 khối rỗng ở phần cuối. biểu đồ sau minh hoạ cho việc lưu trữ trong từ điển, các phần không đánh dấu là rỗng:
Cách từ điển làm việc
Hasthable ( hay bất kì lớp từ điển nào khác) sử dụng vài thuật toán để thực hiện việc đặt mỗi đối tượng dựa trên khoá. có 2 giai
đoạn, và phần mã cho từng giai đoạn phải được cung cấp bởi lớp khoá.nếu sử dụng lớp do Microsoft viết, mà dùng làm khoá
( như chuỗi), thì không có vấn đề gì ( Microsoft đã viết sẵn rồi) .nhưng nếu lớp khoá do ta viết thì ta phải tự viết phần thuật toán
này.
1 phần của thuật toán thực thi bởi lớp khoá gọi là băm ( vì vậy có thuật ngữ bảng băm)và lớp Hasthable tìm 1 nơi cụ thể cho thuật
toán băm. nó nhìn vào phương thức Gethashcode() trong đối tượng của ta, mà thừa kế từ System.Object() nếu ta nạp chồng
GetHashCode().
Cách nó làm việc là Gethashcode() trả vế 1 số nguyên.bằng cách nào đó nó dùng giá trị của khoá để sinh ra 1 số
nguyên.Hasthable sẽ lấy số nguyên này và làm các việc xử lí khác trên nó mà liên quan đến việc tính toán toán học phức tạp,và
trả về chỉ mục của mục đưọc lưu trữ tương ứng với khóa trong từ điển.ta không đi sâu vào thuật toán này nhưng ta sẽ tìm hiểu tại
sao nó liên quan đến số nguyên tố và tại sao dung lượng bảng băm nên là số nguyên tố.
Có một số yêu cầu nghiêm ngặt khi ta nạp chồng GetHashCode(). những yêu cầu này nghe có vẻ trừu tượng nhưng qua ví dụ
MortimerPhonesEmployees ta sẽ thấy rằng không quá khó để viết lớp khoá thỏa mãn những đòi hỏi sau:
- Nó phải nhanh ( bởi vì việc đặt và lấy các mục trong 1 từ điển được coi là nhanh)
- Nó phải được đồng nhất - nếu ta cho 2 khoá cùng giá trị thì chúng phải cho cùng giá trị trong băm.
- Cho những giá trị khả dĩ trong khoảng giá trị của 1 số kiểu int ( it should ideally give values that are likely to be evenly
distributed across the entire range of numbers that an int can store )
Lí do của điều kiện cuối là : điều gì sẽ xảy ra nếu ta lấy 2 mục trong từ điển mà khi băm cả hai đều cho cùng 1 chỉ mục?
Nếu điều này xảy ra, lớp từ điển sẽ phải bắt đầu tìm kiếm vị trí trống có giá trị gần nhất để lưu trữ mục thứ hai.
Xung đột giữa các khóa cũng gia tăng khi từ điển đầy,vì thế cách tốt nhất là bảo đảm dung lượng lớn hơn số phần tử thực sự
trong nó.vì lí do này mà Hasthable tự định vị lại kích cỡ của nó để tăng dung lượng trước khi nó đầy.tỷ lệ của bảng mà đầy gọi là
load. ta có thể thiết lập giá trị lớn nhất mà ta muốn load đến trước khi Hasthable tái định vị theo hàm dựng Hasthable khác :
// dung lượng =50, Max Load = 0.5
Hasthable employees = new Hasthable(50, 0.5);

Max load càng nhỏ bảng băm làm việc càng hiệu quả nhưng càng cần nhiều vùng nhớ.khi bảng băm tái định vị để tăng dung
lượng , nó luôn chọn 1 số nguyên tố làm dung lượng mới.
1 điểm quan trọng khác là thuật toán băm phải đồng nhất.nếu 2 đối tượng chứa những gì ta coi như là dữ liệu trùng, thì chúng
phải cho cùng 1 giá trị băm, và điều này dẫn đến 1 giới hạn quan trọng trên cách nạp chồng phương thức Equals() và
Gethashcode() của System.Object. cách mà Hasthable quyết định 2 khoá a và b là bằng nhau là nó gọi a.equals(b). nghĩa là ta
phải chắc rằng điều sau luôn đúng :
Nếu a.equals(b) là đúng thì a.gethashcode() và b.gethashcode() phải luôn trả về cùng mã băm.
nếu ta cố ý nạp chồng những phương thức này để những câu lệnh trên không đúng thì bảng băm sẽ không làm việc bình thường.
ví dụ như ta đặt 1 đối tượng vào bảng băm nhưng không nhận lại được nó hay nhận lại được nhưng không đúng mục.
trong system.object điều kiện này đúng , vì Equals() đơn giản so sánh 2 tham chiếu và gethashcode() thực sự trả về 1 băm dựa
trên địa chỉ của đối tượng.nghĩa là bảng băm dựa trên 1 khoá mà không nạp chồng những phương thức này sẽ làm việc đúng.tuy
nhiên ,vấn đề với cách làm này là những khóa coi là bằng chỉ nếu chúng là cùng đối tượng.nghĩa là khi đặt 1 đối tượng vào từ
điển ta phải nối tham chiếu đến khóa.ta không thể khởi tạo 1 khóa khác sau đó mà có cùng giá trị,vì cùng giá trị được định nghĩa
theo nghĩa là cùng một thực thể. nghĩa là nếu ta không nạp chồng bản object của Equals() và Gethashcode(), lớp của ta sẽ không
thuận lợi để dùng trong bảng băm. tốt hơn nếu thi hành gethashcode() sinh ra 1 băm dựa trên giá trị của khoá hơn là điạ chỉ của
nó trong bộ nhớ.do đó ta sẽ cần nạp chồng gethashcode() va equals() trong bất kì lớp nào mà ta muốn nó được sử dụng như khoá
System.String có những phương thức nạp chồng tương đương, Equals() được nạp chồng để cung cấp giá trị so sánh, và
gethashcode() được nạp chồng để trả về 1 băm dựa trên giá trị của chuỗi.vì lí do này thuận lợi để dùng chuỗi như là khoá trong từ
điển.
Ví dụ MortimerPhonesEmployees
Đây là chương trình thiết lập từ điển nhân viên.chương trình khởi tạo từ điển , thêm vài nhân viên và sau đó mời người dùng gõ
vào Id nhân viên. mỗi khi gõ , chương trình dùng ID để trỏ vào tử điển và nhận chi tiết nhân ivên. quy trình lặp lại cho đến khi
người dùng gõ X :
MortimerPhonesEmployees
Enter employee ID (format:A999, X to exit)> B001
Employee: B001: Mortimer £100,000.00

Enter employee ID (format:A999, X to exit)> W234


Employee: W234: Arabel Jones £10,000.00
Enter employee ID (format:A999, X to exit)> X
Các lớp của chương trình :

class EmployeeID
{
private readonly char prefix;
private readonly int number;

public EmployeeID(string id)


{
prefix = (id.ToUpper())[0];
number = int.Parse(id.Substring(1,3));

public override string ToString()


{
return prefix.ToString() + string.Format("{0,3:000}", number);
}

public override int GetHashCode()


{
return ToString().GetHashCode();
}

public override bool Equals(object obj)


{
EmployeeID rhs = obj as EmployeeID;
if (rhs == null)
return false;
if (prefix == rhs.prefix && number == rhs.number)
return true;
return false;
}
}

Phần định nghĩa đầu tiên của lớp lưu trữ ID.bao gồm 1 kí tự chữ đứng đầu theo sau là 3 kí tự số. ta dùng kiểu char để lưu chữ đầu
và int để lưu phần sau.
Hàm dựng nhận 1 chuỗi và ngắt nó thành những trường này.phuơng thức Tostring() trả về ID là chuỗi:
return prefix.ToString() + string.Format("{0,3:000}", number);
Phần đặc tả định dạng (0,3:000) để phần int chứa số được điền thêm số 0 ví dụ ta sẽ có B001 không phải B1
ta đến 2 phương thức nạp chồng trong từ điển :
Đầu tiên là Equals() để so sánh giá trị của những thể hiện EmployeeID :
public override bool Equals(object obj)
{
EmployeeID rhs = obj as EmployeeID;
if (rhs == null)
return false;
if (prefix == rhs.prefix && number == rhs.number)
return true;
return false;
}
}
Đầu tiên ta kiểm tra xem đối tượng trong thông số có phải là 1 thể hiện của EmployeeID không bằng cách thử ép kiểu nó thành
đối tượng EmployeeID . sau đó ta chỉ việc so sánh những trường giá trị của nó có chứa cùng giá trị như đối tuợng này không.
Tiếp theo là Gethashcode() :
public override int GetHashCode()
{
string str = this.ToString();
return str.GetHashCode();
}
Phần trên ta đã xem xét các yêu cầu giới hạn mà mã băm được tính phải thỏa mãn.tất nhiên có những cách để nghĩ ra những thuật
toán băm hiệu quả và đơn giản. nói chung, lấy 1 trường , nhân nó với 1 số nguyên tố lớn,và công những kết quả lại với nhau là 1
cách tốt. nhưng ta không phải làm những điều đó vì MIcrosoft đã làm toàn bộ trong lớp String, vì thế ta có thể lợi dụng lớp này
để tạo ra số dựa trên nội dung của chuỗi.nó sẽ thoã mãn tất cả những yêu cầu của mã băm.
Chỉ có 1 khuyết điểm khi dùng phương thức này là có vài việc thi hành đã mất kết hợp với việc chuyển đổi lớp EmployeeID
thành chuỗi trong phần đầu tiên.nếu không muốn điều này ta sẽ cần thiết kế mã băm riêng thiết kế thuật toán băm là 1 chủ đề
phức tạp mà ta không không thể đi sâu trong cuốn sách này.tuy nhiên ta sẽ đưa ra 1 cách đơn giản cho vấn đế này, mà chỉ việc
nhân số dựa trên những trường thành phần của lớp với số nguyên tố khác( nhân bởi 1 số nguyên tố khác giúp ta ngăn ngừa sự kết
hợp giá trị khác nhau của các trường từ việc cho cùng mã băm) :
public override int GetHashCode() // alternative implementation
{
return (int)prefix*13 + (int)number*53;
}
Ví dụ này sẽ làm việc nhanh hơn Tostring() mà ta dùng ở trên. tuy nhiên khuyết điểm là mã băm sinh ra bởi các employeeID
khác nhau thì không trải rộng trên vùng số kiểu int ( are less likely to be evenly spread across the range of int ).
tiếp theo ta xem lớp chứa dữ liệu nhân viên :
class EmployeeData
{
private string name;
private decimal salary;
private EmployeeID id;

public EmployeeData(EmployeeID id, string name, decimal salary)


{
this.id = id;
this.name = name;
this.salary = salary;
}

public override string ToString()


{
StringBuilder sb = new StringBuilder(id.ToString(), 100);
sb.Append(": ");
sb.Append(string.Format("{0,-20}", name));
sb.Append(" ");
sb.Append(string.Format("{0:C}", salary));
return sb.ToString();
}
}

Ta dùng đối tượng StringBuilder để sinh ra chuỗi đại diện cho đối tượng Employeedata. cuối cùng ta viết đoạn mã kiểm tra lớp
TestHarness:
class TestHarness
{

Hashtable employees = new Hashtable(31);

public void Run()


{
EmployeeID idMortimer = new EmployeeID("B001");
EmployeeData mortimer = new EmployeeData(idMortimer, "Mortimer",
100000.00M);
EmployeeID idArabel = new EmployeeID("W234");
EmployeeData arabel= new EmployeeData(idArabel, "Arabel Jones",
10000.00M);

employees.Add(idMortimer, mortimer);
employees.Add(idArabel, arabel);

while (true)
{
try
{
Console.Write("Enter employee ID (format:A999, X to exit)> ");
string userInput = Console.ReadLine();
userInput = userInput.ToUpper();
if (userInput == "X")
return;
EmployeeID id = new EmployeeID(userInput);
DisplayData(id);
}
catch (Exception e)
{
Console.WriteLine("Exception occurred. Did you use the correct
format for the employee ID?");
Console.WriteLine(e.Message);
Console.WriteLine();
}

Console.WriteLine();
}
}

private void DisplayData(EmployeeID id)


{
object empobj = employees[id];
if (empobj != null)
{
EmployeeData employee = (EmployeeData)empobj;
Console.WriteLine("Employee: " + employee.ToString());
}
else
Console.WriteLine("Employee not found: ID = " + id);
}
}
Đầu tiên ta thiết lập dung lượng của từ điển là số nguyên tố, 31, phần chính của lớp này là phương thức run().đầu tiên là thêm vài
nhân viên vào từ điển mortimer và arabel và thêm chi tiết của họ vào:
employees.Add(idMortimer, mortimer);
employees.Add(idArabel, arabel);
Tiếp theo ta bước vào vòng lặp để yêu cầu người dùng nhập vào EmployeeID. có khối try bên trong vòng lặp, bẫy những lỗi khi
người dùng không gõ đúng định dạng cuả EmployeeID :
string userInput = Console.ReadLine();
userInput = userInput.ToUpper();
if (userInput == "X")
return;
EmployeeID id = new EmployeeID(userInput);
Nếu hàm dựng EployeeID đúng, ta trình bày kết hợp nhân viên bằng cách gọi ,DisplayData(). đây là phương thức mà ta muốn
truy nhập vào từ điển với cú pháp mảng. nhận dữ liệu nhân viên với ID là việc đầu tiên trong phương thức này:
private void DisplayData(EmployeeID id)
{
object empobj = employees[id];
Nếu không có nhân viên với ID tên , thì employees[id] trả về Null,mà ta sẽ đưa ra thông báo lỗi nếu ta tìm thấy. nếu không ta ép
kiểu tham chiếu empobj thành EmployeeData ( nhờ rằng trong từ điển nó lưu đối tượng, vì thế khi nhận lại phần tử từ nó là 1
tham chiếu dối tượng , ta phải ép kiểu tường minh trả về kiểu mà ta đã đặt trong từ điển.) khi ta có tham chiếu EmployeeID , ta
trình bày dữ liệu của nó bằng phương thức EmployeeData.ToString() :
EmployeeData employee = (EmployeeData)empobj;
Console.WriteLine("Employee: " + employee.ToString());
Ta có phần cuối của mã - phương thức main() kích hoạt ví dụ trên . khởi tạo đối tượng TestHarness và chạy nó.:
static void Main()
{
TestHarness harness = new TestHarness();
harness.Run();
}

Code for Download:

MortimerPhonesEmployees

Attribute tuỳ chọn


Trong chương 4 ta đã xem xét một số attribute được định nghĩa trên một số mục của chương trình.Đó là các attribute mà trình
biên dịch biết cách xử lý .trong phần này ta sẽ xét việc định nghĩa các attribute của riêng ta .nếu ta làm điều này có thể các
attribute này sẽ không được thừa nhận như là dữ liệu thêm (metadata) để trình biên dịch có thể xử lí. nhưng các metadata này có
thể hữu ích cho mục đích tài liệu hướng dẫn .tuy nhiên, bằng cách dùng các lớp trong namespace System.Reflection, mã của ta có
thể đọc được các metadata vào lúc chạy.nghĩa là các attribute tùy chọn mà ta định nghĩa có thể ảnh hưởng trực tiếp đến cách mà
mã chạy.

Viết attribute tuỳ chọn


Để hiểu cách viết attribute tuỳ chọn, ta cần xem trình biên dịch làm gì khi nó gặp 1 mục trong mã được đánh dấu với 1 attribute,
mà việc hổ trợ không tường minh không được xây dựng trong trình biên dịch.giả sử ta có thuộc tính khai báo như sau:
[FieldName("SocialSecurityNumber")]
public string SocialSecurityNumber
{
get {
// vv...
Như ta thấy thuộc tính này có 1 attribute ,Fieldname, trình biên dịch sẽ nối chuổi attribute với tên này thành FieldNameattribute,
sau đó tìm trong tất cả các namespace ( đưọc đề cập trong using ) lớp có tên này.tuy nhiên nếu ta đánh dấu 1 mục với 1 attribute
mà tên của nó có phần cuối là attribute thì trình biên dịch sẽ không thêm chuỗi attribute lần nữa ví dụ:
[FieldNameattribute("SocialSecurityNumber")]
public string SocialSecurityNumber
{

// vv...
Nếu trình biên dịch không tìm thấy 1 lớp attribute đáp ứng, hoặc thấy nhưng cách mà ta dùng attribute không phù hợp với thông
tin trong lớp attribute.thì trình biên dịch sẽ sinh ra lỗi.
Các lớp attribute tuỳ chọn
Giả sử như ta đã định nghĩa 1 attribute FieldName như sau :

[AttributeUsage(AttributeTargets.Property,
AllowMultiple=false,
Inherited=false)]
public class FieldNameAttribute : Attribute
{
private string name;
public FieldNameAttribute(string name)
{
this.name = name;
}
}

Attribute attributeUsage
Điều đầu tiên ta chú ý là lớp attribute được đánh dấu với 1 attribute - attributeUsage .
attributeUsage chỉ định các mục nào trong mã của ta attribute tuỳ chọn được áp dụng. thông tin này được cho bởi thông số đầu
tiên, mà phải được trình bày. thông số này là 1 kiểu liệt kê,attributeTargets .trong ví dụ trên ta chỉ định attribute Fieldname có thể
chỉ ứng dụng đến các thuộc tính. định nghĩa của kiểu liệt kê attributeTargets là :
public enum attributeTargets
{
All = 0x00003FFF,
Assembly = 0x00000001,
Class = 0x00000004,
Constructor = 0x00000020,
Delegate = 0x00001000,
Enum = 0x00000010,
Event = 0x00000200,
Field = 0x00000100,
Interface = 0x00000400,
Method = 0x00000040,
Module = 0x00000002,
Parameter = 0x00000800,
Property = 0x00000080,
ReturnValue = 0x00002000,
Struct = 0x00000008
}

Danh sách cho ta tất cả các attribute có thể đưọc áp dụng.khi áp dụng attribute đến các phần tử chương trình , ta đặt attribute
trong ngoặc vuông ngay trước phần tử. tuy nhiên có 1 giá trị trong dánh sách trên không phù hợp đến bất kì phần tử nào trong
chương trình :Assembly . 1 attribute có thể được áp dụng đến 1 Assembly như là thay thế hoàn toàn 1 phần tử trong mã. trong
trường hợp này attribute có thể được đặt ở bất cứ đâu trong mã nguồn, nhưng cần được đánh dấu với từ khoá Assembly:
[assembly: SomeAssemblyattribute(Parameters)]

Ta có thể kết hợp nhiều kiểu áp dụng trên các phần tử ta dùng các tác tử, ví dụ như ta chỉ định attribute Fielaname có thể áp dụng
đến 1 property hay 1 field ta viết như sau :
[attributeUsage(attributeTargets.Property | attributeTargets.Field,
AllowMultiple=false,
Inherited=false)]
public class FieldNameattribute : attribute
Ta cũng có thể dùng attributeTargets.All để áp dụng attribute ở mọi nơi. attribute attributeUsage còn chứa 2 thông số khác là
AllowMultiple and Inherited. chỉ định với cú pháp khác của <attributeName>=<attributeValue>, .thông số này là tuỳ chọn.
thông số AllowMultiple chỉ định 1 attribute có thể áp dụng nhiều hơn 1 lần đến cùng 1 mục.nếu thiết đặt là false thì trình biên
dịch sẽ thông báo lỗi nếu nó thấy :[FieldName("SocialSecurityNumber")]
[FieldName("NationalInsuranceNumber")]
public string SocialSecurityNumber
{

// vv...

Nếu thông số Inherited là true, thì 1 attribute có thể áp dụng đến 1 lớp hay 1 interface cũng sẽ được áp dụng đến tất cả các lớp
hay interface được thừa kế.nếu attribute được áp dụng đến phương thức hay thuộc tính thì nó tự động áp dụng đến bất kì override
của phương thức hay thuộc tính đó.
Đặc tả các thông số attribute
Ta sẽ kiểm tra làm thế nào ta có thể chỉ định bất kì thông số mà attribute tuỳ chọn nhận.cách nó làm việc khi trình biên dịch gặp 1
lệnh như :
[FieldName("SocialSecurityNumber")]
public string SocialSecurityNumber
{

// etc.

Nó kiểm tra thông số truyền vào attribute - trong trường hợp này là chuỗi và tìm hàm dựng của attribute mà nhận các thông số
này.nếu thấy thì không có vấn đề gì ngược lại trình biên dịch sẽ sinh ra lỗi.
Các thông số tuỳ chọn
Ta thấy attribute attributeUsage có 1 cú pháp thay thế mà các thông số tuỳ chọn có thể được thêm vào thành 1 attribute.cú pháp
này có liên quan đến việc chỉ định tên của các thông số tùy chọn.giả sử ta cập nhật lại thuộc tính SocialSecurityNumber như sau :
[FieldName("SocialSecurityNumber", Comment="This is the primary key field")]
public string SocialSecurityNumber
{

// etc.
Trong trường hợp này , trình biên dịch sẽ nhận ra<ParameterName>= cú pháp của thông số thứ hai.nó sẽ tìm 1 thuộc tính public (
hoặc field) của tên đó mà nó có thể dùng để đặt giá trị của thông số này. nếu ta muốn đoạn mã trên làm việc ta thêm mã sau vào
FieldNameattribute:
[attributeUsage(attributeTargets.Property,
AllowMultiple=false,
Inherited=false)]
public class FieldNameattribute : attribute
{
private string comment;
public string Comment
{

// etc.
Ví dụ WhatsNewattributes
Ví dụ cung cấp 1 attribute chỉ định khi nào 1 mục được cập nhật lần cuối.trong đó nó bao gồm 3 assembly riêng biệt :
assembly WhatsNewattributes chứa đựng định nghĩa của các attribute
assembly VectorStruct ,chứa đựng mã mà attribute được áp dụng.
assembly LookUpWhatsNew chứa đựng dự án trình bày chi tiết các mục mà vừa thay đổi.
Thư viện assembly WhatsNewattributes
Mã nguồn chứa trong tập tin WhatsNewattributes.cs.để biên dịch ta gõ :
csc /target:library WhatsNewattributes.cs
Mã nguồn của assembly này chứa 2 lớp attribute . LastModifiedattribute and SupportsWhatsNewattribute. LastModifiedattribute
là attribute mà ta có thể dùng để đánh dấu khi một mục được cập nhật lần cuối . nó nhận 2 thông số bắt buộc ( các thông số mà
đưọc truyền đến hàm dựng) ngày tháng cập nhật , và chuỗi chứa mô tả các thay đổi. cũng có 1 thông số tuỳ chọn missuses, được
dùng để mô tả bất kì vấn đề nào phát sinh trong mục.
Mã nguồn cho phần này như sau :
namespace Wrox.ProCSharp.WhatsNewAttributes
{
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple=true, Inherited=false)]
public class LastModifiedAttribute : Attribute
{
private DateTime dateModified;
private string changes;
private string issues;

public LastModifiedAttribute(string dateModified, string changes)


{
this.dateModified = DateTime.Parse(dateModified);
this.changes = changes;
}

public DateTime DateModified


{
get
{
return dateModified;
}
}

public string Changes


{
get
{
return changes;
}
}

public string Issues


{
get
{
return issues;
}
set
{
issues = value;
}
}
}

[AttributeUsage(AttributeTargets.Assembly)]
public class SupportsWhatsNewAttribute : Attribute
{
}
}
Dùng các attribute này - assembly VectorClass
Tiếp theo ta cần dùng các attribute này trong ví dụ VectorAsCollection . ta cần tham chiếu tuờng minh thư viện
WhatsNewattributes mà ta vừa mới tạo. ta cũng cần chỉ định namespace để trình biên dịch nhận ra attribute :
using System;
using Wrox.ProCSharp.WhatsNewattributes;
using System.Collections;
using System.Text;

[assembly: SupportsWhatsNew]

Ta thêm vào dòng mà sẽ đánh dấu assembly với attribute SupportsWhatsNew


Trong phần mã của lớp Vector . ta không thật sự thay đổi bất cứ thứ gì trong lớp này , chỉ thêm vào vài attribute LastModified.
tuy nhiên ta cũng tạo ra 1 thay đổi , ta vừa định nghĩa Vector như là 1 lớp thay vì là 1 struct. lý do là mã mà ta sẽ viết sau trình
bày các attribute .trong ví dụ VectorAsCollection ,Vector là 1 struct nhưng bảng kiệt kê của nó là 1 lớp .có cả hai kiểu là lớp
nghĩa là ta không phải lo lắng về sự tồn tại của bất kì struct , do đó làm cho ví dụ của ta ngắn hơn 1 chút .

namespace Wrox.ProCSharp.VectorClass
{
[LastModified("14 Feb 2002", "IEnumerable interface implemented\n" +
"So Vector can now be treated as a collection")]
[LastModified("10 Feb 2002", "IFormattable interface implemented\n" +
"So Vector now responds to format specifiers N and VE")]
class Vector : IFormattable, IEnumerable
{
public double x, y, z;

public Vector(double x, double y, double z)


{
this.x = x;
this.y = y;
this.z = z;
}

[LastModified("10 Feb 2002",


"Method added in order to provide formatting support")]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format == null)
return ToString();

Ta cũng sẽ đánh dấu lớp VectorEnumerator chứa như new:


[LastModified("14 Feb 2002",
"Class created as part of collection support for Vector")]
private class VectorEnumerator : IEnumerator
{
Tuy nhiên ta vẫn chưa thể chạy bởi vì ta chỉ mới có 2 thư viện .ta sẽ phát triển phần cuối của ví dụ mà sẽ tìm và trình bày các
attribute sau khi học xong Reflection.
Để biên dịch đoạn mã này ta gõ :
csc /target:library /reference:WhatsNewattributes.dll VectorClass.cs

Code for Download:

WhatsNewattributes.cs

VectorClass.cs

Thread ( luồng )
1 thread là 1 chuỗi liên tiếp những sự thực thi trong chương trình. trong 1 chương trình C# ,việc thực thi bắt đầu bằng phương
thức main() và tiếp tục cho đến khi kết thúc hàm main().
Cấu trúc này rất hay cho những chương trình có 1 chuỗi xác định những nhiệm vụ liên tiếp . nhưng thường thì 1 chương trình cần
làm nhiều công việc hơn vào cùng một lúc.ví dụ trong internet explorer khi ta đang tải 1 trang web thì ta nhấn nút back hay 1 link
nào đó , để làm việc này Internet Explorer sẽ phải làm ít nhất là 3 việc :
- Lấy dữ liệu đưọc trả về từ internet cùng với các tập tin đi kèm .
- Thể hiện trang web
- Xem người dùng có nhập để làm thứ gì khác không
Để đơn giản vấn đề ta giả sử Internet Ecplorer chỉ làm 2 công việc :
- Trình bày trang web
- Xem người dùng có nhập gì không
Để thực hành việc này ta sẽ viết 1 phương thức dùng để lấy và thể hiện trang web .giả sử rằng việc trình bày trang web mất nhiều
thời gian ( do phải thi hành các đoạn javascript hay các hiệu ứng nào đó .. ) .vì thế sau một khoảng thời gian ngắn khoảng 1/12
giây , phương thức sẽ kiểm tra xem người dùng có nhập gì không .nếu có thì nó sẽ đuơc xử lí, nếu không thì việc trình bày trang
sẽ được tiếp tục. và sau 1/12 giây việc kiểm tra sẽ được lặp lại.tuy nhiên viết phương thức này thì rất phức tạp do đó ta sẽ dùng
kiến trúc event trong window nghĩa là khi việc nhập xảy ra hệ thống sẽ thông báo cho ứng dụng bằng cách đưa ra một event . ta
sẽ cập nhật phương thức để cho phép dùng các event :
Ta sẽ viết 1 bộ xử lí event để đáp ứng đối với việc nhập của người dùng.
Ta sẽ viết 1 phương thức để lấy và trình bày dữ liệu . phương thức này được thực thi khi ta không làm bất cứ điều gì khác.
Ta hãy xem cách phương thức lấy và trình bày trang web làm việc : đầu tiên nó sẽ tự định thời gian. trong khi nó đang chạy, máy
tính không thể đáp ứng việc nhập của người dùng . do đó nó phải chú ý đến việc định thời gian để gọi phương thức kiểm tra việc
nhập của người dùng ,nghĩa là phương thức vừa chạy vừa quan sát thời gian.bên cạnh đó nó còn phải quan tâm đến việc lưu trữ
trạng thái trước khi nó gọi phương thức khác để sau khi phương thức khác thực hiện xong nó sẽ trả về đúng chỗ nó đã dừng.Vào
thời window 3.1 đây thực sự là những gì phải làm để xử lí tình huống này. tuy nhiên ở NT3.1 và sau đó là windows 95 đã có việc
xử lí đa luồng điều này làm việc giải quyết vấn đề tiện lợi hơn.
Các ứng dụng với đa luồng
Trong ví dụ trên minh hoạ tình huống 1 ứng dụng cần làm nhiều hơn 1 công việc.vì vậy giải pháp rõ ràng là cho ứng dụng thực
thi nhiều luồng. như ta đã nói ,1luồng đaị diện cho 1 chuỗi liên tiếp các lệnh mà máy tính thực thi . do đó không có lí do nào 1
ứng dụng lại chỉ có 1 luồng.thực vậy ta có thể có nhiều luồng nếu ta muốn. tất cả điều cần là mỗi lần ta tạo 1 luồng thực thi mới,
ta chỉ định 1 phương thức mà thực thi nên bắt đầu với. luồng đầu tiên trong ứng dụng luôn được thực thi trong main() vì đây là
phương thức mà .NET runtime luôn lấy để bắt đầu.các luồng sau sẽ được bắt đầu bên trong ứng dụng.
Công việc này làm như thế nào ?
- Một bộ xử lí chỉ có thể làm một việc vào một lúc,nếu có một hệ thống đa xử lí , theo lí thuyết có thể có nhiều hơn một lệnh
được thi hành đồng bộ, mỗi lệnh trên một bộ xử lí, tuy nhiên ta chỉ làm việc trên một bộ xử lí do đó các công việc không thể xảy
ra cùng lúc.thực sự thì hệ điều hành window làm điều này bằng một thủ tục gọi là pre emptive multitasking
- Thủ tục này nghĩa là window lấy 1 luồng vào trong vài tiến trình và cho phép luồng đó chạy 1 khoảng thời gian ngắn .gọi là
time slice. khi thời gian này kế thúc. window lấy quyền điều khiển lại và lấy 1 luồng khác và lại cấp 1 khoảng thời gian time
slice . vì khoảng thời gian này quá ngắn nên ta có cảm tưởng như mọi thứ đều xảy ra cùng lúc.
- Thậm chí khi ứng dụng ta chỉ có một luồng thì thủ tục này vẫn đưọc dùng vì có thể có nhiều tiến trình khác đang chạy trên hệ
thống mỗi tiến trình cần các time slice cho mỗi luồng của nó.vì vậy khi ta có nhiều cửa sổ trên màn hình mỗi cái đại diện cho một
tiến trình khác nhau, ta vẫn có thể nhấn vào bất kì của sổ nào
Và nó đáp ứng ngay lập tức.thực sự việc đáp ứng nay không phải ngay lập tức- nó xảy ra vào sau khoảng thời gian time slice của
luồng đương thời.vì thời gia này quá ngắn nên ta cảm thấy như nó đáp ứng ngay lập tức.
Thao tác luồng
Luồng được thao tác bằng cách dùng lớp Thread nằm trong namespace System.Threading . 1 thể hiện của luồng đaị diện cho 1
luồng.ta có thể tạo các luồng khác bằng cách khởi tạo 1 đối tượng luồng
Bắt đầu 1 luồng
Giả sử rằng ta đang viết 1 trình biên tập hình ảnh đồ hoạ, và người dùng yêu cầu thay đổi độ sâu của màu trong ảnh. ta bắt đầu
khởi tạo 1 đối tượng luồng như sau :
// entryPoint được khai báo trước là 1 delegate kiểu ThreadStart
Thread depthChangeThread = new Thread(entryPoint);
Đoạn mã trên biểu diễn 1 hàm dựng của Thread 1 thông số chỉ định điểm nhập của 1 luồng. - đó là phương thức nơi luồng bắt
đầu thi hành.trong tình huống này ta dùng thông số là delegate, 1 delegate đã được định nghĩa trong System.Threading gọi là
aThreadStart , chữ kí của nó như sau :
public delegate void ThreadStart();

Thông số ta truyền cho hàm dựng phải là 1 delegate kiểu này .


Ta bắt đầu luồng bằng cách gọi phương thức Thread.Start() , giả sử rằng ta có phương thức ChangeColorDepth() :
void ChangeColorDepth()
{
// xử lí để thay đổi màu
}
Sắp xếp lại ta có đoạn mã sau :
ThreadStart entryPoint = new ThreadStart(ChangeColorDepth);
Thread depthChangeThread = new Thread(entryPoint);
depthChangeThread.Name = "Depth Change Thread";
depthChangeThread.Start();
Sau điểm này ,cả hai luồng sẽ chạy đồng bộ
Trong mã này ta đăng kí tên cho luồng bằng cách dùng thuộc tính Thread.Name. không cần thiết làm điều này nhưng nó có thể
hữu ích :

Lưu ý rằng bởi vì điểm đột nhập luồng ( trong ví dụ này là ChangeColorDepth() ) không thể lấy bất kì thông số nào.ta sẽ phải tìm
1 cách nào đó để truyền thông số cho phương thức nếu cần.cách tốt nhất là dùng các trường thành viên của lớp mà phương thức
này là thành viên.cũng vậy phương thức không thể trả về bất cứ thứ gì .
Mỗi lần ta bắt đầu 1 luồng khác ,ta cũng có thể đình chỉ, hồi phục hay bỏ qua nó.đình chỉ nghĩa là cho luồng đó ngủ ( sleep) -
nghĩa là không chạy trong 1 khoảng thời gian. sau đó nó thể đưọc phục hồi ,nghĩa là trả nó về thời diểm mà nó bị định chỉ. nếu
luồng đưọc bỏ , nó dừng chạy. window sẽ huỷ tất cả dữ liệu mà liên hệ đến luồng đó, để luồng không thể được bắt đầu lại.
tiếp tục ví dụ trên , ta giả sử vì lí do nào đó luồng giao diện người dùng trình bày 1 hộp thoại cho người dùng cơ hội để đình chỉ
tạm thời sự đổi tiến trình . ta sẽ soạn mã đáp ứng trong luồng main :
depthChangeThread.Suspend();
Và nếu người dùng được yêu cầu cho tiến trình được phục hồi :
depthChangeThread.Resume();
Cuối cùng nếu người dùng muốn huỷ luồng :
depthChangeThread.Abort();
Phương thức Suspend() có thể không làm cho luồng bị định chỉ tức thời mà có thể là sau 1 vài lệnh, điều này là để luồng được
đình chỉ an toàn. đối với phương thức Abort() nó làm việc bằng cách ném ra biệt lệ ThreadAbortException.
ThreadAbortException là 1 lớp biệt lệ đặc biệt mà không bao giờ được xử lí.nếu luồng đó thực thi mã bên trong khối try, bất kì
khối finally sẽ được thực thi trước khi luồng bị huỷ.
Sau khi huỷ luồng ta có thể muốn đợi cho đến khi luồng thực sự bị huỷ trước khi tiếp tục luồng khác ta có thể đợi bằng cách dùng
phương thức join() :
depthChangeThread.Abort();
depthChangeThread.Join();

Join() cũng có 1 số overload khác chỉ định thời gian đợi . nếu hết thời gian này việc thi hành sẽ được tiếp tục.
nếu một luồng chính muốn thi hành 1 vài hành động trên nó , nó cần 1 tham chiếu đến đối tượng luồng mà đại diện cho luồng
riêng. nó có thể lấy 1 tham chiếu sử dụng thuộc tính static, CurrentThread ,của lớp Thread :
Thread myOwnThread = Thread.CurrentThread;

Có hai cách khác nhau mà ta có thể thao tác lớp Thread:


- Ta có thể khởi tạo 1 đối tượng luồng , mà sẽ đại diện cho luồng đang chạy và các thành viên thể hiện của nó áp dụng đến
luồng đang chạy
- Ta có thể gọi 1 số phương thức static . những phương thức này sẽ áp dụng đến luồng mà ta thực sự đang gọi phương thức từ
nó.
1 phương thức static mà ta muốn gọi là Sleep(), đơn giản đặt luồng đang chạy ngủ 1 khoảng thời gian, sau đó nó sẽ tiếp tục.
Ví dụ ThreadPlayaround
Phần chính của ví dụ này là 1 phương thức ngắn , DisplayNumber() , phương thức này cũng bắt đầu bằng việc trình bày tên và
bản điạ ( culture) của luồng mà phương thức đang chạy :
static void DisplayNumbers()
{
Thread thisThread = Thread.CurrentThread;
string name = thisThread.Name;
Console.WriteLine("Starting thread: " + name);
Console.WriteLine(name + ": Current Culture = " +
thisThread.CurrentCulture);
for (int i=1 ; i<= 8*interval ; i++)
{
if (i%interval == 0)
Console.WriteLine(name + ": count has reached " + i);
}
}

Việc trình bày số tuỳ thuộc vào interval,là 1 trường mà giá trị được gõ bởi người dùng .nếu người dùng gõ 100 sẽ trình bày đến
800, trình bày giá trị 100,200,300,400,500,600,700,800. nếu ngươì dùng gõ 1000 thì sẽ trình bày đến 8000, trình bày giá trị
1000,2000,3000,4000,5000,6000,7000,8000 .
Những gì ThreadPlayaround() làm là bắt đầu luồng công việc thứ hai,mà chạy DisplayNumber(), nhưng ngay sau khi bắt đầu
luồng công việc, luồng chính bắt đầu thi hành cùng phương thức. nghĩa là ta thấy cả hai việc đếm xảy ra cùng lúc.
Phương thức main() cho ThreadPlayaround và lớp chứa đựng của nó là :
class EntryPoint
{
static int interval;

static void Main()


{
Console.Write("Interval to display results at?> ");
interval = int.Parse(Console.ReadLine());

Thread thisThread = Thread.CurrentThread;


thisThread.Name = "Main Thread";

ThreadStart workerStart = new ThreadStart(StartMethod);


Thread workerThread = new Thread(workerStart);
workerThread.Name = "Worker";
workerThread.Start();

DisplayNumbers();
Console.WriteLine("Main Thread Finished");

Console.ReadLine();

Trong phương thức main() đầu tiên ta hỏi người dùng interval, sau đó ta lấy 1 tham chiếu đến đối tượng luồng mà đại diện cho
luồng chính .
Kế tiếp ta tạo ra luồng công việc , đặt cùng tên , và bắt đầu nó ,truyền cho nó 1 delegate mà chỉ định phương thức mà nó phải bắt
đầu trong đó , phương thức workerStart. cuối cùng , ta gọi phương thức DisplayNumber() để bắt đầu đếm. điểm đột nhập của
luồng này là :
static void StartMethod()
{
DisplayNumbers();
Console.WriteLine("Worker Thread Finished");
}
Lưu ý rằng tất cả các phương thức này đều là phương thức static trong cùng 1 lớp,EntryPoint. biến i trong phương thức
DisplayNumber() được dùng để đếm là 1 biến cục bộ. biến này không chỉ có phạm vi đối với phương thức mà được định nghĩa
mà còn khả kiến đối với luồng đang thực thi phương thức đó. nếu 1 luồng khác bắt đầu thi hành cùng 1 phương thức , thì luồng
đó sẽ lấy 1 bản sao chép riêng của biến điạ phương . ta sẽ bắt đầu bằng việc chạy đoạn mã cho giá trị interval là 100 :
ThreadPlayaround
Interval to display results at?> 100
Starting thread: Main Thread
Main Thread: Current Culture = en-GB
Main Thread: count has reached 100
Main Thread: count has reached 200
Main Thread: count has reached 300
Main Thread: count has reached 400
Main Thread: count has reached 500
Main Thread: count has reached 600
Main Thread: count has reached 700
Main Thread: count has reached 800
Main Thread Finished
Starting thread: Worker
Worker: Current Culture = en-GB
Worker: count has reached 100
Worker: count has reached 200
Worker: count has reached 300
Worker: count has reached 400
Worker: count has reached 500
Worker: count has reached 600
Worker: count has reached 700
Worker: count has reached 800
Worker Thread Finished

Qua kết quả ta thấy luồng chính bắt đầu đếm đến 800 và sau đó hoàn thành.sau đó luồng worker bắt đầu và chạy riêng rẻ.
vấn đề ở đây là việc bắt đầu 1 luồng là 1 tiến trình lớn .sau khi khởi tạo 1 luồng mới ,luồng chính gặp dòng mã :
workerThread.Start();
Cái này sẽ gọi đến Thread.Call() thông báo cho window rằng luồng mới được bắt đầu, sau đó trả về ngay lập tức. torng khi ta
đang đếm đến 800, thì window đang bận rộn trong việc sắp xếp cho các luồng được bắt đầu .vào lúc luồn mới thực sự bắt đầu thì
luồng chính vừa mới hoàn thành công việc của nó.
Ta có thể giải quyết vấn đề này bằng cách chọn interval lớn hơn ,để cả hai luồng chạy lâu hơn trong phương thức
DisplayNumbes() , ta thử cho interval là 1 triệu :
ThreadPlayaround
Interval to display results at?> 1000000
Starting thread: Main Thread
Main Thread: Current Culture = en-GB
Main Thread: count has reached 1000000
Starting thread: Worker
Worker: Current Culture = en-GB
Main Thread: count has reached 2000000
Worker: count has reached 1000000
Main Thread: count has reached 3000000
Worker: count has reached 2000000
Main Thread: count has reached 4000000
Worker: count has reached 3000000
Main Thread: count has reached 5000000
Main Thread: count has reached 6000000
Worker: count has reached 4000000
Main Thread: count has reached 7000000
Worker: count has reached 5000000
Main Thread: count has reached 8000000
Main Thread Finished
Worker: count has reached 6000000
Worker: count has reached 7000000
Worker: count has reached 8000000
Worker Thread Finished

Ta có thể thấy các luồng thực sự làm việc song song. luồng chính bắt đầu và đếm đến 1 triệu. ở 1 vài thời điểm , trong khi luồng
chính đang đếm thì luồng công việc ( worker thread ) bắt đầu, và từ đó 2 luồng xử lí cùng tốc độ cho đến khi chúng hoàn thành.
1 điều lưu ý là khi ta xử lí đa luồng trên 1 CPU thì ta không hề tiết kiệm được thời gian , ví dụ trên máy có 1 bộ xử lí , nếu có 2
luồng đếm đến 8 triệu sẽ bằng thời gian 1 luồng đếm đến 16 triệu. nhưng lợi điểm của đa luồng là : đầu tiên ta có thể tăng tốc độ
đáp ứng, nghĩa là 1 luồng có thể giao tiếp với người dùng trong khi luồng khác làm một công việc gì đó phía sau hậu trường.thứ
hai, ta sẽ tiết kiệm thời gian nếu 1 hay nhiều luồng đang làm 1 công việc gì đó không dính dáng đến thời gian CPU, như là đợi
cho dữ lệu đến đển nhận từ internet, bởi vì các luồng khác có thể nhảy vào tiến trình của chúng trong khi luồng không hoạt động
đang chờ.
Độ ưu tiên luồng
Ta có thể đăng kí các độ ưu tiên khácnhau cho các luồng khác nhau trong 1 tiến trình. nói chung , 1 luồng không đưọc cấp phát 1
time slice nào nếu có 1 luồng có độ ưu tiên cao hơn đang làm việc. lợi điểm của điều này là ta có thể thiết lập độ ưu tiên cao hơn
cho luồng xử lí việc nhập của người dùng.
Các luồng có độ ưu tiên cao có thể cản trở các luồng có độ ưu tiên thấp cho đó ta cần thận trọng khi cấp quyền ưu tiên . độ ưu
tiên của luồng được định nghĩa là các giá trị trong bản liệt kê ThreadPriority. các giá trị : Highest, AboveNormal, Normal,
BelowNormal, Lowest
Lưu ý rằng mỗi luồng có 1 độ ưu tiên cơ sở. và những giá trị này liên quan đến độ ưu tiên trong tiến trình. cho 1 luồng có độ ưu
tiên cao hơn đảm bảo nó sẽ chiếm quyền ưu tiên so với các luồng khác trong tiến trình. nhưng cũng có 1 số luồng khác của hệ
thống đang chạy có quyền ưu tiên còn cao hơn . Windows có khuynh hướng đặt độ ưu tiên cao cho các luồng hệ điều hành của
riêng nó.
Ta có thể thấy tác động của việc thay đổi độ ưu tiên của luồng bằng cách thay đổi phương thức main() trong ví dụ
ThreadPlayaround :
ThreadStart workerStart = new ThreadStart(StartMethod);
Thread workerThread = new Thread(workerStart);
workerThread.Name = "Worker";
workerThread.Priority = ThreadPriority.AboveNormal;
workerThread.Start();
Ở đây ta thiết lập luồng công việc ( worker) có độ ưu tiên cao hơn luồng chính . kết quả là :
ThreadPlayaroundWithPriorities
Interval to display results at?> 1000000
Starting thread: Main Thread
Main Thread: Current Culture = en-GB
Starting thread: Worker
Worker: Current Culture = en-GB
Main Thread: count has reached 1000000
Worker: count has reached 1000000
Worker: count has reached 2000000
Worker: count has reached 3000000
Worker: count has reached 4000000
Worker: count has reached 5000000
Worker: count has reached 6000000
Worker: count has reached 7000000
Worker: count has reached 8000000
Worker Thread Finished
Main Thread: count has reached 2000000
Main Thread: count has reached 3000000
Main Thread: count has reached 4000000
Main Thread: count has reached 5000000
Main Thread: count has reached 6000000
Main Thread: count has reached 7000000
Main Thread: count has reached 8000000
Main Thread Finished
Sự đồng bộ
1 khía cạnh chủ yếu khác của luồng là sự đồng bộ hay là việc truy nhập 1 biến bởi nhiều luồng vào cùng thời điểm.nếu ta không
đảm bảo được sự đồng bộ thì sẽ gây ra các lỗi tinh vi .
Đồng bộ là gì ?
Hãy nhìn câu lệnh sau :
message += ", there"; // message là 1 chuỗi chứa chữ "hello"
Trông có vẻ như là 1 lệnh nhưng thực sự thì lệnh phải thi hành nhiều thao tác khi thực thi câu lệnh này. bộ nhớ sẽ cần được cấp
phát để lưu trữ 1 chuỗi dài hơn, biến message sẽ cần được tham chiếu đến vùng nhớ mới, chuỗi thực sự cần được sao chép ..
Trong tính huống 1 câu lệnh đơn có thể được phiên dịch thành nhiều lệnh của mã máy , có thể xảy ra trường hợp time slice của
luồng đang xử lí các lệnh trên kết thúc. nếu điều này xảy ra , 1 luồng khác trong cùng tiến trình có thể nhận time slice và nếu việc
truy nhập vào biến có liên quan đến câu lệnh trên ( ví dụ như biến message ở trên ) không được đồng bộ, thì luồng khác có thể
đọc và viết vào cùng 1 biến , với ví dụ trên liệu luồng khác đó sẽ thấy giá trị mới hay cũ của biến message ?
thât may mắn là C# cung vấp 1 cách thức dễ dàng để giải quyết việc đồng bộ trong việc truy nhập biến bằng từ khóa lock :
lock (x)
{
DoSomething();
}

Câu lệnh lock sẽ bao 1 đối tượng gọi là mutual exclusion lock hay mutex .trong khi mutex bao 1 biến,thì không 1 luồng nào được
quyền truy nhập vào biến đó.trong đoạn mã trên khi câu lệnh hợp thực thi và nếu time slice của luồng này kết thúc và luồng kế
tiếp thử truy xuất vào biến x , việc truy xuất đến biến sẽ bị từ chối. thay vào đó window đơn giản đặt luồng đó ngủ cho đến khi
mutex được giải phóng.
Mutex là 1 cơ chế đơn giản được dùng để điều khiển việc truy nhập vào các biến.tất cả việc điều khiển này nằm trong lớp
System.Threading.Monitor . câu lệnh lock gồm 1 số phương thức gọi đến lớp này .
Các vấn đề đồng bộ
Việc đồng bộ các luồng là quan trọng trong các ứng dụng đa luồng . tuy nhiên có 1 số lỗi tinh vi và khó thăm dò có thể xuất hiện
cụ thể là deadlock và race condition
Deadlock
Deadlock là 1 lỗi mà có thể xuất hiện khi hai luồng cần truy nhập vào các tài nguyên mà bị khoá lẫn nhau. giả sử 1 luồng đang
chạy theo đoạn mã sau , trong đó a, b là 2 đối tượng tham chiếu mà cả hai luồng cần truy nhập :

lock (a)
{
// do something

lock (b)
{
// do something

}
}

Vào cùng lúc đó 1 luồng khác đang chạy :

lock (b)
{

// do something

lock (a)
{

// do something

}
}

Có thể xảy ra biến cố sau : luồng đầu tiên yêu cầu 1 lock trên a, trong khi vào cùng thời điểm đó luồng thứ hai yêu cầu lock trên
b. 1 khoảng thời gian ngắn sau , luồng a gặp câu lệnh lock(b) , và ngay lập tức bước vào trạng thái ngủ, đợi cho lock trên b được
giải phóng . và tương tự sau đó , luồng thứ hai gặp câu lệnh lock(a) và cũng rơi vào trạng thái ngủ chờ cho đến khi lock trên a
được giải phóng . không may , lock trên a sẽ không bao giờ được giải phóng bởi vì luồng đầu tiên mà đã lock trên a đang ngủ và
không thức dậy . cho đến khi lock trên b được giải phóng điều này cũng không thể xảy ra cho đến khi nào luồng thứ hai thức
dậy . kết quả là deadlock. cả hai luồng đều không làm gì cả, đợi lẫn nhau để giải phóng lock . loại lỗi này làm toàn ứng dụng bị
treo , ta phải dùng Task Manager để hủy nó .
Deadlock có thể được tránh nếu cả hai luồn yêu cầu lock trên đối tượng theo cùng thứ tự . trong ví dụ trên nếu luồng thứ hai yêu
cầu lock cùng thứ tự với luồng đầu , a đầu tiên rồi tới b thì những luồng mà lock trên a đầu sẽ hoàn thành nhiệm vụ của nó sau đó
các luồng khác sẽ bắt đầu.
Race condition
Race condition là 1 cái gì đó tinh vi hơn deadlock .nó hiếm khi nào dừng việc thực thi của tiến trình , nhưng nó có thể dẫn đến
việc dữ liệu bị lỗi .nói chung nó xuất hiện khi vài luồng cố gắng truy nhập vào cùng 1 dữ liệu và không quan tâm đến các luồng
khác làm gì để hiểu ta xem ví dụ sau :
Giả sử ta có 1 mảng các đối tượng, mỗi phần tử cần được xử lí bằng 1 cách nào đó , và ta có 1 số luồng giữa chúng làm tiến trình
naỳ .ta có thể có 1 đối tuợng gọi là ArrayController chứa mảng đối tưọng và 1 số int chỉ định số phẩn tử được xử lí .tacó phương
thức :
int GetObject(int index)
{

// trả về đối tượng với chỉ mục được cho

Và thuộc tính read/write


int ObjectsProcessed
{

// chỉ định bao nhiêu đối tượng được xử lí

Bây giờ mỗi luồng mà dùng để xử lí các đối tượng có thể thi hành đoạn mã sau :

lock(ArrayController)
{
int nextIndex = ArrayController.ObjectsProcessed;
Console.WriteLine("object to be processed next is " + nextIndex);
++ArrayController.ObjectsProcessed;
object next = ArrayController.GetObject();
}
ProcessObject(next);

Nếu ta muốn tài nguyên không bị giữ quá lâu , ta có thể không giữ lock trên ArrayController trong khi ta đang trình bày thông
điệp người dùng . do đó ta viết lại đoạn mã trên :
lock(ArrayController)
{
int nextIndex = ArrayController.ObjectsProcessed;
}
Console.WriteLine("object to be processed next is " + nextIndex);
lock(ArrayController)
{
++ArrayController.ObjectsProcessed;
object next = ArrayController.GetObject();
}
ProcessObject(next);

Ta có thể gặp 1 vấn đề . nếu 1 luồng lấy 1 đối tưọng ( đối tượng thứ 11 trong mảng) và đi tới trình bày thông điệp nói về việc xử
lí đối tượng này . trong khi đó luồng thứ hai cũng bắt đầu thi hành cũng đoạn mã gọi ObjectProcessed, và quyết định đối tượng
xử lí kế tiếp là đối tượng thứ 11, bởi vì luồng đầu tiên vẫn chưa được cập nhật .
ArrayController.ObjectsProcessed trong khi luồng thứ hai đang viết đến màn hình rằng bây giờ nó sẽ xử lí đối tượng thứ 11 ,
luồng đầu tiên yêu cầu 1 lock khác trên ArrayController và bên trong lock này tăng ObjectsProcessed. không may , nó quá trễ . cả
hai luồng đều đang xử lí cùng 1 đối tượng và loại tình huống này ta gọi là Race Condition

Code for Download:


ThreadPlayaround

ThreadPlayaroundWithPriorities

Tóm tắt

Trong chương này,ta đã xem xét 1 vài giao diện được cung cấp bởi các lớp cơ sở của .NET. ta đã thấy cách để khởi tạo và xử lí
chuỗi bằng cách dùng lớp StringBuilder, và cũng biết cách dùng các biểu thức regular để thực thi các tìm kiếm phứ ctạp trên
chuỗi.Sau đó ta xét tới các đối tượng collection khác nhau.System.Collections và namespace System.Collectionis.Specailized
chứa 1 số lượng lớn các lớp cho phép ta lưu trữ các kiểu khác nhau của các collection đối tượng. Lớp Hashtable cho phép viết
các mã lưu trữ dữ liệu trong từ điển với cách tìm kiếm dựa trên bất kì kiểu dữ liệu nào.

Các attribute tùy biến, được dùng kết hợp với reflection, cung cấp cho code khả năng mạnh để kiểm tra các code khác, hoặc thậm
chí là tự kiểm tra nó. Điều này nghĩa là sự thực thi tùy thuộc vào attribute tự tạo áp dụng đến các đối tượng trong mã của ta.Ta đã
xem qua ví dụ WhatsNewAttributes mả có khả năng cung cấp các báo cáo tự động những đặc tính mới được thêm vào phần
mềm.

Visual studio.NET
Visual studio.NET là một môi trường tích hợp triển khai phần mềm(Intergrated Development Environmet, IDE). Nó được thiết
kế để lập ra một tiến trình viết mã, gỡ rối, và biên dịch thành một assembly một cách dễ dàng. Visual studio.NET cho bạn một
ứng dụng multiple-document-interface rất tinh vi, trong đó bạn có thể liên kết mọi thứ để phát triển đoạn mã của bạn. Nó bao
gồm:

Một Text Editor : trong đó bạn có thể viết đoạn mã C#. Text e ditor này thì hơi phức tạp, và rất rành cú pháp C#. Tức là, khi bạn
gõ các câu lệnh vào, nó sẽ tự động bố trí đoạn của bạn, ví dụ như bằng cách thụt canh cột các dòng lệnh, cho khớp cặp dấu {}, và
tô màu những từ khoá. Ngoài ra, nó sẽ thực hiện kiểm tra vài cú pháp khi bạn gõ và sẽ gạch dưới những dòng mã bị sai. Nó còn
có thêm một chức năng đặc biệt là Intelliense, nó sẽ tự động hiển thị tên của các lớp, trường hay phương thức khi bạn bắt đầu gõ
chúng. Khi bạn bắt đầu đánh các tham số cho phương thức, nó sẽ hiển thị danh sách tham số. Màn hình bên dưới sẽ chỉ đặc trưng
này với một lớp cơ sở .NET là ListBox :

Một Design view editor, nó cho phép bạn đặt giao diện người dùng và các control dữ liệu truy cập trong dự án của bạn. Khi bạn
làm như vậy, Visual studio.NET sẽ tự động thêm những mã C# cần thiết cho tập tin nguồn của bạn để tạo những control này
trong dự án của bạn.

Các cửa sổ hỗ trợ cho phép bạn xem và sửa đổi những khía cạnh khác nhau trên dự án của bạn. Ví dụ có những cửa sổ cho bạn
thấy những lớp hình thành đoạn mã nguồn cũng như các thuộc tính trên các lớp Windown Form hoặc Web Form. Bạn cũng có
thể sử dụng những cửa sổ này để khai báo các tuỳ chọn biên dịch.

Biên dịch trong lòng môi trường: Để thay cho việc chạy trình biên dịch C# từ dòng lệnh, bạn có thể chọn một tuỳ chọn menu
để biên dịch và Visual Studio.NET sẽ gọi trình biên dịch cho bạn. Nó cũng có thể chạy một chương trình khả thi đã được biên
dịch, như vậy bạn có thể biết chương trình chạy tốt hay không, và bạn có thể chọn giữa hai cấu hình xây dựng chương trình khác
nhau : debug build hoặc release build.

Một Intergate Debugger hỗ trợ việc gỡ rối xuyên ngôn ngữ trong khuôn viên IDE. Ngoài ra bạn có thể gỡ rối trong một lúc
nhiều chương trình. Bạn có thể chỉnh sửa đoạn mã ngay trong Text editor Visual tsudio.NET để sữa chữa bug, rồi cho biên dịch
lại và cho chạy lại chương trình đã được sửa chữa ngay tại chỗ bỏ lở vì lỗi.

Intergated MSDN help Visual studio.NET có thể gọi tài liệu MSDN cho bạn. Ví dụ như khi bạn không biết ý nghĩa của một từ
khoá thì bạn chọn nó và nhấn F1 thì nó sẽ gọi MSDN lên để giải thích từ đó cho bạn.

Truy cập đến một chương trình khác: Nếu tất cả các tiện ích trên chưa đủ thì Visual studio.NET có thể gọi các tiện ích khác để
cho phép bạn kiểm tra và sửa đổi các khía cạnh khác của máy tính bạn hay mạng mà bạn không phải rời khỏi môi trường phát
triển. Giữa nhiều công cụ có sẳn, bạn có thể kiểm tra việc chạy các dịch vụ, và sự kết nối dữ liệu , và có một cửa sổ internet
explorer cho phép bạn lướt Web.

Chắc chắn rằng bạn đã có kinh nghiệm trong C++ hay VB trước khi bạn làm quen với phiên bản Visual studio.NET, do đó bạn
biết rằng nhiều chức năng ở trên không mới mẽ. Tuy nhiên những gì mới trong Visual studio.NET là nó liên kết tất cả chức năng
trong môi trường phát triển của VS 6. Có nghĩa là những ngôn ngữ gì bạn sử dụng trong VS6, bạn sẽ tìm thấy một vài chức năng
mới trong Visual Studio.NET.
Từ bất kỳ nền nào, bạn sẽ tìm thấy tầm nhìn tổng thể của môi trường phát triển đã thay đổi để điều tiết các chức năng mới, những
IDE xuyên ngôn ngữ đơn, và sự hợp nhất với .NET. Có nhiều menu tuỳ chọn và thanh công cụ tuỳ chọn mới, và nhiều tiện ích
khác trong VS6 đã được đổi tên. Vì thế bạn cần bỏ một khoảng thời gian để làm quen với việc trình bày và làm chủ trong Visual
studio.NET.

Mục đích chính của Visual studio.NET là bảo đảm bạn làm quen với tất cả các khái niệm về việc xây dựng và gỡ rối trong một
ứng dụng C# và có thể sử dụng các chức năng cụ thể, điều mà không thể làm được trong môi trường phát triển của bất kỳ ngôn
ngữ nào trước.

Màn hình sau sẽ thể hiện màn hình của Visual studio.NET khi bạn viết mã:

Tạo một dự án:


Khi bạn cài đặt Visual studio.NET, bạn sẽ muốn bắt đầu dự án đầu tiên. Với Visual studio.NET, ít khi bạn bắt đầu với một tâp tin
trắng mà bạn sẽ gõ vào đoạn mã C# từ điểm bắt đầu. Thay vào đó bạn sẽ cho Visual studio.NET biết loại dự án mà bạn muốn tạo,
sau đó nó sẽ tự động tạo ra một đoạn mã C# để tạo thành một sường outline cho dự án đó. Và bạn chỉ có việc là thêm mã của bạn
vào sường đó. Ví dụ, bạn muốn viết một ứng dụng trên Window GUI, thì Visual studio.NET sẽ bắt đầu tạo một tập tin chứa đoạn
mã nguồn C# cho phép tạo một form cơ bản. Form này có khả năng giao tiếp với Window, và tiếp nhận những tình huống. Nó
cho phép được thu nhỏ hoặc phóng to hoặc thay đổi kích thước ... Và lúc này bạn sẽ thêm các chức năng bạn mong muốn có đối
với biểu mẫu. Nếu ứng dụng của bạn được dùng theo kiểu dòng lệnh thì visual studio.NET sẽ cho bạn một namespace cơ bản,
lớp, và phương thức Main() để bắt đầu. Dĩ nhiên là nếu bạn muốn bắt đầu từ số không, thì Visual studio.NET cũng cho bạn chọn
một ứng dụng rỗng.

Khi bạn tạo ra một dự án, nó sẽ dàn dựng những tuỳ chọn biên dịch cần thiết để cung cấp cho trình biên dịch C# có hay không để
biên dịch một ứng dụng dòng lệnh, một thư viên, hay một ứng dụng Windown. Ngoài ra, những thư viện lớp cơ sở nào bạn sẽ
cần. Tuy nhiên bạn có thể sửa đổi những cài đặt này nếu bạn cần.

Khi bạn bắt đầu với visual studio.NET lần đầu tiên, bạn sẽ được thấy một start page. Đó là một trang HTML chứa các liên kết
khác nhau để dẫn bạn đến với các trang web hữu ích, cho phép bạn tạo dáng và cấu hình của Visual studio.NET, hoặc mở những
dự án hiện hữu hoặc khởi động dự án mới . Trên giữa màn hình start page, bạn thấy liệt kê môt số dự án mà bạn đã làm việc gần
đây nhất:
Chọn một kiểu dự án:
Bạn có thể tạo một dự án mới bằng việc click trên các liên kết thích hợp trên trang bắt đầu, hay click trên menu file chọn
New/project. Hay bạn có thể chọn các mẫu trong hộp dialog New project.

Các hộp Dialog này sẽ hỏi bạn muốn loại loại mã sường nào cần được tạo ra, và những tuỳ chọn biên dịch nào bạn muốn. Những
trình biên dịch nào bạn muốn biên dich mã của bạn: C#, VB.NET hay C++.

Tuy nhiên, trong sách này chỉ đưa ra những tuỳ chọn có thể ứng dụng trong dự án C#

Nếu bạn chọn .. Bạn sẽ lấy những mã C# và tuỳ chọn biên dịch được tạo ra

Windows Application Một mẫu trống cơ bản

Class Library Một lớp .NET có thể được gọi bởi các mã khác

Windows Control Library Một lớp .NET có thể được gọi bởi mã khác và có một giao diện người dùng

ASP.NET Web Application Một ASP.NET site web cơ sở : trang ASP.NET và lớp C# tạo ra HTML gửi đến trình duyệt từ
các trang này

ASP.NET Web Service Một lớp C# hành động như một Web service trọn vẹn

Web Control Library Một control có thể được gọi lên bởi trang ASP.NET, để tạo ra mã HTML đưa bề mặt của một
điều khiển khi thể hiện trên trình duyệt.

Console Application Một ứng dụng chạy tại chế độ dòng lệnh hay một của sổ console

Windows Service Một dịch vụ chạy trên nền Windows NT và Windows 2000

Empty Project Không có gì hết. Bạn phải viết tất cả

Empty Web Project Như empty project nhưng các cài đặt biên dịch được cài vào cấu trúc trình biên dịch đê tạo mã
cho trang AS.NET.

New Project In Existing Folder Những tập tin dự án mới cho một empty project

Các dự án console mới:


Khi ta click OK ở hộp dialog trên để tạo một ứng dụng console. Visual studio.NET sẽ cung cấp một số tập tin, bao gồm một tập
tin mã nguồn, class1.cs chứa mã sường khởi tạo.

Hình dưới sẽ chỉ cho ta thấy những mã mà visual studio.NET viết cho ta:
Ở đây chúng ta có một chương trình C# và chưa có ý nghĩa thực thi nhưng nó chứa những yêu cầu cơ bản của một chương trình
C# như là: một namespace, một lớp chứa phương thức main(). Đoạn mã này sẳn sàng để chạy nếu bạn nhấn f5 hay chọn
debug/start. Tuy nhiên, trước hết chúng ta phải thêm một dòng lệnh để chương trình bạn làm gì đó.

static void Main(string[] args)


{
//
// TODO: Add code to start application here
//
Console.WriteLine("Hello from all the editors at Wrox Press");
}

Nếu bạn biên dich hay chạy dự án, bạn sẽ thấy một cửa sổ console xuất hiện và biến mất chỉ cho bạn một thời gian nhỏ để xem
một tin nhắn. Nguyên nhân của vấn đề này là visual studio.NET nhớ những cài đặt của bạn khi tạo dự án và sắp xếp chúng lại để
biên dịch như một ứng dụng console và chạy ngay lập tức. Sau đó Windowns nhận ra rằng nó cần chạy một ứng dụng console
nhưng không có một cửa sổ console để chạy. Vì thế, Windows đã tạo ra một cửa sổ console và chạy chương trình. Ngay khi
chương trình vừa thoát thì Windowns thấy nó không cần cửa sổ console nữa và huỷ nó ngay. Đó là tất cả tính logic của chương
trình nhưng nó không thực sự giúp ta nhiều nếu ta muốn nhìn thấy kết quả của dự án.

Một cách tốt để giải quyết vấn đề này là chèn thêm một dòng lệnh Console.Readline() trước khi rời khỏi phương thức main():

static void Main(string[] args)


{
//
// TODO: Add code to start application here
//
Console.WriteLine("Hello from all the editors at Wrox Press");
Console.ReadLine();
}

Theo cách này, chương trình của bạn sẽ chạy và hiển thị kết quả. Sau đó nó đọc dòng lệnh Console.Readline() và đợi một phím
để kết thúc chương trình.

Thực thi chương trình trên

Tạo những tập tin khác:


Tập tin mã nguồn class1.cs không phải là tập tin duy nhất mà visual studio.NET tạo ra cho bạn. Nếu nhìn vào trong một thư mục
nơi mà bạn yêu cầu Visual studio.NET tạo dự án của bạn thì bạn sẽ không thấy những tập tin C# mà là một cấu trúc thư mục như
sau:
Hai thư mục bin và obj chứa những tập tin biên dịch và trung giang. Những thư mục con của obj sẽ chứa những tập tin tạm thời
và trung giang sẽ được tạo ra, còn thư mục con của bin chứa assembly được biên dịch.

Những tập tin còn lại trong thư mục main của dự án, baisicConsoleApp, chứa thông tin về dự án và những tập tin bên trong nó.
Như vậy, Visual studio.NET sẽ biết cách phải biên dịch dự án như thế nào, và cách để đọc lại nó trong lần tiếp theo khi mở dự
án.

Giải pháp(solution) và dự án(project):


Điểm quan trọng ở đây là bạn cần phân biệt giữa giải pháp và dự án:

Một dự án: là một tập hợp các tập tin mã nguồn và tài nguyên(resource) sẽ được biên dịch đến một asembly đơn. Ví dụ như một
dự án phải là một thư viện lớp hay ứng dụng Window GUI.

Một giải pháp: là một tập hợp tất cả các dự án để tạo thành một gói phần mềm cụ thể.

Muốn thấy sự khác biệt, chúng tôi thử lấy ví dụ khi bạn phân phối một ứng dụng cho một người sử dụng, có thể nó gồm nhiều
assembly hơn là chỉ là một. Thí dụ, có thể là một giao diện người dùng; nó sẽ có vài control hay những thành phần khác được gữi
đi như là những thư viện thuộc các phần của ứng dụng. Kể cả việc có thể có một giao diện khác dành cho những ngừơi quản lý.
Mỗi một thành phần này có thể được chứa thành những assembly riêng lẽ, và do đó đối với Visual studio.NET như là một dự án
riêng biệt. Tuy nhiên, có thể là bạn thực hiện những dự án này song hành và phối hợp với nhau giữa các dự án. Do đó, xem ra
tiện lợi khi có khả năng chỉnh sửa chúng như một đơn vị duy nhất trong visual studio.NET. Visual studio.NET cho phép điều này
bằng cách xem tất cả dự án như là một giải pháp, và đối xử giải pháp như là một đơn vị được đọc vào và cho phép bạn làm việc
trên đó.

Đến bây giờ, ta khá lỏng lẻo về việc tạo một dự án.Trên thực tế, ví dụ ta đang làm việc, Visual studio.NET có một giải pháp cho
chúng ta- một console đặc biệt chỉ chứa duy nhất một dự án. Chúng ta có thể thấy tình trạng này trên một cửa sổ của Visual
studio.NET là Solution Explorer, nơi chứa một cấu trúc cây định nghĩa giải pháp của bạn.

Màn hình trên chỉ dự án chứa tập tin nguồn, class1.cs, và tập tin khác, assemblyInfo.cs chứa thông tin mô tả những assembly và
thông tin đến việc biên dịch dự án. Solution explorer cho biết những assembly mà dự án tham khảo về.

Nếu bạn thay đổi các cài đặt bạn sẽ tìm thấy solution Explorer trong góc phải trên đầu màn hình. Nếu không thấy bạn vào
view/Solution Explorer để hiện nó.

Các giải pháp được mô tả bởi một tập tin với đuôi .sln. Vì thế trong trường hợp trên nó là BasicConsoleApp.sln. Dự án sẽ được
mô tả bởi các tập tin khác nhau trong thư mục main của dự án. Nếu bạn cố sửa những tập tin này thì bạn sẽ thấy chúng toàn là
những tập tin plain text .
Thêm dự án vào một giải pháp:
Chúng tôi muốn cho bạn thấy Visual studio.NET hoạt động như thế nào với ứng dụng Window cũng như với ứng dụng Console.

Chúng tôi sẽ tạo ra một dự án Window mang tên BasicWindow, nhưng thay vì đưa vào một giải pháp mới, ta yêu cầu Visual
studio.NET đưa nó vào giải pháp hiện hành BasicConsoleApp. Nghĩa là ta sẽ có một giải pháp với một ứng dụng Window và một
ứng dụng console. Đấy là cách tạo một trình tiện ích mà bạn muốn chạy trên Window hoặc chạy trên dòng lệnh command line.

Có hai cách để thực hiện điều này. Cách thứ nhất là click phải lên tên của giải pháp trên cửa sổ solution explorer, để cho hiện lên
trình đơn shortcut rồi chọn add/new project. Cách thứ 2 là ra lệnh file/new/new project. Cả hai cách điều hiện lên hộp thoại New
Project như sau:

Trên cửa sổ solution explorer này ta có thể thấy Visual studio.NET tự động đưa một số tham khảo về các lớp cơ bản quan trọng
đối với chức năng của biểu mẫu window nằm trong các spacename System.Drawing, System.Windows.Forms.

Ngoài ra, bạn cũng sẽ thấy là tên của tập tin giải pháp cũng đổi thành DemoSolution.sln. Nói chung, nếu bạn muốn thay đổi bất
cứ tên tập tin nào thì solution explorer là nơi tốt nhất để tiến hành những thay đổi, vì Visual studio.NET sẽ tự động cho cập nhật
bất cứ những tham khảo nào về tập tin trong tất cả các tập tin thuộc dự án. Bạn không nên sử dụng Window Explorer để thay đổi
tập tin dự án vì nó sẽ phá vỡ giải pháp.

Cài đặt dự án startup


Một điểm bạn nên nhớ là cho dù bạn có nhiều dự án trong cùng một giải pháp, thì chỉ một trong những dự án này chạy trong một
lúc. Khi bạn cho biên dịch một giải pháp, thì tất cả cá dự án trong giải sẽ được biên dịch. Tuy nhiên, bạn phải khai báo trong
visual studio.NET biết dự án nào sẽ bắt đầu chạy khi bạn muốn gỡ rối chương trình. Nếu bạn có một EXE và nhiều thư viên mà
EXE sẽ gọi thì đương nhiên EXE sẽ là dự án khởi động. Trong trường hợp của chúng ta, ta có hai EXE độc lập, ta chỉ cần lần
lượt gỡ rối từng dự án.

Bạn có thể bảo Visual studio.NET dự án nào phải chạy trước, bằng cách click phải lên tên giải pháp để hiện lên trình đơn
shortcut, rồi bạn chọn click mục set startup project để cho hiện lên khung đối thoại solution "DemoSolution" Property Pages.
Bạn có thể cho biết dự án startup hiện hành, vì nó sẽ là dự án hiện lên in đậm trên cửa sổ Solution Explorer.

Đoạn mã ứng dụng Window:


Một ứng dụng Window chứa đoạn mã khởi động phức tạp hơn nhiều so với một ứng dụng chạy trên console, vì tạo một cửa sổ là
một tiến trình phức tạp. Chúng tôi sẽ không đề cập chi tiết đến đoạn mã của một ứng dụng window. Sẽ có một chương dành cho
vấn đề này. Trong tạm thời, chúng tôi cho in ra ở đây bảng liệt kê đoạn mã kết sinh bởi visual studio.NET đối với dự án
BasicForm. Bạn để ý ở đây được gọi là Form1, tượng trưng cho cửa sổ chính.

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace BasicForm
{
/// <summary>
/// Summary description for Form1.
/// </summary>
public class Form1 : System.Windows.Forms.Form
{
private System.Windows.Forms.TextBox textBox1;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;

public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Windows Form Designer generated code


/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.textBox1 = new System.Windows.Forms.TextBox();
this.SuspendLayout();
//
// textBox1
//
this.textBox1.Location = new System.Drawing.Point(8, 8);
this.textBox1.Name = "textBox1";
this.textBox1.TabIndex = 0;
this.textBox1.Text = "textBox1";
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 273);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.textBox1});
this.Name = "Form1";
this.Text = "Basic Form - Hello!";
this.ResumeLayout(false);

}
#endregion

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
}
}

Đọc vào các dự án Visual studio.NET 6


Nếu bạn lập trình theo C#, rõ ràng là bạn không cần đọc đến những dự án cũ xưa viết trên Visual studio 6, vì C# không có trên
Visual studio 6. Tuy nhiên, liên thông ngôn ngữ là phần chủ chốt của .NET Framework, do đó có thể bạn muốn đoạn mã C# của
bạn làm việc chung với VB.NET hoặc C++. Trong tình trạng như vậy, có thể bạn cần chỉnh sửa những dự án được tạo ra trong
Visual studio 6.

Khi đọc vào những dự án và Workspace viết theo Visual Studio 6, Visual studio.NET sẽ cho nâng cấp lên thành những giải pháp
visual studio.NET. Tình trạng lại khác so với các dự án C++, VB hoặc J++:

• Ví dụ như trên C++, không cần thiết thay đổi đối với mã nguồn. Tất cả các chương trình C++ cũ xưa sẽ còn hoạt động
tốt với trình biên dịch C++ mới. Rõ ràng là không phải đoạn mã được quản lý, nhưng vẫn biên dịch chạy ngoài .NET
runtime. Nếu bạn yêu cầu Visual studio.NET đọc vào một dự án cũ xưa C++, thì nó đơn giản thêm vào một tập tin
solution mới và cập nhật những tin dự án. Nó để yên các tập tin .dsm và .dsp không thay đổi để dự án có thể chỉnh sửa
bởi Visual studio 6 nếu thấy cần thiết.
• Đối với Visual basic thì có vấn đề, vì nó đã được thay thế bởi VB.NET. Trên VB6, mã nguồn phần lớn bao gồm những
bộ điều khiển sự kiện đối với những control . Đoạn mã hiện lo việc hiển lộ cửa sổ chính và tất cả control trên ấy, không
phải là thành phần của VB, nhưng lại nằm ẩn ở bên dưới như là thành phần cấu hình của dự án. Ngược lại, VB.NET
hoạt động giống C#, bằng cách trưng ra toàn bộ chương trình mở toang như là mã nguồn, do đó tất cả đoạn mã sẽ hiển
thị cửa sổ chính và tất cả các ô control trên ấy, đều nằm trong tập tin mã nguồn. Giống C#, VB .NET đòi hỏi mọi việc
phải thiên đối tượng và thuộc lớp, trong khi ấy VB 6 không công nhận khái niệm lớp như đúng ý nghĩa của nó. Nếu bạn
cố thử đọc một dự án VB6 với Visual studio .NET nó sẽ nâng cấp toàn bộ mã nguồn của VB6. Visual studio.NET cũng
có thể tự động thực hiện những thay đổi này và tạo một giải pháp VB.NET mới và lúc này nó sẽ khác nhiều so với mã
nguồn VB6 bị chuyển đổi và bạn sẽ phải kiểm tra để đảm bảo dự án vẫn hoạt động đúng đắn. Có nhiều khúc trên đoạn
mã Visual studio.NET còn ghi chú những chú giải khi nó không biết sẽ làm gì với đoạn mã này, và buộc lòng bạn phải
chỉnh sửa bằng tay.

Khảo sát và viết đoạn mã một dự án:


Folding editor:
Điều khá lý thú trên Visual studio.NET là việc sử dụng một folding editor như là code editor mặc nhiên. Bạn thử nhìn xem hình
sau đây, cho thấy đoạn mã kết sinh bởi ứng dụng console.

Bạn có nhận thấy ở phía tay trái màn hình, những ô vuông nhỏ có dấu trừ (-) ở trong; Những ký hiệu này cho biết một khối lệnh
bắt đầu, bạn có thể click lên ký hiệu này, nó biến thành dấu cộng (+), và khối lệnh teo lại vói một ô hình chữ nhật với 3 dấu
chấm.
Đây có nghĩa là khi bạn đang chỉnh sửa, bạn có thể tập trung chỉ vào vùng nào bạn quan tâm, cho đóng lại những phần đoạn mã
nào bạn không quan tâm. Không chỉ thế, nhưng nếu bạn không hài lòng cách editor chọn phần thành khối lệnh, bạn có thể cho
biết một cách khác bằng cách sử dụng những chỉ thị tiền xử lý C#, chẳng hạn #region và #endregion. Thí dụ, ta quyết định ta
muốn có khả năng cho teo lại đoạn nằm trong phương thức Main(), ta chèn thêm #region và #endregion như hình dưới:

Code editor sẽ tự động phát hiện khối #region và cho đặt dấu trừ ngay hàng có chỉ thị #region. Cho bao một khối lệnh nằm giữa
cặp chỉ thị #region và #endregion có nghĩa là ta muốn cho teo lại tuỳ ý khối lệnh này, như hình dưới. Bạn thấy dấu cộng với chú
giải ta thêm vào.
Các cửa sổ khác
Ngoài code editor, visual studio.NET còn cung cấp một số cửa sổ khác giúp bạn nhìn xem dự án theo nhiều góc độ khác nhau.

Cửa sổ Design view

Khi bạn thiết kế một ứng dụng Window, một cửa sổ mà bạn sử dụng nhiều nhất là Design view. Cửa sổ cho phép bạn hình dung
toàn bộ mặt mũi ứng dụng của bạn có thể nhìn thấy được. Thông thường, bạn sử dụng phối hợp cửa sổ design view với cửa sổ
Toolbox. Toolbox chứa vô số thành phần .NET mà bạn có thể lôi thả dễ dàng lên mặt bằng form của bạn.

Bạn có thể thêm riêng của bạn một loại control nào đó, gọi là custom control, vào toolbox, bằng cách click phải lên bất cứ loại
nào trên toolbox rồi chọn mục Add Tab từ trình đơn shortcut. Bạn có thể thêm công cụ khác vào toolbox bằng cách chọn mục
customize Toolbox cũng trên trình đơn shortcut này. Điều này rất hữu ích khi bạn muốn thêm những thành phần COM hoặc
ActiveX control không có trên toolbox theo mặc định.

Nếu bạn muốn thêm một thành phần COM lên dự án bạn có thể click thành phần này lôi thả lên form giống như bạn đã làm với
các ô control .NET. Lúc này visual studio.NET sẽ tự động thêm đoạn mã liên thông COM cần thiết cho phép dự án có thể gọi
hàm control COM.

Bây giờ bạn muốn thấy toolbox hoạt động thế nào. Bạn muốn đặt một textbox lên dự án BasicForm. Bạn cho hiện lên Toolbox
bằng cách click lên icon hoặc ấn Ctrl+Alt+X, hoặc ra lệnh View/Toolbox. Rồi bạn click tiếp lên biểu mẫu đang ở chế độ Design.
Bây giờ biểu mẫu mang dáng dấp như sau
Tuy nhiên, điểm lý thú là nếu ta nhìn vào đoạn mã, ta thấy IDE đã thêm đoạn mã khởi tạo một đối tượng textbox được đặt lên
biểu mẫu. Có thêm một biến thành viên trên lớp Form1:

public class Form1 : System.Windows.Forms.Form


{
private System.Windows.Forms.TextBox textBox1;

Ngoài ra, một đoạn mã được thêm vào phương thức InitializeComponent(), khởi gán thành phần. Hàm này sẽ được gọi bởi
contructor của Form1:

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.textBox1 = new System.Windows.Forms.TextBox();
this.SuspendLayout();
//
// textBox1
//
this.textBox1.Location = new System.Drawing.Point(8, 8);
this.textBox1.Name = "textBox1";
this.textBox1.TabIndex = 0;
this.textBox1.Text = "textBox1";
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 268);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.textBox1});

Nếu muốn, bạn cũng có thể thêm bằng tay đoạn mã như trên vào tập tin mã nguồn, và Visual studio.NET sẽ phát hiện ra đoạn mã
của bạn và cho hiện lên control tương ứng. Tuy nhiên, tốt hơn hết là thêm bằng mắt những control trên rồi để cho Visual
studio.NET lo phần còn lại là thêm những đoạn mã thích hợp. Việc click vào toolbox rồi click tiếp lên biểu mẫu, điều chỉnh vị trí
và kích thước các control này xem ra là nhanh hơn là suy nghĩ và gõ vào các lệnh.

Một lý do là nên thêm bằng mắt các control là vì Visual studio.NET yêu cầu những đoạn mã thêm vào bằng tay phải tuân thủ một
số qui tắc, mà có thẻ bạn không tuân thủ. Đặc biệt, bạn để ý trên phương thức InitializeComponent() lo khởi gán textbox có dòng
chú giải ở đầu cảnh cáo bạn là chớthay đổi. Nói thế, nhưng nếu cẩn thận, bạn cũng có thể hiệu chỉnh chẳng hạn giá trị của vài
thuộc tính như vậy control có thể hiển thị khác đi. Nói tóm lại, muốn tiến hành những thay đổi như thế, bạn phải có kinh nghiệm
dày dạn và phải cẩn thận hiểu mình muốn gì.

Xem toàn bộ mã của BasicForm Thực thi chương trình BasicForm

Cửa sổ properties
Một cửa sổ khác, được gọi là Properties, xuất xứ từ IDE của VB. Chúng ta biết rằng cửa sổ properties hiển thị và cho phép bạn
sửa đổi hầu hết mọi giá trị khởi tạo của các thuộc tính đối với những control mà Visual studio.NET có khả năng phát hiện bằng
cách đọc mã nguồn của bạn.

Cửa sổ Properties cũng có thể thấy những sự kiện. Bạn có thể nhìn thấy sự kiện bằng cách click lên icon giống như một tia sáng ở
trên đầu cửa sổ.

Trên đầu cửa sổ Properties là một list box cho phép bạn chọn control nào để quan sát hoặc chỉnh sửa. Chúng tôi chọn Form1, lớp
form chính trong dự án BasicForm, và cho chỉnh sửa thuộc tính text " Basic Form - SAMIS hello". Nếu bạn cho kiểm tra lại mã
nguồn, bạn có thể thấy hiện chúng ta đã chỉnh sửa mã nguồn, thông qua giao diện thân thiện hơn:

this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);


this.ClientSize = new System.Drawing.Size(292, 268);
this.Controls.AddRange(new System.Windows.Forms.Control[] {this.textBox1});
this.Name = "Form1";
this.Text = "Basic Form - Hello!";

Không phải tất cả các thuộc tính mà bạn thấy trên cửa sổ Properties sẽ được ghi rõ trên mã nguồn. Rõ ràng khi bạn thay đổi trị
của một thuộc tính nào đó trên cửa sổ Properties thì một câu lệnh cài đặt rõ ra thuộc tính này sẽ xuất hiện lên mã nguồn của bạn
và ngựơc lại

Cửa sổ Properties được xem là cách tiện lợi nhất để có cái nhìn về tất cả các thuộc tính của một ô control nào đó.

Cửa sổ Class view

Khác với cửa sổ Properties, cửa sổ Class view xuất xứ từ môi trường triển khai Visual studio.NET, cửa sổ class view được xem
như là trang tab của cửa sổ Solution Explorer, cho thấy cây đẳng cấp của các namespace và các lớp trong đoạn mã của bạn. Nó
cho một tree view mà bạn có thể bung ra để xem namespace nào chứa những lớp nào, và trong lớp chứa những thành viên nào.
Một chức năng hay của class view là nếu bạn click phải lên tên của bất cứ mục tin nào bạn có thể thâm nhập vào đoạn mã nguồn,
bạn chọn mục Go To Definition, trên trình đơn shortcut, thì trên code editor sẽ hiện lên đoạn mã định nghĩa mục tin này. Hoặc
bạn double click lên mục tin bạn cũng được kết quả như trên. Nếu bạn muốn thêm một phần tử cho Form : Bạn click phải trên
tên form sau đó chọn add phần tử bạn muốn thêm(Add Method, Add Property, Add Indexer hoặc Add Field.

Pin Buttons
Khi khảo sát Visual studio.NET, có thể bạn nhận thấy có nhiều cửa sổ mà chúng tôi mô tả có vài chức năng mang dáng dấp của
những thanh công cụ. Đặc biệt, ngoại trừ code editor, thì các cửa sổ có thể docking. Có một icon pin cạnh nút minimize nằm ở
góc phải phía trên mỗi cửa sổ. Icon này khi chúc đầu xuống, thì cửa sổ này hoạt đọng như một cửa sổ bình thường mà bạn đã
quen. Nhưng khi icon này nằm ngang, thì cửa sổ này chỉ hiện lên khi nó có focus. Nhưng khi nó mất focus, nghĩa là con trỏ chỉ
vào chỗ khác, thì cửa sổ biến mất, thu mình lại qua phía tay phải hoặc tay trái.

Xây dựng một dự án


Building, Compiling và Making
Trước khi ta đi sâu vào việc xem xét những lựa chọn khác nhau liên quan đến việc xây dựng một dự án, thiết tưởng ta nên làm
sáng tỏ những từ ngữ liên quan đến tiến trình biến mã nguồn của bạn thành một loại đoạn mã khả thi. Bạn thường biết đến 3 từ:
compiling, building, making. Nguyên nhân của những từ ngữ khác nhau là do việc chuyển một mã nguồn thành một mã khả thi
đòi hỏi không chỉ một bước mà thôi. Trên C++ chẳng hạn mỗi tập tin nguồn phải được biên dịch riêng lẽ, cho ra một tập tin đối
tượng, mang dáng dấp đoạn mã khả thi nhưng mỗi tập tin đối tượng chỉ liên hệ với một tập tin nguồn. Muốn cho ra một mã kết
sinh các tập tin đối tượng này phải liên kết lại một tiến trình mà người ta gọi là linking. Tiến trình phối hợp thường gọi là xây
dựng (building) đoạn mã theo từ ngữ windows. Tuy nhiên, theo từ ngữ C#, thì trình biên dịch phức tạp hơn nhiều có khả năng
đọc và xử lý tất cả các tập tin nguồn như một khối. Do đó, đối với C# không có giai đoạn liên riêng lẽ nên trong phạm trù C# các
từ compiling và building coi như giống nhau.

Còn từ making cũng có nghĩa là xây dựng nhưng nó không được dùng trong phạm trù C#. Từ này xuất xứ từ những máy tính
mainframe, cho biết khi một dự án gồm nhiều tập tin nguồn, thì có một tập tin riêng lẽ được viết ra chứa những chỉ thị dành cho
trình biên dịch biết cách xây dựng một dự án. Tập tin riêng rẽ này được gọi là make file, do đó từ này tiếp tục được dùng như là
chuẩn trên Unix hoặc Linux. Các tập tin make thường không cần đến trên Windows, mặc dù bạn có thể viết chúng hoặc yêu cầu
Visual Studio.NET kết sinh chúng nếu bạn muốn.

Debug build và Release Build


Khi bạn gỡ rối chương trình, bạn thường muốn chương trình khả thi có một cách ứng xử khác đi khi bạn cho phân phối chương
trình. Khi phân phối chương trình ngoại trừ việc chương trình phải chạy tốt không có lỗi, nó còn phải có kích thước càng nhỏ
càng tốt và còn phải chạy thật nhanh. Rất tiết là những đòi hỏi trên không hợp với nhu cầu gỡ rối chương trình, với nhiều lý do
sau đây.

Tối ưu hoá

Hiệu năng cao của đoạn mã phần lớn là do việc trình biên dịch đã tối ưu hoá đoạn mã. Đây có nghĩa khi biên dịch, trình biên dịch
sẽ nhìn vào mã nguồn xem có thể nhận diện những đoạn mà nó có thể thay thế bởi một đoạn mã khác cho ra kết quả y chang
nhưng tối ưu hơn, chạy hiệu quả hơn.
double InchesToCm(double Ins)
{
return Ins*2.54;
}

// later on in the code

Y = InchesToCm(X);

Trình biên dịch có thể thay thế câu lệnh trên bởi:

Y = X * 2.54;

Hoặc chẳng hạn trình biên dịch gặp phải đoạn mã như sau:

{
string Message = "Hi";
Console.WriteLine(Message);
}

Thì nó thay thế bởi

Console.WriteLine("Hi");

Như vậy, ta có thể tiết kiệm những nơi nào có những khai báo tham khảo đối tượng một cách không cần thiết.

Các ký hiệu Debugger

khi bạn gỡ rối chương trình, thường bạn cần xét đến trị của những biến mà bạn sẽ khai báo chúng theo tên trên mã nguồn. Khổ
nỗi là chương trình khả thi EXE lại không chứa tên biến, vì trình biên dịch đã cho thay thế bởi những vị chỉ ký ức. .NET đã thay
đổi tình trạng trên bằng cách cho trữ vài tên trên assembly nhưng chỉ một số nhỏ liên quan đến các lớp và phương thức.

Các chỉ thị gỡ rối extra trên mã nguồn

Khi bạn đang gỡ rối chương trình, trong đoạn mã của bạn sẽ có phụ thêm những dòng lệnh cho phép hiển thi những thông tin cốt
tử liên quan đến gỡ rối. Lẽ dĩ nhiên là trước khi gữi đi phân phối chương trình của bạn, bạn muốn cho gỡ bỏ những dòng lệnh
extra này. Bạn có thể làm điều này bằng tay nhưng tốt hơn là bạn cho đánh dấu những dòng lệnh này làm thế nào trình biên dịch
sẽ bỏ qua khi biên dịch sẽ bỏ qua khi biên dịch đoạn mã cần được gửi đi. Chúng ta đã biết qua phần tiền chỉ thị (preprocessor
directive), các chỉ thị này có thể dùng phối hợp với thuộc tính Conditional, xem như là điều kiện biên dịch.

Cuối cùng bạn thấy là trình biên dịch một phiên bản dùng cho ra một sản phẩm phần mềm khác so với phiên bản đang được gỡ
rối. Visual studio.NET thực hiện việc này bằng cách trữ nhiều chi tiết hơn để có thể hỗ trợ hai loại xây dựng khác nhau. Những
bộ chi tiết khác nhau về thông tin xây dựng đựoc gọi là Configuration. Khi bạn tạo một dự án mới, Visual studio.NET sẽ tự động
tạo cho bạn hai cấu hình được cho mang tên là Debug và Release :

• Debug configuration: thường sẽ cho biết cho cần tối ưu hóa, thông tin gỡ rối extra sẽ hiệu sẽ chỉnh sửa trên đoạn mã
khả thi, và trình biên dịch giả định là chỉ thị tiền xử lý debug hiện diện trừ khi nó được ghi rõ ra là #undefined trong mã
nguồn.
• Release configuration: thường cho trình biên dịch biết là phải tối ưu hoá và như vậy sẽ không có những thông tin extra
liên quan đến gỡ rối và trình biên dịch không được giả định là bất cứ chỉ thị tiền xử lý hiện diện trong đoạn mã .

Chọn cấu hình


Một câu hỏi được đặt ra là visual studio.NET trữ những chi tiết trên hai cấu hình, thế thì cấu hình nào sẽ được chọn khi xây dựng
một dự án. Câu trả lời là bao giờ cũng có một cấu hình hiện dịch mà Visual studio.NET phải dùng đến. Theo mặc nhiên, khi bạn
tạo một dự án, cấu hình debug bao giờ cũng là cấu hình hiện dịch. Bạn có thể thay đổi cấu hình hiện dịch bằng cách ra lệnh
Build/Configuration Manager... để hiện lên khung đối thoại Configuration Manager, rồi chọn mục Debug hoặc Release trên ô
liệt kê Active solution Configuration hoặc trên thanh công cụ chính có ô liệt kê cho phép bạn chọn debug hoặc Release hoặc
Copnfiguration Manager.

Hiệu đính cấu hình


Chọn dự án muốn chỉnh sửa trên cửa sổ Solution explorer, rồi ra lệnh project / Properties, hoặc click phải trên dự án để hiện lên
trình đơn short cut, rồi bạn chọn mục properties. Lúc này khung đối thoại Properties pages hiện lên.
Khung đối thoại này chứa một tree view phía tay trái cho phép bạn lựa chọn khá nhiều vùng khác nhau để quan sát hoặc chỉnh
sửa. Chúng tôi không thể cho thấy tất cả các lĩnh vực nhưng chúng ta sẽ cho thấy vài lĩnh vực quan trọng.

Ở hình trên cho thấy hai mắt nút cấp cao: Common properties và Configuration properties. Ta thấy Common properties/General
đối với dự án Basic ConsoleApp. Bạn có thể chọn tên assembly cần được kết sinh. Mục chọn Output type ở đây là: Console
application, window application và class library. Bạn có thể thay đổi loại assembly nếu bạn muốn nhưng xem ra hơi vô lý, vì lúc
ban đầu bạn đã chọn rồi trên khung đối thoại New Project.

Hình bên dưới cho thấy cấu hình xây dựng :

Trên đầu khung đối thoại, bạn thấy có một list box " Configuration" cho phép bạn khai báo cấu hình nào bạn muốn quan sát.
Trong trường hợp cấu hình Debug, ta có thể thấy là những chỉ thị tiền xử lý DEBUG và TRACE được giả định là có, việc tối ưu
hoá không được thực hiện và những thông tin gỡ rối sẽ được kết sinh.

Nhìn chung, sở dĩ chúng tôi đi vào chi tiết liên quan đến cấu hình, nhưng phần lớn trưòng hợp ít khi bạn phải điều chỉnh chúng.
Tuy nhiên bạn phải hiểu để chọn đúng cấu hình tuỳ theo việc bạn xây dựng thế nào dự án của bạn và cũng là điều bổ ích khi biết
tác dụng của những cấu hình khác nhau.

Gỡ rối chương trình


Trên C#, cũng như trên các ngôn ngữ đi trước .NET, kỹ thuật chính trong việc gỡ rối chuơng trình là đơn giản đặt những chốt
dừng, rồi sử dụng chúng để xem xét việc gì xảy ra trong đoạn mã của bạn tại một điểm nào đó trong khi thi hành chương trình.

Các chốt ngừng:


Bạn có thể đặt các chốt ngừng từ Visual studio.NET lên bất cứ dòng nào trên đoạn mã hiện đang được thi hành. Cách đơn giản
nhất là click lên dòng lệnh trên code editor, trên biên trái màu xám. Dòng lệnh đổi màu với một chấm tròn ở biên trái. Lúc này
một chốt ngừng được đặt để trên hàng này, làm cho việc thi hành sẽ tạm ngừng khi Debugger đạt đến hàng này và chuyển quyền
điều khiển cho debuger. Nếu bạn click trên chấm tròn của dòng lệnh này thì xem như chốt ngừng bị gỡ bỏ, dòng lệnh trở lại màu
bình thường của văn bản.
Các cửa sổ quan sát
Khi một chốt ngừng đã đạt đến, thường thì bạn muốn khảo sát. Lúc này một ô hình chữ nhật màu vàng hiện lên cho biết trị của
biến. Tuy nhiên, có thể bạn lại muốn sử dụng của sổ Watch window để xem nội dung của các biến cửa sổ dạng thẻ. Cuối màn
hình code editor là một loạt thẻ các cửa sổ Local, Auto, Watch, Call stack, Breakpoint, Command, Output ... Các cửa sổ này chỉ
hiện lên khi chương trình đang chạy dưới sự điều khiển của Debugger.

Có 3 loại cửa sổ kiểu tab dùng điều khiển những biến khác nhau:

Autos: cho biết tình trạng của một vài biến được truy xuất khi chương trình đang thi hành.

Locals: Cho biết tình trạng của biến được truy xuất trong phương thức đang thực thi

Watch : cho biết tình trạng của bất cứ biến nào mà bạn đã khai báo rõ ra bằng cách gõ vào tên biến trong cửa sổ Watch.

Biệt lệ
Biệt lệ cho phép bạn thụ lý cách thích ứng những điều kiện sai lầm xảy ra trong ứng dụng mà bạn muốn phân phối. Nếu được sử
dụng đúng đắn, biệt lệ bảo đảm ứng dụng của bạn kiểm soát tốt hoạt động và người sử dụng không phải phiền lòng khi nhận
những khung đối thoại mang tính quá kỹ thuật. Tuy nhiên, điều phiền toái là khi gỡ rối, biệt lệ gây không ít khó khăn cho bạn. Có
hai vấn đề:

Nếu biệt lệ xảy ra, thì liền sau đó khi bạn đang gỡ rối thường thì bạn không muốn biệt lệ được tự động thụ lý bởi chương trình
của bạn. Thay vào đó, bạn muốn Debugger nhập cuộc để xem ra vì sao biệt lệ xảy ra, để có thể gỡ bỏ lý do sai lầm trước khi phân
phối sản phẩm.

Nếu một biệt lệ xảy ra mà bạn chưa hề viết một hàm thụ lý biệt lệ này, thì .NET runtime sẽ đi tìm tiếp một hàm thụ lý biệt lệ. Tuy
nhiên, đến khi .NET Runtime nhận ra là không có hàm nào, nó sẽ cho chấm dứt chương trình. Và lúc này, cửa sổ Call Stack cũng
sẽ biến mất không còn gì để bạn có thể xem xét trị của các biến, vì chúng đã ra ngoài phạm vi.

Những công cụ .NET khác:


Chúng ta mất khá nhiều thời giờ khảo sát Visual studio.NET, vì đây là công cụ mà bạn phải sử dụng nhiều trong suốt thời gian
triển khai hệ thống phần mềm của bạn. Tuy nhiên, có một số công cụ khác giúp bạn trong việc lập trình. Ở đây chúng tôi đề cập
đến một tiện ích đó là WinCV, bạn có thể sử dụng để lướt qua các lớp cơ bản.

WinCV là một trình tiện ích mà Microsoft cung cấp cho phép bạn khảo sát các lớp cơ sở và xem các phương thức nào có sẳn. Nó
cũng tương tự như Object Browser của Visual studio.NET, ngoại trừ nó là một ứng dụng hoàn toàn độc lập, và nó cho thấy tất cả
các lớp cơ bản, trong khi Object Browser chỉ cho thấy những lớp trong assembly mà dự án của bạn tham khảo về.

Sử dụng WinCV cũng khá dễ dàng, mở cửa sổ command line và đánh Wincv. Khi nó chạy, bạn đánh văn bản vào listbox gần
đỉnh của cửa sổ Wincv. Khi bạn đánh, wincv sẽ tìm các lớp cơ sở và chọn ra tất cả lớp có tên chứa các từ bạn đánh. Những lớp
này hiển thị trên một listbox phía tay trái. Nếu bạn click lên một lớp cụ thể thì các thành phần của nó sẽ hiển thị bên phía tay
phải.

Kết luận
Trong chương này chúng đã xem hai khía cạnh của lập trình trong môi trường .NET. Phần lớn chúng ta tìm hiểu các công cụ mà
Microsoft đã cung cấp cho ta để soạn thảo mã C# dễ dàng hơn. Sau đó chúng ta kiểm tra vài qui tắc bạn nên theo khi viết mã c#.
Tóm lại, chúng ta đã thông qua những phần sau:

• Tham khảo về Visual studio.NET và vài công cụ và cửa sổ thông thường mà bạn sẽ sử dụng để viết mã trong IDE.
• WinCV là một tiện ích cho bạn kiểm tra các lớp cơ sở
• Usage guideline và qui tắc đặt tên, guideline là cái mà bạn nên bám chặt vào khi viết mã để mã của bạn theo chuẩn
.NET và có thể dễ hiểu hơn.

Chương 7: Windows application


Tổng quan:
Trong lúc những ứng dụng kinh doanh phát triển ngày nay được thiết kế bởi World Wide Web, những client cổ điển vẫn tồn tại
và sẽ luôn luôn được yêu cầu. Nó là một ứng dụng Intranet sử dụng bên trong một tổ chức hay là mẫu phần mềm cài đặt trên máy
để bàn. Những chức năng mạnh và kinh nghiệm của người sử dụng như một môi trường cung cấp sẽ luôn luôn được yêu cầu cho
những kiểu ứng dụng. Web form thì tuyệt vời nhưng chúng không thể so sánh kinh nghiệm của người sử dụng thông qua một
client tốt.

May mắn, .NET cung cấp một khả năng để tạo những client mạnh thực thi bên trong Common Language Runtime. Ứng dụng này
gọi là Window form. Bất kỳ ngôn ngữ .NET nào cũng có thể sử dụng Window Form để xây dựng Windows Applications. Những
ứng dụng này được truy cập đến .NET FrameWork của các namespace và đối tượng.

Trong chương này ta bàn luận về cách để xây dựng các ứng dụng Windows trong .NET. Chúng ta sẽ bàn luận về một số chủ đề
sau:

• Cách xây dựng ứng Window form sử dụng .NET FrameWork.


• Cách sử dụng Visual studio.NET để xây dựng ứng dụng Window form nhanh chóng.
• Thêm những menu hỗ trợ vào một ứng dụng bao gồm dynamic và context-sensitive menu.
• Các tài nguyên Utilizing custom và common dialog trong một đề án
• Cách sử dụng Visual inheritance để xây dựng ứng dụng Window Form.
• Cách sử dụng Window Form để điều khiển một ứng dụng
• Cách tạo và mở rộng những điều khiển cho những chức năng đặc biệt.
• Các sự kiện từ custom control

Windows Forms

Hầu hết mọi ứng dụng Windows Form mở rộng chức năng của System.Windows.Forms. Chức năng cơ bản của lớp Form không
thể tạo một cửa sổ có thể sống và tương tác trong môi trường Windows một cách đúng đắn. Đây là một thuận lợi như một điểm
khởi đầu và bằng việc mở rộng lớp Form và thêm các control tuỳ biến và các bộ điều khiển sự kiện tuỳ biến, một ứng dụng rất
hữu ích được tạo để có thể tương tác với người dùng và dữ liệu hiện tại thông qua một giao diện người dùng tinh vi.

Chúng ta đang xem xét cách tiến trình này làm việc theo hai cách. Để hiểu tốt hơn cách mà Windows Forms hoạt động và cách
nó tương tác với .NET Framework, chúng ta sẽ xây dựng một ứng dụng Window hoàn toàn mà không sử dụng Visual
studio.NET. Nó sẽ cung cấp cho bạn một sự đánh giá mạnh mẽ về Visual studio.NET khi chúng ta chuyển đến xây dựng một ứng
dụng Window Form sử dụng nó. VS.NET cho phép các nhà phát triển tạo ứng dụng Window Form nhanh hơn và hiệu quả hơn.

Windows Forms không sử dụng Visual Studio .NET


Hầu hết mọi ứng dụng Window Form sẽ mở rộng lớp System.Windows.Form để tuỳ chỉnh và thêm nguyên lý kinh doanh. Vì thế,
ứng dụng Windows Form đơn giản nhất sẽ trình bày bên dưới:

using System;
using System.Windows.Forms;

namespace WindowsFormsApp
{
class MyForm : Form
{
static void Main(string[] args)
{
MyForm aForm = new MyForm();
Application.Run(aForm);
}
}
}
Để xem vấn đề này trong hành động, bạn hãy lưu đoạn mã trên với tên BasicForm.cs, sau đó biên dịch và chạy nó. Khi đó bạn sẽ
thấy kết quả như sau:

Khi ứng dụng trên được chạy, một cửa sổ cơ bản sẽ được mở ra. Chú ý rằng cửa sổ hành động giống như một cửa sổ chuẩn và có
thể được thu nhỏ, mở to, kéo đi, hay đóng lại. Nó là một ứng dụng Window đầy đủ chức năng trong 13 dòng mã. Hãy xem đoạn
mã của nó để hiểu những gì đang xảy ra trước khi ta thấy những điều thú vị hơn sau đây.

class MyForm : Form

Dòng này chỉ rằng lớp của chúng ta đang thừa hưởng từ lớp System.Windows.Forms.Form, có nghĩa là chúng giành được truy
cập đến tất cả chức năng của lớp Form cơ bản. Tiếp đến, chú ý rằng trong phương thức Main() chúng ta tạo một thể hiện của đối
tượng MyForm và chuyển nó đến phương thức Application.Run():

static void Main(string[] args)


{
MyForm aForm = new MyForm();
Application.Run(aForm);
}

Application là một lớp static trong System.Windows.Forms namespace, nó chứa các phương thức để bắt đầu và dừng các ứng
dụng và các luồng. Phương thức Run() có thể chấp nhận vài tham số; bằng việc truyền vào một đối tượng Form chúng ta đang
báo hiệu với .NET Framework bắt đầu xử lý các tin nhắn Window cho form này, và để thoát khỏi ứng dụng khi form này đóng.

Các Control
Hãy thêm một control đơn giản Button vào form. Chúng ta sẽ thấy các sự kiện bao quát hơn, bây giờ chúng ta chỉ xem xét những
gì nó làm để thêm một control vào một ứng dụng Window Form không dùng Visual studio.NET.

Về cơ bản, mọi control trên form là một thành phần dữ liệu của lớp custom Form. Vì thế, để thêm một Button vào form, chúng ta
sẽ thêm một thành phần dữ liệu Button mới vào lớp MyForm. Thêm dòng sau vào tập tin BasicForm.cs:

class MyForm : Form


{
//Data member to hold Button control
private Button BigButton;

Trước khi thành phần dữ liệu này làm bất cứ điều gì hoặc hiển thị một nút trên form nó phải được khởi tạo và các thuộc tính khác
nhau của Button phải được định hình. Nó nên được thực hiện trong constructor cho đối tượng MyForm. Tại thời điểm đó chúng ta
sẽ cài các thuộc tính cho chính đối tượng Form, như là size và name. Chú ý rằng có nhiều thuộc tính có thể được cài và thực hiện.
Vì thế trong constructor là thời điểm tốt nhất để thực hiện khởi tạo giá trị. Thêm khối mã sau vào constructor của MyForm:

public MyForm()
{
//Set the properties for the Button
BigButton = new Button();
BigButton.Location = new System.Drawing.Point(50, 50);
BigButton.Name = "BigButton";
BigButton.Size = new System.Drawing.Size(100, 100);
BigButton.Text = "Click Me!";

//Set properties of the Form itself


ClientSize = new System.Drawing.Size(200, 200);
Controls.Add(BigButton);
Text = "My Windows Form!";
}
Đoạn mã này đầu tiên khởi tạo một đối tượng Button mới và ấn định nó vào thành phần dữ liệu riêng BigButton. Nó sau đó cài
các thuộc tính Location, Name, Size, và Text để với các giá trị thích hợp. Bất kỳ thuộc tính nào không cài ở đây sẽ lấy giá trị mặc
định.

Những dòng tiếp theo cài kích cở của form, và sau đó phương thức this.Controls.Add() được gọi để thêm control Button vào tập
hợp Controls của form. Việc này được yêu cầu trước khi nút sẽ được hiển thị trên form. Tập hợp Controls sẽ chứa tất cả các
control trên một form và có thể cập nhật và sửa đổi tự động trong thời gian chạy để thêm và xoá các control nếu cần. Chúng ta sẽ
xem xét cách chúng thực hiện ở phần sau của chương.

Nếu bạn chạy ứng dụng tại điểm này, bạn sẽ thấy một cửa sổ như sau:

Tuy nhiên, không có gì xảy ra khi nút được click. Để thay đổi chúng ta sẽ cần thêm một sự kiện vào đoạn mã.

Các sự kiện (event):


Mỗi đối tượng trong một ứng dụng Windows Form có một tập sự kiện. Nếu bạn muốn có một đoạn mã thực hiện một điều gì đó
khi các sự kiện xảy ra, bạn nên thêm một bộ điều khiển sự kiện(event handler) vào lớp và kết hợp nó với đối tượng.

Để Windows Forms được sử dụng đoạn mã của bạn, bạn phải truyền cho nó vị trí của phương thức bộ điều khiển sự kiện trong
đoạn mã của bạn. Bạn thực hiện bằng cách tạo một thể hiện delegate thích hợp kết hợp với một phương thức trong lớp custom
Form.

Để thêm vài chức năng cho nút đó, ta cần thêm vài dòng mã vào lớp chúng ta. Thêm phương thức sau vào lớp Form của chúng
ta. Nó sẽ hành động như bộ điều khiển sự kiện cho sự kiện Click của nút. Chú ý rằng bộ điều khiển sự kiện có thể được gọi bất kỳ
đối tượng nào. Sự kiện của control tự định nghĩa tham số cho phù hợp với bộ điều khiển.

static void Main(string[] args)


{
MyForm aForm = new MyForm();
Application.Run(aForm);
}

private void ClickHandler(object sender, System.EventArgs e)


{
MessageBox.Show("Clicked!","My Windows Form",MessageBoxButtons.OK);
}

Hầu hết các bộ điều khiển sự kiện Windows Forms có dạng phương thức này. Thông số đầu tiên chứa đối tượng khởi sự kiện.
Trong trường hợp này nó sẽ là đối tượng Button từ lớp MyForm. Thông số tiếp theo chứa dữ liệu về sự kiện trong một thông số
System.EventArgs hay lớp thừa hưởng. Lớp System.EventArgs không chứa dữ liệu- Nó chỉ hành động như một lớp cơ sở. Nếu
một sự kiện phải truyền dữ liệu đến client thì nó phải sử dụng một lớp thừa hưởng. Sự kiện Button.Click không cần truyền bất kỳ
thông tin thêm vào, vì thế nó sử dụng lớp System.EventArgs cơ sở.

Cuối cùng, thêm đoạn mã sau vào constructor MyForm để sự kiện gắn bộ điều khiển sự kiện của chúng ta vào sự kiện trong lớp
MyForm.

public MyForm()
{
//Set the properties for the Button
BigButton = new Button();
BigButton.Location = new System.Drawing.Point(50, 50);
BigButton.Name = "BigButton";
BigButton.Size = new System.Drawing.Size(100, 100);
BigButton.Text = "Click Me!";
BigButton.Click += new EventHandler(ClickHandler);

//Set properties for the Form itself


ClientSize = new System.Drawing.Size(200, 200);
Controls.Add(BigButton);
Text = "My Windows Form!";
}

Ví dụ này trình bày cách Windows Form sử dụng delegates để wrap một phương thức của đối tượng trước khi ấn định nó vào sự
kiện chúng ta muốn vận dụng. System.EventHandler delegate được sử dụng để tham khảo phương thức ClickHandler() và nó
được liên kết với sự kiện Click của nút bằng cách thêm nó vào bộ điều khiển sự kiện Click. Chú ý cú pháp sử dụng - có nghĩa là
các bộ điều khiển sự kiện thêm vào có thể được liên kết với một sự kiện đơn. Chúng sẽ được xử lý để chúng được thêm vào bộ
điều khiển sự kiện.

Biên dịch ứng dụng lại, và chạy nó. Lúc này khi click nút bạn sẽ thấy một hộp tin nhắn nhỏ.

Thực thi ứng dụng trên

Windows Form sử dụng Visual Studio .NET


Giống như trong .NET, sử dụng Visual studio.NET tạo các ứng dụng Windows Form đơn giản hơn nhiều. Visual studio.NET
giảm số lượng mã rắc rối mà các nhà phát triển phải viết, cho phép các nhà phát triển tập trung vào giải quyết các vấn đề kinh
doanh.

Hãy xem cách tạo một ứng dụng Window Forms đơn giản sử dụng Visual studio.NET. Chúng ta sẽ tạo một màn hình thực thể dữ
liệu đơn giản cho một hệ thống quản lý thông tin cá nhân hư cấu. Loại màn hình này sẽ được gắn vào một số form của cơ sở dữ
liệu sử dụng để lưu trữ dữ liệu cá nhân. Chúng ta xem xét cách để tạo một tầng giao diện người dùng trong chương này. Tạo một
dự án Windows Application C# mới trong Visual studio.NET với tựa đề là SimpleDataEntry.
Sau khi dự án được tạo, bạn sẽ thấy một form đơn giản trong Visual Studio.NET trong màn hình thiết kế. Màn hình thiết kế được
dùng để thêm control vào form. Click phải trên tập tin Form1.cs trong Solution Explorer và chọn View Code. Nó sẽ hiển thị mã
phát ra bởi form được hiển thị trong màn hình thiết kế. Nhìn qua đoạn mã này. Với việc thêm vào của một vài tiêu chuẩn, mà
Visual studio.NET biên dịch như là phương thức InitalizeComponent(), đoạn mã nhìn rất giống với ứng dụng Windows Forms
ban đầu. Chú ý cách dùng của Application.Run trong phương thức Main, và sự thật là lớp Form này thừa hưởng từ
System.Windows.Forms.Form.

Phương thức InitializeComponent() được dùng bởi Visual studio.NET để xây dựng Form tại thời gian chạy. Tất cả control và
thuộc tính mà một nhà phát triển cài suốt thời gian thiết kế được cài tại thời gian chạy trong phương thức này. Khi có những sự
thay đổi được tạo ra cho Form trong thời gian thiết kế, Visual Studio.NET sẽ cập nhật phương thức này.

Quay lại màn hình thiết kế để thêm vài control vào form này để làm cho nó hữu dụng và thú vị hơn. Chú ý rằng khi bạn chọn
form, cửa sổ properties cài đặt các thuộc tính khác nhau của các control trong ứng dụng Windows Forms của chúng ta. Nó là một
bộ phận quan trọng của Visual studio.NET IDE, khi sử dụng nó thì dễ tìm kiếm tên của mọi thuộc tính, của mỗi control trong tài
liệu .NET hơn. Có một số nút ở tại đầu của cửa sổ này. Hai thay đổi đầu tiên là cách mà các thuộc tính được hiển thị. Nhóm đầu
tiên hiển thị các mục chọn trong phạm trù luận lý, như là tất cả thuộc tính với hình thức, cách cư xử, thiết kế và vân vân. Nút thứ
hai sắp xếp tất cả thuộc tính theo thứ tự alphabe. Hai nút kế tiếp chốt vào giữa sự hiển thị thuộc tính hoặc các sự kiện. Chúng ta
sẽ bàn luận những sự kiện và cách thêm chúng vào các control tiếp đó. Nút cuối cùng mở trang thuộc tính của dự án này:

Cài các thuộc tính sau của form bằng cách sửa đổi chúng trực tiếp trong cửa sổ Properties:

Property Value

Text Data Entry Form

Size 300, 220

(Name) frmMain

StartPosition CenterScreen

Các cài đặt này sẽ tạo một cửa sổ 300 tới 220 pixel ở giữa màn hình. Thuộc tính Name là một thuộc tính quan trọng trên tất cả
các controls. Giá trị này được dùng như tên đối tượng của các biến thành viên của lớp, và được dùng để tham khảo đế control
trong đoạn mã.

Bây giờ thêm hai control Button vào form. Cài các thuộc tính của hai control Button như sau:

Property button1 Value button2 Value

(Name) btnSave btnCancel

Location 125, 157 210, 157

Size 78, 25 78, 25


Property button1 Value button2 Value

Text Save Cancel

Ở đây chúng ta đang thay đổi các tên mặc định của Button đến một giản đồ đặt tên chuẩn hơn, và định vị chúng vào vị trí chúng
ta muốn chúng trên Form1.

Quay về màn hình mã để xem xét những gì Visual studio.NET đã làm suốt thời gian này. Bạn sẽ thấy phần thêm của hai biến
thành viên mới trong lớp Form. Nếu bạn mở rộng vùng tiêu đề "Windows Form Designer generated code" bạn sẽ thấy phương
thức InitializeComponent(), nơi mà tất cả control trên form được khởi tạo và định hình chính xác. Phương thức này được gọi
trong constructor của form.

Tiếp đó thêm ba control TextBox và ba Control Label Next vào Form. Gán các thuộc tính như sau:

Property TextBox1 TextBox2 TextBox3 Label1 Label2 Label3

(Name) txtFName txtLName txtSSN label1 label2 label3

Location 97, 25 97, 61 97, 99 20, 25 20, 62 20, 99

Size 115, 20 115, 20 115, 20 70, 18 70, 18 70, 18

Text (Blank) (Blank) (Blank) First Name: Last Name: SSN:

Bạn nên có một Form giống một màn hình thực thể dữ liệu về thông tin người dùng. Một end-user có thể sử dụng màn hình này
để nhập tên đầu và cuối của chúng như Social Security Number. Tại lúc này Form1 sẽ giống như sau:

Các sự kiện (event):


Các ứng dụng Windows là event-driven và không thêm mã, nó đáp ứng các sự kiện. Visual studio.NET tạo ra nó rất đơn giản
bằng cách thêm mã đáp ứng các sự kiện phát sinh bởi ngừơi dùng và hệ thống.

Cửa sổ Properties được cập nhật để phản ánh toàn bộ danh sách sự kiện có thể được điều khiển từ đối tượng này. Để thấy danh
sách này, chọn nút thứ tư từ bên trái. Nó sẽ hiển thị một danh sách sự kiện cho đối tượng đang chọn, và chọn bất kỳ đối tượng
nào nó sẽ có liên kết mã với chúng. Màn hình bên dưới chỉ dãy sự kiện khi đối Form được chọn.
Việc thêm một sự kiện có thể được vận dụng một trong hai cách. Để thêm một sự kiện mặc định cho một control bằng cách click
đôi lên nó trong màn hình thiết kế.

Cách khác để thêm các bộ điều khiển sự kiện vào đoạn mã của bạn và các tuỳ chọn nếu bạn không thêm các sự kiện mặc định,
bằng cách sử dụng cửa sổ Properties. Một lần nữa, đoạn mã pluming đúng sẽ được chèn vào lớp Form và bạn sẽ lấy các bộ điều
khiển sự kiện cho sự kiện được chọn.

Chú ý cửa sổ Properties, bạn phải làm như vậy nếu bạn có nhiều nút mà tất cả chúng cùng làm những việc giống nhau và yêu cầu
cùng một sự xử lý.

Hãy thêm vài đoạn mã trong bộ điều khiển sự kiện của hai control Button của chúng ta. Thêm bộ điều khiển sự kiện Click vào hai
Buttons đang tồn tại. Thêm đoạn mã sau vào tập tin:

private void btnSave_Click(object sender, System.EventArgs e)


{
SaveFile();
}

private void btnCancel_Click(object sender, System.EventArgs e)


{
Clear();
}
private void SaveFile()
{
//Save the values to an XML file
//Could save to data source, Message Queue, etc.
System.Xml.XmlDocument aDOM = new System.Xml.XmlDocument();
System.Xml.XmlAttribute aAttribute;

aDOM.LoadXml("<UserData/>");

//Add the First Name attribute to XML


aAttribute = aDOM.CreateAttribute("FirstName");
aAttribute.Value = txtFName.Text;
aDOM.DocumentElement.Attributes.Append(aAttribute);

//Add the Last Name attribute to XML


aAttribute = aDOM.CreateAttribute("LastName");
aAttribute.Value = txtLName.Text;
aDOM.DocumentElement.Attributes.Append(aAttribute);

//Add the SSN attribute to XML


aAttribute = aDOM.CreateAttribute("SSN");
aAttribute.Value = txtSSN.Text;
aDOM.DocumentElement.Attributes.Append(aAttribute);

//Save file to the file system


aDOM.Save("UserData.xml");
}
private void Clear()
{
//Erase all the text
txtFName.Text = "";
txtLName.Text = "";
txtSSN.Text = "";
}

Ví dụ đơn giản này lưu dữ liệu được nhập bởi một tập tin XML trên hệ thống tập tin. Mọi ứng dụng sẽ sử dụng ADO.NET để lưu
thông tin vào một nguồn dữ liệu back-end. Tuy nhiên, trong ví dụ này chúng ta sẽ xuất khẩu một tập tin XML nhỏ.

Chúng ta sử dụng các phương thức private để thể hiện chức năng thực sự, vì thế chúng ta có thể sử dụng cùng chức năng từ các
tuỳ chọn menu sau này. Mọi đoạn mã trong phương thức SaveFile() bao gồm việc viết ra tập tin XML chứa dữ liệu user-supplied.
Sự kiện Click của nút Cancel gọi phương thức Clear() để xoá tất cả các control textbox. Chú ý rằng trong một ứng dụng hoàn
chỉnh, nó có thể đóng cửa sổ này và trả về người dùng một màn hình chính.

Chú ý rằng có một lỗi trong Visual studio.NET thỉnh thoảng yêu cầu một nhà phát triển thay đổi tên của lớp Form sử dụng trong
phương thức Main() bằng tay. Nếu bạn có một lỗi khi biên dịch phương thức Main() và bảo đảm nó giống như đoạn mã sau. Phải
bảo đảm rằng đoạn mã tạo đối tượng sử dụng tên lớp frmMain. Khi một tên lớp Form bị thay đổi thì dòng này không luôn luôn
cập nhật.

static void Main()


{
Application.Run(new frmMain());
}

Nếu bạn chạy ứng dụng này tại lúc này bạn sẽ có một cửa sổ thực thể dữ liệu nhỏ có đầy đủ chức năng. Bạn có thể nhập dữ liệu,
lưu nó vào một tập tin XML, và xoá tất cả giá trị. Việc đó thì đơn giản nhưng nó biểu lộ cách tạo các ứng dụng sử dụng Visual
studio.NET.

Thực thi ứng dụng trên

Resizing Windows
Một vấn đề với cửa sổ thực thể dữ liệu của chúng ta là khi nó được thay đổi kích thước thì các control bị khoá lại trong một vùng.
Điều đó có vẽ buồn cười và không chuyên nghiệp với một ứng dụng cao cấp,do đó nên hỗ trợ khả năng thay đổi kích thước lại và
định vị một cửa sổ trong bất kỳ hình dạng nào người dùng mong muốn. Bất kỳ nhà phát triển nào viết mã để điều khiển việc thay
đổi kích thước và thay thế của các control sẽ đánh giá sự dễ dàng khi sử dụng .NET Framework và Window Forms để làm việc
này. Với một thuộc tính đơn thì tất cả công việc này có thể được điều khiển bởi .NET Framework.

Thuộc tính Anchor thể hiện năng lực kỳ diệu này, và nó là một thành viên của hầu hết tất cả các lớp trong
System.Windows.Forms namespace bởi vì nó là một thuộc tính của lớp System.Windows.Forms.Control. Nhắc lại là, mọi control
đều thừa hưởng từ lớp này.

Thuộc tính Anchor được cài một liên kết cuả một hay nhiều cạnh của cha mẹ nó. Cài một trong những cạnh này trong thuộc tính
sẽ dẫn đến control duy trì mối quan hệ vị trí giữa cạnh của nó và cạnh của cha mẹ nó khi form được thay đổi kích thước và di
chuyển. Thuộc tính này rất quan trọng để thiết kế giao diện người dùng thân thiện, và nên được thí nghiệm để hiểu cách nó làm
việc.

Visual Studio .NET bao gồm một cửa sổ pop-up để cài thuộc tính này vào đúng mối liên kết. Cửa sổ pop-up này cho phép một
nhà phát triển chọn cạnh để neo control. Cửa sổ pop-up này có thể được tìm thấy như một phần của cửa sổ Properties.

Chúng ta sẽ dùng thuộc tính Anchor để tạo một giao diện người dùng hiệu quả hơn cho màn hình thực thể dữ liệu của chúng ta.

Chọn các control TextBox trong môi trường thiết kế Visual studio.NET. Thay đổi thuộc tính Anchor vào Top, Left, Right sử dụng
cửa sổ pop-up. Nó sẽ duy trì khoảng cách giữa top, left, và right của các cạnh của cha mẹ, bằng cách đó thay đổi kích thước
control chính xác.

Chọn các control Button thứ hai và thay đổi thuộc tính Anchor của nó ở Bottom, Right. Nó sẽ duy trì vị trí đóng của chúng ở
bottom-right của form. Chạy ứng dụng và thay đổi kích thước cửa sổ để thấy cách các control điều chỉnh chính bản thân chúng.

Một lần nữa, thuộc tính này được là quyết định hoàn toàn trong thiết kế giao diện người dùng chuyên nghiệp trong .NET, và sử
dụng nó giảm số lượng của công việc yêu cầu bởi các nhà phát triển. Các nhà phát triển tự do này tập trung giải quýêt vấn đề
kinh doanh thực tế để thay cho việc thay đổi kích thước cấp thấp.

Menus
Các menu được dùng trong hầu hết mọi ứng dụng Window, và chúng cung cấp một cách tuyệt vời để giao tiếp người dùng với
các tuỳ chọn để họ làm việc theo các chức năng có sẳn. Có hai kiểu menu khác nhau. Thông thường nhất là một menu
chính(main menu), ở đầu của một cửa sổ và thường bao gồm các mục như File, Edit, và Help. Vài ứng dụng chứa các menu theo
ngữ cảnh để cho phép người dùng truy cập đến thông tin về các chủ đề hay mục đặc biệt. Menu theo ngữ cảnh được ẩn cho đến
khi người dùng nhấn chuột phải - sau đó menu được hiển thị tại vị trí con trỏ .

Windows Forms cung cấp hỗ trợ đầy đủ cho việc thêm hai kiểu menu vào một ứng dụng. Lớp System.Windows.Forms.Menu
cung cấp lớp cơ sở cho tất cả lớp menu trong hệ thống. Lớp MainMenu tượng trưng cho menu chính, và có thể liên kết với một
form. Menu này chứa một tập hợp đối tượng MenuItem tượng trưng cho một tuỳ chọn menu riêng rẽ.

Lớp ContextMenu thì có thể thêm các menu theo ngữ cảnh cho một ứng dụng. Lớp này cũng chứa một tập hợp đối tượng
MenuItem, nhưng ContextMenu có thể xuất hiện trong bất kỳ vị trí nào trong một form, nó không chỉ tại đầu của một cửa sổ như
lớp MainMenu.

Chúng ta sẽ thêm một menu vào ứng dụng thực thể dữ liệu của chúng ta. Thêm một menu vào một ứng dụng Window Form thì
dễ như thêm bất kỳ control chuẩn nào như là một Button hay TextBox. Chọn control MainMenu từ thanh công cụ và vẽ một hộp
trên bề mặt thiết kế. Nó sẽ thêm một menu tại đầu của form. Chọn menu và gõ File để thêm mục menu đầu tiên. Bây giờ khi bạn
click trên File một menu mới sẽ hiển thị bên dưới, nó có thể thêm vào như chúng ta thêm mục menu File. Bạn có thể tiếp tục gõ
vào MenuItem, bằng cách đó tạo ra cấu trúc thực sự của hệ thống menu trong IDE

Sử dụng hệ thống menu để tạo menu sau. Chú ý rằng: bằng cách nhập một ký tự gạch(–) đơn lẽ thì một dòng riêng lẽ được tạo.
Nó rất hữu ích cho việc phân chia các nhóm chọn lựa trong một menu. Một phần quan trọng khác để nhớ là bằng cách mở đầu
một ký tự với ký hiệu là (&) thì ký tự đó trở thành phím tắt cho mục menu này. Vì thế một người dùng có thể chọn menu bằng
cách chỉ sử dụng bàn phím.

Top Level Menu Item Contained Menu Items


Text – &File Text – &Save

Name – mnuFile Name – mnuSave


Text – &Cancel

Name – mnuCancel
Text – "-" (Single Dash)

Text – E&xit

Name – mnuExit
Text – &Color Text – &Gray

Name – mnuColor Name – mnuGray

RadioCheck – true

Checked – true
Text – G&reen
Top Level Menu Item Contained Menu Items

Name – mnuGreen

RadioCheck – true
Text – &Blue

Name – mnuBlue

RadioCheck – true
Text – &Red

Name – mnuRed

RadioCheck – true
Text – &Purple

Name – mnuPurple

RadioCheck – true

Chạy ứng dụng và thấy rằng bạn có một cửa sổ với một menu đang làm việc trên đó. Tuy nhiên không có gì xảy ra khi một menu
được chọn. Để thay đổi, bộ diều khiển sự kiện này phải được thêm bên dưới những mục menu riêng lẽ. Chúng ta sẽ tiếp tục với
cùng ví dụ này và thêm sự kiện điều khiển để người dùng có thể sử dụng menu.

Các MenuItems riêng rẽ là mọi control giống như các cái khác, và chúng có thể được chọn trong bề mặt thiết kế. Làm các việc
này bằng cách chỉ ra các thuộc tính và sự kiện của chúng trong cửa sổ Properties. Sử dụng danh sách sự kiện để thêm bộ điều
khiển sự kiện Click cho các mục chọn Save, Cancel, và Exit. Thêm đoạn mã sau trong bộ điều khiển sự kiện mới:

private void mnuSave_Click(object sender, System.EventArgs e)


{
SaveFile();
}

private void mnuCancel_Click(object sender, System.EventArgs e)


{
Clear();
}
private void mnuExit_Click(object sender, System.EventArgs e)
{
Close();
}

Dynamic Menus
Các menu thường được dùng để phản ánh trạng thái của ứng dụng. Khi người dùng tạo các chọn lựa và thay đổi trong ứng dụng,
menu phải phản ánh các sự thay đổi này. Các mục menu có thể được thêm, xoá và chỉnh sửa để phản ánh tình trạng ứng dụng
hiện tại. Một lần nữa, các MenuItem hành động như các thành phần khác và có thể được vận dụng.

MenuItems có thể có một nút kiểm kế bên để minh hoạ tuỳ chọn hiện tại. Nó rất hữu dụng cho người dùng để họ có thể đánh giá
chính tình trạng của ứng dụng của họ. Thuộc tính Checked như một biến cờ, nó có thể cài để hiện hay dấu một điểm kiểm tra kế
bên mục menu. Nếu thuộc tính RadioCheck được cài bằng true thì nút kiểm sẽ xuất hiện như một chấm đơn giản. Vì thế chỉ một
mục menu đơn giản được chọn tại một thời điểm với thuộc tính RadioCheck.

Chúng ta đang thêm một số mã bên dưới các mục menu color để thay đổi màu nền của form. Chúng ta sẽ thực hiện bằng cách sử
dụng một bộ điều khiển sự kiện cho mọi đối tượng MenuItem.

Trong ứng dụng của chúng ta, thêm phương thức sau vào:

private void mnuItems_Click(object sender, System.EventArgs e)


{

}
Chúng ta thêm một bộ điều khiển sự kiện ở đây để thay cho việc cho phép Visual Studio.NET IDE làm giùm chúng ta. Chúng ta
cần làm như vậy để chúng ta có thể liên kết phương thức đơn này với mọi bộ điều khiển sự kiện Click của các mục menu. Nó sẽ
cho phép chúng ta điều khiển tình trạng của menu và ứng dụng từ phương thức đơn này.

Trở lại với màn hình thiết kế của IDE, click trên mục menu Gray. Trong cửa sổ Properties chuyển tới màn hình sự kiện và chọn
sự kiện Click. Click trên mũi tên thả xuống để hiển thị một danh sách tên phương thức có thể liên kết với sự kiện này. Đây là
cách để gắn các phương thức vào các sự kiện. Chọn phương thức mnuItems_Click() từ danh sách. Lập lại thủ tục này với mọi
mục trong menu Color. Sự kiện Click của mọi mục nên được liên kết với cùng phương thức.

Bây giờ, mọi đối tượng được liên kết với cùng phương thức bộ điều khiển sự kiện, thêm đoạn mã sau để cập nhật BackColor của
form và tình trạng menu.

private void mnuItems_Click(object sender, System.EventArgs e)


{
MenuItem aObj;

//Set the BackColor of the form based on the selected object


if(sender == mnuGray)
this.BackColor = System.Drawing.SystemColors.Control;
else if(sender == mnuGreen)
this.BackColor = Color.Green;
else if(sender == mnuBlue)
this.BackColor = Color.Blue;
else if(sender == mnuRed)
this.BackColor = Color.Red;
else if(sender == mnuPurple)
this.BackColor = Color.Purple;

//Set all checkboxes to false


mnuGray.Checked = false;
mnuGreen.Checked = false;
mnuBlue.Checked = false;
mnuRed.Checked = false;
mnuPurple.Checked = false;

//Change the selected item to checked


aObj = (MenuItem)sender;
aObj.Checked = true;
}

Đoạn mã này sử dụng sự kiện là tham số sender trong một bộ điều khiển sự kiện là đối tượng để khởi sự kiện. Điều này được yêu
cầu bởi vì bộ điều khiển sự kiện này được dùng bởi tất cả đối tượng MenuItem. Vì thế, bước đầu là xác định mục menu được
chọn bởi người dùng. Và sau đó thay đổi BackColor của form theo màu sắc tương ứng.

Bước kế tiếp là cài một nút kiểm kế bên mục menu thích hợp. Chúng ta có thể thực hiện việc này bằng cách cài đặt đơn giản
thuộc tính Checked bằng true, sau đó đặt nó vào đối tượng MenuItem. Tuy nhiên, trước khi chúng ta làm việc này, chúng ta cần
cài tất cả đối tượng MenuItem là unchecked.

Chạy ứng dụng và chọn các tuỳ chọn màu sắc khác nhau. Bạn sẽ thấy màu nền của của sổ thay đổi, và hộp kiểm trong menu cập
nhật để phản ánh màu sắc hiện tại

Menus ngữ cảnh


Mọi ứng dụng Window cho phép người dùng click phải và hiện lên một menu theo ngữ cảnh. Nó có nghĩa là các sự chọn lựa
menu được dựa vào đối tượng, hay ngữ cảnh , người dùng đã chọn. Các menu ngữ cảnh cho phép ứng dụng biểu thị thông tin
thêm vào hay các chọn lựa người dùng.

Các menu theo ngữ cảnh có thể được thêm vào các ứng dụng Windows Forms rất dễ dàng. Chúng ta sẽ thêm một menu theo ngữ
cảnh vào cửa sổ thực thể dữ liệu của chúng ta, các chọn lựa Save và Cancel khi người dùng click phải ở bất kỳ đâu trên Form.

Để thêm một menu ngữ cảnh vào một form, đơn giản thêm control ContextMenu từ thanh công cụ vào Form1. Khi đối tượng
ContextMenu được thêm vào form nó sẽ xuất hiện trong vùng footer bên dưới bề mặt thiết kế form. Khi biểu tượng này được
chọn, menu chính, nếu nó tồn tại sẽ không xuất hiện trong form và được thay thế với chính menu ngữ cảnh đó. Nó có thể được
chỉnh sửa trong bề mặt thiết kế bằng cách gõ các mục menu khác nhau, như là sửa các menu chính. Mặc dù nó xuất hiện, các
menu sẽ được hiển thị ở đỉnh của form như menu chính, nó sẽ được ẩn cho đến khi chúng ta gán nó vào form.

Sau khi thêm ContextMenu vào Form1, thêm các mục menu dưới đay bằng cách gõ vào các giá trị sau:
Menu Item Name Text Property Value

mnuSaveContext Save

mnuCancelContext Cancel

Một lần nữa, mọi mục menu là các đối tượng MenuItem riêng lẽ và có các thuộc tính có thể cài trong cửa sổ Properties và chọn
sự kiện Click. Trong dãy thả xuống chọn mnuSave_Click cho mnuSaveContext MenuItem và mnuCancel_Click cho
mnuCancelContext MenuItem. Nó sẽ nối những sự kiện này với cùng bộ điều khiển sự kiện được gọi khi các mục menu chính
được click.

Bây giờ chúng ta có một menu ngữ cảnh, nó xuất hiện khi chúng ta click phải trên Form. Để thêm một menu ngữ cảnh vào một
Form, thuộc tính ContextMenu của đối tượng Form phải được cài vào đối tượng ContextMenu của chúng ta. Khi nó được cài,
form sẽ tự động hiển thị ContextMenu khi ngừơi dùng click phải. Nó sẽ hiển thị ContextMenu tại vị trí người dùng click phải.
Chú ý rằng thuộc tính này có thể được cập nhật trong thời gian chạy. Thật là quan trọng để ghi chú rằng thuộc tính này là một
thành viên của lớp Control, có nghĩa là tất cả control Windows Forms đều có thuộc tính này.

Cài thuộc tính ContextMenu của Form1 vào contextMenu1 sử dụng cửa sổ properties. Một combo box sẽ hiển thị các đối tượng
ContextMenu hiện tại để chọn từ trên form. Multiple ContextMenus có thể được thêm vào một form, mặc dù chỉ một được gán
vào form tại một thời điểm.

Chạy ứng dụng và click phải bất kỳ đâu trên trên form để thấy menu ngữ cảnh hiển thị hai tuỳ chọn Save và Cancel.

Thực thi ứng dụng với các menu

Dialogs
Dialogs là một kiểu đặc biệt của Form để lấy thông tin người dùng và tương tác với ngừơi dùng trong các ứng dụng Window. Có
một tập các hộp dialog định nghĩa trước để lấy thông tin như vị trí tập tin, màu sắc, và cài đặt máy in. Một ứng dụng tuỳ biến
thường sử dụng hộp thoại dialog để thuận tiện chọn dữ liệu từ endusers.

Tạo một hộp dialog thì rất giống với tạo một Form chuẩn. Trên thực tế, cùng tiến trình xử lý được dùng để thêm vào một dự án
Visual studio.NET. Sự khác chính là bản liệt kê FormBorderStyle, nó phải được cài là Fixel Dialog. Nó tạo cửa sổ không lớn và
là nguyên nhân nó giống với hộp dialog Window. Nó cũng là một thực hành Window chuẩn để huỷ ControlBox, MinimizeBox, và
MaximizeBox từ một hộp dialog, vì thế các thuộc tính này nên được cài là false trong cửa sổ properties.

Bất kỳ control Windows Forms chuẩn nào cũng có thể tồn tại trên một hộp dialog. Bề mặt thiết kế trong Visual studio.NET được
dùng để thiết kế các Form chuẩn, và các tuỳ chọn giống nhau có thể dùng cho các nhà phát triển.

Modal vs. Modeless


Khi chúng ta muốn hiển thị hộp dialog, có hai chọn lựa: modal hay modeless. Hai khái niệm này chỉ cách dialog tương tác với
ứng dụng. Một modal dialog ngăn chận các luồng hiện tại và yêu cầu người dùng trả lời vào hộp dialog trứơc khi tiếp tục với ứng
dụng. Một Modeless dialog thì giống một cửa sổ chuẩn hơn.

Dialog Box Results


Thường rất quan trọng để hiểu cách người dùng đóng một hộp dialog. Một ví dụ điển hình đó là một dialog File Open. Nếu người
dùng chọn một tập tin thì hành động tiếp theo cho ứng dụng là load tập tin đó, tuy nhiên nếu người dùng click nút Cancel hay
đóng hộp dialog thì ứng dụng sẽ không load bất kỳ tập tin nào.
Bí quyết để hiểu cách người dùng tương tác với một hộp dialog là bảng liệt kê DialogResult. Các giá trị cho bảng liệt kê này như
sau:

Value Description

Abort Giá trị này thì được trả về khi một người dùng chọn một nút có nhãn Abort. Trong trường hợp này, người dùng
muốn huỹ thao tác hiện tại và không lưu sự thay đổi.

Cancel Giá trị này thường được trả về khi một người dùng chọn một nút có nhãnlà Cancel, đóng hộp thoại bằng cách
nhấn nút "x", hay nấn phím Esc. Người dùng muốn huỹ các thay đổi và trả về trạng thái trước khi mở hộp thoại.

Ignore Giá trị này được trả về khi một người dùng chọn nút có nhãn Ignore. Giá trị này có thể được sử dụng khi ứng
dụng cảnh báo người dùng về các điều kiện xảy ra lỗi, nhưng người dùng chọn lệnh Ignore.

No Giá trị này thường được trả về khi một người dùng chọn nút có nhãn No. Nó thì bình thường khi hộp thoại được
dùng để hỏi người dùng một câu hỏi es/no.

Yes Giá trị này thường được trả về khi một ngừời dùng chọn nút có nhãn Yes. Nó là con trỏ đếm đến kết quả trả về
Nó và được dùng trong cùng tình huống.

None Không có gì được trả về từ hộp thoại.

OK Giá trị này đươc trả về khi một người dùng chọn nút có nhãn OK. Nõ thì bình thường trong các hộp thoại và các
tin cảnh báo nơi nào quan trong cho người dùng thừa nhận thông tin.

Retry Giá trị này được trả về khi một người dùng chọn nút có nhãn Retry. Nó có ích khi một thao tác không thành công
sẽ thành công nếu được thử lại.

Để truy cập vào giá trị này bạn phải sử dụng thuộc tính DialogResult của Form. Thuộc tính này là public và có thể truy cập ngay
khi người dùng đã đóng hộp dialog.

Mở một Dialog
Có hai cách để mở một hộp dialog, một là hiển thị modal và một là hiển thị modeless. Để hiện một dialog như một modal dialog,
thì sử dụng phương thức sau:

DialogResult Form.ShowDialog()

Phương thức này là một bộ phận của lớp Form. Phương thức này có thể chấp nhận không có tham số hoặc một đối tượng Form
như một tham số. Đối tượng Form này đại diện cho ower của hộp dialog, hộp này có ích bởi vì tất cả đối tượng Form có một con
trỏ quay về cha mẹ của chúng, cho phép một hộp dialog có thể lấy hoặc cài dữ liệu vào cha mẹ của nó. Còn nếu không tham số
thì truyền các mặc định cửa sổ hiện tại vào cha mẹ nó.

Chú ý phương thức này trả về một giá trị bảng liệt kê DialogResult. Phương thức này ngăn chặn sự thực thi, và không có mã nào
sau khi nó thực thi cho đến khi người dùng đóng hộp dialog. Khi sự việc này xảy ra thì DialogResult mã được trả về và ứng dụng
có thể tiếp tục xử lý. Đoạn mã sẽ như sau:

if (aDialogObject.ShowDialog() == DialogResult.Yes)
{
//User selected Yes
//Use the properties of the aDialogObject to perform actions
}
else
{
//User selected No – do not perform action
}

Đoạn mã trên hiển thị hộp dialog để hỏi ngừơi dùng nếu họ muốn lưu tập tin hiện tại. Nếu ngừời dùng chọn Yes thì đoạn mã sẽ
thả vào khối if nếu chọn No thì khối else sẽ được thực thi.

Common Dialogs
.NET Framework cung cấp truy cập đến những common dialog này thông qua các lớp sau. Mỗi lớp này tượng trưng một
common dialog và có thể được hiển thị như một hộp dialog. Tất cả lớp này tồn tại trong System.Windows.Forms namespace:

Class Description

ColorDialog Nó cho phép một ngừơi dùng chọn một màu từ bảng màu.

FontDialog Hộp dialog này hiển thị tất cả font hiện có trên hệ thống và cho phép người dùng chọn một cái
để dùng trong ứng dụng.

OpenFileDialog Cho phép một người dùng mở một tập tin sử dụng hộp thoại mở tập tin chuẩn.

SaveFileDialog Nó cho phép người dùng chọn một tập tin, thư mục hay địa chỉ mạng để lưu dữ liệu của ứng
dụng.

PageSetupDialog Nó cho phép người dùng cài kích cỡ trang, canh lề, và các đặc tính in ấn khác.

PrintDialog Nó cho phép người dùng cài định dạng trang hiện hành và các đặc tính in ấn qua hộp dialog
thuộc tính in ấn chuẩn.

PrintPreviewDialog Nó hiển thị một tài liệu như nó xuất hiệnk trên máy in đang chọn với các cài đặt trang hiện hành.

Tất cả lớp này thừa kế từ lớp System.Windows.Forms.CommonDialog, ngoại trừ lớp PrintPreviewDialog. Lớp
System.Windows.Forms.CommonDialog cung cấp các chức năng cơ bản yêu cầu hiện một hộp combox dialog. Mọi lớp common
dialog được hiển thị sử dụng phương thức ShowDialog(), nhưng chúng chứa các thuộc tính tuỳ biến sử dụng để định hình và hỏi
chức năng tuỳ biến của chúng.

ColorDialog

Dialog này hiển thị hộp dialog màu sắc chung. Nó hữu ích khi một ngừơi dùng được cho phép định dạng nền của một Form hay
control, và bạn muốn cung cấp chúng như một cách để chọn màu ưu tiên.

Thuộc tính chính được dùng với lớp này là thuộc tính Color. Nó chứa màu sắc chọn lựa khi hộp dialog trả điều klhiển cho ứng
dụng. Thuộc tính này là một cấu trúc màu, chúng có lợi bởi vì .NET framework cần cấu trúc này trong các phương thức và thuộc
tính khác.

Một tiện lợi khác là nó cho phép người dùng định nghĩa và sử dụng một tập màu sắc custom-defined. Đặc trưng này có thể hiện
bằng mặc định, nhưng có thể bị ẩn bằng cách cài thuộc tính AllowFullOpen bằng giá trị false.

Như các hộp dialog khác, giá trị trả về từ ShowDialog() phải được xem xét để hiểu cách người dùng thoát khỏi dialog. Đây là
một đoạn mã nhỏ sử dụng lớp này:

ColorDialog aClrDialog = new ColorDialog();


aClrDialog.AllowFullOpen = false;
aClrDialog.Color = this.BackColor;
if (aClrDialog.ShowDialog() == DialogResult.OK)
{
this.BackColor = aClrDialog.Color;
}

aClrDialog.Dispose();

Chú ý rằng bạn có thể sử dụng bất kỳ hộp dialog nào như ví dụ này, hay bạn có thể thêm thành phần vào một Form trong Visual
studio.NEY. Đoạn mã sẽ rất giống, nhưng lớp dialog sẽ có giá trị cho mọi phương thức trong Form, và tất cả thuộc tính này có
thể được cài tại thời gian thiết kế trong cửa sổ Properties.

FontDialog

Lớp này cho phép một người dùng chọn một kiểu font, size, và color. Nó rất hữu ích trong các ứng dụng, nhưng nó cũng có thể
được dùng để cho phép người dùng định dạng bằng cách chọn kiểu font để hiển thị trong nhập liệu và màn hình báo cáo.

Lớp này chứa một số lượng lớn các thuộc tính mà có thể được cài để định hình các chức năng của hộp dialog :
Property Description

Color Thuộc tính này lấy hay cài màu sắc font được chọn. Chú ý thuộc tính ShowColor phải là true để nó hợp lệ.

Font Đây là thuộc tính quan trọng nhất củ hộp dialog này. Nó trả về cấu trúc Font mô tả việc chọn font của người
dùng. Nó có thể sau đó được áp dụng vào một đối tượng Control hay Form để thay đổi font.

MaxSize Lấy và cài kích cỡ điểm lớn nhất mà mọt ngừơi dùng có thể chọn trong hộp dialog.

MinSize Lấy hay cài kích cỡ điểm nhỏ nhất mà một người dùng có thể chọn trong hộp dialog.

ShowApply Thuộc tính Boolean có thể được cài là true để hiển thị một nút Apply. Nếu nó được dùng, một bộ điều khiển
sự kiện được viết để bắt được sựu kiện Apply khi nó xảy ra. Bởi vì khi người dùng chọ Apply, control vẫn
không trả về cho ứng dụng, nhưng bộ điềukhiển sự kiện có thể sau đó xử lý font thay đổi trong ứng dụng.

ShowColor Thuộc tính Boolean có thể được cài là true để hiển thị một danh sách màu. Nó cho phép ngừơi dùng chọn
màu của font cũng như kiểu hay kích cỡ.

ShowEffects Thuộc tính Boolean có thể được cài là true để cho phép ngừời dùng chỉ định các tuỳ chọn strikethrough,
underline, và text color.

Một bộ điều khiển sự kiện có thể được viết để trả lời sự kiện Apply khi nó được kích bởi người dùng ấn vào nút Apply.

FontDialog aFontDialog = new FontDialog();


aFontDialog.ShowColor = true;
aFontDialog.ShowEffects = true;
aFontDialog.MinSize = 1;
aFontDialog.MaxSize = 35;
aFontDialog.Font = SomeControl.Font;
if (aFontDialog.ShowDialog() == DialogResult.OK)
{
SomeControl.Font = aFontDialog.Font;
}

aFontDialog.Dispose ();

Đầu tiên chúng ta khởi tạo đối tượng FontDialog() với một vài cài đặt, trong đó có thuộc tính
font của SomeControl để mô tả đối tượng ta đang cập nhật. Việc khởi tạo thuộc tính
font như trên là rất tốt bởi vì người dùng sẽ thấy font được chọn trong hộp dialog, và
họ không lúng túng. Nếu người dùng chọn OK trong FontDialog thì thuộc tính font
của SomeControl được cập nhật để phản ánh font mà người dùng chọn.

OpenFileDialog

Lớp này rất hữu ích, như nhiều ứng dụng yêu cầu người dùng điều hướng hệ thống tập tin để mở và dùng các tập tin dữ liệu.
Dialog này là dialog Window chuẩn cho việc mở tập tin, và người dùng nên làm quen với nó.

Lớp này chứa nhiều thuộc tính dùng để cài hình thức và cách cư xử của chính hộp dialog đó. Cả hai lớp này và lớp
SaveFileDialog thừa kế từ lớp cơ sở FileDialog. Vì nguyên nhân này nhiều thuộc tính được chia sẽ. Thuộc tính chính được trình
bày ở trong bảng sau:

Property Description

CheckFileExists Cài thuộc tính này là true để làm cho hộp dialog hiển thị một cảnh báo nếu người dùng chỉ định
một tên tập tin không tồn tại. Cách này làm cho đoạn mã của bạn không phải kiểm tra một đường
dẫn. Mặc định là true.

FileName Đây là thuộc tính quan trọng được dùng để cài và khôi phục tên tập tin được chọn trong hộp
dialog tập tin.
Property Description

FileNames Nếu Multiselect là enabled, thuộc tính này sẽ trả về một mảng tên tập tin mà người dùng chọn.

Filter Nó cài chuỗi lọc tên tập tin, để xác định các chọnlựa xuất hiện trong hộp "Files of type" trong hộp
dialog.

FilterIndex Chỉ mục của bộ lọc chọn trong hộp dialog tập tin.

InitialDirectory Thư mục khởi tạo hiển thị bởi hộp dialog tập tin.

Multiselect Thuộc tính Boolean được cài để cho biết hộp dialog có cho phép các tập tin bội có được chọn hay
không. Mặc định là false.

ReadOnlyChecked Thuộc tính Boolean cho biết nếu check box chỉ đọc được chọn. Mạc định là false.

RestoreDirectory Thuộc tính Boolean cho biết hộp dialog có trả lại thư mục hiện tai trước khi đóng hay không. Mặc
định là false.

ShowHelp Thuộc tính Boolean cho biết nút Help có được hiển thị trong hộp dialog tập tin hay không.

ShowReadOnly Thuộc tính Boolean cho biết dialog có chứa một check box chỉ đọc hay không.Mặc định là false.

Title Tiều đề của dialog tập tin được hiển thị.

Lớp này được dùng để lấy một tên tập tin hay nhiều tên tập tin từ người dùng. Khi nó đựợc thực hiện thì ứng dụng có thể xử lý
một tập tin hay nhiều tập tin được cho biết bởi người dùng. Thuộc tính Filter là một khoá để cung cấp một giao diện hữu ích cho
người dùng. Bằng cách thu hẹp các tập tin hiển thị thích hợp vào ứng dụng hiện hành, người dùng chắc chắn hơn để tìm tập tin
chính xác.

Thuộc tính Filter là một chuỗi có thể chứa các tuỳ chọn lọc phức tạp. Mọi filter chứa một diện mạo tóm tắc, theo sau bởi một
thanh dọc ( | ) và mẫu filter là một chuỗi tìm kiếm DOS. Các chuỗi cho các tuỳ chọn lọc khác nhau sẽ phân biệt bởi một thanh
dọc.

Bạn có thể thêm các mẫu đa filter vào một filter đơn bằng cách phân các kiểu tập tin với dấu chấm phẩy. Ví dụ "Image
Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|All files (*.*)|*.*"

Đoạn mã sau tạo một đối tượng OpenFileDialog, định hình một số thuộc tính trên nó, và hiển thị nó với người dùng để cho phép
họ chọn một tập tin. Ứng dụng có thể sau đó sử dụng thuộc tính FileName hay FileNames để xử lý tham khảo đến tập tin hay các
tập tin.

OpenFileDialog aOpenFileDialog = new OpenFileDialog();


aOpenFileDialog.Filter = "Text Files (*.txt)|*.txt|Word Documents" +
"(*.doc)|*.doc|All Files (*.*)|*.*";
aOpenFileDialog.ShowReadOnly = true;
aOpenFileDialog.Multiselect = true;
aOpenFileDialog.Title = "Open files for custom application";
if (aOpenFileDialog.ShowDialog() == DialogResult.OK)
{
//Do something useful with aOpenFileDialog.FileName
//or aOpenFileDialog.FileNames
}

aOpenFileDialog.Dispose();

Hộp dialog này có ba tuỳ chọn trong combo box "Files of Type". Một tuỳ chọn là Text Files, một cái khác là Word Documents,
và thứ ba là All Files.

SaveFileDialog

Hộp dialog này rất giống OpenFileDialog, và trong thực tế chúng thừa hưởng từ một lớp cơ sở. Các chức năng cơ bản của hộp
dialog này là cho phép một người dùng chọn một nơi để lưu dữ liệu.
Nhiều thuộc tính giống như lớp OpenFileDialog; tuy nhiên các thuộc tính sau là các thành viên của OpenFileDialog và không
tồn tại trong lớp SaveFileDialog:

• CheckFileExists
• Multiselect

• ReadOnlyChecked

• ShowReadOnly

Nhưng, hai thuộc tính sau chỉ hợp lệ khi là thành viên của lớp SaveFileDialog:

• CreatePrompt
• OverwritePrompt

Chú ý rằng các thuộc tính khác tạo cùng một kiểu, bao gồm các thuộc tính Filter, Title, và FileName. Đoạn mã sau trình bày cách
sử dụng lớp này:

SaveFileDialog aSaveFileDialog = new SaveFileDialog();


aSaveFileDialog.Filter = "Text Files (*.txt)|*.txt|Word Documents" +
"(*.doc)|*.doc|All Files (*.*)|*.*";
aSaveFileDialog.CreatePrompt = true;
aSaveFileDialog.OverwritePrompt = true;
aSaveFileDialog.Title = "Save file for custom application";
if (aSaveFileDialog.ShowDialog() == DialogResult.OK)
{
//Do something useful with aSaveFileDialog.FileName;
}

aSaveFileDialog.Dispose();

PageSetupDialog

Hộp dialog này được dùng để cài định hướng và lề của trang.

Thuộc tính chính trong lớp này là Document. Nó được yêu cầu trước khi phương thức ShowDialog() có thể được gọi, và một
ngoại lệ được ném nếu nó không được gán một giá trị. Thuộc tính Document chấp nhạn một đối tượng PrintDocument, đối tượng
này là thành viên của System.Drawing.Printing namespace. Đối tượng này là cốt yếu cho tiến trình in ấn trong .NET, và tượng
trưng các trang mà ạôt ứng dụng sẽ in. Bằng cách cài các thuộc tính của đối tượng này và sử dụng GDI+ để vẽ bề mặt của nó,
ứng dụng có thể in bởi máy in.

Ví dụ bên dưới mô tả cách các hộp Dialog được gọi và sử dụng:

PageSetupDialog aPageSetup = new PageSetupDialog();


System.Drawing.Printing.PrintDocument aDoc = new
System.Drawing.Printing.PrintDocument();
aPageSetup.Document = aDoc;
if (aPageSetup.ShowDialog() == DialogResult.OK)
{
//Do something useful with aPageSetup.Document;
}

aPageSetup.Dispose();

Đoạn mã này tạo một đối tượng PageSetupDialog mới, liên kết với một đối tượng PrintDocument mới với nó và hiển thị dialog.

PrintDialog

Dialog này được dùng để chọn máy in, số lượng bản sao, và các trang để in trong một tài liệu. Như dialog trước, đối tượng này
yêu cầu một đối tượng PrintDocument có hiệu lực để được liên kết với thuộc tính Document trước khi nó được hiển thị.

Đối tượng cũng chứa các thuộc tính khoá sau:


Property Description

AllowPrintToFile thuộc tính Boolean có thể được cài là true để hiển thị checkbox "Print to file" trog dialog. mặc
định là true.

AllowSelection Thuộc tính Boolean có thể được cài là true để cho phép in ấn chỉ những phần hiện hành. Mặc
định là false.

AllowSomePages thuộc tính Boolean có thể được cài là true để cho biết các tuỳ chọn From Page và To Page là
enabled. Mặc định là false.

Document Thuộc tính PrintDocument mô tả bề mặt in ấn hiện hành.

PrintToFile thuộc tính Boolean có thể được cài là true để cho biết checkbox "Print to file" được checked. khi
diaog trả về nó có thẻ được check để thấy nếu ngừơi dùng muốn ứng dụng in tài liệu vào một tập
tin. Mặc định là false.

ShowHelp Thuộc tính Boolean có thể được cài là true để cho biết nut Help nên được hiển thị. Mặc định là
false.

Giống như các lớp dialog thông thường, các thuộc tính này được định hình trước khi gọi phương thức ShowDialog() để hiển thị
hộp dialog chính xác cho người dùng. Đoạn mã dùng lớp này rất giống với đoạn mã trước:

PrintDialog aPrintDialog = new PrintDialog();


System.Drawing.Printing.PrintDocument aDoc = new
System.Drawing.Printing.PrintDocument();
aPrintDialog.Document = aDoc;
aPrintDialog.AllowSomePages = true;
aPrintDialog.AllowSelection = true;
if (aPrintDialog.ShowDialog() == DialogResult.OK)
{
//Do something useful with aPrintDialog.Document;
}

aPrintDialog.Dispose();

PrintPreviewDialog

Lớp này cung cấp một cách nhanh chóng để giới thiệu các khả năng duyệt trước khi in vào một ứng dụng. Lớp này chấp nhận đối
tượng PrintDocument trong thuộc tính Document của nó, và cùng đoạn mã mà điều khiển việc in ấn của mộtmáy in sẽ trả lại tài
liệu cho hộp dialog này.

Hộp dialog này hỗ trợ co dãn, thu nhỏ, và đánh số trang và một tập các tuỳ chọn khác .

Thực thi ứng dụng với các dialog

Visual Inheritance
.NET Framework lấy khái niệm thừa kế và cho phép một nhà phát triển sử dụng nó để phát triển các ứng dụng Windows Forms.
Một đối tượng Form có thể thừa kế từ một đối tượng Form khác, vì thế chiếm được sự truy cập đến tất cả Buttons, TextBoxes, và
Menus. Nó là một đặc trưng rất mạnh trong .NET khi sử dụng để giảm số lượng mã yêu cầu cho việc tạo các cửa sổ và màn hình
giống nhau. Khái niệm này gọi visual inheritance.

Một Form luôn luôn thừa kế từ System.Windows.Forms. Có nghĩa là nó có thể truy cập đến tất cả thành phần dữ liệu và các
phương thức của lớp Form cơ bản. Việc thực thi sự thừa kế yêu cầu một nhà phát triển thừa hưởng đối tượng Form từ một lớp
Form tuỳ biến thay cho System.Windows.Forms. Đó là nguyên nhân tất cả control và thuộc tính trong lớp Form tuỳ biến truyền
qua các lớp Form được tạo mới.

Tuy nhiên, có vài điều quan trọng phải nhớ. Cấp truy cập của các control khác nhau phải được hiểu, giống như cấp truy cập của
các thừa kế chuẩn. Một thành phần dữ liệu private thì không thể được truy cập bởi bất kỳ đối tượng nào bên ngoài đối tượng ban
đầu. Vì thế, nếu một control không được đánh dấu là protected hay public, lớp thừa hưởng sẽ không tham khảo đến control hay
override bất kỳ phương thức của control.
Sử dụng thừa kế trực quan có thể rất có lợi khi thừa kế tạo ra một số lượng lớn màn hình mà phải có một thiết kế giống nhau
và/hoặc làm các chức năng như nhau. Một ví dụ điển hình là một màn hình thực thể dữ liệu. Nếu ứng dụng của chúng ta không
cần nhập các mẫu tin cá nhân, mà còn thông tin automobie, sử dụng thừa kế trực quan để định nghĩa một kiểu thông thường phải
là một sự chọn lựa tốt. Hiển nhiên, chúng ta sẽ muốn một màn hình trông giống nhau, nhưng vài control sẽ thay đổi. Hãy sửa đổi
ví dụ trước của chúng ta để sử dụng kỹ thuật này.

Tạo ra một Windows Application mới trongVisual Studio .NET và đặt tên nó là VisualInheritance.

Thay đổi các thuộc tính sau của đối tượng Form1 mặc định. Chúng ta sẽ tạo một cửa sổ menu cung cấp cho ngứời dùng các khả
năng nhập các mẫu tin cá nhân hay các mẫu tin automobie.

• FormBorderStyle – FixedDialog
• MaximizeBox – False

• MinimizeBox – False

• Size – 200, 200

• StartPosition – CenterScreen

• Text – Main Menu

Đặt hai control trên Form. Định vị chúng ở giữa cửa sổ, đặt nhãn là Person và Automobie, và đặt tên là btnPerson và btnAuto.
Chúng ta sẽ thêm các bộ điều khiển sự kiện vào sau để mở mọi Form thừa hưởng.

Bây giờ chúng ta sẽ thêm lớp Form cơ bản của chúng ta. Form này sẽ không bao giờ hiển thị trực tiếp, nhưng chúng ta sẽ dùng
kiểu trực quan của nó trong mọi form thừa hưởng.

Thêm một Form mới vào ứng dụng bằng cách chọn Project | Add Windows Form. Bỏ qua các tên mặc định và chọn OK trong
hộp dialog Visual studio.NET. Sửa đổi các thuộc tính sau của Form để tạo một kiểu trực quan duy nhất.

• Name – frmBase
• BackColor – White

• FormBorderStyle – FixedDialog

• MaximizeBox – False

• MinimizeBox – False

• Size – 250, 250

• StartPosition – CenterScreen

• Text – Base Form

Nó sẽ tạo một hộp dialog trắng. Bây giờ thêm hai Buttons vào góc phải của form. Chúng sẽ hành động như hai nút Save và
Cancel. Bằng cách thêm chúng vào lớp cơ bản chúng sẽ được hiện trên các form thừa hưởng, vì thế bảo đảm một giao diện người
dùng thông thường. Định vị hai nút trong góc phải và cài các thuộc tính:

Button Name Anchor Location Modifiers Size Text

Button1 btnSave Bottom, Right 159, 151 Protected 75, 23 Save

Button2 btnCancel Bottom, Right 159, 185 Protected 75, 23 Cancel

Thuộc tính quan trọng nhất phải chú ý đó là Modifiers. Nó có thể cài mức cách ly của lớp Button bên trong form. Nó có thể được
cài bất kỳ mức : public, protected, private, hay internal. Sau khi chỉnh sửa thuộc tính trong cửa sổ Properties, xem xét đoạn mã
để thấy các khai báo của hai đối tượng Button đã chỉnh sửa thành protected. Nó sẽ rất quan trọng trong việc cho phép các đối
tượng Form thừa hưởng truy cập vào Buttons.

Các thành phần protected chỉ có thể được truy cập bởi các lớp thừa hưởng; chúng không được truy cập bởi bất kỳ đoạn mã nào
bên trong. Các Form thừa hưởng không thể truy cập các control khai báo với mức private mặc định. Các buttons sẽ được hiển thị
trên Forms thừa hưởng, nhưng không có bộ điều khiển sự kiện nào có thể được thêm, như các đối tượng không thể được truy cập
từ các lớp thừa hưởng.

Cuối cùng chúng ta sẳn sàn thêm một Form thừa hưởng. Tuy nhiên, Visual studio.NET yêu cầu các lớp Form cơ bản được biên
dịch đầu tiên, vì vậy đầu tiên chúng ta phải xây dựng dự án ít nhất một lần. Khi hoàn thành, chọn Project | Add Inherited Form.
Bỏ qua tên mặc định của tập tin lớp bằng cách click Open trong hộp dialog kết quả. Tiếp đó chọn lớp Form cơ bản đúng để dùng.
Một hộp dialog hiển thị các Form có giá trị hiện tại trong dự án, và cho phép bạn thừa hưởng lớp Form mới từ bất kỳ lớp nào của
chúng. Chọn lớp frmBase và click OK

Một Form mới sẽ được tạo, nhưng nó sẽ trông giống như lớp frmBase ban đầu. Nó có cùng BackColor trắng và hai nút Save và
Cancel. Thay đổi thuộc tính Text của Form theo các thông tin cá nhân, và thêm bốn control Label và bốn control TextBox. Thay
đổi thuộc tính Text của các Label là "First Name:," "Last Name:", "DOB:" và "SSN:" Thay đổi thuộc tính Name của các control
TextBox thành txtFName, txtLName, txtDOB, và txtSSN, và để trống các thuộc tính Text. Form mới sẽ trông giống như màn
hình bên dưới:

Lập lại tiến trình thêm một Form được thừa kế mới vào dự án. Một lần nữa thừa hưởng nó từ lớp frmBase. Thời điểm này thay
đổi thuộc tính Text của Form để thông tin Automobie và thuộc tính Name vào frmAuto và thêm bốn Label vào Form với tựa :
"Manufacturer:", "Model:", "Year:", và "Color:" Thêm bốn control TextBox định vị bên cạnh các Label, và thay đổi thuộc tính
Name thành txtManufact, txtModel, txtYear, txtColor cho mọi TextBox.

Bây giờ chúng ta sẳn sàng thêm các bộ điều khiển sự kiện vào các Form thừa hưởng của ta. Nhớ rằng trong một ứng dụng chức
năng thì nút Save sẽ được dùng giống như ADO.NET hay một đối tượng kinh doanh back-end để lưu dữ liệu vào một kho dữ
liệu. Trong mẫu nàu thì dữ liệu được đưa vào một tập tin XML nhỏ.

Thêm bộ điều khiển sự kiện Click cho Form thông tin cá nhân cả hai nút Save và Cancel. Những nút này được thừa kế từ một lớp
cơ bản, chúng có thể vẫn được thao tác và các sự kiện thêm vào giống như bất kỳ control khác.

private void btnSave_Click(object sender, System.EventArgs e)


{
//Save the values to an XML file
//Could save to data source, Message Queue, etc.
System.Xml.XmlDocument aDOM = new System.Xml.XmlDocument();
System.Xml.XmlAttribute aAttribute;

aDOM.LoadXml("<PersonnelData/>");

//Add the First Name attribute to XML


aAttribute = aDOM.CreateAttribute("FirstName");
aAttribute.Value = txtFName.Text;
aDOM.DocumentElement.Attributes.Append(aAttribute);
//Add the Last Name attribute to XML
aAttribute = aDOM.CreateAttribute("LastName");
aAttribute.Value = txtLName.Text;
aDOM.DocumentElement.Attributes.Append(aAttribute);
//Add the DOB attribute to XML
aAttribute = aDOM.CreateAttribute("DOB");
aAttribute.Value = txtDOB.Text;
aDOM.DocumentElement.Attributes.Append(aAttribute);
//Add the SSN attribute to XML
aAttribute = aDOM.CreateAttribute("SSN");
aAttribute.Value = txtSSN.Text;
aDOM.DocumentElement.Attributes.Append(aAttribute);

//Save file to the file system


aDOM.Save("PersonnelData.xml");
}

private void btnCancel_Click(object sender, System.EventArgs e)


{
txtLName.Text = "";
txtFName.Text = "";
txtDOB.Text = "";
txtSSN.Text = "";
}

Đoạn mã này có vẽ quen với bạn, bởi vì nó giống với đoạn mã XML từ ví dụ trước. Ý tưởng cơ bản là xếp thứ tự các nội dung
của TextBoxes vào một tập tin XML và lưu nó vào một hệ thống tập tin. Nút Cancel để xoá các control TextBox.

Đoạn mã cho Automobile Information Form thì càng giống ngoại trừ sự khác nhau về tên TextBox được dùng và một tập tin
XML khác được tạo. Khi nó không biểu lộ các tin tức chúng ta sẽ không hiện nó.

Cuối cùng , thêm bộ điều khiển sự kiện sau vào sự kiện Click của nút trong Form1.

private void btnPerson_Click (object sender, System.EventArgs e)


{
Form3 aForm = new Form3();
aForm.ShowDialog();
}

private void btnAuto_Click(object sender, System.EventArgs e)


{
Form4 aForm = new Form4();
aForm.ShowDialog();
}

Thực thi ứng dụng VisualInheritance

Windows Controls

Các ứng dụng Windows Forms bao gồm nhiều contrrol khác nhau. Các control này có thể đơn giản như các control Button và
TextBox, hay chúng có thể tinh vi và phức tạp hơn như các control Charting và TreeView. .NET framework có nhiều control sẳn
sàng kết hợp với các ứng dụng Windows Forms, và có hàng trăm control được dùng trong các phát triển ứng dụng .NET tuỳ biến.
Chính vì thế, chúng ta sẽ xem xét cách tất cả control hoạt động và tương tác tại một cấp cao hơn.

Các control trong Windows Forms bao gồm những cái mà một nhà phát triển sẽ muốn tìm trong một thư viện lớp được thiết kế
cho các giao diện người dùng đồ hoạ:

• Labels
• Buttons

• Checkboxes

• Menus

• Radio buttons

• Combo boxes

• Listboxes

• Textboxes

• Tabcontrols

• Toolbars

• Tree views

Như chúng ta thấy, Visual Studio .NET có thể thêm các control này vào một Form cho bạn. Các bước xảy ra khi một control
được thêm vào một Form như sau:
1. Một biến của kiểu control yêu cầu được khai báo như một đối tượng riêng trong lớp Form.
2. Trong phương thức InitializeComponent(), đối tượng control đựơc tạo và gán vào một biến riêng.

3. Các thuộc tính của control, như là Location, Size, và Color được cài bên trong phương thức InitializeComponent().

4. Control được thêm vào tập hợp control trên form.

5. Cuối cùng, các bộ điều khiển sự kiện được thực thi khi nhà phát triển thêm chúng vào thông qua IDE

Mọi control thừa kế từ System.Windows.Forms.Control. Lớp cơ bản này chứa các phương thức và các thuộc tính cơ bản được
dùng bởi bất kỳ control nào cung cấp một giao diện người dùng cho người sử dụng. Control này quản lý chức năng cơ bản được
yêu cầu để chiếm bàn phím và chuột như là định nghĩa kích cở của nó và vị trí trên cha mẹ của nó.

Dynamic Controls

Kể từ khi tất cả control khả thị thừa kế từ lớp Control, chúng ta có thể thấy những thuận lợi của đa hình khi làm việc với các tập
hợp control. Tất cả control chứa một thuộc tính Controls hoạt động như một tập hợp control chứa đựng. Nó cho phép bạn viết mã
lặp qua các tập hợp Controls và vận dụng hay yêu cầu mọi control riêng lẽ sử dụng các thuộc tính và phương thức của lớp
Control.

Thuộc tính Control này thì động và có thể đựơc dùng để tuỳ chỉnh hình thức của giao diện người dùng tại thời gian chạy bằng
cách thêm và xoá các control vào một Form hay Control. Giống như tất cả tập hợp, tập hợp control có các phương thức cho phép
thêm và huỹ các đối tượng, đó là nguyên nhân các control đựơc thêm vào và huỹ từ các giao diện người dùng. Nó có thể là một
kỹ thuật mạnh để thiết kế giao diện người dùng tuỳ biến. Trên thực tế, nếu bạn xem xét phương thức InitializeComponent() tạo
bởi Visual studio.NET bạn sẽ thấy chính xác cách đoạn mã thêm các control vào các Form, và nó cũng là cách mà chúng ta thêm
control Button vào ứng dụng Windows Forms đầu tiên của chúng ta ở đầu chương. Hãy tạo một ứng dụng có các thuận lợi về khả
năng này để tuỳ chỉnh giao diện người dùng tại thời gian chạy.

Một ứng dụng thông thường yêu cầu các màn hình khác nhau cho mọi đối tượng khác nhau. Một ví dụ là một hệ thống quản lý
hàng tồn, hệ thống này phải quản lý các máy tính, phần mềm, và trang bị. Mọi đối tượng này có các thuộc tính duy nhất; tuy
nhiên chúng chia sẽ một vài đặc tính chung. Phụ thuộc vào thiết kế giao diện, nó rất hữu ích để tuỳ chỉnh giao diện người dùng
dựa vào kiểu đối tượng đang đựơc thao tác trên hệ thống và chỉ hiển thị các trường đó. Tuỳ chỉnh động của giao diện người dùng
này có thể được hoàn hảo bởi việc vận dụng thuộc tính Control tại thời gian chạy.

Mở Visual Studio .NET và tạo một ứng dụng Window C# mới với tiêu đề DynamicUI.

Thêm ba control Button ở đâu đó trên bên trái của Form. Gán các nút với thuộc tính Text là Computer, Software, và Furniture.
Form nên giống như bên dưới:

Khi một ngừơi dùng chọn nút thích hợp thì giao diện người dùng sẽ tự động tuỳ chỉnh để nhập kiểu đối tượng đó. Một ứng dụng
xí nghiệp sẽ dùng các cài đặt này từ một nguồn dữ liệu back-end hay tập tin định hình; tuy nhiên, chúng ta sẽ nhấn mạnh tính
logic của hiển thị một cách trực tiếp trong ứng dụng. Nó không là một ví dụ tốt, và nếu ví dụ này đựơc mở rộng thì bước đầu tiên
là tạo một phương thức tượng trưng cho giao diện người dùng.

Có một số thuộc tính chúng ta sẽ cài đặt cho mọi control chúng ta thêm vào Form. Nó bao gồm các thuộc tính Size và Location
khi các control được định vị ở đâu đó. Chúng ta cũng thường cài các thuộc tính Text và Name. Khi thêm một số lượng lớn control
vào một form thì tiến trình này có thể nhanh chóng dẫn đến dư thừa và lặp lại mã, do đó để tránh chúng ta sẽ tạo một phương
thức tiện ích để cài các thuộc tính này một lần. Chúng ta có thể gọi phương thức này cho mọi control mà chúng ta thêm vào
Form. Thêm các phương thức riêng vào lớp Form1.

private void AddControl(Control aControl, Point Location, Size Size,


String strText, int TabIndex, string strName)
{
aControl.Location = Location;
aControl.Size = Size;
aControl.Text = strText;
aControl.TabIndex = TabIndex;
aControl.Name = strName;
this.Controls.Add(aControl);
}

Phương thức này chấp nhận một đối tượng Control và cài các thuộc tính public trên nó. Chú ý rằng khi chúng tra gọi phương
thức này chúng ta sẽ truyền vào một lớp thừa hưởng, giống như một Label hay TextBox. Việc này có thể làm được thông qua đa
hình, bởi vì lớp Control cơ bản định nghĩa các thuộc tính được truy cập bởi các thuộc tính này.

Bây giờ, chúng ta thêm các bộ điều khiển sự kiện cho các Button. Thêm một bộ điều khiển sự kiện Click cho mọi nút và thêm
đoạn mã sau. Tên của các bộ điều khiển sự kiện sẽ khác nhau phụ thuộc vạo tên của các Button:

private void btnComp_Click(object sender, System.EventArgs e)


{
Controls.Clear();
InitializeComponent();

AddControl(new Label(),new Point(125,24),new Size(45,20),"ID:",0,"");


AddControl(new TextBox(),new Point(174,21),new Size(125, 20),
"",0,"txtID");
AddControl(new Label(),new Point(125,54),new Size(45,20),"OS:",0,"");
AddControl(new TextBox(),new Point(174,50),new Size(125,20),
"",1,"txtOS");
AddControl(new Label(),new Point(125,84),new Size(45,20),
"Speed:",0,"");
AddControl(new TextBox(),new Point(174,78),new Size(125,20),
"",2,"txtSpeed");
}

private void btnSoft_Click(object sender, System.EventArgs e)


{
Controls.Clear();
InitializeComponent();

AddControl(new Label(),new Point(125,24),new Size(45,20),


"ID:",0,"");
AddControl(new TextBox(),new Point(174,21),new Size(125, 20),
"",0,"txtID");
AddControl(new Label(),new Point(125,54),new Size(45,20),
"Vendor:",0,"");
AddControl(new TextBox(),new Point(174,50),new Size(125, 20),
"",1,"txtVendor");
AddControl(new Label(),new Point(125,84),new Size(45,20),
"Name:",0,"");
AddControl(new TextBox(),new Point(174,78),new Size(125, 20),
"",2,"txtName");
}

private void btnFurn_Click(object sender, System.EventArgs e)


{
Controls.Clear();
InitializeComponent();

AddControl(new Label(),new Point(125,24),new Size(45,20),"ID:",0,"");


AddControl(new TextBox(),new Point(174,21),new Size(125, 20),
"",0,"txtID");
AddControl(new Label(),new Point(125,54),new Size(45,20),
"Color:",0,"");
AddControl(new TextBox(),new Point(174,50),new Size(125, 20),
"",1,"txtColor");
AddControl(new Label(),new Point(125,84),new Size(45,20),
"Type:",0,"");
ComboBox aCombo = new ComboBox();
aCombo.Items.AddRange(new Object[] {"Desk","Chair","Whiteboard"});
AddControl(aCombo,new Point(174,78),new Size(125, 20),"",2,"");
}

Chạy ứng dụng và chọn các tuỳ chọn khác nhau thông qua các nút. Bạn sẽ thấy giao diện người dùng tự tuỳ chỉnh dựa vào kiểu
nhập vào từ người dùng.
Thực thi ứng dụng DynamicUI

Tóm tắt

Chúng ta đã được xem xét nhiều nội dung trong chương này. Xây dựng các ứng dụng Window là một chủ đề lớn. Hy vọng
chương này đã giúp bạn hiểu biết về các nguyên tắc cơ bản khi xây dựng các ứng dụng Window Form.

Trong chương này chúng ta đã được xem xét những nội dung sau:

• Cách xây dựng các ứng dụng Windows Forms sử dụng .NET Framework
• Sử dụng Visual Studio .NET để xây dựng các ứng dụng Windows Forms một cach nhanh chóng.

• Thêm một menu hỗ trợ cho một ứng dụng, bao gồm các menu dynamic và theo ngữ cảnh.

• Sử dụng các dialog custom và common trong một dự án.

• Cách sử dụng visual inheritance để xây dựng các ứng dụng Windows Forms mạnh và dynamic.

• Cách sử dụng các control Windows Forms trong một ứng dụng.

• Cách tạo và mở rộng các control cho chức năng đặc biệt.

• Tìm hiểu về các sự kiện tuỳ biến từ một control tuỳ biến.

Chương 8: Assemblies
Tổng quan
Trong chương này chúng ta sẽ thảo luận về Assemblies. Assembly là khoá học về .NET dành cho một bộ phận về phát triển
và cấu hình. Chúng ta sẽ thảo luận một cách chính xác chúng là gì, chúng được sử dụng như thế nào, và tại sao chúng được xem
là một tính năng hữu dụng. Cụ thể chúng ta sẽ học :
• Sự đổi mới được cung cấp bởi assembly vượt trội hơn công nghệ trước đây.
• Cách để tạo và hiển thị Assembly.
• Đặc tả ngôn ngữ chung (Common Language Specification CLS ) là gì, Sự hỗ trợ ngôn ngữ xuyên xuốt có được khả
năng như thế nào.
• Làm thế nào để tạo resource-only assemblies và sử dụng chúng tại một vị trí xác định.
• Làm thế nào để dùng chung Assemblies- để làm chúng ta phải biết tạo ra các tên duy nhất và xem xét phiên bản sử
dụng.

Nào chúng ta bắt đầu chương này với tổng quan về assemblies.

Cấu trúc Assembly


Một assembly thường bao gồm nhiều thành phần như:
• Một Assembly metadata mô tả trọn vẹn assembly
• Một Type Metadata mô tả các kiểu dữ liệu và các phương thức
• Một MSIL code là đoạn mã viết theo ngôn ngữ trung gian
• Các Resources là những tài nguyên như hình ảnh, chuỗi . . .
Được thể hiện như hình dưới đây :
Hình trên đây chỉ gồm một file component.dll duy nhất, tất cả nằm trọn trong một tâp tin.
Tuy nhiên một assembly cũng có thể trải dài trên nhiều tập tin mà ta gọi là multifile assembly Ở
hình dưới đây assembly được trải dài trên ba tập tin và ba tập tin kể trên hình thành một
assembly duy nhất.

Metadata là gì ?
Metadata là thông tin được lưu trữ trên assembly mô tả những kiểu dữ liệu và những phương
thức thuộc assembly cung cấp những tin tức hữu ích khác liên quan đến assembly .
Chính nhờ metadata nên các assembly thường được gọi là seft-describing (mô tả bản thân) vì nó
mô tả một cách trọn vẹn mỗi module.
Assembly Manifests
Mỗi assembly đều có một mainfest được gắn liền còn được gọi là assembly metadata mô tả
những gì được chứa trong assembly, bao gồm :
• Tên nhận diện (identity name)
• Một danh sách các tập tin thuộc assembly. Một assembly đơn lẻ phải có ít nhất một tập
tin, nhưng có thể chứa số các tập tin.
• Một danh sách các assembly được qui chiếu. tất cả các tập tin khác được dùng bởi
assembly này bao gồm số phiên bản và các khoá dùng chung. Khoá dùng chung được sử
dụng duy nhất để xác định assemblies.
• Một bộ các yêu cầu cho phép (permission request) những quyền hạn cần có dể được phép
chạy assembly này.
• Một danh sách các kiểu dữ liệu và nguồn lực trong assembly, một bản đồ nối liền các
kiểu dữ liệu public với các đoạn mã thi công.

Private and Shared Assemblies

Assemblies nằm ở hai dạng: private assembly và shared assembly. Dạng thứ nhất private
assembly là một tập hợp (collection) những kiểu dữ liệu chỉ dành riêng cho một ứng dụng sử
dụng. Dạng thứ hai shared assembly thì lại được chia sẽ sử dụng bởi nhiều ứng dụng.

Viewing Assemblies
Assemblies có thể được hiển thị khi sử dụng tiện ích command-line ildasm dưới hỗ trợ của
MSIL . Một assembly được mở khi bắt đầu ildasm từ command_line với đối số assembly hoặc
chọn lựa từ File | open menu
hình sau sẽ miêu tả một ildasm khi chúng ta xây dựng HelloCSharp.exe. ildasm hiển thị the
manifest, và HelloCSharp type trong Wrox.ProCSharp.Assemblies.CrossLanguage namespace.
Chúng ta có thể thấy the version number, và thuộc tính assembly ngay khi mở assemblies và
versions của chúng. Mở phương thức của lớp , chúng ta có thể thấy đuợc đoạn mã MSIL

ildasm Symbols

Các kí hiệu được sử dụng với ildasm trình bày dưới đây:

Symbol Description

Biểu diễn một namespace.

Biểu diễn một kiểu tham chiếu một lớp.Tương tự các ký hiệu được sử
dụng bởi các kiểu giá trị, nó có màu sáng

Biểu diễn một phương thức nhận và thiết lập các accessors của một thuộc
tính; "S" trong đồ hoạ có nghĩa là phương thức tĩnh

Biểu diễn một trường

Biểu diễn một sự kiện

Biểu diễn một thuộc tính

Có nghĩa là có nhiều thông tin có giá trị như thông tin manifest hoặc
thông tin về miêu tả một lớp
Xây dựng (Building Assemblies)

Chúng ta vừa biết được assembly tiếp theo chúng ta biết cách để xây dụng chúng. chúng ta sẽ
xây dựng xuyên suốt bời vì .NET có khả năng thi hành assembly phức tạp.nhưng chúng ta hãy
xem trường hợp cụ thể cho assemblies.

Tạo các Modules và Assemblies

Tất cả dự án (project) C# trong Visual Studio .NET đều tạo một assembly. Cho dù bạn chọn
một kiểu dự án DLL hoặc EXE , một assembly luôn được tạo. Với trình biên dịch command-line
C# csc, Nó cũng có khả năng tạo modules. Một module là một DLL assembly không thuộc tính
(nên nó không có assembly, nhưng nó được thêm vào một assemblies lúc sau). Dòng lệnh như
sau:

csc /target:module hello.cs

Tạo một module hello.netmodule. Nó có thể nhìn thấy module này khi sử dụng ildasm.

Một module cũng có một manifest, Nhưng chúng không có phần .assembly bên trong the
manifest (ngoại trừ assemblies nội , Chúng được kham khảo), Vì một module không có assembly
thuộc tính. Nó không thể được cấu hình versions hoặc chấp nhận với modules; Chúng chỉ có thể
tại assembly gốc. Trong manifest của module, kham khảo đến assemblies có thể được tìm thấy.

Để so sánh modules của assemblies, Ta tạo một lớp A và biên dịch nó:

csc /target:module A.cs

Biên dịch tạo ra một file A.netmodule, Nó không có thông tin của assembly (như chúng ta
thấy khi sử dụng ildasm nhìn thấy ở manifest). The manifest của module miêu tả kham khảo
assembly mscorlib và the .module entry:

Tiếp theo ,Ta tạo một assembly B nó bao gồm module A.netmodule. Nó không cần thiết
source file để sinh ra assembly này. Dòng lệnh sinh assembly là:

csc /target:library /addmodule:A.netmodule /out:B.dll

Khi nhìn vào assembly sử dụng ildasm, chỉ có một manifest có thể thấy. Trong manifest,
assembly mscorlib là kham khảo .Tiếp theo chúng ta thấy assembly section với một thuật toán
(algorithm) và version. số lượng algorithm phụ thuộc loại algorithm được sử dụng để tạo ra code
của assembly. Khi tạo một assembly lập trình đi vào chọn lựa algorithm. Phần của manifest là
một danh sách tất cả modules đang thuộc assembly. Ta thấy .module A.netmodule phụ thuộc
assembly. Các lớp xuất ra ngoài từ các modules là phần assembly manifest;
Mục đích của modules là cái gì? Modules có thể giúp khởi động nhanh hơn assemblies bởi vì
không phải tất cả ở trong một file đơn lẻ. modules chỉ kích hoạt khi cần. Một lí do khác của
modules là nếu bạn muốn tạo 1 assembly với nhiều hơn 1 ngôn ngữ lập trình; một module có thể
được viết sử dụng VB.NET, module khác sử dụng C#,và cả hai modules có thể được chứa trong
1 assembly đơn lẻ.

Tạo Assemblies sử dụng Visual Studio .NET

Như vừa đề cập tất cả loại dự án (project types) trong Visual Studio .NET tạo assemblies. Với
Visual Studio .NET 7.0 chúng không hổ trợ để tạo thư mục modules.

Khi tạo một Visual Studio .NET project, Nguồn file AssemblyInfo.cs được sinh ra một cách tự
động. Chúng ta sử dụng sourcecode bình thường soạn thảo để cấu hình assembly thuộc tính file
này. Đây là file được sinh ra từ wizard:

using System.Reflection;
using System.Runtime.CompilerServices;

//
// General Information about an assembly is controlled through
the following
// set of attributes. Change these attribute values to modify
the
// information associated with an assembly.
//

[assembly: AssemblyTitle("")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("")]
[assembly: AssemblyCopyright("")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

//
// Version information for an assembly consists of the following
four
// values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the
Revision and Build
// Numbers by using the '*' as shown below:

[assembly: AssemblyVersion("1.0.*")]

//
// In order to sign your assembly you must specify a key to use.
Refer to
// the Microsoft .NET Framework documentation for more
information on
// assembly signing.
//
// Use the attributes below to control which key is used for
signing.
//
// Notes:
// (*) If no key is specified - the assembly cannot be signed.
// (*) KeyName refers to a key that has been installed in the
Crypto
// Service Provider (CSP) on your machine.
// (*) If the key file and a key name attributes are both
specified, the
// following processing occurs:
// (1) If the KeyName can be found in the CSP - that key is
used.
// (2) If the KeyName does not exist and the KeyFile does
exist, the
// key in the file is installed into the CSP and used.
// (*) Delay Signing is an advanced option - see the
Microsoft .NET
// Framework documentation for more information on this.
//

[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("")]
[assembly: AssemblyKeyName("")]

Sau đâu là danh sách tất cả assembly thuộc tính tương đương với các lớp trong
System.Reflection namespace.

Assembly Attribute Description

AssemblyCompany Tên công ty

AssemblyConfiguration Thông tin xây dựng, hay thông tin debug

AssemblyCopyright and Thông tin bản quyền và nhà sản xuất


AssemblyTrademark

AssemblyDefaultAlias Thuộc tính một bí danh được xác định

AssemblyDescription Miêu tả assembly hoặc sản phẩm. Xem ở thuộc tính


Assembly Attribute Description

của file thi hành.

AssemblyProduct Tên của sản phẩm nơi assembly phụ thuộc .

AssemblyInformationalVersion Thuộc tính này không dùng cho kiểm tra fiên bản khi
assemblies được kham khảo, nó chỉ cung cấp thông
tin Xác định phiên bản của một chương trình ứng
dụng.

AssemblyTitle Sử dụng để cho một tên thân thiện, bao gồm cả


khoảng trắng.

Ví dụ các thuộc tính đã được cấu hình :

[assembly: AssemblyTitle("Professional C#")]


[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("Retail version")]
[assembly: AssemblyCompany("Wrox Press")]
[assembly: AssemblyProduct("Wrox Professional Series")]
[assembly: AssemblyCopyright("Copyright (C) Wrox Press 2002")]
[assembly: AssemblyTrademark("Wrox is a registered trademark of
Wrox Press Ltd")]
[assembly: AssemblyCulture("en-US")]

Thuộc tính tương đương với các lớp trong namespace System.Reflection :

• AssemblyCulture nói về văn hoá của assembly. Chúng ta sẽ nói về nền văn hoá theo từng
địa phương.
• AssemblyDelaySign, AssemblyKeyFile, and AssemblyKeyName được sử dụng để tạo
strong names cho shared assemblies.

• AssemblyVersion đặc tả số phiên bản của assembly. Phiên bản rất quan trong khi sử dụng
assemblies chia sẽ.

Hỗ trợ ngôn ngữ xuyên xuốt (Cross-Language Support)

Trước tiên chúng ta biết Common Type System (CTS) là gì ? .NET định nghĩa thế nào các
kiểu giá trị và các kiểu tham chiếu. Bố trí bộ nhớ của các kiểu dữ liệu. Nhưng CTS không đảm
bảo kiểu mà nó định nghĩa từ bất kì ngôn ngữ nào, được sử dụng từ bất kì ngôn ngữ khác. Đây là
vai trò của Common Language Specification (CLS). CLS xác định yêu cầu tối thiểu của các
kiểu dữ liệu mà chúng được hỗ trợ bởi .NET language.

Chúng ta đề cập một cách ngắn gọn về CTS và CLS Trong chương này chúng ta sẽ được học:

• Common Type System (CTS) và Common Language Specification (CLS)


• Ngôn ngữ độc lập trong hành động (Language independence in action) tạo bởi C++,
Visual Basic .NET và C# . Chúng ta quan sát mã MSIL để biết chúng được sinh ra từ
trình biên dịch của chúng.

• Những yêu cầu của đặc tả ngôn ngữ chung (Common Language Specification.).
CTS và CLS
Tất cả các kiểu dữ liệu được khai báo dưới sự chỉ đạo của Common Type System (CTS).
CTS định nghĩa một bộ quy tắc mà trình biên địch phải tuân thủ, tham chiếu, sử dụng, và lưu trữ
cả hai kiểu tham khảo và kiểu giá trị. Do đó CTS, các đối tượng được viết bằng các ngôn ngữ
khác nhau có thể được tương tác lẫn nhau.

Tuy nhiên, Không phải tất cả các kiểu đều sử dụng trong tất cả ngôn ngữ lập trình. Để xây dựng
các thành phần có thể truy cập từ tất cả ngôn ngữ .NET , Đặc tả ngôn ngữ chung Common
Language Specification (CLS) nên được sử dụng. Với CLS, Trình biên dịch có thể kiểm tra sự
hợp lệ đoạn code trong đặc tả của CLS .

Nhiều ngôn ngữ hỗ trợ .NET không chỉ hạn chế các tập con tính năng chung, nó được định nghĩa
với CLS; thậm chí với .NET nó vẫn có khả năng tạo các thành phần mà không thể sử dụng từ các
ngôn ngữ khác.

.NET Framework đã được thiết kế với mục tiêu hỗ trợ đa ngôn ngữ. Trong suốt giai đoạn thiết
kế .NET, Microsoft đã mời nhiều nhà sản xuất trình biên dịch để xây dựng .NET languages cho
chính họ. Chính Microsoft phân phối Visual Basic .NET, Managed C++, C#, J#, và JScript.NET.
Thêm vào đó hơn hai mươi ngôn ngữ từ các nhà sản xuất khác nhau như COBOL, Smalltalk,
Perl, và Eiffel. Mỗi loại đều có những thuận lợi riêng biệt và nhiều tính năng khác nhau. Các
trình biên dịch của những ngôn ngữ này đều được mở rộng để hỗ trợ .NET.

Quan trọng
CLS là đặc tả những yêu cầu tối thiểu mà một ngôn ngữ phải hỗ trợ. Có nghĩa là
nếu chúng ta giới hạn các phương thức công cộng (public methods) của chúng ta
cho CLS, thì tất cả các ngôn ngữ đang hỗ trợ .NET có thể sử dụng các lớp của
chúng ta !

Common Type System (CTS)


Common Type System ( còn gọi là Common Type Specification, đặc tả kiểu dữ liệu thông
dụng), có nghĩa là ngôn ngữ trung gian IL (Intermediate Language) xuất hiện với một lô kiểu dữ
liệu bẩm sinh (predefined, hoặc built-in) được định nghĩa rõ ràng. Trong thực tế, những kiểu dữ
liệu này được tổ chức theo một đẳng cấp kiểu (type hierachy), điển hình trong môi trường thiên
đối tượng.
Lý do CTS rất quan trọng là, nếu một lớp được dẫn xuất từ một lớp nào đó hoặc chứa những thể
hiện (instance) của những lớp khác, nó cần biết kiểu dữ liệu mà các lớp khác sử dụng đến. Trong
quá khứ, chính việc thiếu một cơ chế khai báo loại thông tin này là một cản trở cho việc kế thừa
xuyên ngôn ngữ. Loại thông tin này đơn giản không có trong tập tin .EXE chuẩn hoặc DLL.

Một phần nào đó, vấn đề lấy thông tin về kiểu dữ liệu đã được giải quyết thông tin qua
metadate trong assembly. Thí dụ, giả sử bạn đang viết một lớp theo C#, và bạn muốn lớp này
được dẫn xuất từ một lớp được viết theo VB.NET. Muốn làm được điều này, bạn sẽ yêu cầu
trình biên dịch qui chiếu về assembly mà lớp VB.NET đã được khai báo. Lúc này, trình biên
dịch sẽ dùng metadata trên assembly này để lần ra tất cả các phương thức, thuộc tính và vùng
mục tin (field), v.v... liên quan đến lớp VB.NET. Rõ ràng là trình biên dịch cần đến thông tin này
để có thể biên dịch đoạn mã của bạn.

Tuy nhiên, trình biện dịch lại cần đến nhiều thông tin hơn những gì metadata cung cấp. thí dụ,
giả sử một trong những phương thức trên lớp VB.NET được định nghĩa trả về một Integer - là
một trong những kiểu dữ liệu bẩm sinh trên VB.NET. Tuy nhiên, C# lại không có kiểu dữ liệu
mang tên Integer này. Rõ là chúng ta chỉ có thể dẫn xuất từ lớp và sử dụng phương thức này và
sử dung kiểu dữ liệu trả về từ đoạn mã C# nếu trình biên dịch biết làm thế nào ánh xạ (map) kiểu
dữ liệu Integer của VB.NET lên một kiểu dữ liệu nào đó được định nghĩa trên C#.

Điều này có thể thực hiện được vì CTS đã định nghĩa những kiểu dữ liệu bẩm sinh có sẵn trên
Intermediate Language (IL), như vậy tất cả các ngôn ngữ tuân thủ .NET Framework sẽ kết sinh
ra đoạn mã dựa cuối cùng vào những kiểu dữ liệu này. Thí dụ, ta thử xét lại Integer của
VB.NET, hiện là một số nguyên có dấu 32-bit, được ánh xạ lên kiểu dữ liệu Int32 của IL. Như
vậy đây sẽ là kiểu dữ liệu được khai báo trong đoạn cấp mã nguồn, C# dùng từ chốt int để ám
chỉ int32, do đó trình biên dịch sẽ cư xử với phương thức VB.NET xem như hàm sẽ trả về một
int.

Nhìn chung, CTS là một đặc tả hình thức có nhiệm vụ mô tả một kiểu dữ liệu nào đó (lớp, cấu
trúc, giao diện, kiểu dữ liệu bẩm sinh, v.v...) phải được định nghĩa thế nào để CLR có thể chấp
nhận làm việc. Ngoài ra, CTS cũng định nghĩa một số cấu trúc cú pháp (chẳng hạn nạp chồng
các tác từ - overloading operrator) mà một số ngôn ngữ "ăn ý với .NET" (.NET aware language)
có thể chấp nhận hỗ trợ hoặc không. Khi bạn muốn xây dựng những đoạn mã có thể đem sử
dụng bởi tất cả các ngôn ngữ .NET aware, bạn cần tuân thủ các quy tắc của CLS ( ta sẽ xem sau
trong chốc lát) khi trưng ra những kiểu dữ liệu.

Common Language Specification ( CLS, đặc tả ngôn ngữ thông


dụng)
Làm việc "ăn ý" với CTS để bảo đảm suôn sẻ việc hợp tác liên ngôn ngữ. CLS đề ra một số
chuẩn mực mà tất cả các trình biên dịch nhắm vào .NET Framework phải chấp nhận hổ trợ. Có
thể có một người viết trình biên dịch muốn hạn chế khả năng của một trình biên dịch nào đó
bằng cách chỉ hổ trợ một phần nhỏ những tiện nghi mà IL và CTS cung cấp. Việc này không hề
gì miễn là trình biên dịch hổ trợ mọi thứ đã được định nghĩa trong CLS.

Để lấy một thí dụ, CTS định nghĩa một kiểu dữ liệu số nguyên có dấu 32-bit, int32 và không
dấu, uint32. C# nhận biết hai kiểu dữ liệu này là int và uint. Nhưng VB.NET thì chỉ công nhận
độc nhất int32, mang từ chốt integer mà thôi.

Ngoài ra, CLS hoạt động theo hai cách. Trước tiên, có nghĩa là những trình biên dịch riêng rẽ
không nhất thiết phải mạnh, chịu hổ trợ tất cả các chức năng của .NET Framework, đây là một
cách khuyến khích việc phát triển những trình biên dịch đối với ngững ngôn ngữ khác sử dụng
sàn diễn .NET. Thứ đấy, CLS đưa ra một bảo đảm nếu bạn chỉ giới hạn các lớp của bạn vào việc
trưng ra những chức năng chiều ý CLS (CLS-compliant), thì CLS bảo đảm là đoạn mã viết theo
các ngôn ngữ khác có thể sử dụng các lớp của bạn. Thí dụ, nếu bạn muốn đoạn mã của bạn
là CLS-compliant, bạn sẽ không có bất cứ phương thức nào trả về uint32, vì kiểu dữ liệu này
không phải là thành phần của CLS. Nếu bạn muốn, bạn cũng có thể trả về một Uint32, những lúc
ấy không bảo đảm đoạn mã của bạn hoạt động xuyên ngôn ngữ. Nói cách khác, hoàn toàn chấp
nhận khi bạn viết một đoạn mã không CLS-compliant. Nhưng nếu bạn làm thế, thì không chắc gì
đoạn mã biên dich IL của bạn hoàn toàn độc lập về mặt ngôn ngữ (language independent).

CLS là một tập hợp những quy tắc hướng dẫn mô tả một cách chi tiết sống động những tính
năng (feature) tối thiểu và trọn vẹn mà một trình biên dịch .NET aware nào đó phải chấp nhận hổ
trợ để có thể tạo ra đoạn mã chiều ý CLS, và đồng thời có thể được sử dụng theo một cách đồng
nhất giữa các ngôn ngữ theo đuôi sàn diễn .NET. Trong chừng mực nào đó, CLS có thể được
xem như một tập hợp con (subset) của chức năng chọn vẹn được định nghĩa bởi CTS.

CLS bao gồm một tập hợp những qui tắc mà các nhà tạo công cụ phải tuân thủ nếu muốn sản
phẩm của mình làm ra có thể hoạt động không trục trặc trong thế giới .NET. Mỗi qui tắc sẽ mang
một tên (chẳng hạn "CLS rule 6"), và mô tả qui tắc này sẽ ảnh hưởng thế nào đối với người thiết
kế công cụ cũng như đối với người tương tác với công cụ.

Ngoài ra, CLS còn định nghĩa vô số qui tắc khác; chẳng hạn CLS mô tả một ngôn ngữ nào đó
phải biểu diễn thế nào các chuỗi chữ, liệt kê kiểu enumeration phải được biểu diễn thế nào về
mặt nội tại, v.v... Nếu bạn muốn biết chi tiết về CLS, thì mời tham khảo MSDN trên máy của
bạn.

Những yêu cầu của CLS

Chúng ta chỉ thấy hoạt động CLS khi chúng ta đã khoá kế thừa cross-language giữa MC++,
VB.NET, và C#. Cho đến bây giờ chúng ta không cần chú ý đến yêu cầu của CLS khi xây dựng
project. Chúng ta may mắn phương thức chúng ta định nghĩa trong các lớp cơ bản đã có thể gọi
từ các lớp dẫn xuất. Nếu phươnghtức của chúng ta có kiểu dữ liệu System.UInt32 như là thông
số của nó chúng ta không thể sử dụng nó từ VB.NET. Kiểu dữ liệu không tên không có CLS-
compliant; nó không cần thiết hỗ trợ kiểu dữ liệu này.

Common Language Specification chính xác định nghĩa những yêu cầu để tạo thành phần CLS-
compliant, với khả năng này nó được sử dụng với các ngôn ngữ .NET khác nhau. Với COM
chúng ta phải chú ý đến yêu cầu đặc tả ngôn ngữ khi thiết kế một thành phần. JScript có yêu cầu
khác với VB6, và điều kiện của VJ++ cũng khác. Trong trường hợp không phải .NET. khi thiết
kế một thành phần nên sử dụng những ngôn ngữ khác nhau chúng ta chỉ phải tạo CLS cho nó,
hoặc CLS_compliant; Nó đảm bảo rằng thành phần này có thể sử dụng từ tất cả ngôn ngữ .NET,
nếu bạn đánh dấu một lớp như CLS, compliant, trình biên dịch có thể báo trước chúng ta về
phương thức non-compliant.

Common Language Runtime (CLR)


Từ ngữ runtime ( vào lúc chạy) ám chỉ một tập hợp những dịch vụ cần thiết để cho thi hành một
đoạn mã nào đó. Mỗi ngôn ngữ đều có riêng cho mình một runtime library hoặc runtime module.
Thí dụ, MFC ( Microsoft Foundation Class) hoạt động cùng với Visua C++ cần kết nối với MFC
runtime library (mfc42.dll), Visua Basic 6.0 thì lại gắn chặt với một hoặc hai runtime module
(msvbvm60.dll), còn Java Virtual Machinee (JVM) như là module runtime.

Với sàn diễn .NET, bộ phận runtime được gọi là Comon Language Runtime (CLR) có hơi
khác với những runtime kể trên: CLR cung cấp một tầng lớp (layer) runtime duy nhất được định
nghĩa rõ ràng và được chia sẻ sử dụng bởi tất cả các ngôn ngữ .NET aware ("ăn ý" với .NET).

CLR gồm hai bộ phận chủ chốt :

Phần thứ nhất, là "cỗ máy" thi hành vào lúc chạy (runtime execution engine), mang tên
mscoree.dll ( tắt chữ Microsoft Core Execution Engine). Khi một assembly được gọi vào thi
hành, thì mscoree.dll được tự động nạp vào, và đến phiên nó nạp assembly cần thiết vào ký ức.
Runtime Execution Engine chịu trách nhiệm thực hiện một số công tác. Trước tiên, nó phải lo
việc giải quyết "vị trí đóng quân" của assembly và tìm ra kiểu dữ liệu yêu cầu ( nghĩa là lớp, giao
diện, cấu trúc, v.v...) thông qua metadata trong assembly. Execution Engine biên dịch IL được
gắn liền dựa theo chỉ thị cụ thể sàn diễn, tiến hành một số kiểm tra an toàn cũng như một số công
việc liên hệ.

Phần thứ hai của CLR là thư viện lớp cơ bản, mang tên mscorlib.dll ( tắt chữ Microsoft Core
Library), chứa vô số công tác lập trình thông dụng. Khi bạn xây dựng .NET Solutions ( giải pháp
.NET) bạn sẽ cần đến nhiều phần thư viện này.

Global Assembly Cache

Global assembly cache là một tên ngụ ý, một nơi lưu trữ (cache) cho toàn bộ các assemblies
sẵn dùng. Hầu hết shared assemblies được cài đặt bên trong cache này, nhưng một vài private
assemblies cũng được tìm thấy ở đây. Nếu một private assembly được biên dịch thành ngôn ngữ
máy sử dụng sinh ra ảnh, mã máy được biên dịch cũng được đưa vào trong cache này.

Nội dung chính:

• Tạo ảnh bẩm sinh (native images) lúc cài đặt


• Tổng quan shared assemblies với Global Assembly Cache Viewer và Global Assembly
Cache Utility

Native Image Generator


Với native image generator tiện ích Ngen.exe chúng ta có thể biên dịch mã IL thành ngôn ngữ
máy ngay lúc cài đặt. Bằng cách này chương trình khởi đầu nhanh hơn vì sự biên dịch trong quá
trình chạy là không cần thiết. Tiện ích ngen cài đặt native image trong native image cache, nó là
một phần của global assembly cache.

Lưu ý:
Tạo native images với ngen chỉ cần thiết nếu native images được tạo cho tất cả
assemblies dùng bởi ứng dụng. nếu không trình biên dịch JIT đã phải bắt đầu một
cách không hệ thống.

Với ngen myassembly, chúng ta có thể biên dịch mã MSIL thành mã máy, và cài đặt nó vào
bên trong nơi lưu native image . Điều này nên được thực hiện từ chương trình cài đặt nếu chúng
ta muốn đặt assembly trong native image cache.

Lưu ý:
Sau khi biên dịch assembly thành mã máy bạn không thể huỷ assembly ban đầu với
mã MSIL bởi bì metadata vẫn còn cần đến và nếu bảo vệ thay đổi trên hệ thống mã
máy sẽ được xây dưng lại.

Với ngen chúng ta cũng hiển thị tất cả assemblies từ native image cache bằng cách chọn
option /show. Nếu chúng ta thêm tên assembly thì vào /show option chúng ta lấy thông tin về
phiên bản đã cài đặt của assembly này:

Global Assembly Cache Viewer


GAC Viewer được hiển thị sử dụng shfusion.dll, Nó là một tiện ích của Windows (Windows shell extension )để hiển thị và chế
tác các nội dung của cache. Windows shell extension là một COM DLL nó kết hợp với Windows explorer. Bạn phải mở explorer
và dùng <windir>/assembly directory.

Hình sau là GAC viewer:


Và thuộc tính của nó như sau:

Global Assembly Cache Utility (gacutil.exe)


Assembly viewer có thể được dùng để hiển thị và huỷ các assemblies đang sử dụng trong Windows explorer, nhưng nó không thể
sử dụng trong đoạn code đang dùng, như tạo chương trình cài đặt .gacutil.exe là một tiện ích để install, uninstall, và danh sách
các assemblies sử dụng command line. Tất nhiên, nó có thể được sử dụng mà không có mã code sinh kèm dành cho mục đích
quản trị.

Vài chọn lựa của tiện ích gacutil :

• gacutil /l danh sách tất cả assemblies từ assembly cache


• gacutil /i mydll cài đặt shared assembly mydll vào trong assembly cache

• gacutil /u mydll huỷ cài đặt assembly mydll

• gacutil /ungen mydll huỷ cài đặt assembly từ native image cache

Tạo các Assembly chia sẽ

Assemblies có thể được tách biệt cho người sử dụng bởi một ứng dụng độc lập – không chia sẽ một assembly là mặc định. Khi
sử dụng assemblies nó không cần thiết lưu ý các yêu cầu dành cho chia sẽ.

Nội dung chính chúng ta sẽ được khảo sát:

• Strong names
• Tạo shared assemblies

• Cài đặt shared assemblies trong global assembly cache


• Sử dụng một assembly chia sẽ

Tên Assembly chia sẽ


Mục đích của tên assembly chia sẽ phải là duy nhất, và nó phải có khả năng để bảo vệ tên. Tránh 1 lúc nào đó có người lạ tạo một
assembly sử dụng cùng tên. Bạn có thể tạo ra những assembly được chia sẻ sử dụng bởi những ứng dụng khác. Nếu bạn viết ra
một ô control mang tính chung chung (general control) hoặc một lớp có thể được chia sẻ sử dụng bởi nhiều nhà triển khai khác,
đó là bạn đang thực hiện một shared assembly.

Trước tiên, assembly của bạn phải có một strong name, một tên duy nhất được chia sẻ sử dụng. Thứ đến, shared assembly của
bạn phải được bảo vệ không cho các phiên bản mới phá bĩnh, như vậy phải có cách kiểm tra phiên bản trước khi được nạp vào
theo yêu cầu của ứng dụng client. Cuối cùng, muốn cho chia sẻ sử dụng assembly bạn tạo ra, bạn phải đưa assembly bày vào
Global Assembly Cache (GAC). Đây là một vùng ký ức của hệ thống các tập tin được dành riêng bởi CLR để trữ các shared
assembly. Bản thân GAC nằm ở thư mục <ổ đia>:\WINNT\Assembly. Trên .NET, các shared assembly đều được tập trung đưa
vào một nơi là GAC.

Khái niệm về Strong Name


Để có thể sử dụng shared assembly, bạn cần tuân thủ 3 đòi hỏi sau đay:

• Bạn phải có khả năng báo đúng assembly mà bạn muốn nạp vào. Do đó, bạn cần có một tên duy nhất mang tính toàn
cục (global unique name) được gán cho shared asembly.
• Bạn cần bảo đảm là assembly không bị giả mạo. Nghĩa là, bạn cần có một digital signature (dấu ấn kỹ thuật số) khi
assembly được tạo dựng.

• Bạn cần bảo đảm là assembly bạn đang nạp vào đúng là assembly được phép của tác giả tạo ra assembly. Do đó, bạn
cần ghi nhận mã nhận diện người sáng tác (originator).

Do đó, khi bạn muốn tạo ra một assembly có thể được chia sẻ sử dụng bởi nhiều ứng dụng khác nhau trên một máy tính nào đó,
bước đầu tên là tạo ra một tên chia sẻ duy nhất đối với assembly, Tên này được gọi là strong name thường chứa những thông tin
sau đây:

• Một tên kiểu string mang tính thân thiện và thông tin tuỳ chọn về culture (giống như với private assembly)
• Một mã nhận diện phiên bản

• Một cặp mục khoá public-private

• Một dấu ấn kỹ thuật số (digital signature).

Việc hình thành một strong name đều được dựa trên một phương thức mật mã hoá chuẩn mục khoá công cộng (standard public
key encryption). Khi bạn tạo một shared assembly bạn phải kết sinh một cặp mục khoá public-private. Một đoạn mã băm (hash
code) được lấy ra từ những tên và nội dung của các tập tin trong assembly. Mã băm này sẽ được mật mã hoá với một private key
đối với assembly và được đưa vào manifest. Đây được biết là ký tên assembly (signing the assembly). Public key sẽ được sát
nhập vào strong name của assembly. Nói tóm lại, một cặp mục khoá sẽ được bao gồm trong chu kỳ build sử dụng mọt trình biên
dịch .NET, trình này sẽ liệt kê một token của public key trên manifest của assembly (thông qua tag [.publickeytoken]). Private
key sẽ không được liệt kê ra trên manifest nhưng lại được "ký tên" với public key. Dấu ấn kết quả sẽ được trữ tập tin định nghĩa
assembly manifest).

Khi một ứng dụng cho nạp assembly vào, thì CLR sẽ dùng public key để giải mã đoạn mã băm đối với các tập tin thuộc
assembly bảo đảm là chúng không bị giả mạo. Việc này cũng bảo vệ chống đụng độ về tên.

Tạo một shared assembly


Muốn kết sinh một strong name bạn sử dụng trình tiện ích sn.exe. (nằm trên thư mục C:\program
Files\Microsoft.NET\SDK\v1.1\bin).

Mặc dù trình tiện ích này có nhiều mục chọn, ta chỉ cần dùng flag "-k" yêu cầu trình tiện ích kết sinh một strong name mới và
được trữ trên một tập tin được khai báo.

vi du: sn -k d:\vidu_C#\chuong8\example.snk

Bạn có thể đặt tên tập tin bất cứ tên gì bạn muốn. Bạn nhớ cho strong name là một chuỗi kexadecimal digit và không dành cho
người đọc. Nếu bạn quan sát nội dung của tập tin mới này (example.snk), bạn thấy binary markings của cặp key.

Bước kế tiếp là cho ghi nhận public key lên assembly manifest. Cách dễ nhất là sử dụng attribute AssemblyKeyfile. Khi bạn tạo
một C# project workspace, có thể bạn để ý một trong những tập tin khởi sự của dự án mang tên "AssemblyInfo.cs",
Tập tin này chứa một số attributes được tiêu thụ bởi một trình biên dịch . NET. Nếu bạn quan sát tập tin này, bạn sẽ thấy
attribute AssemblyKeyFile này.

Muốn khai báo một strong name đối với một shared assembly, bạn chỉ cần nhập từ trị của attribute này cho biết nơi tá túc của
tập tin *.snk.

[asssembly:AssemblyKeyFile(@"D:\vidu_C#\chuong8\example.snk:)]

Sử dụng assembly level attribute này, C# compiler giờ đây trộn với nhau thông tin cần thiết vào manifest tương ứng. Bạn có thể
dùng ILDasm.exe để xem tag [.publickey].

Ví dụ: ta có file mykey.snk

sn -k mykey.snk

Ở đây ta thay thành AssemblyInfo.cs. Thuộc tính AssemblyKeyFile được thiết lập đến file mykey.snk:

[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("../../mykey.snk")]
[assembly: AssemblyKeyName("")]

Sau khi xây dựng lại , Khoá công cộng có thể tìm thấy trong manifest khi nhìn vào assembly sử dụng ildasm:

Trong ví dụ này chúng tôi sẽ tạo một shared assembly và một client để sử dụng nó.Đầu tiên ta bắt đầu với Visual C# Class
Library project. Tôi thay đổi namespace thành Wrox.ProCSharp.Assemblies.Sharing, và tên lớp thành SimpleShared. Lớp này sẽ
đọc tất cả hàng của file đã truyền bên trong constructor trong một StringCollection tại thời gian tạo thành, và trả về một chuỗi
ngẫu nhiên trong phương thức GetQuoteOfTheDay()

using System;
using System.Collections.Specialized;
using System.IO;
namespace Wrox.ProCSharp.Assemblies.Sharing
{
public class SimpleShared
{
private StringCollection quotes;
private Random random;
public SimpleShared(string filename)
{
quotes = new StringCollection();
Stream stream = File.OpenRead(filename);
StreamReader streamReader = new StreamReader(stream);
string quote;
while ((quote = streamReader.ReadLine()) != null)
{
quotes.Add(quote);
}
streamReader.Close();
stream.Close();
random = new Random();
}
public string GetQuoteOfTheDay()
{
int index = random.Next(1, quotes.Count);
return quotes[index];
}
}
}

Tên chương trình bạn tham khảo kèm theo SimpleShared

Cài đặt assembly vào GAC


Một khi bạn đã thiết lập một strong name đối với một shared assembly, bước cuối cùng là cho cài đặt nó vào GAC, một thư mục
hệ thống được dành riêng. Cách đơn giản nhất để cài đặt một shared assembly vào GAC là cho mở Window Explorer rồi lôi thả
tập tin SharedAssembly.dll vào thư mục GAC hiên dịch (active window). Muốn thấy GAC, bạn cho mở Windows Explorer rồi
hướng về\WINNT\Assembly sẽ nhảy qua một trình tiện ích GAC.

Hoặc bạn cũng có thể tự do dùng trình tiện ích gacutil.exe như tiện ích trên command line như sau:

gacutil /i: SharedAssembly.dll

Theo ví dụ trên thì bạn nhập vào gacutil /i SimpleShared.dll

Bạn nên nhớ là bạn phải có quyền Administrative đối với máy tính để có thể cài đặt assembly lên GAC. Điều này tốt thôi, vì nó
ngăn ngừa người sử dụng nào đó phá bĩnh một cách vô tình những ứng dụng hiện hữu

Kết quả cuối cùng là assembly của bạn giời đây nằm trong GAC và có thể được chia sẻ sử dụng bởi nhiều ứng dụng trên máy
tính đích. Bạn để ý, khi nào bạn muốn gỡ bỏ một assembly khỏi GAC, bạn chỉ cần right-click lên assembly, rồi chon lick mục
Delete trên trình đơn shortcut.

Sử dụng một Shared Assembly


Bây giờ để chứng minh, giả sử bạn tạo một ứng dụng C# Console Appication mới, cho mang tên SharedAssembly User chẳng
hạn, cho qui chiếu về SharedAssembly binary, và bạn định nghĩa lớp sau đây

Bạn nhớ cho, là khi một qui chiếu về một private assembly, IDE tự động tạo một bản sao của private assembly nàydành cho ứng
dụng client dùng. Tuy nhiên, khi bạn qui chiếu một assembly có chứa một public key (như trong trường hợp sharedAssembly.dll)
bạn sẽ không nhận một bản sao local. Giả định là những assembly nào có chứa public key là dùng chia sẻ sử dụng (và như thế
phải nằm trong GAC).

Bạn nhớ cho là Visual Studio .NET IDE cho phép bạn điều khiển một cách tường minh việc sao chép một assembly vào đó sử
dụng cửa sổ properties. Thí dụ, nếu bạn cho đặt để một qui chiếu về một external binary, bạn chọn assembly vày sử dụng đến
axternal binary, bạn chọn assembly này sử dụng đến Solution Explorer cho hiện lên cửa sổ Properties, rồi cho đặt để thuộc tính
Copy Local về false. Việc lamf này sẽ gỡ bỏ local copy.

Bây giờ, bạn cho chạy ứng dụng client lại nữa. Nó sẽ chạy tốt vì .NET Runtime sẽ tham khảo GAC để giải quyết việc dò tìm nơi
tá túc của assembly được qui chiếu.

Nếu bạn cho gỡ bỏ SharedAssembly.dll trên GAC, rồi cho chạy lại chương trình thì một biệt lệ System.IO.FileNotfound sẽ
được tung ra.

Trở lại ví dụ ban đầu để sử dụng Shared Assembly

Để dùng shared assembly chúng ta sẽ tạo một C# Console Application gọi Client. Thay cho việc thêm project mới cho solution
trước đây, bạn nên tạo một solution để shared assembly không nhận xây dựng lại khi xây dựng lại client. Tôi thay tên của
namespace thành Wrox.ProCSharp.Assemblies.Sharing, và tên lớp thành Client.

Chúng ta tham khảo assembly SimpleShared giống như cách chúng ta tham khảo private assemblies: dùng menu Project | Add
Reference, hoặc Solution Explorer. sau đó chọn nút Browse để tìm assembly SimpleShared.

Sau đây là đoạn mã của ứng dụng client

using System;

namespace Wrox.ProCSharp.Assemblies.Sharing
{
class Client
{
[STAThread]
static void Main(string[] args)
{
SimpleShared quotes = new SimpleShared(
@"C:\ProCSharp\Assemblies\Quotes.txt");
for (int i=0; i < 3; i++)
{
Console.WriteLine(quotes.GetQuoteOfTheDay());
Console.WriteLine();
}
}
}
}

Download chương trình Client

Khi hiển thị manifest trong client assembly dùng ildasm chúng ta có thể tham khảo vào shared assembly SimpleShared:
.assembly mở rộng SimpleShared. Phần thông tin tham khảo là số phiên bản chúng ta sẽ học ở phần tới và mã thông báo của
khoá công cộng.

Chúng ta dùng tiện ích sn –T hiển thị mã thông báo của khoá công cộng trong assembly. Kết quả của chương tình được hiển thị
như sau:

Triển khai

Các ứng dụng Windows Forms nên được gói trong một Windows Installer Package và phân
phối sử dụng Windows Installer. Các điều khiển bên trong trang web nên được gói trong file .cab
hoặc là file .DLL. Sự phân phối có thể qua đường tải mã.

Các ứng dụng ASP.NET nên được phân phối sử dụng xcopy hoặc ftp.

Bây giờ chúng ta sẽ nới một chút nữa về sự triển khai của các DLL đơn giản.

Sự triển khai của các DLL (Deployment of DLLs)


Việc đóng gói DLL có thể thực hiện bằng nhiều cách – nếu assembly tồn tại là một đơn DLL,
điều này đủ khả năng đóng gói. Bằng cách dùng cabinet file cho multiple DLLs, các file cấu hình
và các file phụ thuộc khác có thể được đặt trong một file đã được nén thành file .cab nó cũng
thuận lợi cho tốc độ download, Nhưng cabinet file không được quá lớn bởi vì quá nhiều
assemblies. Một cabinet file được tạo nhờ vào Cab Project Wizard của Setup và Deployment
Projects với Visual Studio .NET. Files trong menu Project | Add | File.

Một lợi ích nữa là Visual Studio .NET hỗ trợ tạo Microsoft Installer Packages bằng Setup
Wizard và Merge Modules có thể sử dụng mà không cần Windows Installer Packages với Môdun
kết hợp (Merge Module Project).

Tạo một Module kết hợp


Một môdun kết hợp dễ dàng được tạo không cần Visual Studio .NET. Tất nhiên InstallShield
hoặc Wise của Windows có nhiều tính năng hơn Merge Module Project trong Visual Studio,
nhưng Merge Module Project được kèm theo không cần Visual Studio .NET sẽ giải quyết nhiều
vấn đề cài đặt mà không cần đến "người anh lớn hơn"

Khi chọn Build | Deploy Solution với thư viện lớp đã mở project, một hộp thông điệp xuất hiện yêu cầu một deployment
project phải được tạo. Merge Module Project nên được chọn. File System View được mở trong Visual Studio .NET; ô bên trái
hiển thị chỉ dẫn nơi files có thể được cấu hình cho cài đặt, ba folders được tạo ra tự động như sau :

• Common files folder được dùng cho các file phổ biến được chia sẽ giữa các ứng dụng; C:\Program Files\Common
• Assemblies đặt bên trong global assembly cache folder sẽ được cài đặt trong global assembly cache trong suốt quá
trình cài.

• Module retargetable folder phần lớn dùng để kết hợp các module. Các module kết hợp sẽ được sùng từ MSI Package.
MSI package sau đó định nghĩa trong thư mục các files

Khi chọn Module Retargetable Folder dùng menu Add | Project Output chúng ta thấy như sau:

Bây giờ cần thiết chọn Primary output, nó gồm DLL và EXE files, và (nếu có khả năng) Localized resources bao gồm cả
satellite assemblies. Tất cả assemblies tham khảo đều tự động gộp trong module kết hợp. Module kết hợp này bây giờ có thể sử
dụng không cần Windows Installer packages cho các ứng dụng khác.

Tóm tắt

Assemblies là đơn vị cài đặt mới cho nền .NET. Microsoft rút ra được nhiều kinh nghiệm từ
kiến trúc trước đây và đã thiết kế lại thành công tránh những lổi cũ trong chương này chúng ta đã
được học những tính năng của assemblies chúng là chương trình tự mô tả bản thân và các thông
tin cần thiết Phiên bản độc lập được ghi một cách chính xác với DLL Hell. Nhờ những tính năng
này mà sự hiện hiện và quản lý càng dễ dàng hơn nhiều.

Chúng ta đã thảo luận CLS (cross-language support) và đã tạo một lớp C# được chuyển từ lớp
VB.NET nó sử dụng lớp điều khiển C++, và thấy được sự khác nhau lúc sinh ra mã MSIL

Chúng ta đã khảo sát việc sử dụng tài nguyên . Chúng ta đã tạo satellite assemblies với sự giúp
đỡ của Visual Studio .NET. Bên cạnh đó sử dụng tài nguyên cục bộ, và thực thi các định dạng
ngày tháng, số học cho từng ngôn ngữ người dùng.

Chúng ta thảo luận sự khác biết giữa private và shared assemblies, shared assemblies được tạo
như thế nào. Với private assemblies chúng ta không cần lưu ý đến số phiên bản assemblies được
sao chép và chỉ sử dụng một ứng dụng đơn lẻ. Assembly chia sẽ có thêm một số yêu cầu về
khoá, xác định phiên bản rõ ràng. Chúng ta xem qua GAC (global assembly cache) nó được sử
dụng như một kho lưu trữ thông minh cho shared assemblies.
Chúng ta đã xem xét các phiên bản ghi đè để sử dụng một phiên bản assembly khác với cái mà
nó được sử dụng trong quá trình phát triển. Chúng ta cũng thảo luận các công việc tìm kiếm với
private assemblies làm thế nào, và cuối cùng là sự triển khai của các assembly.

Tổng quan về ADO.NET


Giống như hầu hết các thành phần của .NET Framework, ADO.NET không chỉ là vỏ bọc của một vài API sẵn có. Nó chỉ giống
ADO ở cái tên - các lớp và phương thức truy xuất dữ liệu đều khác hoàn toàn.

ADO (Microsoft's ActiveX Data Objects) là một thư viên của các thành phần COM đã từng được ca ngợi trong một vài năm trở
lại đây. Phiên bản hiện tại là 2.7, các thành phần chủ yếu của ADO là Connection, Command, Recordset, và các Field object.
Một connection có thể mở cơ sở dữ liệu, một vài dữ liệu được chọn vào một recordset, bao gồm các trường, dữ liệu này sau đó có
thể thao tác, cập nhập lên server, và connection cần phải được đóng lại. ADO cũng giới thiệu một disconnected recordset, cái
được dùng khi không muốn giữ kếp nối trong một thời gian dài.

Có một vài vấn đề với ADO đó là sự không hài lòng về địa chỉ, sự cồng kềnh của một disconnected recordset. Hỗ trợ này không
cần thiết với sự tiến hoá của tin học "web-centric", vì vậy nó cần được loại bỏ. Có một số giống nhau giữa lập trình ADO.NET và
ADO (không phải ở cái tên), vì thế việc chuyển từ ADO không qua khó khăn. Hơn thế nữa, nếu bạn dùng SQL Server, có một bộ
các quản mới rất tuyệt cho viêc thao tác bên ngoài cơ sở dữ liệu. Chừng đó lí do cũng đủ để các bạn quan tâm đến ADO.NET.

ADO.NET chứa hai không gian tên cơ sơ dữ liệu - một cho SQL Server, và một cái khác cho các cơ sở dữ liệu được trình bày
thông qua một giao diện OLE DB. Nếu cơ sở dữ liệu của bạn chọn là một bộ phận của OLE DB, bạn có thể dễ dàng kết nối với
nó từ .NET - chỉ cần dùng các lớp OLE DB và kết nối thông qua các driver cơ sở dữ liêu hiện hành của bạn.

Các Namespace

Tất cả các ví dụ trong chương này truy xuất dữ liệu trong một vài cách. Các không gian tên sau chỉ ra các lớp và các giao diện
được dùng cho việc truy xuất dữ liệu trong .NET:

• System.Data - Các lớp truy xuất dữ liệu chung


• System.Data.Common - Các lớp dùng chung bởi các data provider khác nhau

• System.Data.OleDb - Các lớp của OLE DB provider

• System.Data.SqlClient - Các lớp của SQL Server provider

• System.Data.SqlTypes - Cac kiểu của SQL Server

Các lớp chính trong ADO.NET được liệt kê dưới đây:

Các lớp dùng chung

ADO.NET chứa một số lớp được dùng không quan tâm là bạn đang dùng các lớp của SQL Server hay là các lớp của OLE DB.

Các lớp trong không gian tên System.Data được liệt kê sau đây:

• DataSet - Đối tượng này chứa một bộ các DataTable, có thể bao gồm quan hệ giữa các bảng, và nó được thiết kế cho
truy xuất dữ liệu không kết nối.
• DataTable - Một kho chứa dữ liệu. Một DataTable bao gồm một hoặc nhiều DataColumns, và khi được tạo ra nó sẽ có
một hoặc nhiều DataRows chứa dữ liệu.

• DataRow - Một bộ giá trị, có bà con với một dòng trong bảng cơ sở dữ liệu, hoặc một dòng của bảng tính.

• DataColumn - Chứa cá định nghĩa của một cột, chẳng hạn như tên và kiểu dữ liệu.

• DataRelation - Một liên kết giữa hai DataTable trong một DataSet. Sử dụng cho khóa ngoại và các mối quan hệ chủ tớ.

• Constraint - Định nghĩa một qui tắt cho một DataColumn (hoặc môt bộ các cột dữ liệu), như các giá trị là độc nhất.

Sau đây là hai lớp được tìm thấy trong không gian tên System.Data.Common:

• DataColumnMapping - Ánh xạ tên của một cột từ cơ sở dữ liệu vào tên của một cột trong một DataTable.

• DataTableMapping - Ánh xạ tên của một bảng từ cơ sở dữ liệu vào một bảng trong một DataSet.
Các lớp cơ sở dữ liệu chuyên biệt

Bổ sung cho các lớp dùng chung ở trên, ADO.NET có một số các lớp dữ liệu chuyên biệt được đưa ra dưới đây. Các lớp này thực
thi một bộ các giao diện chuẩn được định nghĩa trong không gian tên System.Data, cho phép sử dụng các lớp có cùng kiểu giao
diện. Ví dụ cả hai lớp SqlConnection và OleDbConnection thực thi giao diện IDbConnection.

• SqlCommand, OleDbCommand - Một vỏ bọc của các câu lệnh SQL hoặc các lời gọi stored procedure.
• SqlCommandBuilder, OleDbCommandBuilder - Một lớp sử dụng các câu lệnh SQL (chẳng hạn như các câu lệnh
INSERT, UPDATE, vàDELETE) từ một câu lệnh SELECT.

• SqlConnection, OleDbConnection - Kết nối với cơ sở dữ liệu. Giống như một ADO Connection.

• SqlDataAdapter, OleDbDataAdapter - Một lớp giữ các câu lệnh select, insert, update, và delete, chúng được sử dụng để
tạo một DataSet và cập nhật Database.

• SqlDataReader, OleDbDataReader - Chỉ đọc, kết nối với data reader.

• SqlParameter, OleDbParameter - Định nghĩa một tham số cho một stored procedure.

• SqlTransaction, OleDbTransaction - Một giao tiếp cơ sở dữ liện, được bọc trong một đối tượng.

Một đặc tính quan trọng của các lớp ADO.NET là chúng được thiết kế để làm việc trong môi trường không kết nối, đóng một vai
trò quan trọng trong thế giới "web-centric". Nó hiện được dùng để kiến trúc một server (chẳng hạn như mua sách qua mạng) để
kết nối một server, lấy một vài dữ liệu, và làm việc trên những dữ liệu này trên PC khách trước khi kết nối lại và truyền dữ liệu
trở lại để xử lí.

ADO 2.1 giới thiệu recordset không kết nối, nó cho phép dữ liệu có thể được lấy từ một cơ sở dữ liệu, được truyền cho trình
khách để xử lí. Nó thường khó xử dụng do cách ứng xử không kết không được thiết kế từ đâu. Các lớp ADO.NET thì khác -
Sql/OleDb DataReader được thiết kết cho để dùng cho các cơ sở dữ liệu offline.

Sử dụng các lớp và các giao diện để truy cập cơ sở dữ liệu trong được trình bày trong chương sau. Tôi chủ yếu tập trung vào các
lớp Sql khi kết nối cơ sở dữ liệu, bởi vì các ví dụ được cài đặt một cơ sở dữ liệu MSDE (SQL Server). Trong hầu hết các trường
hợp các lớp OleDb ứng sử tương tự như mã Sql.

Sử dụng các Database Connection


Trong trìn tự truy xuất cơ sở dữ liệu, bạn cần cung cấp các thông số kết nối, chẳng hạn như thiết bị mà cơ sở dữ liệu đang chạy,
và khả năng đăng nhập của bạn. Bất kì ai đã từng làm việc với ADO sẽ dễ dàng quen với các lớp kết nối của .NET,
OleDbConnection và SqlConnection:

Đoạn mã sau đây mô tả cách để tạo, mở và đóng một kết nối đến cơ sở dữ liệu Northwind. Các ví dụ trong chương này chúng ta
dùng cơ sở dữ liệu Northwind, được cài đặt chung với các ví dụ của .NET Framework SDK:

using System.Data.SqlClient;

string source = "server=(local)\\NetSDK;" +


"uid=QSUser;pwd=QSPassword;" +
"database=Northwind";
SqlConnection conn = new SqlConnection(source);
conn.Open();

// Do something useful

conn.Close();

Chuỗi kết nối sẽ trở nên thân thiện nếu bạn đã từng dùng ADO hay OLE DB trước đây - thật vậy, bạn có thể cắt và dán từ mã cũ
của bạn, nếu bạn dùng OleDb provider. Trong ví dụ chuỗi kết nối này, các tham số được dùng như sau (các tham số cách nhau
bởi dấu chấm phẩy trong chuỗi kết nối).
• server=(local)\\NetSDK - Nó biểu diễn database server được kết nối. SQL Server cho phép một số các tiến trình
database server processes khác nhau chạy trên cùng một máy, vì vậy ở đây chúng ta thực hiện kết nối với tiến trình
NetSDK trên máy cụ bộ.
• uid=QSUser - Tham số này mô tả người dùng cơ sở dữ liệu. Bạn cũng có thể sử dụng User ID.

• pwd=QSPassword - và đây là password cho người dùng đó. .NET SDK là một bộ các cơ sở dữ liệu giống nhau, và
user/password này được liên kết và được thêm vào trong quá trình cài đặt các ví dụ .NET. Bạn cũng có thể dùng
Password.

• database=Northwind - Cái này mô tả loại dữ liệu để kết nối - mỗi tiến trình SQL Server có thể đưa ra một vài loại dữ
liệu khác nhau.

Ví trên mở một kết nối cơ sở dữ liệu ùng chuỗi kết nối đã được định nghĩa, sau đó đóng kết nối lại. Khi kết nối đã được mở, bạn
có thể phát các lệnh để thao tác trên cơ sở dữ liệu, và khi hoàn tất, kết nối có thể được đóng lại.

SQL Server có một chế độ bảo mật khác - nó có thể dùng chế độ bảo mật của Windows, vì thế các khả năng truy cập của
Windows có thể truyền cho SQL Server. Với lựa chọn này bạn có thể bỏ đi các vị trí uid và pwd trong chuỗi kết nối, và thêm vào
Integrated Security=SSPI.

Trong lúc download mã nguỗn sẵn có cho chương này, bạn cần tìm file Login.cs nó đơn giãn hóa các ví dụ trong chương này. Nó
được kết nối với tất cả các mã ví dụ, và bao gồm thông tin kết nối cơ sở dữ liệu dùng cho các ví dụ; sau đó bạn có thể cung cấp
tên server, user, and password một cách thích hợp. Nếu mặc định dùng Windows integrated security; bạn cần thay đổi username
và password cho phù hợp.

Bây giờ chúng ta đã biết cách mở các kết nối, trước khi chuyển qua vấn đề khác chúng ta cần xem xét một vài thực hành tốt có
liên quan đến các kết nối.

Sử dụng hiệu quả các Connection


Một cách tổng quát, khi sử dụng các tài nguyên "hiếm" trong .NET, chẳng hạn như các kết nối cơ sở dữ liệu, các cửa sổ,hoặc các
đối tượng đồ họa, tốt hơn hết bạn nên đảm bảo rằng các tài nguyên này luôn phải được đóng lại sau khi đã sử dụng xong. Dù vậy
các nhà thiết kết của .NET có thể làm điều này nhờ trình thu gom rác, nó luôn làm sau bộ nhớ sau một khoảng thời gian nào đó,
tuy nhiên nó nên được giải phóng càng sớm càng tốt.

Rõ ràng là khi viết mã truy xuất một cơ sở dữ liệu, việc giữ một kết nối càng ít thời gian càng tốt để không làm ảnh hưởng đến
các phần khác. Trong nhiều tình huống tiêu cực, nếu không đóng một kết nối có thể khoá không cho các người dùng khác truy
nhập vào các bảng dữ liệu đó, một tác hại to lớn đối với khả năng thực thi của ứng dụng. Việc đóng một kết nối cơ sở dữ liệu có
thể coi là bắt buộc, vì thế ứng dụng này chỉ ra cách cấu trúc mã của bạn để giảm thiểu các rủi ro cho một mã nguồn mở.

Có hai cách để đảm bảo rằng các kết nối cơ sở dữ liệu được giải phóng sau khi dùng.

Tùy chọn một - try/catch/finally


Tùy chọn thứ nhất để đảm bảo rằng các tài nguyên được dọn sạch là sử dụng các khối lệnh try…catch…finally, và đảm bảo rằng
bạn đã đóng các kết nối trong khối lệnh finally. Đây là một ví dụ nhỏ:

try
{
// Open the connection
conn.Open();
// Do something useful
}
catch ( Exception ex )
{
// Do something about the exception
}
finally
{
// Ensure that the connection is freed
conn.Close ( ) ;
}

Với khối kết nối bạn có thể giải phóng bất kì tài nguyên nào mà bạn đã dùng. Vấn đề duy nhất trong phương thức này là bạn phải
bảo đảm rằng bạn có đóng các kết nối - rất là dễ quên việc thêm vào khối finally, vì vậy một phong cách lập trình tốt rất quan
trọng.

Ngoài ra, bạn có thể mở một số tài nguyên (chẳng hạn hai kết nối cơ sở dữ liệu và một file) trong một phương thức, vì vậy đôi
khi các khối try…catch…finally trở nên khó đọc. Có một cách khác để đảm bảo rằng các tài nguyên được dọn dẹp - sử dụng câu
lệnh.
Tùy chọn hai - Sử dụng khối câu lệnh

Trong lúc phát triển C#, phương thức .NET's dọn dẹp các đối tượng khi chúng không còn được tham chiếu nữa sử dụng các huỷ
bất định trở thành một vấn đề nóng hổi. Trong C++, ngay khi một đối tượng rời khỏi tầm vực, khối huỷ tử của nó sẽ tự động
được gọi. Nó là một điều rất mới cho các nhà thiết cớ các lớp sử dụng tài nguyên, khi một huỷ tử được sử dụng để đóng các tài
nguyên nếu các người dùng quên làm điều đó. Một huỷe tử C++ được gọi bất kì khi nào một đối tượng vượt quá tầm vực của nó -
vì vậy khi một ngoại lệ được phát ra mà không được chặn, tât cả các hủy tử cần phải được gọi.

Với C# và các ngôn ngữ có quản khác, tất cả đều tự động, các khối huỷ tử định trước được thay thế bởi trình thu gom rác, cái
được dùng để tháo các tài nguyên tại một thời điểm trong tương lai. Chúng mang tính bất định, nghĩa là bạn sẽ không biết trước
được khi nào thì việc đó sẽ xảy ra. Nếu quên không đóng một kết nối cơ sở dữ liệu có thể là nguyên nhân gây ra lỗi khi chạy
trong .NET. Mã sau đây sẽ giải thích cách để sử dụng giao diện IDisposable (đã được bàn kĩ trong chương 2) để giải phóng tài
nguyên khi thoát khỏi khối using .

string source = "server=(local)\\NetSDK;" +


"uid=QSUser;pwd=QSPassword;" +
"database=Northwind";

using ( SqlConnection conn = new SqlConnection ( source ) )


{
// Open the connection
conn.Open ( ) ;

// Do something useful
}

Mệnh đề using đã được giới thiệu trong chương 2. Đối tượng trong mệnh đề using phải thực thi giao diện IDisposable, nếu không
một se tạo ra một lỗi biên dịch. Phương thức Dispose() sẽ tự động được gọi trong khi thoát khỏi khối using.

Khi xem mã IL của phương thưc Dispose() của SqlConnection (và OleDbConnection), cả hai đều kiểm tra trạng thái của đối
tượng kết nối, và nếu nó đang mở phương thức Close() sẽ được gọi.

Khi lập trình bạn nên dùng cả hai tùy chọn trên.Ở nhưng chỗ bạn cần các tài nguyên tốt nhất là sử dụng mệnh đề using(), dù vậy
bạn cũng có thể sử dụng câu lệnh Close(), nếu quên không sử dụng thì khối lệnh using sẽ đóng lại giúp bạn. Không gì có thể thay
thế được mọt bẫy ngoại lệ tốt, vì thế tốt nhất bạn dùng trộn lẫn hai phương thức như ví dụ sau:

try
{
using (SqlConnection conn = new SqlConnection ( source ))
{
// Open the connection
conn.Open ( ) ;

// Do something useful

// Close it myself
conn.Close ( ) ;
}
}
catch (Exception e)
{
// Do something with the exception here...
}

Ở đây tôi đã gọi tường minh phương thức Close() mặc dù điều đó là không bắt buộc vì khối lệnh using đã làm điều đó thay cho
bạn; tuy nhiên, bạn luôn chắc rằng bất kì tài nguyên nào cũng được giải phóng sớm nhất có thể - bạn có thể có nhiều mã trong
khối lệnh mã không khoá tài nguyên.

Thêm vào đó, nếu một ngoại lệ xảy ra bên trong khối using, thì phương thức IDisposable.Dispose sẽ được gọi để bảo đảm rằng
tài nguyên được giải phóng, điều này đảm bảo rằng kết nối cơ sở dữ liệu luôn luôn được đóng lại. Điều này làm cho mã dễ đọc và
luôn đảm bảo rằng kết nối luôn được đóng khi một ngoại lệ xảy ra.

Cuối cùng, nếu bạn viết các lớp bao bọc một tài nguyên có lẽ luôn thưc hiện giao diện IDisposable để đóng tài nguyên. Bằng
cách dùng câu lệnh using() nó luôn đảm bảo rằng tài nguyên đó sẽ được dọn dẹp.

Các Transaction (giao dịch)


Thường khi có nhiều hơn một cập nhật dữ cơ sở dữ liệu thì các thực thi này được thực hiện bên trong tầm vực của một
transaction. Một transaction trong ADO.NET được khởi tạo bằng một lời gọi đến các phương thức BeginTransaction() trên đối
tượng kết nối cơ sở dữ liệu. Những phương thức này trả về một đối tượng có thể thực thi giao diện IDbTransaction, được định
nghĩa trong System.Data.

Chuỗi mã lệnh dưới đây khởi tạo một transaction trên một kết nối SQL Server:

string source = "server=(local)\\NetSDK;" +


"uid=QSUser;pwd=QSPassword;" +
"database=Northwind";
SqlConnection conn = new SqlConnection(source);
conn.Open();
SqlTransaction tx = conn.BeginTransaction();

// Execute some commands, then commit the transaction

tx.Commit();
conn.Close();

Khi bạn khởi tạo một transaction, bạn có thể chọn bậc tự do cho các lệnh thực thi trong transaction đó. Bậc này chỉ rõ sự tự do
của transaction này với các transaction khác xảy ra trên cơ sở dữ liệu. Các hệ cơ sở dữ liệu có thể hỗ trợ bốn tùy chọn sau đây:

Isolation Level Description

ReadCommitted Mặc định cho. Bậc này đảm bảo rằng dữ liệu đã ghi bởi transaction sẽ chỉ có thể truy cập được
bởi một transaction khác sau khi nó hoàn tất công việc của mình.

ReadUncommitted Tùy chọn này cho phép transaction của bạn có thể đọc dữ liệu trong cơ sở dữ liệu, dù cho dữ liệu
đó đang được một transaction khác sử dụng. Ví dụ như, nều hai người dùng truy cập cùng lúc vào
một cơ sở dữ liệu, và người thứ nhất chièn một vài dữ liệu trong transaction của họ (đó là một
Commit hoặc Rollback), và người thứ hai với tuỳ chọn bậc tự do là ReadUncommitted có thể đọc
dữ liệu.
Bậc này là một mở rộng của ReadCommitted, nó bảo đảm rằng nếu một lệnh tương tự được phát
RepeatableRead ra trong transaction, ensures that if the same statement is issued within the transaction, regardless
of other potential updates made to the database, the same data will always be returned. This level
does require extra locks to be held on the data, which could adversely affect performance.

This level guarantees that, for each row in the initial query, no changes can be made to that data.
It does however permit "phantom" rows to show up - these are completely new rows that another
transaction may have inserted while your transaction is running.
This is the most "exclusive" transaction level, which in effect serializes access to data within the
Serializable database. With this isolation level, phantom rows can never show up, so a SQL statement issued
within a serializable transaction will always retrieve the same data.

The negative performance impact of a Serializable transaction should not be underestimated - if


you don't absolutely need to use this level of isolation, it is advisable to stay away from it.

Bậc tự do mặc định của The SQL Server, ReadCommitted, là một kết hợp tốt giữa tính chặc chẽ và sẵn dùng của dữ liệu. Tuy
nhiêu, có nhiều trường hợp bậc tự do có thể tăng lên, trong .NET bạn đơn giản khởi tạo một transaction khác với bậc khác bậc
mặc định. Điều đó còn dựa vào kinh nghiệm của mỗi người.

Một điếu cuối cùng về transactions - nếu bạn đang dùng một cơ sở dữ liệu không hỗ trợ các transaction, thì tốt nhất bạn nên đổi
sang một cơ sở dữ liệu có dùng chúng!

Truy cập nhanh cơ sở dữ liệu với Data Reader


Một data reader là cách đơn giản nhất và nhanh nhất để chọn một vài dữ liệu từ một nguồn cơ sơ dữ liệu, nhưng cũng ít tính năng
nhất. Bạn có thể truy xuất trực tiếp một đối tượng data reader – Một minh dụ được trả về từ một đối tượng SqlCommand hoặc
OleDbCommand từ việc gọi một phương thức ExecuteReader() – có thể là một đối tượng SqlCommand, một đối tượng
SqlDataReader, từ một đối tượng OleDbCommand là một OleDbDataReader.

Mã lệnh sau đây sẽ chứng minh cách chọn dữ liệu từ bản Customers của cơ sở dữ liệu Northwind. Ví dụ kết nối với cơ sở dữ liệu
chọn một số các mẫu tin, duyệt qua các mẫu tin được chọn và xuất chúng ra màn hình console.

Ví dụ này có thể dùng cho OLE DB provider. Trong hầu hết các trường hợp các phương thức của SqlClient đều được ánh xạ một
một vào các phương thức của đối OleDBClient.
Để thực thi lại các lệnh đối với một OLE DB data source, lớp OleDbCommand được sử dụng. Mã lệnh dưới đây là một ví dụ một
câu lệnh SQL đơn giảnvà đọc các mẫu tin được trả về bởi đối tượng OleDbDataReader.

Mã của ví dụ có thể được tìm thấy trong thư mục Chapter 09\03_DataReader.

Chú ý hai câu lệnh using dưới đây được dùng trong lớp OleDb:

using System;
using System.Data.OleDb;

Tất cả các trình cung cấp dữ liệu đều sẵn chứa bên trong các data DLL, vì vậy chỉ cần tham chiếu đến System.Data.dll assembly
để dùng cho các lớp trong phần này:

public class DataReaderExample


{
public static void Main(string[] args)
{
string source = "Provider=SQLOLEDB;" +
"server=(local)\\NetSDK;" +
"uid=QSUser;pwd=QSPassword;" +
"database=northwind";
string select = "SELECT ContactName,CompanyName FROM Customers";
OleDbConnection conn = new OleDbConnection(source);
conn.Open();
OleDbCommand cmd = new OleDbCommand(select , conn);
OleDbDataReader aReader = cmd.ExecuteReader();
while(aReader.Read())
Console.WriteLine("'{0}' from {1}" ,
aReader.GetString(0) , aReader.GetString(1));
aReader.Close();
conn.Close();
}
}

Mã nguôn trên đây bao gồm các đoạn lệnh quen thuộc đã được trình bày trong các chương trước. Để biên dịch ví dụ này, ta dùng
các dòng lệnh sau:

csc /t:exe /debug+ DataReaderExample.cs /r:System.Data.dll

Mã sau đây từ ví dụ trên cho phép tạo một kết nối OLE DB .NET, dựa trên chuỗi kết nối:

OleDbConnection conn = new OleDbConnection(source);


conn.Open();
OleDbCommand cmd = new OleDbCommand(select, conn);

Dòng thứ ba tạo một đối tượng OleDbCommand mới, dựa vào câu lệnh SELECT, kết nối sẽ thực thi câu lệnh lệnh này. Nếu bạn
tạo một command hợp lệ, bạn có thể thực thi chúng để trả về một minh dụ OleDbDataReader:

OleDbDataReader aReader = cmd.ExecuteReader();

Mội OleDbDataReader chỉ là một con trỏ "connected" định trước. Mặt khác, bạn có thể chỉ duyệt qua các mẫu tin được trả về, kết
nối hiện tạo sẽ lưu giữ các mẫu tin đó cho đến khi data reader bị đóng lại.

Lớp OleDbDataReader không thể tạo minh dụ một cách trực tiếp – nó luôn được trả về thông qua việc gọi phương thức
ExecuteReader() của lớp OleDbCommand. Nhưng bạn có thể mở một data reader, có một số cách khác nhau để truy cập dữ liệu
trong reader.

Khi một đối tượng OleDbDataReader bị đóng lại (thông qua ciệc gọi phương thức Close(), hoặc một đợt thu dọn rác), kết nối bên
dưới có thể bị đóng lại thông qua một lời gọi phương thức ExecuteReader(). Nếu bạn gọi ExecuteReader() và truyền
CommandBehavior.CloseConnection, bạn có thể ép kết nối đóng lại khi đóng reader.

Lớp OleDbDataReader có một bộ các quyền truy xuất thông qua các mảng quen thuộc:

object o = aReader[0];
object o = aReader["CategoryID"];

Ở đây CategoryID là trường đầu tiên trong câu lệnh SELECT của reader, cả hai dòng trên đều thực hiện công việc giống nhau tuy
nhiên cách hai hơi chậm hơn cách một – Tôi đã viết một ứng dụng đơn giản để thực thi việc lập lại quá trình truy cập cho hàng
triệu lần một cột trong một mẫu tin reader, chỉ để lấy một vài mẫu. Tôi biết bạn hầu như không bao giờ đọc một cột giống nhau
hàng triệu lần, nhưng có thể là một số lần, bạn nên viết mã để tối ưu quá trình đó.

Bạn có biết kết quả là thế nào không, việc truy cập môt triệu lần bằng số thứ tự chỉ tốn có 0.09 giây, còn dùng chuỗi kí tự phải
mất 0.63 giây. Lí do của sự chậm trễ này là vì khi dùng chuỗi kí tự ta phải dò trong schema để lấy ra số thứ tự của cột từ đó mới
truy xuất được cơ sở dữ liệu. Nếu bạn biết được các thông tin này bạn có thể viết mã truy xuất dữ liệu tốt hơn.

Vì vậy việc dùng chỉ số cột là cách dùng tốt nhất.

Hơn thế nữa, OleDbDataReader có một bộ các phương thức type-safe có thể dùng để đọc các cột. Những phương thức này có thể
đọc hầu hết các loại dữ liệu như GetInt32, GetFloat, GetGuid, vân vân.

Thí nghiệm của tôi khi dùng GetInt32 là 0.06 giây. Nhanh hơn việc dùng chỉ số cột, vì khi đó bạn phải thực hiện thao tác ép kiểu
để đưa kiểu trả về kiểu integer. Vì vậy nếu biết trước schema bạn nên dùng các chỉ số thay vì tên.

Chắc bạn cũng biết nên giữ sự cân bằng giữa tính dễ bảo trì và tốc độ. Nếu bạn muốn dùng các chỉ mục, bạn nên định nghĩa các
hằng số cho mỗi cột mà bạn sẽ truy cập.

Ví dụ dưới đây giống như ví dụ ở trên nhưng thay vì sử dụng OLE DB provider thì ở đây sử dụng SQL provider. Nhưng phần
thay đổi của mã so với ví dụ trên được tô đậm. Ví dụ này nằm trong thư mục 04_DataReaderSql:

using System;
using System.Data.SqlClient;
public class DataReaderSql
{
public static int Main(string[] args)
{
string source = "server=(local)\\NetSDK;" +
"uid=QSUser;pwd=QSPassword;" +
"database=northwind";
string select = "SELECT ContactName,CompanyName FROM Customers";
SqlConnection conn = new SqlConnection(source);
conn.Open();
SqlCommand cmd = new SqlCommand(select , conn);
SqlDataReader aReader = cmd.ExecuteReader();
while(aReader.Read())
Console.WriteLine("'{0}' from {1}" , aReader.GetString(0) ,
aReader.GetString(1));
aReader.Close();
conn.Close();
return 0;
}
}

Tôi đã chạy thử nghiệm của mình trên SQL provider, và kết quả là 0.13 giây cho một triệu lần truy cập bằng chỉ mục, và 0.65
giây nếu dùng chuỗi. Bạn có mong rằng SQL Server provider nhanh hơn so với OleDb, tôi đã test thử nghiệm của mình trong
phiên bản .NET.

Nếu bạn có hứng thú chạy mã này trên máy tính của bạn thì nó nằm trong các ví dụ 05_IndexerTestingOleDb và
06_IndexerTestingSql trong mã bạn đã down về.

Managing Data và Relationships: The DataSet


Lớp DataSet được thiết kế như là một thùng chứa các dữ liệu không kết nối. Nó không có khái niệm về các kết nối dữ liệu. Thật
vậy, dữ liệu được giữ trong một DataSet không quan tâm đến nguồn cơ sở dữ liệu – nó có thể chỉ là những mẫu tin chứa trong
một file CSV, hoặc là những đầu đọc từ một thiết bị đo lường.

Một DataSet bao gồm một tập các bảng dữ liệu, mỗi bảng là một tập các cột dữ liệu và dòng dữ liệu. Thêm vào đó là các định
nghĩa dữ liệu, bạn có thể định nghĩa các link giữa các DataSet. Mối quan hệ phổ biến giữa các DataSet là parent-child
relationship. Một mẫu tin trong một bảng (gọi là Order) có thể liên kết với nhiều mẫu tin trong bảng khác (Bảng Order_Details).
Quan hệ này có thể được định nghĩa và đánh dấu trong DataSet.
Phần dưới đây giải thích các lớp được dùng trong một DataSet.

Data Tables
Một data table rất giống một bảng cơ sở dữ liệu vật lí – nó bao gồm một bộ các cột với các thuộc tính riêng, và có thể không chứa
hoặc chứa nhiều dòng dữ liệu. Một data table có thể định nghĩa một khóa chínhm, bao gồm một hoặc nhiều cột, và cũng có thể
chứa các ràng buộc của các cột. Tất cả các thông tin đó được thể hiện trong schema.

Có nhiều các để định nghĩa một schema cho một bảng dữ liệu riêng. Chúng sẽ được thảo luận ngay sau phần giới thiệu về cột dữ
liệu và dòng dữ liệu.

Sơ đồ dưới đây chỉ ra một vài đối tượng có thể truy cập thông qua một bảng dữ liệu:

Một đối tượng DataTable (cũng như một DataColumn) có thể có một số các mở rộng riêng liên quan đến thuộc tính của nó. Tập
hợp này có thể nằm trong thông tin user-defined gắng liền với đối tượng. Ví dụ, một cột có thể đưa ra một mặt nạ nhập liệu dùng
để giới hạn các giá trị hợp lệ cho cột đó – một ví dụ về số phúc lợi xã hội Mĩ. Các thuộc tính mở rộng đặc biệt quan trọng khi dữ
liệu được cấu trúc ở một tầng giữa và trả về cho client trong một số tiến trình. Bạn có thể lưu một chuẩn hợp lệ (như min và max)
cho các số của các cột.

Khi một bảng dữ liệu được tạo ra, có thể do việc chọn dữ liệu từ một cơ sở dữ liệu, đọc dữ liệu từ một file, hoặc truy xuất thủ
công trong mã, tập hợp Rows được dùng để chứa giá trị trả về.

Tập hợp Columns chứa các thể hiện DataColumn có thể được thêm vào bảng này. Những định nghĩa schema của dữ liệu, ví dụ
như kiểu dữ liệu, tính khả rỗng, giá trị mặc định, vân vân... Tập Constraints có thể được tạo ra bởi các ràng buộc khóa chính hoặc
tính độc nhất.

Thông tin về sơ đồ của một bảng dữ liệu có thể được sử dụng trong việc biểu diễn của một bảng dữ liệu bằng DataGrid (chúng ta
sẽ bàn về vấn đề này trong chương sau). Điều khiển DataGrid sử dụng các thuộc tính như kiểu dữ liệu của cột để quyết định điều
khiển gì dùng cho cột đó. Một trường bit trong cơ sở dữ liệu có thể được biểu diễn như một checkbox trong DataGrid. Nếu một
cột được định nghĩa trong cơ sở sơ đồ dữ liệu như là một NOT NULL, lựa chọn này được lưu trữ trong DataColumn vì vậy nó sẽ
được kiểm tra khi người dùng cố gằng di chuyển khỏi một dòng.

Data Columns

Một đối tượng DataColumn định nghĩa các thuộc tính của một cột trong DataTable, chẳng hạn như kiểu dữ liệu của cột đó, chẳng
hạn cột là chỉ đọc, và các sự kiện khác. Một cột có thể được tạo bằng mã, hoặc có thể được tạo tự động trong thời gian chạy.

Khi tạo một cột, tốt hơn hết là nên đặt cho nó một cái tên; nếu không thời gian chạy sẽ tự động sinh cho bạn một cái tên theo định
dạng Columnn, n là mố sô tự động tăng.
Kiểu dữ liệu của một cột có thể cài đặt bằng cách cung cấp trong cấu trúc của nó, hoặc bằng cách cài đặt thuộc tính DataType.
Một khi bạn đã load dữ liệu vào một bảng dữ liệu bạn không thể sửa lại kiểu dữ liệu của một cột – nếu không bạn sẽ nhận một
ngoại lệ.

Các cột dữ liệu có thể được tạo để giữ các kiểu dữ liệu của .NET Framework sau:

Boolean Decimal Int64 TimeSpan

Byte Double Sbyte UInt16

Char Int16 Single UInt32

DateTime Int32 String UInt64

Một khi đã được tạo, bước tiếp theo là cài các thuộc tính khác cho đối tượng DataColumn, chẳng hạn như tính khả rỗng
nullability, giá trị mặc định. Đoạn mã sau chỉ ra một số các tùy chọn được cài đặt trong một DataColumn:

DataColumn customerID = new DataColumn("CustomerID" , typeof(int));


customerID.AllowDBNull = false;
customerID.ReadOnly = false;
customerID.AutoIncrement = true;
customerID.AutoIncrementSeed = 1000;
DataColumn name = new DataColumn("Name" , typeof(string));
name.AllowDBNull = false;
name.Unique = true;

Các thuộc tính sau có thể được cài đặt trong một DataColumn:

Property Description

AllowDBNull Nếu là true, cho phép cột có thể chấp nhận DBNull.

AutoIncrement Cho biết rằng dữ liệu của cột này là một số tự động tăng.

AutoIncrementSeed Giá trị khởi đầu cho một cột AutoIncrement.

AutoIncrementStep Cho biết bước tăng giữa các giá trị tự động, mặc định là 1.

Caption Có thể dùng cho việc biểu diễn tên của cột trên màn hình.

ColumnMapping Cho biết cách một cột ánh xạ sang XML khi một DataSet được lưu bằng cách gọi phương thức
DataSet.WriteXml.

ColumnName Tên của cột. Nó tự động tạo ra trong thời gian chạy nếu không được cài đặt trong cấu trúc.

DataType Kiểu giá trị của cột.

DefaultValue Dùng để định nghĩa giá trị mặc định cho một cột

Expression Thuộc tính này định nghĩa một biểu thức dùng cho việct tính toán trên cột này

Data Rows

Lớp này cấu thành các phần khác của lớp DataTable. Các cột trong một data table được định nghĩa trong các thuộc tính của lớp
DataColumn. Dữ liệu của bảng thật sự có thể truy xuất được nhờ vào đối tượng DataRow. Ví dụ sau trình bày cách truy cập các
dòng trong một bảng dữ liệu. Mã của ví dụ này có sẵn trong thư mục 07_SimpleDatasetSql. Trước tiên là các thông tin về kết
nối:

string source = "server=(local)\\NetSDK;" +


"uid=QSUser;pwd=QSPassword;" +
"database=northwind";
string select = "SELECT ContactName,CompanyName FROM Customers";
SqlConnection conn = new SqlConnection(source);

Mã sau đây giới thiệu lớp SqlDataAdapter, được dùng để điền dữ liệu cho một DataSet. SqlDataAdapter sẽ phát ra các SQL, và
điền vào một bảng Customers trong DataSet. Chúng ta sẽ bàn về lớp data adapter trong phần Populating a DataSet dưới đây.

SqlDataAdapter da = new SqlDataAdapter(select, conn);


DataSet ds = new DataSet();
da.Fill(ds , "Customers");

Trong mã dưới đây, bạn chú ý cách dùng chỉ mục của DataRow để truy xuất giá trị trong dòng đó. Giá trị của một cột có thể trả
về bằng cách dụng một trong những chỉ mục được cài đè. Chúng cho phép bạn trả về một giá trị cho biết số, tên, hoặc
DataColumn:

foreach(DataRow row in ds.Tables["Customers"].Rows)


Console.WriteLine("'{0}' from {1}" , row[0] ,row[1]);

Một trong những điều quan trọng nhất của một DataRow là phiên bản của nó. Điều đó cho phép bạn nhận được những giá trị
khác nhau cho một dòng cụ thể. Các phiên bản được mô tả trong bảng sau:

DataRowVersion Value Description

Current Giá trị sẵn có của cột. Nếu không xảy một hiệu chỉnh nào, nó sẽ mang giá trị gốc. Nếu có
một hiệu chỉnh xảy ra, giá trị sẽ là giá trị hợp lệ cuối cùng được cập nhật.

Default Giá trị mặc định (nói một cách khác, giá trị mặc định được cài đặt cho cột).

Original Giá trị của cột trong cơ sở dữ liệu vào lúc chọn. Nếu phương thức AcceptChanges
DataRow được gọi, thì giá trị này sẽ được cập nhật thành giá trị hiện tại.

Proposed Khi các thay đổi diễn ra trên một dòng nó có thể truy lục giá trị thay đổi này. Nếu bạn gọi
BeginEdit() trên mộg dòng và tạo các thay đổi, mỗi một cột giữ một giá trị cho đến khi
phương thức EndEdit() hoặc CancelEdit() được gọi.

Phiên bản của một cột có thể dùng theo nhiều cách. Một ví dụ cho việc cập nhật các dòng trong cơ sở dữ liệu, đó là một câu lệnh
SQL phổ biến như sau:

UPDATE Products
SET Name = Column.Current
WHERE ProductID = xxx
AND Name = Column.Original;

Rõ ràng mã này không bao giờ được biên dịch, nhưng nó chỉ ra một cách dùng cho các giá trị hiện tại và gốc của một cột trong
một dòng.

Để trả về một giá trị từ DataRow, dùng các phương thức chỉ mục thừa nhận một giá trị DataRowVersion như là một tham số.
Đoạn mã sau đây chỉ ra cách đạt được tất cả các giá trị cho mỗi cột của một DataTable:

foreach (DataRow row in ds.Tables["Customers"].Rows )


{
foreach ( DataColumn dc in ds.Tables["Customers"].Columns )
{
Console.WriteLine ("{0} Current = {1}" , dc.ColumnName ,
row[dc,DataRowVersion.Current]);
Console.WriteLine (" Default = {0}" , row[dc,DataRowVersion.Default]);
Console.WriteLine (" Original = {0}" , row[dc,DataRowVersion.Original]);
}
}

Mỗi dòng có một cờ trạng thái gọi là RowState, nó có thể dùng để xác định thực thi nào là cần thiết cho dòng đó khi nó cập nhật
cơ sở dữ liệu. Thuộc tính RowState có thể được cài đặ để theo dõi tất cả các trạng thái thay đổi trên DataTable, như thêm vào các
dòng mới, xóa các dòng hiện tại, và thay đổi các cột bên trong bảng. Khi dữ liệu được cập nhật vào cơ sở dữ liệu, cờ trạng thái
được dùng để nhận biết thực thi SQL nào sẽ xảy ra. Những cờ này được định nghĩa bởi bảng liệt kê DataRowState:
DataRowState Value Description

Added Dòng được vừa mới được thêm vào tập hợp DataTable's Rows. Tất cả các dòng đựoc tạo
trên máy khách đều được cài đặt giá trị này, và cuối cùng là phát ra câu lệnh SQL INSERT
khi cập nhật cho cơ sở dữ liệu.

Deleted Giá trị này cho biết dòng đó có thể được đánh dấu xoá trong DataTable bởi phương thức
DataRow.Delete(). Dòng này vẫn tồn tại trong DataTable, nhưng không thể trông thấy từ
màn hình (trừ khi một DataView được cài đặt rõ ràng). Các DataView sẽ được trình bày
trong chương tiếp theo. Các dòng được đánh dấu trong DataTable sẽ bị xoá khỏi cơ sở dữ
liệu khi nó được cập nhật.

Detached Một dòng sẽ có trạng thái này ngay sau khi nó đươc tạo ra , và có thể cũng trả về trạng thái
này bởi việc gọi phương thức DataRow.Remove(). Một dòng detached không được coi là
một thành phần của bảng dữ liệu.

Modified Một dòng sẽ được Modified nếu giá trị trong cột bất kì bị thay đổi.

Unchanged Một dòng sẽ không thay đổi kể từ lần cuối cùng gọi AcceptChanges().

Trạng thái của một dòng phụ thuộc vào phương thức mà dòng đó đã gọi. Phương thức AcceptChanges() thường được gọi sau một
cập nhật dữ liệu thành công (có nghĩa là sau khi thực hiện cập nhật cơ sở dữ liệu).

Cách phổ biến nhất để thay đổi dữ liệu trong một DataRow là sử dụng chỉ số, tuy vậy nếu bạn có một số thay đổi bạn ccũgn cần
gọi các phương thức BeginEdit() và EndEdit() methods.

Khi một cập nhật được tạo ra trên một cột trong một DataRow, sự kiện ColumnChanging sẽ được phát ra trên các dòng của
DataTable. Nó cho phép bạn ghi đè lên thuộc tính ProposedValue của các lớp DataColumnChangeEventArgs, và thay đổi nó nếu
muốn. Cách này cho phép các giá tri trên cột có hiệu lực . Nếu bạn gọi BeginEdit() trước khi tạo thay đổi, sự kiện
ColumnChanging vẫn xảy ra. Chúng cho phép bạn tạo một sự thay đổi kép khi cố gọi EndEdit(). Nếu bạn muốn phục hồi lại giá
trị gốc, hãy gọi CancelEdit().

Một DataRow có thể liên kết với một vài dòng khác của dữ liệu. Điều này cho phép tạo các liên kết có thể điều khiển được giữa
các dòng, đó là kiểu master/detail. DataRow chứa một phương thức GetChildRows() dùng để thay đổi một mảng các dòng liên
quan đến các cột từ một bản khác trong cùng DataSet như là dòng hiện tại. Chúng sẽ được trình bày trong phần Data
Relationships nằm ở phần sau của chương này.

Schema Generation

Có ba cách để tạo một schema cho một DataTable. Đó là:

• Hãy để thời gian chạy làm điều đó giúp bạn


• Viết mã tạo các bảng

• Dùng trình tạo sơ đồ XML

Runtime Schema Generation

Ví dụ về DataRow ở trên đã chỉ ra mã để chọn dữ liệu từ một cơ sở dữ liệu và tạo ra môt DataSet:

SqlDataAdapter da = new SqlDataAdapter(select , conn);


DataSet ds = new DataSet();
da.Fill(ds , "Customers");

Nó rõ ràng dễ sử dụng, nhưng nó cũng có môt vài trở ngại. Một ví dụ là bạnc phải làm việc với tên cột được chọn từ cơ sở dữ
liệu, điều đó cũng tốt thôi, nhưng chăc rằng muốn đổi tên vật lí thành tên thân thiện hơn.

Bạn có thể thực hiện việc đổi tên một cách thủ công trong mệnh đề SQL, chẳng hạn như trong SELECT PID AS PersonID
FROM PersonTable; bạn luôn được cảnh báo không nên đổi tên các cột trong SQL, chỉ thay thế một cột khi thật sự cần để tên
xuất hiện trên màn hình được thân thiện hơn.

Một vấn đề tiềm ẩn khác không các trình phát DataTable/DataColumn tự động là bạn không thể điều khiển vượt quá kiểu của cột,
các kiểu này được thời gian chạy lựa chọn cho bạn. Nó rất có ích trong việc chọn kiểu dữ liệu đúng cho bạn, nhưng trong nhiều
trường hợp bạn muốn có nhiều khả năng hơn . Ví dụ bạn cần định nghĩa một tập các kiểu giá trị dùng cho một cột, vì vậy mã cần
phải được viết lại. Nếu bạn chấp nhận kiểu giá trị măc định cho các cột đươc tạo ra trong thời gian chạy, có thể là một số nguyên
32-bit.

Cuối cùng một điều rất quan trọng, đó là sử dụng các trình tạo bảng tự động, bạn không thể truy xuất dữ liệu access to the data
within the DataTable – you are at the mercy of indexers, which return instances of object rather than derived data types. If you
like sprinkling your code with typecast expressions then skip the following sections.

Hand-Coded Schema

Việc phát ra mã để tạo một DataTable, với đầy đủ các cột là một việc tương đối đơn giản. Các ví dụ trong phần này sẽ truy cập
bảng Products từ cơ sỏ dữ liệu Northwind. Mã của phần này sẵn có trong ví dụ 08_ManufacturedDataSet.

Dưới đây là mã để tạo thủ công một DataTable, có sơ đồ như trên.

public static void ManufactureProductDataTable(DataSet ds)


{
DataTable products = new DataTable("Products");
products.Columns.Add(new DataColumn("ProductID", typeof(int)));
products.Columns.Add(new DataColumn("ProductName", typeof(string)));
products.Columns.Add(new DataColumn("SupplierID", typeof(int)));
products.Columns.Add(new DataColumn("CategoryID", typeof(int)));
products.Columns.Add(new DataColumn("QuantityPerUnit", typeof(string)));
products.Columns.Add(new DataColumn("UnitPrice", typeof(decimal)));
products.Columns.Add(new DataColumn("UnitsInStock", typeof(short)));
products.Columns.Add(new DataColumn("UnitsOnOrder", typeof(short)));
products.Columns.Add(new DataColumn("ReorderLevel", typeof(short)));
products.Columns.Add(new DataColumn("Discontinued", typeof(bool)));
ds.Tables.Add(products);
}

Bạn có thể sửa đổi mã trong ví dụ DataRow và sử dụng các định nghĩa sau:

string source = "server=localhost;" +


"integrated security=sspi;" +
"database=Northwind";
string select = "SELECT * FROM Products";
SqlConnection conn = new SqlConnection(source);
SqlDataAdapter cmd = new SqlDataAdapter(select, conn);
DataSet ds = new DataSet();
ManufactureProductDataTable(ds);
cmd.Fill(ds, "Products");
foreach(DataRow row in ds.Tables["Products"].Rows)
Console.WriteLine("'{0}' from {1}", row[0], row[1]);

Phương thức ManufactureProductDataTable() tạo một DataTable mới, thay đổi cho từng cột, và sau đó thêm nó vào danh sách
các bảng trong DataSet. DataSet có một bộ chỉ mục nắm giữ tên của bảng và trả về DataTable được gọi.

Ví dụ trên không thật sự là bảo toàn kiểu, Tôi đã dùng bộ chỉ mục cột để lấy dữ liệu. Tốt hơn hết là dùng một lớp (hoặc một bộ
các lớp) để điều khiển các DataSet, DataTable, và DataRow, dùng để định nghĩa các bộ truy xuất bảo vệ kiểu cho các bảng, các
dòng, các cột. Bạn có thể viết mã của mình – đó quả là một công việc chán nản, bạn có thể sử dụng các lớp bảo vệ kiểu sẵn có.
.NET Framework có các hỗ trợ cho việc dùng các sơ đồ XML để định nghĩa một DataSet, DataTable, và các lớp khác mà chúng
ta có thể làm trong phần này. Phần XML Schemas nằm trong chương này sẽ bàn về các phương thức này, nhưng trước tiên, chúng
ta sẽ xem xét về các quan hệ và ràng buộc trong một DataSet.

Các quan hệ dữ liệu

Khi viết một ứng dụng, thường cần phải có sẵn nhiều bảng để lưu trữ thông tin. Lớp DataSet là một nơi để chứa các thông tin
này.

Lớp DataSet là một thiết kế để tạo nên các mối quan hệ giữa các các bảng. Mã trong phần này được tôi thiết kế để tạo bằng tay
mối quan hệ cho hai bảng dữ liệu. Vì vậy, nếu bạn không có SQL Server hoặc cơ sở dữ liệu NorthWind, bạn cũng có thể chạy ví
dụ này. Mã có sẵn trong thư mục 09_DataRelationships:

DataSet ds = new DataSet("Relationships");


ds.Tables.Add(CreateBuildingTable());
ds.Tables.Add(CreateRoomTable());
ds.Relations.Add("Rooms",
ds.Tables["Building"].Columns["BuildingID"],
ds.Tables["Room"].Columns["BuildingID"]);

Các bảng đơn giản chứa một khóa chính và một trường tên, trong đó bảng Room có một khóa ngoại BuildingID.

Sau đó thêm một số dữ liệu cho mỗi bảng.

foreach(DataRow theBuilding in ds.Tables["Building"].Rows)


{
DataRow[] children = theBuilding.GetChildRows("Rooms");
int roomCount = children.Length;
Console.WriteLine("Building {0} contains {1} room{2}",
theBuilding["Name"],
roomCount,
roomCount > 1 ? "s" : "");
// Loop through the rooms
foreach(DataRow theRoom in children)
Console.WriteLine("Room: {0}", theRoom["Name"]);
}

Sự khác biệt lớn nhất giữa DataSet và kiểu đối tượng Recordset cổ điển là sự biểu hiện của quan hệ. Trong một Recordset cổ
điển, một quan hệ được biểu diễn là một cột giả trong dòng. Cột này bản thân nó là một Recordset có thể lập lại. Trong
ADO.NET, một quan hệ đơn giản là một lời gọi phương thức GetChildRows():

DataRow[] children = theBuilding.GetChildRows("Rooms");

Phương thức này có một số kiểu, ví dụ trên chỉ ra cách dùng tên của quan hệ. Nó trả về một mảng các dòng có thể cập nhật bằng
bộ chỉ mục như đã đề cập ở các ví dụ trước đây.

Thích thú hơn là quan hệ dữ liệu có thể xem xét theo hai cách. Không chỉ có thể đi từ cha đến con, mà có thể tìm được các dòng
cha của một mẫu tin con bằng cách sử dụng thuộc tính ParentRelations trên lớp DataTable. Thuộc tính này trả về một
DataRelationCollection, có thể truy cập bằng kí hiệu mảng [] (ví dụ, ParentRelations["Rooms"]), hoặc dùng luân phiên phương
thức GetParentRows() như mã dưới đây:

foreach(DataRow theRoom in ds.Tables["Room"].Rows)


{
DataRow[] parents = theRoom.GetParentRows("Rooms");
foreach(DataRow theBuilding in parents)
Console.WriteLine("Room {0} is contained in building {1}",
theRoom["Name"],
theBuilding["Name"]);
}
Có hai phương thức với rất nhiều các cài đặt đè khác nhau để trả về các dòng cha – GetParentRows() (trả về một mảng các dòng),
hoặc GetParentRow() (trả về một dòng cha duy nhất của một quan hệ).

Ràng buộc dữ liệu

Thay đổi kiểu dữ liệu của một cột đã được tạo trên một máy đơn không chỉ là một khả năng tuyệt vời của một DataTable.
ADO.NET cho phép bạn tạo một tập các ràng buộc trên một cột (hoặc nhiều cột), dùng cho các nguyên tắc chuẩn hóa dữ liệu.

Thời gian chạy hỗ trợ các kiểu ràng buộc sau, như là các lớp trong không gian System.Data.

Constraint Description

ForeignKeyConstraint Thực một liên kết giữa hai DataTables trong một DataSet

UniqueConstraint Bảo đảm tính độc nhất của cột

Cài đặt khóa chính

Một điều phổ biến của một bảng trong một cơ sở dữ liệu quan hệ, bạn có thể cung cấp một khóa chính, dựa vào một hoặc nhiều
cột trong một DataTable.

Mã sau tạo một khóa chính cho bảng Products, mà sơ đồ của nó đã được tạo bằng thủ công trong các ví dụ trên, chúng ta có thể
tìm thấy trong thư mục 08_ManufactureDataSet.

Chú ý rằng một khóa chính cảu một bảng chỉ là một kiểu của ràng buộc. Khi một khóa chính được thêm vào một DataTable, thời
gian chạy cũng phát ra một ràng buộc độc nhất trên khóa chính. Bởi vì thực tế không tồn tại kiều ràng buộc PrimaryKey – một
khóa chính đơn giản là một ràng buộc duy nhất trên một hoặc nhiều cột.

public static void ManufacturePrimaryKey(DataTable dt)


{
DataColumn[] pk = new DataColumn[1];
pk[0] = dt.Columns["ProductID"];
dt.PrimaryKey = pk;
}

Một khóa chính có thể bao gồm một vài cột, nó được xem như là một mảng các DataColumns. Một khóa chính của một bảng
được cài đặt trên những cột này đơn giản được còi là một mảng của các cột làm nên thuộc tính.

Để kiểm tra các ràng buộc của một bảng, bạn có thể lập lại ConstraintCollection. Đối với ràng buộc tự sinh như ví dụ trên, tên
của ràng buộc sẽ là Constraint1. Nó không phải là một tên tốt, vì vậy tốt nhất là nên tạo ràng buộc trước sau đó định nghĩa các cột
tạo nên khóa chính, như chúng ta sẽ làm dưới đây.

Là một lập trình viên cơ sở dữ liệu lâu năm, tôi nhận thấy ràng tên của một ràng buộc cần phải thật rõ nghĩa. Mã dưới đây định
danh ràng buộc trước khi tạo khóa chính:

DataColumn[] pk = new DataColumn[1];


pk[0] = dt.Columns["ProductID"];
dt.Constraints.Add(new UniqueConstraint("PK_Products", pk[0]));
dt.PrimaryKey = pk;

Ràng buộc duy nhất có thể áp dụng cho bao nhiêu cột tùy thích.

Tạo một khóa ngoại

Ngoài các ràng buộc duy nhất, một DataTable có thể chứa các ràng buộc khóa ngoại. Nó thường được áp dụng cho các mối quan
hệ chủ tớ, nhưng cũng có thể dùng để tạo bảng sao các cột giữa các bảng nếu bạng tạo một ràng buộc chính xác. Một quan hệ chủ
tớ là một mẫu tin cha có thể có nhiều mẫu tin con, liên kết với khóa chính của mẫu tin cha.

Một ràng buộc khóa ngoại có thể chỉ thực thi trên các bảng bên trong một DataSet, ví dụ dưới đây sử dụng bảng Categories trong
cơ sở dữ liệu Northwind, và tạo một ràng buộc giữa nó với bảng Products table.
Bước đầu tiên là tạo một bảng dữ liệu mới cho bảng Categories. Ví dụ 08_ManufactureDataSet:

DataTable categories = new DataTable("Categories");


categories.Columns.Add(new DataColumn("CategoryID", typeof(int)));
categories.Columns.Add(new DataColumn("CategoryName", typeof(string)));
categories.Columns.Add(new DataColumn("Description", typeof(string)));
categories.Constraints.Add(new UniqueConstraint("PK_Categories",
categories.Columns["CategoryID"]));
categories.PrimaryKey = new DataColumn[1]
{categories.Columns["CategoryID"]};

Dòng cuối cùng của mã trên tạo một khóa chính cho bảng Categories. Khóa chính là một cột đơn, tất nhiên nó cũng thể tạo một
khóa chính trên nhiều cột bằng các dùng kí tự mảng.

Sau đó tôi tạo một ràng buộc giữa hai bảng:

DataColumn parent = ds.Tables["Categories"].Columns["CategoryID"];


DataColumn child = ds.Tables["Products"].Columns["CategoryID"];
ForeignKeyConstraint fk =
new ForeignKeyConstraint("FK_Product_CategoryID", parent, child);
fk.UpdateRule = Rule.Cascade;
fk.DeleteRule = Rule.SetNull;
ds.Tables["Products"].Constraints.Add(fk);

Ràng buộc này dùng để liên kết giữa Categories.CategoryID và Products.CategoryID. Có bốn cấu trúc khác nhau cho
ForeignKeyConstraint, nhưng tôi khuyên bạn nên dùng tên của ràng buộc.

Tạo các ràng buộc Update và Delete

Bổ sung cho phần định nghĩa tất nhiên là một vài kiểu của ràng buộc giữa các bảng cha và con, bạn có thể định nghĩa phải làm gì
trong một ràng buộc cập nhật.

Ví dụ trên tạo một qui tắc cập nhật và một qui tắc xóa. Những qui tắc này được dùng khi môt sự kiện được phát ra trên cột (hoặc
dòng) trong bảng cha, và qui tắc được dùng để quyết định chuyện gì sẽ xảy ra trong bảng con. Có bốn qui tắc khác nhau có thể áp
dụng được liệt kê trong Rule enumeration:

• Cascade – Nếu khóa cha được cập nhật sau đó copy giá trị mới này cho tất cả các mã của khóa con. Nếu mẫu cha bị
xoá, thì xóa luôn các mẫu con. Nó là tùy chọn mặc định.
• None – Không làm gì hết. Tùy chọn này sẽ bỏ các dòng mô côi khỏi bảng dữ liệu con.

• SetDefault – Mỗi thay đổi trên dòng con được mang giá trị mặc định của nó, nếu nó được định nghĩa trước.

• SetNull – Tất cả các dòng được chọn là DBNull.

Chú ý:
Các ràng buộc chỉ có hiệu lực trong một DataSet nếu thuộc tính EnforceConstraints của DataSet là
true.

Tôi đã đổi các lớp chính dùng để tạo nên DataSet, chỉ ra cách tạo thủ công các lớp bằng mã. Có một cách khác để định nghĩa một
DataTable, DataRow, DataColumn, DataRelation, và Constraint – bằng cáchc dùng các file sơ đồ XML và công cụ XSD sẵn có
trong .NET. Đoạn dưới đây trình bày cách để tạo một sơ đồ đơn giản và tạo các lớp bảo vệ kiểu để truy cập dữ liệu của bạn.

Tạo một DataSet


Trước tiênn bạn đã định nghĩa sơ đồ của bộ dữ liệu của bạn, với đầy đủ các DataTable, DataColumn, Constraint, và những gì cần
thiết, bạn nên tạo DataSet v̕i một vài thông tin bổ sung. Có hai cách chính để đọc dữ liệu từ một nguồn bên ngoài và chèn nó vào
DataSet:
• Dùng trình cung cấp dữ liệu
• Đọc XML vào trong DataSet

Tạo một DataSet dùng một DataAdapter


Đoạn mã về dòng dữ liệu được giới thiệu trong lớp SqlDataAdapter, được trình bày như sau:

string select = "SELECT ContactName,CompanyName FROM Customers";


SqlConnection conn = new SqlConnection(source);
SqlDataAdapter da = new SqlDataAdapter(select , conn);
DataSet ds = new DataSet();
da.Fill(ds , "Customers");

Hai dòng in đậm chỉ ra cách dùng của SqlDataAdapter – OleDbDataAdapter cũng có nhưng tính năng ảo giống như Sql
equivalent.

SqlDataAdapter và OleDbDataAdapter là hai lớp xuất phát từ một lớp cơ bản chứ không phải là một bộ các giao diện, và nhất là
các lớp SqlClient- hoặc OleDb. Cây kế thưa được biểu diễn như sau:

System.Data.Common.DataAdapter
System.Data.Common.DbDataAdapter
System.Data.OleDb.OleDbDataAdapter
System.Data.SqlClient.SqlDataAdapter

Trong quá trình lấy dữ liệu từ một DataSet, cần phải có một vài lệnh được dùng để chọn dữ liệu. Nó có thể là một câu lệnh
SELECT, một stored procedure, hoặc OLE DB provider, một TableDirect command. Ví dụ trên sử dụng một trong những cấu
trúc sẵn có trong SqlDataAdapter để truyền câu lệnh SELECT vào một SqlCommand, và phát nó khi gọi phương thức Fill() trên
adapter.

Trở lại với các ví dụ về stored procedures trong chương trước, Tôi đã định nghĩa các stored procedure INSERT, UPDATE, và
DELETE, nhưng chưa đưa ra một procedure để SELECT dữ liệu. Chúng ta sẽ lấp lỗ hổng này trong phần sau, và chỉ ra cách làm
sao để gọi một stored procedure từ một SqlDataAdapter để tạo dữ liệu cho một DataSet.

Sử dụng một Stored Procedure trong một DataAdapter

Trước tiên chúng ta cần định nghĩa một stored procedure và cài nó vào cơ sở dữ liệu database. Mã cho ví dụ này sẵn có trong thư
mục 11_DataAdapter. Stored procedure đẻ SELECT dữ liệu như sau:

CREATE PROCEDURE RegionSelect AS


SET NOCOUNT OFF
SELECT * FROM Region
GO

Ví dụ này tương đối đơn giản nó thật không xứng tầm với một stored procedure, chỉ là một câu lệnh SQL đơn giản. Stored
procedure này có thể đánh vào SQL Server Query Analyzer, hoặc bạn có thể chạy file StoredProc.sql để sử dụng ví dụ này.

Tiếp theo, chúng ta cần định nghĩa một SqlCommand để thực thi stored procedure này. Một lần nữa mã rất đơn giản, và hầu hết
đã được đưa ra trong các phần trên:

private static SqlCommand GenerateSelectCommand(SqlConnection conn )


{
SqlCommand aCommand = new SqlCommand("RegionSelect" , conn);
aCommand.CommandType = CommandType.StoredProcedure;
aCommand.UpdatedRowSource = UpdateRowSource.None;
return aCommand;
}

Phương thức này phát ra SqlCommand để gọi thủ tục RegionSelect khi thực thi. Và cuối cùng là móc nói nó với một
SqlDataAdapter thông qua lời gọi phương thức Fill():

DataSet ds = new DataSet();


// Create a data adapter to fill the DataSet
SqlDataAdapter da = new SqlDataAdapter();
// Set the data adapter's select command
da.SelectCommand = GenerateSelectCommand (conn);
da.Fill(ds , "Region");
Ở đây tôi tạo một SqlDataAdapter mới, xem SqlCommand được phát ra thông qua thuộc tính SelectCommand của data adapter,
và gọi Fill(), để thực thi stored procedure và chèn tất cả các dòng vào the Region DataTable.

Tạo một DataSet từ XML


Ngoài việc tạo sơ đồ cho mọt DataSet và các bảng tương ứng, một DataSet có thể đọc và ghi các dữ liệu bẩm sinh XML, giống
như một file trên đĩa, một stream, hoặc một text reader.

Để load XML vào một DataSet, đơn giản gọi một trong những phương thức ReadXML(), chẳng hạn như mã sau, dùng để đọc từ
một file trên đĩa:

DataSet ds = new DataSet();


ds.ReadXml(".\\MyData.xml");

Các cố gắng thay đổi DataSet


Sau khi soạn thạo dữ liệu trong một DataSet, cũng có những lúc cần phải thay đổi nó. Một ví dụ khá phổ biến đó là chọn dữ liệu
từ một cơ sở dữ liệu, biểu diễ nó cho người dùng, và cập nhật cho cơ sở dữ liệu.

Cập nhật với các Data Adapter


Một SqlDataAdapter có thể bao gồm SelectCommand, một InsertCommand, UpdateCommand, và DeleteCommand. Giống như
tên gọi, những đối tượng này là những thể hiện của SqlCommand (hoặc OleDbCommand dùng cho OleDbDataAdapter), vì vậy
những câu lệnh này có thể chuyển thành SQL hoặc một stored procedure.

Trong ví dụ này, tôi đã khôi phục lại các mã stored procedure từ phần Calling Stored Procedures để chèn, cập nhật, và xóa các
mẫu tin Region. Mã có sẵn trong thư mục 12_DataAdapter2.

Chèn một dòng mới

Có hai cách để thêm một dòng mới vào một DataTable. Cách thứ nhât là gọi phương thức NewRow, để trả về một dòng trống sau
đó định vị và thêm vào tập Rows như sau:

DataRow r = ds.Tables["Region"].NewRow();
r["RegionID"]=999;
r["RegionDescription"]="North West";
ds.Tables["Region"].Rows.Add(r);

Cách thứ hai để thêm một dòng mới là truyền một mảng dữ liệu vào phương thức Rows.Add() giống như sau:

DataRow r = ds.Tables["Region"].Rows.Add
(new object [] { 999 , "North West" });

Mỗi dòng trong DataTable sẽ cài RowState là Added. Ví dụ sẽ xổ ra các mẫu tin trước khi nó thay đổi được cập nhật cho dữ liệu,
vì vậy sau khi thêm các dòng sau vào DataTable, các dòng sẽ giống như sau. Chú ý rằng cột bên phải là trạng thái dòng.

New row pending inserting into database


1 Eastern Unchanged
2 Western Unchanged
3 Northern Unchanged
4 Southern Unchanged
999 North West Added

Để cập nhật cơ sở dữ liệu từ một DataAdapter, gọi phương thức Update() như sau đây:

da.Update(ds , "Region");

Đối với một dòng mới trong DataTable, sẽ thực thi stored procedure và xuất ra các mẫu tìn trong DataTable một lần nữa.

New row updated and new RegionID assigned by database


1 Eastern Unchanged
2 Western Unchanged
3 Northern Unchanged
4 Southern Unchanged
5 North West Unchanged
Hãy nhìn dòng cuối của DataTable. Tôi đã nhập RegionID trong mã là 999, nhưng sau khi sẽ đổi RegionInsert stored procedure
giá trị được đổi thành 5. Có sở dữ liệu thường tạo khoá chính cho bạn.

SqlCommand aCommand = new SqlCommand("RegionInsert" , conn);

aCommand.CommandType = CommandType.StoredProcedure;
aCommand.Parameters.Add(new SqlParameter("@RegionDescription" ,
SqlDbType.NChar ,
50 ,
"RegionDescription"));
aCommand.Parameters.Add(new SqlParameter("@RegionID" ,
SqlDbType.Int,
0 ,
ParameterDirection.Output ,
false ,
0 ,
0 ,
"RegionID" , // Defines the SOURCE column
DataRowVersion.Default ,
null));
aCommand.UpdatedRowSource = UpdateRowSource.OutputParameters;

Chuyện gì sẽ xảy ra khi một data adapter phát các lệnh này, các tham số xuất sẽ được ánh xạ trở lại mã nguồn của dòng. Stored
procedure có một tham số xuất ánh xạ trở lại DataRow.

Giá trị của UpdateRowSource được cho trong bảng sau:

UpdateRowSource Value Description

Both Một stored procedure có thể trả về nhiều tham số xuất và cũng có thể là một cơ sở dữ liệu
gồm các mẫu tin đã được cập nhật.

FirstReturnedRecord Nó trả về một mẫu dữ liệu đơn, nội dung của mẫu tin đó có thể được trộn vào DataRow
nguồn. Nó có có ích đối với một bảng có các cột mang giá trị mặc định hoặc tính toán,
sau khi một câu lệnh INSERT chúng cần phải được đồng bộ với các DataRow trên máy
trạm. Một ví dụ có thể là be 'INSERT (columns) INTO (table) WITH (primarykey)', sau
đó là 'SELECT (columns) FROM (table) WHERE (primarykey)'. Các mẫu tin trả về có
thể trộn vào tập các dòng.

None Tất cả dữ liệu trả về từ câu lệnh đều bị vứt bỏ.

OutputParameters Bất kì tham số xuât nào của câu lệnh đều được ánh xạ vào một cột thích hợp trong
DataRow.

Cập nhật một dòng đã có

Cập nhật một dòng có sẵn trong DataTable chỉ là một trường hợp việc sử dụng bộ chỉ mục của lớp DataRow với tên của một cột
hoặc số thứ tự của cột, giống như ví dụ sau đây:

r["RegionDescription"]="North West England";


r[1] = "North East England";

Các hai câu lệnh đều cho cùng kết quả:

Changed RegionID 5 description


1 Eastern Unchanged
2 Western Unchanged
3 Northern Unchanged
4 Southern Unchanged
5 North West England Modified

Trong quá trính cập nhật cơ sở dữ liệu, trạng của dòng được cập nhật sẽ được gán là Modified.

RowXóa một dòng


Xóa một dòng là kết quả của việc gọi phương thức Delete():

r.Delete();

Một dòng được xóa có trạng thái là Deleted, nhưng bạn không thể đọc các cột từ một dòng đã xoá, nó không còn giá trị nữa. Khi
gọi phương thức Update(), tất cả các dòng bị xoá sẽ sử dụng DeleteCommand, trong trường hợp này sẽ chạy stored procedure
RegionDelete.

Viết XML xuất


Như bạn đã thấy, DataSet hỗ trợ mạnh việc định nghĩa sơ đồ của nó trong một XML, và bạn có thể đọc dữ liệu từ một tài liệu
XML, bạn cũng có thể viết một tài liệu XML.

Phương thức DataSet.WriteXml() cho phép bạn xuất các thành phần khác nhau của dữ liệu được lưu trong DataSet. Bạn có thể
chọn chỉ xuất dữ liệu, hoặc dữ liệu và sơ đồ. Mã sau đây đưa ra một ví dụ của cho cả hai loại trên:

ds.WriteXml(".\\WithoutSchema.xml");
ds.WriteXml(".\\WithSchema.xml" , XmlWriteMode.WriteSchema);

File đầu tiên, WithoutSchema.xml được đưa ra dưới đây:

<?xml version="1.0" standalone="yes"?>


<NewDataSet>
<Region>
<RegionID>1</RegionID>
<RegionDescription>Eastern </RegionDescription>
</Region>
<Region>
<RegionID>2</RegionID>
<RegionDescription>Western </RegionDescription>
</Region>
<Region>
<RegionID>3</RegionID>
<RegionDescription>Northern </RegionDescription>
</Region>
<Region>
<RegionID>4</RegionID>
<RegionDescription>Southern </RegionDescription>
</Region>
</NewDataSet>

File WithSchema.xml bao gồm cả sơ đồ và dữ liệu của DataSet:

<?xml version="1.0" standalone="yes"?>


<NewDataSet>
<xs:schema id="NewDataSet" xmlns=""
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xs:element name="NewDataSet" msdata:IsDataSet="true">
<xs:complexType>
<xs:choice maxOccurs="unbounded">
<xs:element name="Region">
<xs:complexType>
<xs:sequence>
<xs:element name="RegionID"
msdata:AutoIncrement="true"
msdata:AutoIncrementSeed="1"
type="xs:int" />
<xs:element name="RegionDescription"
type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
<Region>
<RegionID>1</RegionID>
<RegionDescription>Eastern </RegionDescription>
</Region>
<Region>
<RegionID>2</RegionID>
<RegionDescription>Western </RegionDescription>
</Region>
<Region>
<RegionID>3</RegionID>
<RegionDescription>Northern </RegionDescription>
</Region>
<Region>
<RegionID>4</RegionID>
<RegionDescription>Southern </RegionDescription>
</Region>
</NewDataSet>

Chú ý rằng, các thuộc tính mở rộng cho các cột trong một DataSet, như AutoIncrement và AutoIncrementSeed - những thuộc tính
này tương ứgn với các thuộc tính có thể định nghĩa trong một DataColumn.

Tóm tắt
Chủ đề về truy cập cơ sở dữ liệu là một lĩnh vực rất lớn, đặc biệt là .NET với nhiều lớp bao bọc. Chương này cung cấp một phác
thảo về các lớp chính trong không gian ADO.NET, và chỉ ra cách dùng các lớp này trong lúc thao tác dữ liệu từ dữ liệu nguồn.

Trước tiên, chúng tôi chỉ ra cách dùng đối tượng Connection, thông qua việc dùng cả hai cách SqlConnection (cụ thể SQL
Server) và OleDbConnection (cho các nguồn dữ liệu OLE DB). Mô hình lập trình cho hai lớp này trương đối giống nhau và có
thể thay thế cho nhau.

Sau khi minh họa cách kết nối và không kết nối với dữ liệu nguồn, chúng tôi trình bày cách hoạt động của nó, vì vậy các tài
nguyên khan hiếm, như các kết nối cơ sở dữ liệu, cần phải được đóng càng sớm càng tốt. Cả hai lớp kết nối đều thực thi giao diện
IDisposable, được gọi khi một đối tượng dùng trong khối lệnh using. Tôi muốn nhấn mạnh với bạn rằng nêu đóng kết nối càng
sớm càng tốt.

Sau đó chúng tôi trình bày về các database command, thông qua các ví dụ không trả về giá trị gì cả, gọi các stored procedure
trong các tham số xuất và nhập. Các phương thức khác cũng đã được mô tả, trong đó phương thức ExecuteXmlReader chỉ có sẵn
trong SQL Server provider. Đây là cách đơn giản để chọn và thao tác trên dữ liệu của XML.

Các lớp chung trong không gian tên System.Data được mô tả chi tiết, từ lớp DataSet đến DataTable, DataColumn, DataRow và
tất nhiên cả các quan hệ, và các ràng buộc. Lớp DataSet là một kho chứa dữ liệu tuyệt vời, và các phương thức khác đã làm nên ít
tưởng truyền thông qua các tầng dữ liệu. Dữ liệu trong một DataSet có thể được mô tả trong XML để truyền liệu, thêm vào đó,
các phương thức có sẵn sẽ truyền dữ liệu nhỏ nhất giữa các lớp. Nhiều bảng trong một DataSet đơn sẽ làm tăng khả năng của nó;
có thể tồn tại các quan hệ tự động giữa các dòng chủ tớ chúng sẽ được khai thác trong chương kế tiếp.

Trong DataSet có lưu trữ sơ đồ cấu tạo, nhưng .NET cũng bao gồm trình cung cấp dữ liệu có sẵn đối tượng Command có thể
dùng để chọn dữ liệu vào một DataSet sau đó cập nhật đữ liệu trong bộ lưu trữ dữ liệu. Một lợi ích khác của các data adapte là
nhóm các lệnh thành bốn hoạt động - SELECT, INSERT, UPDATE và DELETE. Hệ thống có thể tạo một bộ các lệnh dự trên
thông tin về sơ đồ cơ sở dữ liệu và một lệnh SELECT, nhưng tốt nhất là chạy các stored procedure, các lệnh trong DataAdapter
được định nghĩa thích hợp để truyền những thông tin cần thiết cho những stored procedure này.

Một sơ đồ XML và XSD trở nên phổ biến trong một vài năm trở đây, chúng tôi đã trình bày các chuyền từ một sơ đồ XSD thành
một bộ các lớp dữ liệu bằng cách sử dụng công cụ XSD.EXE có trong .NET. Các lớp được tạo ra sẵn sàng dùng trong một ứng
dụng, nó tự động sinh ra vì vậy có thể tiếc kiệm được nhiều thời gian đánh máy.

Trong một vài trang cuối của chương chúng tôi đã giới thiệu một số cách tốt nhất trong thực hành và qui tắc đặt tên cho các nhà
phát triển ứng dụng cơ sở dữ liệu. Dù rằng nó không liên quan gì đến .NET, nhưng chúng cũng có những giá trị đáng kể. Cần
phải dựa vào tập các qui tắc này này khi lập trình trong cả C# với SQL Server database hay Perl scripts trên Linux.

Những kiến thức này sẽ rất tốt khi chuyển qua chương sau, chương sau chúng ta sẽ khám phá cách sử dụng Visual Studio và các
công cụ dữ liệu Windows Forms của .NET.

Chương 10: Viewing .NET data


Tổng quan
Chương cuối cùng sẽ chỉ các cách khác nhau để chọn và thay đổi dữ liệu. Chương này, chúng tôi sẽ minh hoạ cách bạn thể hiện
dữ liệu cho người sử dụng thấy bằng cách gắn kết các control Windows.
Khả năng gắn kết dữ liệu của .NET giống với ADO và các control của VB. Tất cả ngôn ngữ .NET đều có khả năng sử dụng cùng
những Control và phương thức. Khía cạnh mà chúng ta xem xét đó là control DataGrid.

Một trong những đặc tính hay nhất của Datagrid là tính uyển chuyển- nguồn dữ liệu có thể là mảng, DataTable, DataView,
DataSet hay là một thành phần thực thi các giao diện IListSource hay IList. Với một số lượng lớn các tuỳ chọn sẽ chỉ cách mà
mọi nguồn dữ liệu này được sử dụng và được xem trong DataGrid.

Gắn kết dữ liệu là một yêu cầu thông thường, và mặc dù VB 6 có khả năng này nhưng không bằng .NET, tất cả ngôn ngữ quản lý
đều hoàn thiện khả năng gắn kết dữ liệu. Có những gì hơn khi giới hạn các cột dữ liệu, một control sẽ cập nhật tự động khi hàng
dữ liệu hiện tại thay đổi. Chương này chúng ta tìm hiểu một vài khả năng gắn kết dữ liệu và chỉ cách để kết nối dữ liệu với
control Windows Forms. Chúng ta sẽ xem vài công việc bên trong quá trình gắn kết dữ liệu để hiểu rõ hơn cách chúng hoạt động.

Nguồn dữ liệu sẽ trở nên hợp lý hơn trong Visual studio.NET, và chương này sẽ chỉ cách sử
dụng server Explorer để tạo một sự kết nối và phát sinh Dataset. Chúng ta sẽ tìm hiểu cách dùng
của lược đồ XSD trong visual studio.NET.

Gắn kết dữ liệu

Ở ví dụ trứơc đã xem xét tất cả control DataGrid, đó chỉ là một phần trong thời gian chạy.NET có thể dùng để hiển thị dữ liệu.
Một tiến trình gắn kết một control và một nguồn dữ liệu được gọi là data binding.

Nếu bạn có những kinh nghiệm với các ứngdụng lập trình Windows trong MFC. Có lúc nào đó bạn đã sử dụng chức năng Dialog
Data Exchange (DDX)để móc các biến thành viên của một lớp với bộ điều khiển Win32. Bạn sẽ vui sướng khi biết rằng bạn có
thể giấu cửa trên DDX, như nó dễ dàng hơn để móc dữ liệu vào bộ điều khiển trong .NET. Bạn có thể gắn kết dữ liệu không chỉ
đến các bộ điều khiển Window mà còn với các trang Web ASP.NET.

Gắn kết đơn giản


Một control hỗ trợ việc gắn kết đơn hiển thị chỉ những giá trị đơn tại một lúc, như là một hộp văn bản hay một nút chọn. Ví dụ
sau chỉ cách gắn kết một cột từ một DataTable đến một hộp văn bản.

DataSet ds = CreateDataSet();
textBox1.DataBindings.Add("Text", ds , "Products.ProductName");

Sau khi lấy lại vài dữ liệu từ bảng Products và lưu trữ trong một DataSet được trả về từ phương thức CreateDataSet() như trên,
dòng thứ hai gắn kết thuộc tính Text của control đến cột Products.ProductName. Nếu bạn viết đoạn mã này từ cơ sở dữ liệu
Northwind, bạn sẽ thấy màn hình như bên dưới đây:

Hộp văn bản hiển thị vài thứ trong cơ sở dữ liệu. Để kiểm tra rằng nó là cột hay giá trị, bạn sẽ sử dụng công cụ SQL Server
Query Analyzer để kiểm tra nội dung của bảng Procucttool.

Đối tượng gắn kết dữ liệu


Sơ đồ sau chỉ một thừa kế lớp cho các đối tượng được sử dụng trong gắn kết dữ liệu. Trong phần này ta bàn luận về
BindingContext, CurrencyManager, và PropertyManager các lớp của System.Windows.Forms, và trình cách chúng tương tác khi
dữ liệu giới hạn trong một hay nhiều control trên một form. Các đối tượng chuyển màu được dùng trong gắn kết.
Trong ví dụ trước, chúng ta sử dụng thuộc tính DataBinding của control TextBox để gắn kết một cột từ một DataSet đến thuộc
tính Text của bộ điều khiển. Thuộc tính DataBindings là một thể hiện của ControlBindingsCollection :

textBox1.DataBindings.Add("Text", ds, "Products.ProductName");

Dòng này thêm một đối tượng gắn kết từ một đối tượng Binding đến ControlBindingsCollection

Binding Context

Mọi Windows form có một thuộc tính BindingContext. Form được thừa hưởng từ Control . Một đối tượng BindingContext có
một tập thể hiện BindingManagerBase. Những thể hiện này được tạo và thêm vào đối tượng quản lý gắn kết khi một control bị
giới hạn:

BindingContext sẽ chứa vài nguồn dữ liệu, được gói trong một CurrencyManager hay một PropertyManager. Sự quyết định lớp
nào được dùng dựa vào chính nguồn dữ liệu.

Nếu nguồn dữ liệu chứa một dãy item như là DataTable, DataView, hay bất kỳ đối tượng khác thực thi giao diện IList thì một
CurrencyManager sẽ được dùng, như nó có thể duy trì vị trí hiện tại bên trong nguồn dữ liệu. Nếu nguồn dữ liệu chỉ trả về một
giá trị đơn thì một PropertyManager sẽ được lưu trữ trong BindingContext.

Một CurrencyManager hay PropertyManager chỉ được tạo một lần cho một nguồn dữ liệu. Nếu bạn gắn kết hai hộp văn bản với
một hàng từ một DataTable thì chỉ một currencyManager sẽ được tạo bên trong binding context.

Mọi control thêm vào một form được gắn kết với bộ quản lý gắn kết của form, vì thế tất cả control chia sẽ cùng một thể hiện. Khi
một control được tạo thuộc tính BindingContext của nó là null. Khi control được thêm bộ Control của form thì nó sẽ cài
BindingContext đến bộ đó của form.

Để gắn kết một control với một form, bạn cần thêm một thực thể vào thuộc tính DataBinding của nó. Đoạn mã bên dưới tạo một
sự gắn kết mới:

textBox1.DataBindings.Add("Text", ds, "Products.ProductName");

Phương thức Add() của ControlBindingsCollection tạo một thể hiện mới của đối tượng Binding từ những thông số của phương
thưc này và thêm chúng vào bộ những việc gắn kết
Hình trên trình bày những gì đang hoạt động khi bạn thêm một Binding đến một Control. Binding gắn kết control với một nguồn
dữ liệu được duy trì bên trong BindingContext của Form. Sự thay đổi bên trong nguồn dữ liệu được phản ánh vào control như là
những thay đổi trong control đó.

Binding

Lớp này gắn kết một thuộc tính của control với một thành viên của nguồn dữ liệu. Khi những thành viên này thay đổi thì những
thuộc tính của control được cập nhật để phản ánh sự thay đổi này và ngược lại

Bindings có thể cài đặt từ bất kỳ cột nào đến bất kỳ thuộc tính nào của control, vì thế bạn sẽ gắn kết một cột với một hộp văn bản
và có thể gắn kết cột khác với màu hộp văn bản..

Bạn có thể gắn kết các thuôc tính của một control đến các nguồn dữ liệu khác nhau .

CurrencyManager và PropertyManager

Khi một đối tượng Binding được tạo, một đối tượng CurrencyManager hay PropertyManager sẽ được tạo nếu đó là lần đầu tiên
dữ liệu đó từ nguồn bị giới hạn. Mục đích của lớp này là định nghĩa vị trí của mẫu tin hiện hành trong nguồn dữ liệu và kết hợp
tất cả dãy bindings khi mẫu tin hiện hành này bị thay đổi.

Ví dụ sau sẽ hiển thị hai trường từ bảng Product và bao gồm một cách để di chuyển giữa các mẫu tin bằng các phương tiện của
một control TrackBar.

Đoạn mã cho ứng dụng này nằm hoàn toàn trong thư mục 09_ScrollingDataBinding

using System;
using System.Windows.Forms;
using System.Data;
using System.Data.SqlClient;

public class ScrollingDataBinding : System.Windows.Forms.Form


{
private Button retrieveButton;
private TextBox textName;
private TextBox textQuan;
private TrackBar trackBar;
private DataSet ds;

Ứng dụng trên tạo cửa sổ và tất cả control cho cửa sổ đó bên trong một hàm khởi tạo ScrollingDataBinding:

public ScrollingDataBinding()
{
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(464, 253);
this.Text = "09_ScrollingDataBinding";
this.retrieveButton = new Button();
retrieveButton.Location = new System.Drawing.Point(4, 4);
retrieveButton.Size = new System.Drawing.Size(75, 23);
retrieveButton.TabIndex = 1;
retrieveButton.Anchor = AnchorStyles.Top | AnchorStyles.Left;
retrieveButton.Text = "Retrieve";
retrieveButton.Click += new System.EventHandler
(this.retrieveButton_Click);
this.Controls.Add(this.retrieveButton);
this.textName = new TextBox();
textName.Location = new System.Drawing.Point(4, 31);
textName.Text = "Please click retrieve...";
textName.TabIndex = 2;
textName.Anchor = AnchorStyles.Top | AnchorStyles.Left |
AnchorStyles.Right ;
textName.Size = new System.Drawing.Size(456, 20);
textName.Enabled = false;
this.Controls.Add(this.textName);
this.textQuan = new TextBox();
textQuan.Location = new System.Drawing.Point(4, 55);
textQuan.Text = "";
textQuan.TabIndex = 3;
textQuan.Anchor = AnchorStyles.Top | AnchorStyles.Left |
AnchorStyles.Top;
textQuan.Size = new System.Drawing.Size(456, 20);
textQuan.Enabled = false;
this.Controls.Add(this.textQuan);
this.trackBar = new TrackBar();
trackBar.BeginInit();
trackBar.Dock = DockStyle.Bottom ;
trackBar.Location = new System.Drawing.Point(0, 275);
trackBar.TabIndex = 4;
trackBar.Size = new System.Drawing.Size(504, 42);
trackBar.Scroll += new System.EventHandler(this.trackBar_Scroll);
trackBar.Enabled = false;
this.Controls.Add(this.trackBar);
}

Khi nút Retrieve được click, sự kiện handler chọn tất cả mẫu tin từ bảng Product và lưu trữ trong Dataset riêng ds:

protected void retrieveButton_Click(object sender, System.EventArgs e)


{
retrieveButton.Enabled = false ;
ds = CreateDataSet();

Tiếp theo là hai control văn bản được giới hạn

textName.DataBindings.Add("Text" , ds ,
"Products.ProductName");
textQuan.DataBindings.Add("Text" , ds ,
"Products.QuantityPerUnit");
trackBar.Minimum = 0 ;
trackBar.Maximum = this.BindingContext[ds,"Products"].Count – 1;
textName.Enabled = true;
textQuan.Enabled = true;
trackBar.Enabled = true;
}
Ở đây chúng ta có một mẫu tin cuộn để phản ứng lại với sự di chuyển của TrackBar:

protected void trackBar_Scroll(object sender , System.EventArgs e)


{
this.BindingContext[ds,"Products"].Position = trackBar.Value;
}
private DataSet CreateDataSet()
{
string source = "server=(local)\\NetSDK;" +
"uid=QSUser;pwd=QSPassword;" +
"database=northwind";
string customers = "SELECT * FROM Products";
SqlConnection con = new SqlConnection(source);
SqlDataAdapter da = new SqlDataAdapter(customers , con);
DataSet ds = new DataSet();
da.Fill(ds , "Products");
return ds;
}
static void Main()
{
Application.Run(new ScrollingDataBinding());
}
}

Khi dữ liệu được khôi phục, vị trí lớn nhất trên track bar được cài là số lượng của mẫu tin. Sau đó, trong phương thức scroll ở
trên, chúng ta cài vị trí của BindingContext cho DataTable products đến vị trí của scroll bar thumb. Nó thay đổi mẫu tin hiện
hành từ DataTable, vì thế tất cả control giới hạn đến hàng hiện hành.

Visual Studio và Data Access

Với phiên bản mới của Visual studio đưa ra vài cách mới để truy cập dữ liệu trong các ứng dụng của bạn. Phần này sẽ bàn luận
về một số cách mà Visual Studio.NET cho phép dữ liệu được hợp nhất trong GUI, để bạn có thể tương tác với dữ liệu.

Các công cụ cho phép bạn tạo một sự kết nối cơ sở dữ liệu là sử dụng các lớp OleDbConnection hay SqlConnection. Lớp mà bạn
sẽ dùng phụ thuộc vào cơ sở dữ liệu nào bạn muốn kết nối. Khi định nghĩa một sự kết nối, bạn có thể tạo một DataSet và định vị
nó từ bên trong Visual studio.NET. Vấn đề này sẽ tạo ra một tập tin XSD cho DataSet như là chúng ta đã làm bằng tay trong
chương trước và tự động phát ra các mã .cs cho bạn. Kết quả này nằm trong sự tạo thành của một type-safe DataSet.

Trong phần này ta sẽ học cách tạo một sự kết nối, chọn một số dữ liệu và tạo ra một DataSet, và sử dụng tất cả đối tượng được
tạo ra để làm một ứng dụng đơn giản.

Tạo một sự kết nối


Để bắt đầu phần này, ta phải tạo một ứng dụng Windows. Khi tạo bạn sẽ thấy một form trống. Công việc đầu tiên là tạo một sự
kết nối cơ sở dữ liệu mới. Mở Server Explorer bằng cách gõ Ctrl+Alt+S hay chọn mục Server Explorer từ menu. Cửa sổ sẽ hiển
thị như sau:

Trong cửa sổ này bạn có thể quản lý nhiều khía cạnh khác nhau của việc truy cập dữ liệu. Theo ví dụ này, bạn cần tạo một sự kết
nối đến cơ sở dữ liệu Northwind. Chọn Add Connection...từ menu trên mục Data Connections sẽ tự động hiện lên một trình thông
minh để bạn có thể chọn OLEBD provider nào được dùng- ở đây ta chọn Microsoft OLEDB Provider cho SQL server, khi bạn sẽ
được kết nối với cơ sở dữ liệu Northwind được cài đặt như một phần của mẫu Framework SDK. Trang thứ hai của hộp thoại
Data Link như sau:
Phụ thuộc vào cách bạn cài đặt các cơ sở dữ liệu mẫu Framework thì bạn sẽ có một thể hiện của cơ sở dữ liệu Northwind trong
SQL Server, và một thể hiện trong một cơ sở dữ liệu local MSDE (Microsoft Data Engine), hay cả hai.

Để kết nối với cơ sở dữ liệu MSDE thì gõ (local)\NETSDK và tên của server. Để kết nối một thể hiện của SQL server bạn gõ
(local) như hiện ở trên cho bộ máy hiện tại hay tên của server muốn kết nối trên mạng.

Tiếp theo, bạn cần chọn thông tin đăng nhập. Bạn phải chọn lại một lần nữa phụ thuộc vào cách cơ sở dữ liệu của bạn được cài
đặt. Đối với cơ sở dữ liệu local MSDE, bạn có thể dùng một username và Password đặc biệt tương ứng với QSUser và
QSPassword.

Chọn cơ sở dữ liệu Northwind từ danh sách cơ sở dữ liệu, và để chắc rằng bạn đã cài đặt mọi thứ chính xác thì click vào nút Test
Connection. Hành động này sẽ kết nối cơ sở dữ liệu và hiện một hộp tin khi hoàn tất. Dĩ nhiên, bạn phải cài server trên cấu hình
của máy bạn. vì thế Username, password và tên server sẽ khác nhau.

Để tạo một đối tượng kết nối, click và kéo server mới đến cửa sổ ứng dụng chính. Nó sẽ tạo một biến thành viên của kiểu
System.Data.SqlClient.SqlConnection, hay System.Data.OleDb.OleDbConnection nếu bạn chọn một provider khác và thêm đoạn
mã sau vào phương thức InitializeComponent của form chính:

this.sqlConnection1 = new System.Data.SqlClient.SqlConnection();

//
// sqlConnection1
//

this.sqlConnection1.ConnectionString = "data source=skinnerm\\NETSDK;" +


"initial catalog=Northwind;" +
"user id=QSUser;password=QSPassword;" +
"persist security info=True;" +
"workstation id=SKINNERM;" +
"packet size=4096";

Như bạn thấy, sự kết nối thông tin chuỗi được gắn trực tiếp trong đoạn mã.

Khi bạn thêm đối tượng này và dự án bạn sẽ chú ý đối tượng sqlConnection1 xuất hiện trong vùng bên dưới của cửa sổ visual
studio.
Chọn dữ liệu
Khi bạn định nghĩa một sự kết nối dữ liệu, bạn có thể chọn một bản từ danh sách và kéo bảng đó đến một form trên dự án của
bạn.

Ví dụ, ta chọn bảng Customer. khi bạn kéo đối tượng này vào dự án của bạn nó sẽ thêm một đối tượng vào form của bạn được
thừa hưởng từ SqlDataAdapter, hay OleDbDataAdapter nếu bạn không dùng SQL Server.

Data adapter đã tạo ra chứa đựng các lệnh SELECT, INSERT, UPDATE, và DELETE. Đoạn mã tạo trình thông minh sẽ thực
hiện ngay lúc này nhưng visual studio.NET thêm đoạn mã sau vào tập tin .cs của bạn.

private System.Data.SqlClient.SqlCommand sqlSelectCommand1;


private System.Data.SqlClient.SqlCommand sqlInsertCommand1;
private System.Data.SqlClient.SqlCommand sqlUpdateCommand1;
private System.Data.SqlClient.SqlCommand sqlDeleteCommand1;
private System.Data.SqlClient.SqlDataAdapter sqlDataAdapter1;

Có một đối tượng đã định nghĩa cho mọi lệnh SQL và một sqlDataAdapter. Trong phương thức InitializeComponent(), trình
thông minh tạo ra đoạn mã để tạo mọi lệnh này và data adapter. Đoạn mã thì dông dài, vì thế tôi chỉ đưa ra một đoạn ở đây.

Có hai khía cạnh của đoạn mã được tạo bởi Visual studio.NET là các giá trị được nhìn thấy từ các thuộc tính UpdateCommand và
InsertCommand. Đây là một phiên bản tóm tắt hiện thông tin thích đáng:

// sqlInsertCommand1
//
this.sqlInsertCommand1.CommandText = @"INSERT INTO dbo.Customers
(CustomerID, CompanyName, ContactName,
ContactTitle, Address, City, Region,
PostalCode, Country, Phone, Fax)
VALUES(@CustomerID, @CompanyName, @ContactName, @ContactTitle,
@Address, @City, @Region, @PostalCode, @Country, @Phone, @Fax);
SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address,
City, Region, PostalCode, Country, Phone, Fax
FROM dbo.Customers WHERE (CustomerID = @Select2_CustomerID)";
this.sqlInsertCommand1.Connection = this.sqlConnection1;

//
// sqlUpdateCommand1
//

this.sqlUpdateCommand1.CommandText = @"UPDATE dbo.Customers


SET CustomerID = @CustomerID, CompanyName = @CompanyName,
ContactName = @ContactName, ContactTitle = @ContactTitle,
Address = @Address, City = @City, Region = @Region,
PostalCode = @PostalCode, Country = @Country,
Phone = @Phone, Fax = @Fax
WHERE (CustomerID = @Original_CustomerID)
AND (Address = @Original_Address) AND (City = @Original_City)
AND (CompanyName = @Original_CompanyName)
AND (ContactName = @Original_ContactName)
AND (ContactTitle = @Original_ContactTitle)
AND (Country = @Original_Country)
AND (Fax = @Original_Fax)
AND (Phone = @Original_Phone)
AND (PostalCode = @Original_PostalCode)
AND (Region = @Original_Region);
SELECT CustomerID, CompanyName, ContactName, ContactTitle,
Address, City, Region, PostalCode, Country, Phone, Fax
FROM dbo.Customers
WHERE (CustomerID = @Select2_CustomerID)";
this.sqlUpdateCommand1.Connection = this.sqlConnection1;

Điểm chú ý chính trong những lệnh này là SQL đã được tạo. Cả hai lệnh INSERT và UPDATE là hai SQL statement thực sự:
một để thực hiện INSERT hay UPDATE, và cái còn lại để chọn lại hàng từ cơ sở dữ liệu:

Các mệnh đề dư thừa này được dùng như một cách để đồng bộ hoá lại dữ liệu trên các máy client trên server. Có những mặc định
được áp dụng vào các cột khi chèn vào, hay các trigger dữ liệu kích thích để cập nhật một số cột trong mẫu tin. Vì thế việc đồng
bộ hoá lại dữ liệu có vài thuận lợi. Thông số @Select2_CustomerID dùng để chọn lại dữ liệu thì cùng giá trị truyền vào cho
statement INSERT/UPDATE của khoá chính; tên thì được tự tạo ra bởi trình thông minh.

Các bảng gồm một cột IDENTITY, SQL đựơc tạo sử dụng giá trị @@IDENTITY sau statement INSERT. Như mô tả ở chương
trứơc, dựa vào @@IDENTITY để tạo khoá chính có thể dẫn đến vài vấn đề, vì thế có một vùng của SQL bạn sẽ muốn thay đổi.
Nếu bạn không đếm số cột, nó xem như một sự phí phạm để chọn lại tất cả các cột từ bảng ban đầu trong trường hợp có vài thứ
đã thay đổi.

Tạo ra một DataSet


Bây giờ bạn định nghĩa adapter dữ liệu, bạn có thể dùng nó để tạo một DataSet. Để tạo một DataSet, click vào adapter dữ liệu và
hiển thị các thuộc tính của đối tượng. Dưới đáy của bảng thuộc tính bạn chú ý ba tuỳ chọn sau:

Click trên Generate DataSet… sẽ cho phép bạn chọn một tên cho đối tượng DataSet mới. Nếu bạn kéo vài bảng từ Server
Explorer lên form thì bạn có thể liên kết chúng với nhau từ trong hộp dialog vào một DataSet đơn.

Những gì được tạo là một lược đồ XSD, định nghĩa DataSet và mọi bảng mà bạn đã chứa bên trong DataSet. Nó giống như ví dụ
hand-crafted trong chương trước, nhưng ở đây tập tin XSD đã được tạo nên cho bạn.
Tập tin XSD có một tập tin .cs mà định nghĩa một số lượng các lớp type-safe. Để xem tập tin này, click trên nút thanh công cụ
Show All Files để hiện và sau đó mở rộng tập tin XSD. Bạn chú ý là tập tin .cs cùng tên với tập tin XSD. Các lớp được định nghĩa
như dưới đây:

• Một lớp thừa hưởng từ DataSet


• Một lớp thừa hưởng từ DataTable cho adapter bạn chọn

• Một lớp thừa hưởng từ DataRow, định nghĩa các cột có thể truy cập bên trong DataTable

• Một lớp thừa hưởng từ EventArgs, được sử dụng khi một hàng thay đổi.

Bạn sẽ đoán được những công cụ nào được dùng để tạo tập tin này và các lớp này. Nó là XSD.EXE .

Bạn có thể chọn để cập nhật tập tin XSD một lần khi trình thông minh thực hiện việc của nó. Nhưng không nên sửa đổi tập tin .cs
để vặn nó vào một số cách, nó sẽ được tạo lại khi bạn biên dịch lại dự án và tất cả sự thay đổi đó sẽ bị mất.

Cập nhật nguồn dữ liệu


Bây giờ chúng ta đã tạo một ứng dụng mà có thể chọn dữ liệu từ cơ sở dữ liệu, chúng ta sẽ học cách để khôi phục cơ sở dữ liệu.
Nếu bạn làm theo vài bước sau cùng bạn sẽ có một ứng dụng chứa sự kết nối, adapter dữ liệu và đối tượng DataSet. Tất cả bị bỏ
qua việc móc DataSet vào một DataGrid, thêm vài tính logic để khôi phục dữ liệu từ cơ sở dữ liệu và hiện nó, sau đó tạo sự thay
đổi trở lại cơ sở dữ liệu.

Chúng ta cài đặt một form như bên dưới và sau đó tìm hiểu đoạn mã của ứng dụng , nó nằm trong thư mục 10_UpdatingData:

Form bao gồm một control DataGrid và hai nút. Khi người dùng click vào nut Retrive thì đoạn mã sau sẽ chạy:

private void retrieveButton_Click(object sender, System.EventArgs e)


{
sqlDataAdapter1.Fill (customerDataSet , "Customer") ;
dataGrid1.SetDataBinding (customerDataSet , "Customer") ;
}

Đoạn mã này dùng adapter dữ liệu được tạo ra dễ dàng hơn để điền một DataSet. Chúng ta điền vào bảng dữ liệu Customer với
tất cả mẫu tin từ cơ sở dữ liệu. Việc gọi phương thức SetDataBinding() sẽ hiển thị những mẫu tin này trên màn hình.
Sau khi điều khiển các dữ liệu và tạo một số thay đổi bạn có thể click vào nút Update. Đoạn mã sau được hiện tiếp theo:

private void updateButton_Click(object sender, System.EventArgs e)


{
sqlDataAdapter1.Update(customerDataSet , "Customer" ) ;
}

Đoạn mã này cũng rất đơn giản, như adapter dữ liệu đang làm mọi công việc. Phương thức Update() lập qua dữ liệu trong bảng
chọn của DataSet, và cho một sự thay đổi sẽ thực thi các statement SQL chống lại cơ sở dữ liệu. Chú ý rằng phương thức này trả
về một kiểu int là số lượng hàng được chỉnh sửa.

Công dụng của adapter dữ liệu được bàn luận chi tiết trong chương trước, nhưng nhắc lại một tý, nó tượng trưng cho các SQL
statement như các tác vụ SELECT, INSERT, UPDATE, và DELETE. Khi phương thức Update() được gọi, nó thực thi các
statement thích hợp cho mọi hàng chỉnh sửa. Nó là nguyên nhân của tất cả hàng chỉnh sửa thực thi một statement UPDATE, tất
cả hàng bị xoá phát ra một statement DELETE, và vân vân.

Nếu bạn muốn có tất cả lợi ích của việc dùng các thủ tục lưu trữ, nhưng không có thời gian hay kiến thức để viết. Có một cách dễ
dàng hơn trong Visual studio.NET. Hiển thị một menu ngữ cảnh cho adapter dữ liệu và chọn menu Configure Data Adapter. Nó
sẽ hiện một trình thông minh để chọn nguồn của dữ liệu cho adapter.

Sau khi chọn tạo một thủ tục lưu trữ mới. Click Next để tiến trình tự động tạo mới các thủ tục cho các statement SELECT,
INSERT, UPDATE, và DELETE. Và sửa đổi mã bên trong dự án để gọi các thủ tục lưu trữ này thay cho việc gọi các SQL
statements.

Ngoài việc tạo các thủ tục lưu trữ mới, bạn có thể chọn các thủ tục lưu trữ đang tồn tại để phổ biến bốn lệnh SQL trên adapter.
Nó sẽ có lợi khi hand - crafted các thủ tục lưu trữ hay khi một vài chức năng khác đựơc biểu diễn bởi một thủ tục như là thay đổi
kiểm toán hay cập nhật liên kết các mẫu tin.

Xây dựng một lược đồ


Chúng ta mất vài trang để xây dựng một lược đồ XSD bằng tay nhưng đó không phải là cách duy nhất để làm. Visual studio bao
gồm một editor để tạo lược đồ XSD - từ menu Project chọn Add New Item sau đó chọn mục XML Schema từ category Data và
gọi TestSchema.xsd.
Nó thêm hai tập tin mới vào dự án của bạn - tập tin .xsd và một tập tin .xsx. Để tạo một tập hợp tương ứng của mã cho lược đồ ta
chọn Generate Dataset từ menu Schema như bên dưới:

Chọn tuỳ chọn này sẽ thêm một tập tin C# vào dự án, dựa án sẽ hiện lên tập tin XSD trong Solution Explorer. Tập tin này được tự
động tạo ra bất cứ khi nào có sự thay đổi trong lược đồ XSD và không nên chỉnh sửa bằng tay; nó được tạo như ở chương trước
với công cụ XSD.EXE.

Nếu click từ cửa sổ xem Schema đến cửa sổ xem XML, bạn sẽ thấy các mẫu lược đồ:

<?xml version="1.0" encoding="utf-8" ?>


<xs:schema id="TestSchema"
targetNamespace="http://tempuri.org/TestSchema.xsd"
elementFormDefault="qualified"
xmlns="http://tempuri.org/TestSchema.xsd"
xmlns:mstns="http://tempuri.org/TestSchema.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
</xs:schema>

XSD này tạo ra đoạn mã bên dưới trong tập tin TestSchema.cs. Trong đoạn mã bên dưới, tôi đã bỏ qua phần thân của phương
thức và định dạng để đọc dễ dàng hơn.

using System;
using System.Data;
using System.Xml;
using System.Runtime.Serialization;

[Serializable()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Diagnostics.DebuggerStepThrough()]
[System.ComponentModel.ToolboxItem(true)]
public class TestSchema : DataSet
{
public TestSchema() { ... }

protected TestSchema(SerializationInfo info, StreamingContext context)


{ ... }
public override DataSet Clone() { ... }
protected override bool ShouldSerializeTables() { ... }
protected override bool ShouldSerializeRelations() { ... }
protected override void ReadXmlSerializable(XmlReader reader) { ... }
protected override System.Xml.Schema.XmlSchema GetSchemaSerializable()
{ ... }
internal void InitVars() { ... }
private void InitClass() { ... }
private void SchemaChanged(object sender,
System.ComponentModel.CollectionChangeEventArgs e)
{ ... }
}

Thêm một yếu tố


Đều đầu tiên để làm là thêm một phần tử cấp cao mới. Click phải trên workspace và chọn add/New Element:

Nó sẽ tạo ra một phần tử mới không có tên trên màn hình. Bạn nên gõ tên cho phần tử này. Trong ví dụ này chúng ta sẽ dùng
Product. Và ta cũng thêm vài attribute cho phần tử:

Khi bạn lưu tập tin XSD, tập tin C# sẽ được sửa đổi và một số lượng lớp mới được tạo. Chúng ta sẽ bàn luận những khía cạnh
thích hợp nhất của đoạn mã được tạo trong tập tin này, TestSchema.cs:

public class TestSchema : DataSet


{
private ProductDataTable tableProduct;
[System.ComponentModel.DesignerSerializationVisibilityAttribute
(System.ComponentModel.DesignerSerializationVisibility.Content)]
public ProductDataTable Product
{
get
{
return this.tableProduct;
}
}
}

Một biến thành viên mới của lớp ProductDataTable được tạo. Đối tượng này được trả về bởi thuộc tính Product, và được xây
dựng trong phương thức cập nhật InitClass(). Từ phần nhỏ này của đoạn mã chứng minh rằng người dùng của những lớp này có
thể xây dựng một DataSet từ lớp trong tập tin này, và sử dụng DataSet.Procducts để trả về products DataTable.

Tạo DataTable
Đoạn mã bên dưới được tạo cho DataTable mà được thêm vào mẫu lược đồ:

public delegate void ProductRowChangeEventHandler


(object sender, ProductRowChangeEvent e);
public class ProductDataTable : DataTable, System.Collections.IEnumerable
{
internal ProductDataTable() : base("Product")
{
this.InitClass();
}
[System.ComponentModel.Browsable(false)]
public int Count
{
get { return this.Rows.Count;}
}
public ProductRow this[int index]
{
get { return ((ProductRow)(this.Rows[index]));}
}
public event ProductRowChangeEventHandler ProductRowChanged;
public event ProductRowChangeEventHandler ProductRowChanging;
public event ProductRowChangeEventHandler ProductRowDeleted;
public event ProductRowChangeEventHandler ProductRowDeleting;

Lớp ProductDataTable được thừa hưởng từ DataTable, và bao gồm một sự thực thi của giao diện IEnumerable. Bốn sự kiện
được định nghĩa là sử dụng delegate được định nghĩa trên lớp khi được gọi lên. Delegate này được truyền qua một thể hiện của
lớp ProductRowChangeEvent, đựơc định nghĩa lại bởi Visual studio.NET.

Đoạn mã đựơc tạo bao gồm một lớp thừa hưởng từ DataRow, cho phép truy cập đến các cột bên trong bảng. Bạn có thể tạo một
hàng mới theo một trong hai cách sau:

• Gọi phương thức NewRow() để trả về một thể hiện mới của lớp Row. truyền hàng mới này đến phương thức
Rows.Add()
• Gọi phương thức Rows.Add() và truyền một mảng đối tượng, một cho mọi hàng trong bảng.

Phương thức AddProductRow() được trình bày bên dưới:

public void AddProductRow(ProductRow row)


{
this.Rows.Add(row);
}
public ProductRow AddProductRow ( ... )
{
ProductRow rowProductRow = ((ProductRow)(this.NewRow()));
rowProductRow.ItemArray = new Object[0];
this.Rows.Add(rowProductRow);
return rowProductRow;
}

Từ đoạn mã, phương thức thứ hai không chỉ tạo một hàng mới, nó sau đó chèn hàng đó vào tập hợp Rows của DataTable, và sau
đó trả về đối tượng này cho người gọi.

Tạo DataRow
Lớp ProductRow được tạo như trình bày bên dưới:
public class ProductRow : DataRow
{
private ProductDataTable tableProduct;
internal ProductRow(DataRowBuilder rb) : base(rb)
{
this.tableProduct = ((ProductDataTable)(this.Table));
}
public string Name { ... }
public bool IsNameNull { ... }
public void SetNameNull { ... }
// Other accessors/mutators omitted for clarity
}

Khi các attribute được thêm vào một phần tử, một thuộc tính được thêm vào lớp DataRow như ở trên. Thuộc tính có cùng tên như
attribute, vì thế trong ví dụ trên cho hàng Product, có các thuộc tính như Name, SKU, Description, và Price.

Để mọi attribute thêm vào, sự thay đổi được tạo trong tập tin.cs. Trong ví dụ bên dưới sẽ chỉ ta thêm một attribute gọi là
ProductID

Lớp ProductDataTable đầu tiên có một thành viên riêng được thêm là DataColumn:

private DataColumn columnProductId;

Nó được tham gia bởi một thuộc tính có tên ProductIDColumn :

internal DataColumn ProductIdColumn


{
get { return this.columnProductId; }
}

Phương thức AddProductRow() trình bày ở trên được sửa đổi, nó mang một ProductId số nguyên và lưu trữ giá trị trong một cột
được tạo mới:

public ProductRow AddProductRow ( ... , int ProductId)


{
ProductRow rowProductRow = ((ProductRow)(this.NewRow()));
rowProductRow.ItemArray = new Object[] { ... , ProductId};
this.Rows.Add(rowProductRow);
return rowProductRow;
}

Cuối cùng, trong ProductDataTable, có một sự sửa đổi đến phương thức InitClass():

private void InitClass()


{
...
this.columnProductID = new DataColumn("ProductID", typeof(int), null,
System.Data.MappingType.Attribute);
this.Columns.Add(this.columnProductID);
this.columnProductID.Namespace = "";
}

Nó tạo DataColumn mới và thêm nó vào Columns Collection của DataTable. Tham số cuối cùng cho hàm dựng DataColumn
định nghĩa cách cột này được vẽ lên XML. Điều này có lợi khi DataSet được lưu vào một tập tin XML

Lớp ProductRow được cập nhật để thêm một bộ truy cập cho cột này:

public int ProductId


{
get { return ((int)(this[this.tableProduct.ProductIdColumn])); }
set { this[this.tableProduct.ProductIdColumn] = value; }
}

Tạo EventArgs
Lớp cuối cùng được thêm vào mã nguồn là một sự thừa hưởng của EventArgs, lớp này cung cấp các phương thức truy cập trực
tiếp vào hàng đã được thay đổi, và hành động được áp dụng vào hàng đó. Đoạn mã này đã bị xoá cho ngắn gọn hơn.
Những yêu cầu khác
Một yêu cầu chung khi hiển thị dữ liệu là cung cấp một menu Pop-up cho một hàng. Có nhiều cách để thực hiện nhưng ta tập
trung vào một cách có thể đơn giản các đoạn mã được yêu cầu, Nếu phạm vi hiển thị là một DataGrid, nơi có một DataSet với
vài mối quan hệ được hiển thị. Vấn đề ở đây là menu ngữ cảnh phụ thuộc vào hàng đang được chọn, và hàng đó có thể đến từ bất
kỳ DataTable nguồn nào trong DataSet.

Chức năng của menu ngữ cảnh thì thích hợp để đạt mục đích chung, sự thực thi ở đây sử dụng một lớp cơ sở để hổ trợ một menu
pop-up thừa hưởng từ lớp cơ sở này.

Khi người dùng click phải trên bất kỳ phần nào của một hàng trong DataGrid, chúng ta sẽ tìm kiếm hàng và kiểm tra nếu nó thừa
hưởng từ ContextDataRow và phương thức PopupMenu() có thể được gọi. Bạn nên thực thi nó bằng cách sử dụng một giao diện
nhưng trong thể hiện này một lớp cơ sở thì đơn giản hơn.

Ví dụ này sẽ chỉ cách để tạo các lớp DataRow và Datatable, các lớp này có thể sử dụng để cung cấp truy cập type-safe đến dữ
liệu.

Minh hoạ bên dưới trình bày thừa kế lớp cho ví dụ này:

Đoạn mã đầy đủ nằm trong thư mục 11_Miscellaneous:

using System;
using System.Windows.Forms;
using System.Data;
using System.Data.SqlClient;
using System.Reflection;

public class ContextDataRow : DataRow


{
public ContextDataRow(DataRowBuilder builder) : base(builder)
{
}
public void PopupMenu(System.Windows.Forms.Control parent, int x, int y)
{

// Use reflection to get the list of popup menu commands


MemberInfo[] members = this.GetType().FindMembers (MemberTypes.Method,
BindingFlags.Public | BindingFlags.Instance ,
new System.Reflection.MemberFilter(Filter),
null);
if (members.Length > 0)
{

// Create a context menu

ContextMenu menu = new ContextMenu();

// Now loop through those members and generate the popup menu
// Note the cast to MethodInfo in the foreach
foreach (MethodInfo meth in members)
{

// Get the caption for the operation from the


// ContextMenuAttribute

ContextMenuAttribute[] ctx = (ContextMenuAttribute[])


meth.GetCustomAttributes(typeof(ContextMenuAttribute), true);
MenuCommand callback = new MenuCommand(this, meth);
MenuItem item = new MenuItem(ctx[0].Caption, new
EventHandler(callback.Execute));
item.DefaultItem = ctx[0].Default;
menu.MenuItems.Add(item);
}
System.Drawing.Point pt = new System.Drawing.Point(x,y);
menu.Show(parent, pt);
}
}

private bool Filter(MemberInfo member, object criteria)


{
bool bInclude = false;

// Cast MemberInfo to MethodInfo

MethodInfo meth = member as MethodInfo;


if (meth != null)
if (meth.ReturnType == typeof(void))
{
ParameterInfo[] parms = meth.GetParameters();
if (parms.Length == 0)
{

// Lastly check if there is a ContextMenuAttribute on the


// method...

object[] atts = meth.GetCustomAttributes


(typeof(ContextMenuAttribute), true);
bInclude = (atts.Length == 1);
}
}
return bInclude;
}
}

Lớp hàng dữ liệu ngữ cảnh được thừa hưởng từ DataRow, và chứa hai chức năng thành viên. Đầu tiên là PopupMenu dùng sự
phản ánh để tìm các phương thức phù hợp với một dạng cụ thể, và nó hiện một menu pop-up của những tuỳ chọn này đến người
dùng. phương thức Filter() được dùng như một đại diện bởi PopupMenu khi liệt kê các phương thức. Nó trả về kết quả true nếu
chức năng thành viên thực hiện tương ứng với quy ước gọi:

MemberInfo[] members = this.GetType().FindMembers(MemberTypes.Method,


BindingFlags.Public | BindingFlags.Instance,
new System.Reflection.MemberFilter(Filter),
null);

Statement này được dùng để lọc tất cả phương thức trên đối tượng hiện hành, và chỉ trả về theo các tiêu chuẩn sau:

• Thành viên phải là một phương thức


• Thành viên phải là một phương thức thể hiện public

• Thành viên không có kiểu trả về

• Thành viên phải chấp nhận không tham số

• Thành viên phải bao gồm ContextMenuAttribute

Cuối cùng là một custom attribute, chúng ta sẽ bàn luần về nó sau khi tìm hiểu rõ phương thức PopupMenu.

ContextMenu menu = new ContextMenu();


foreach (MethodInfo meth in members)
{
// ... Add the menu item
}
System.Drawing.Point pt = new System.Drawing.Point(x,y);
menu.Show(parent, pt);

Một thể hiện menu ngữ cảnh được tạo, và chúng ta lặp qua mọi phương thức theo tiêu chuẩn trên và thêm mục vào menu. Menu
được hiển thị như trình bày trong màn hình sau:
Rắc rối chính của ví dụ này là phần mã sau, lập lại một lần cho mọi chức năng thành viên để được hiển thị trên pop-up menu.

System.Type ctxtype = typeof(ContextMenuAttribute);


ContextMenuAttribute[] ctx = (ContextMenuAttribute[])
meth.GetCustomAttributes(ctxtype);
MenuCommand callback = new MenuCommand(this, meth);
MenuItem item = new MenuItem(ctx[0].Caption,
new EventHandler(callback.Execute));
item.DefaultItem = ctx[0].Default;
menu.MenuItems.Add(item);

Mọi phương thức nên trình bày trên menu ngữ cảnh được tượng trưng với ContextMenuAttribute. Định nghĩa này là một tên
người dùng quen thuộc cho các tuỳ chọn menu, như một tên phương thức C# không thể bao gồm các khoảng trắng. Attribute
được khôi phục từ phương thức và một mục menu mới được tạo và thêm vào tập hợp mục memu của pop-up menu.

Ví dụ này cũng trình bày cách dùng của một lớp Command đơn giản. Lớp MenuCommand dùng trong thể hiện này được trigger
từ người dùng chọn một mục trên menu ngữ cảnh, và nó định dạng việc gọi đến bộ nhận của phương thức.

Tạo Tables và Rows


Ví dụ XSD dễ dàng hơn trong chương chỉ đoạn mã được viết ra khi visual studio editor được dùng để tạo một tập hợp lớp truy
cập dữ liệu, và bạn sẽ được vui với đoạn mã cho các lớp này như sau:

public class CustomerTable : DataTable


{
public CustomerTable() : base("Customers")
{
this.Columns.Add("CustomerID", typeof(string));
this.Columns.Add("CompanyName", typeof(string));
this.Columns.Add("ContactName", typeof(string));
}
protected override System.Type GetRowType()
{
return typeof(CustomerRow);
}
protected override DataRow NewRowFromBuilder(DataRowBuilder builder)
{
return(DataRow) new CustomerRow(builder);
}
}

Điều cần thíêt đầu tiên của một DataTable là bạn override phương thức GetRowtype(). Nó được dùng bởi các đặc tính .NET khi
tạo các dòng mới cho bảng. Bạn nên trả về kiểu của lớp dùng để mô tả mọi hàng.

Điều cần thiết tiếp theo là bạn thực thi phương thức NewRowFromBuilder(), đựơc gọi lại khi tạo ra các hàng mới cho bảng. Bấy
nhiêu đủ cho một sự thực thi nhỏ. Sự thực thi của chúng ta bao gồm thêm các cột vào DataTable. từ khi chúng ta biết các cột
trong ví dụ này là gì, chúng ta có thể thêm chúng cho phù hợp. Lớp CustomerRow thì khá đơn giản. Nó thực thi các thuộc tính
cho mọi cột trong hàng, và sau đó thực thi các phương thức để hiển thị trên menu ngữ cảnh:

public class CustomerRow : ContextDataRow


{
public CustomerRow(DataRowBuilder builder) : base(builder)
{
}
public string CustomerID
{
get { return (string)this["CustomerID"];}
set { this["CustomerID"] = value;}
}

// Other properties omitted for clarity

[ContextMenu("Blacklist Customer")]
public void Blacklist()
{
// Do something
}
[ContextMenu("Get Contact",Default=true)]
public void GetContact()
{
// Do something else
}
}

Lớp thừa hưởng từ ContextDataRow bao gồm các phương thức getter/setter trên các thuộc tính được đặt tên cùng với mọi trường
và sau đó một tập phương thức sẽ được thêm để được dùng khi phản hồi trên lớp:

[ContextMenu("Blacklist Customer")]
public void Blacklist()
{

// Do something
}

Mọi phương thức mà bạn muốn hiển thị trên menu ngữ cảnh có cùng kiểu và bao gồm attribute ContextMenu tuỳ biến.

Sử dụng một Attribute


Ý tưởng sau viết attribute ContextMenu là để cung cấp một tên văn bản tự do cho một tuỳ chọn menu. Ta thực thi một cờ Default
để cho biết sự chọn lựa menu mặc định. Lớp attribute được mô tả như sau:

[AttributeUsage(AttributeTargets.Method,AllowMultiple=false,Inherited=true)]
public class ContextMenuAttribute : System.Attribute
{
public ContextMenuAttribute(string caption)
{
Caption = caption;
Default = false;
}
public readonly string Caption;
public bool Default;
}

Ở đây, attribute AttributeUsage trên lớp đánh dấu ContextMenuAttrinbute như chỉ có thể dùng trong một phương thức, và nó
cũng định nghĩa là chỉ có một thể hiện của đối tượng này trên bất kỳ phương thức nào.

Bạn có thể nghĩ về một số lượng các thành viên khác để thêm vào attribute này như là:

• một hotkey cho tuỳ chọn menu


• Một hình ảnh để hiển thị

• Vài văn bản đểhiển thị trong thanh công cụ

• Một help context ID

Dispatching Methods
Khi một menu được hiển thị trong .NET, mọi tuỳ chọn menu được liên kết với mã xử lý. Trong quá trình thực thi cơ chế móc
menu chọn đến mã, bạn có hai chọn lựa như sau:

• Thực thi một phương thức với cùng dạng như System.EventHandler.
• public delegate void EventHandler(object sender, EventArgs e);
• Định nghĩa một lớp đại diện thực thi và gọi đến lớp nhận. Nó được biết như mẫu Command.

Mẫu Command tách người gửi và người nhận của sự gọi bằng các phương tiện của một lớp đơn giản. Nó tạo nên các phương
thức trên mọi DataRow đơn giản hơn, và nó có thể mở rộng hơn:

public class MenuCommand


{
public MenuCommand(object receiver, MethodInfo method)
{
Receiver = receiver;
Method = method;
}
public void Execute(object sender, EventArgs e)
{
Method.Invoke(Receiver, new object[] {} );
}
public readonly object Receiver;
public readonly MethodInfo Method;
}

Lớp cung cấp một delegate EventHandler để gọi phương thức trên đối tượng nhận .Ví dụ của chúng ta sử dụng hai kiểu hàng
khác nhau: Hàng từ bảng Customer và hàng từ bảng Orders. Các tuỳ chọn xử lý cho mọi kiểu dữ liệu này từ giống đến khác.
Hình ảnh trên trình bày các tác vụ cho một hàng Customer. Hình bên dưới trình bày các tuỳ chọn cho một hàng Order:

Getting the Selected Row


Vấn đề cuối cùng trong ví dụ này là cách để làm việc ngoài các hàng trong DataSet. Bạn nghĩ rằng nó là một thuộc tính trên
DataGrid, nhưng bạn sẽ không tìm thấy nó ở đó. Bạn sẽ nhìn vào thông tin kiểm mà bạn có thể đạt được từ bên trong sự kiện
MouseUp(), nhưng nó chỉ giúp nếu bạn đang hiển thị dữ liệu từ một DataTable đơn.

Quay lại cách khung lưới được điền ngay lập tức, dòng mã để thực hiện là:

dataGrid.SetDataBinding(ds,"Customers");

Phương thức này thêm một CurrencyManager mới vào BindingContext, để mô tả cho DataTable và DataSet. Bây giờ, DataGrid
có hai thuộc tính DataSource và DataMember. Các thuộc tính này được cài đặt khi bạn gọi phương thức SetDataBinding().
DataSource trong thể hiện này sẽ là một DataSet và ĐataMeber sẽ là Customers.

Chúng ta có một nguồn dữ liệu, một thành viên dữ liệu và biết thông tin này được lưu trữ trong BindingContext của form.

protected void dataGrid_MouseUp(object sender, MouseEventArgs e)


{
// Perform a hit test
if(e.Button == MouseButtons.Right)
{
// Find which row the user clicked on, if any
DataGrid.HitTestInfo hti = dataGrid.HitTest(e.X, e.Y);

// Check if the user hit a cell


if(hti.Type == DataGrid.HitTestType.Cell)
{
// Find the DataRow that corresponds to the cell
//the user has clicked upon

Sau khi gọi dataGrid.HitTest() để tính nơi ngừời dùng click chuột, sau đó chúng ta khôi phục thể hiện BindingManagerBase cho
khung dữ liệu:

BindingManagerBase bmb = this.BindingContext[ dataGrid.DataSource,


dataGrid.DataMember];

Đoạn mã trên sử dụng DataSource và DataMember của DataGrid để đặt tên cho đối tượng chúng ta muốn được trả về.Tất cả
chúng ta muốn làm bây giờ là tìm hàng mà người dùng click và hiện menu ngữ cảnh. Khi nhắp phải chuột trên một hàng, thì hàng
được chọn hiện hành không di chuyển một cách bình thường. Chúng ta muốn di chuyển hàng được chọn và sau đó pop up
menu.Từ đối tượng HitTestInfo chúng ta có số hàng, vì tất cả chúng ta cần là di chuyển vị trí hiện tại của đối tượng
BindingManagerBase :

bmb.Position = hti.Row;

sự thay đổi này chỉ đến ô và tại cùng một lúc các phương tiện ta gọi vào lớp để lấy Row , ta kết thúc với hàng hiện hành và không
chọn một cái cuối cùng:

DataRowView drv = bmb.Current as DataRowView;


if(drv != null)
{
ContextDataRow ctx = drv.Row as ContextDataRow;
if(ctx != null) ctx.PopupMenu(dataGrid,e.X,e.Y);
}
}
}
}

Khi DataGrid đang hiển thị các item từ một DataSet, đốitượng Current bên trong bộ BindingManagerBase là một
DataRowView, nó được kiểm tra bởi một bố cục rõ ràng trong đoạn mã trên. Nêu thành công ta có thể khôi phục hàng mà
DataRowView wraps bằng cách hiện một bố cục khác để kiểm tra nếu nó là một ContextDataRow, và cuối cùng pop up một
menu.

Trong ví dụ, bạn chú ý ta tạo ra hai bảng dữ liệu, Customers và Orders, và định nghĩa một mối quan hệ giữa các bảng này, để khi
bạn click trên CustomerOrders bạn thấy một danh sách orders. Khi bạn làm như vậy, DataGrid thay đổi DataMember từ
Customers đến Customers.CustomerOrders, nó xảy ra để xác định rằng bộ chỉ mục BindingContext dùng để khôi phục dữ liệu
đang trình bày.

Chương 11: Thao tác trên XML


Tổng quan
XML đóng một vai trò quan trọng trong .NET Framework. Không chỉ bởi vì Framework cho phép bạn sử dụng XML trong các
ứng dụng của bạn; bản thân Framework cũng sử dụng XML cho những file cấu hình và tài liệu mã nguồn, như SOAP, các dịch
vụ Web, và ADO.NET.

Để mở rộng cho việc sử dụng XML, .NET Framework cung cấp không gian System.Xml. Không gian tên này chứa các lớp giúp
bạn thao tác trên XML, và trong chương này chúng ta sẽ trình bày các lớp này.

Chúng ta sẽ xem xét cách sử dụng lớp XmlDocument, lớp này thực thi DOM, cũng như .NET hỗ trợ những gì để thay thế cho
SAX (Các lớp XmlReader và XmlWriter). Chúng ta cũng sẽ trình bày lớp thực thi XPath và XSLT. Chúng ta sẽ xem xét làm sao
XML và ADO.NET làm việc chung với nhau và cách thức chuyển đổi qua lại lẫn nhau. Chúng tôi cũng trình bày phát hành từng
đợt các đối tượng của bạn và tạo một đối tượng từ tài liệu XML bằng các lớp trong không gian System.Xml.Serialization. Ngoài
ra chúng tôi sẽ trình bạn cách gắng XML vào các ứng dụng C# của bạn.

Bạn nên biết rằng không gian XML cho phép bạn đạt cùng một kết quả thông qua một số cách khác nhau. Trong một chương
không thể trình bày hết tất cả các cách, chính vì vậy chúng tôi sẽ ví dụ của một cách và sẽ cố gắng đề cập đến các cách khác cho
cùng kết quả.

Chúng tôi không có điều kiện hướng dẫn các bạn về XML, vì vậy chúng tôi thừa nhận rằng bạn đã có một trình độ nhất định về
công nghệ XML. Bởi vậy bạn cần phải biết các yếu tố, thuộc tính, và nút là gì, và bạn cũng nên biết như thế nào là một tài liệu
định kiểu tốt. Bạn cũng phải làm quen với SAX và DOM. Nếu bạn muốn biết thêm về XML, Wrox's Beginning XML (ISBN 1-
861003-41-2), và Professional XML (ISBN 1-861003-11-0) là những thông tin tốt nhất.

Nào bây giờ hãy bắt đầu chương này bằng cách xem qua về các trạng thái chuẩn của XML.

Các hỗ trợ XML chuẩn trong .NET


World Wide Web Consortium (W3C) đã phát triển một bộ các chuẩn, XML là một lựa chọn mạnh mẽ và đầy tiềm lực cho các
chuẩn này. Nếu không có các chuẩn này, XML đã không được biết đến và phát triển mạnh mẽ như ngày nay. Trang web của
W3C (http://www.w3.org/) có rất nhiều mã nguồn về tất cả các lĩnh vực trong XML.

Tại thời điểm tháng 2 năm 2002, .NET Framework hỗ trợ các chuẩn W3C sau đây:

• XML 1.0 (http://www.w3.org/TR/1998/REC-xml-19980210), hỗ trợ DTD


• Các không gian tên XML (http://www.w3.org/TR/REC-xml-names), Cả stream-level và DOM

• Sơ đồ XML (http://www.w3.org/2001/XMLSchema)

• Biểu thức XPath (http://www.w3.org/TR/xpath)

• Các biến đổi XSLT (http://www.w3.org/TR/xslt)

• Lõi DOM bậc 1 (http://www.w3.org/TR/REC-DOM-Level-1/)


• Lõi DOM bậc 2 (http://www.w3.org/TR/DOM-Level-2-Core/)

• SOAP 1.1 (http://www.w3.org/TR/SOAP)

Các hỗ trợ chuẩn sẽ thay đổi bởi các sự hoàn thiện của Framework, và các cập nhật của W3C. Bởi vậy, bạn luôn cần phải bảo
đảm rằng bạn luôn cập nhật các chẩn và các hỗ trợ được cung cấp bởi Microsoft.

Sử dụng MSXML trong .NET


Sẽ ra sao nếu bạn có một tấn mã đã được phát triển bằng những phiên bản cổ xưa nhất của Microsoft (Hiện tại là MSXML 4.0)?
Bạn có phải bỏ đi tất cả khi chuyển qua dùng .NET? Trong khi đó có gì tốt hơn khi sử dụng MSXML 4.0 DOM? Có nên chuyển
sang .NET ngay lập tức?

Câu trả lời là không. XML 4.0, 3.0 hoặc 2.0 có thể dùng trực tiếp trong các ứng dụng của bạn. Trước tiên bạn thêm một tham
chiếu đến msxml4. DLL trong giải pháp của bạn, bạn có thể bắt đầu viết một vài mã.

Một vài ví dụ tiếp theo sẽ dùng books.xml như là nguồn dữ liệu. File này có thể download từ web site Wrox
(http://www.wrox.com/), nó cũng bao gồm các ví dụ khác trong .NET SDK. File books.xml là một thư viện sách ảo. Nó bao gồm
thông tin về sách như thể loại, tên tác giả, giá và số ISBN. Tất cả mã ví dụ trong chương này đều sẵn có trong web site Wrox.
Trong thứ tự thực thi các ví dụ, các file dữ liệu XML sẽ cần phải nằm trong cây thư mục như sau:

/XMLChapter/Sample1
/XMLChapter/Sample2
/XMLChapter/Sample3
...

Thật vậy, bạn có thẻ gọi trực tiếp bất kì thứ gì bạn muốn, nhưng vị trí tương đối đóng một vai trò quan trọng. Bạn cũng có thể sửa
đổi các ví dụ ở bất kì điểm nào bạn muốn. Mã ví dụ sẽ được giải thích để chỉ những dòng nào thay đổi nếu bạn muốn như vậy.

File books.xml trong như thế này:

<?xml version='1.0'?>
<!-- This file represents a fragment of a book store inventory database -->
<bookstore>
<book genre="autobiography" publicationdate="1981" ISBN="1-861003-11-0">
<title>The Autobiography of Benjamin Franklin</title>
<author>
<first-name>Benjamin</first-name>
<last-name>Franklin</last-name>
</author>
<price>8.99</price>
</book>
<book genre="novel" publicationdate="1967" ISBN="0-201-63361-2">
<title>The Confidence Man</title>
<author>
<first-name>Herman</first-name>
<last-name>Melville</last-name>
</author>
<price>11.99</price>
</book>
<book genre="philosophy" publicationdate="1991" ISBN="1-861001-57-6">
<title>The Gorgias</title>
<author>
<name>Plato</name>
</author>
<price>9.99</price>
</book>
</bookstore>

Nào hãy xem một vài mã dùng MSXML 4.0 để tải một listbox với các ISBN từ books.xml. Bạn sẽ tìm thấy mã đầy đủ trong thư
mục MSXML_Sample. Bạn có thể sao chép nó vào Visual Studio IDE hoặc tạo một Windows Form mới. Form này chứa một
listbox và một button, như bạn thấy dưới đây. Cả hai đều dùng tên mặc định listBox1 và button1, với thuộc tính Text của button1
là Load XML.
Một điều lưu ý là từ khi MSXML 4 trở thành một thành phần COM-based, chúng ta sẽ cần phải tạo ra interop assembly. Cách dễ
nhất là chọn Add Reference từ trình đơn Project trong Visual Studio IDE. Đến tab COM và chọn Microsoft XML, v4.0 (or v3.0,
v2.6). Bạn sẽ thấy MSXML2 như một không gian tên được thêm vào Solution Explorer. Tại sao lại là MSXML2? Khi bạn nhập
một thành phần COM không gian tên được truyền cho một assembly mới, là tên thư viện kiểu cho thành phần COM đó. Trong
trường hợp này nó là MSXML2. Nếu bạn dùng TLBIMP bạn có thể đổi không gian tên thành một cái tên khác mà bạn muốn.

Nào bây giờ hày xem các dòng quan trọng từ mã ví dụ MSXML.

Chúng ta có một tham chiếu khi thêm dòng:

using MSXML2;

Chúng ta cũng có một biến cấp lớp.

private DOMDocument40 doc;

Bây giờ chúng ta đã sẵn sàng để dùng MSXML trong ví dụ của chúng ta.

Chúng ta muốn tạo một ISBN từ listbox, và sử dụng một tìm kiếm XPath đơn giản, tìm mục sách nếu tìm thấy thì biếu diễn mục
sách dưới dạng text (tựa sách và giá sách) trong một MessageBox. XML Path Language (XPath) là một ghi chú XML được dùng
cho truy vấn và lọc văn bản trong một tài liệu XML. Chúng ta sẽ tìm hiểu kĩ cách dùng XPath trong .NET sau.

Đây là một mã quản sự kiện cho việc chọn một mẫu từ listbox:

protected void listBox1_SelectedIndexChanged (


object sender, System.EventArgs e)
{
string srch=listBox1.SelectedItem.ToString();
IXMLDOMNode nd=doc.selectSingleNode(
"bookstore/book[@ISBN='" + srch + "']");
MessageBox.Show(nd.text);
}

Giờ đây chúng ta sẽ xem xét sự kiện click của button. Trước tiên, chúng ta load file books.xml - chú ý rằng nếu bạn đang chạy
ứng dụng từ thư mục bin/debug hay bin/release, bạn sẽ cần phải điều chỉnh đường dẫn thích hợp.

protected void button1_Click (object sender, System.EventArgs e)


{
doc=new DOMDocument40 ();
doc.load("..\\..\\..\\books.xml");

Dòng tiếp theo khai báo một NodeList của các mục sách. Trong trường hợp này có ba mục sách:

IXMLDOMNodeList nodes;
nodes = doc.selectNodes("bookstore/book");
IXMLDOMNode node=nodes.nextNode();

Sau đó chúng ta lặp qua các mục, và thêm giá trị văn bản của thuộc tính ISBN vào listBox1:

while(node!=null)
{
listBox1.Items.Add(node.attributes.getNamedItem("ISBN").text);
node=nodes.nextNode ();
}
}

Hình bên trái là kết quả khi chạy ví dụ. Sau khi click button, listbox tải các book ISBN. Sau khi chọn một mục sách hình bên phải
sẽ xuất hiện.

Sử dụng các lớp System.Xml


Nếu bạn đã từng làm việc với MSXML 3.0 hoặc 4.0, mã ở trên trong rất quen thuộc. Vì vậy tại sao không lại không chọn .NET
Framework với những lớp hỗ trợ XML tuyệt vời?

Trong khi không gian tên System.Xml mạnh mẽ và tương đối dễ sử dụng, nó có hơi khác với mô hình MSXML 3.0. Nếu bạn
quen dùng MSXML 3.0, thì hãy cứ sử dụng nó cho đến khi cảm thấy quen thuộc với không gian tên System.Xml.

Dĩ nhiên, các lớp System.Xml có các lớp tiện lợi hơn các lớp MSXML. Trước tiên, System.Xml là mã quản, vì vậy sử dụng bạn
sẽ được hưởng những lợi ích của mã quản. Tất nhiên, dùng COM interop cũng phải chịu những một vài quá tải. Quan trọng nhất,
dĩ nhiên, không gian tên System.Xml rất dễ sử dụng và rất mềm dẻo. Khi kết thúc chương này bạn sẽ nhận ra điều đó.

Bạn nên nhớ rằng chúng ta sẽ dùng file books.xml cho những ví dụ khác nhau trong chương, và mã ví dụ là rất cơ bản cho các ví
dụ.

Sử dụng DOM trong .NET


Document Object Model (DOM) thực thi trong .NET hỗ trợ các kĩ thuật W3C DOM Level 1 và Core DOM Level 2. DOM được
thực thi thông qua lớp XmlNode, một lớp ảo mô tả một nút của tài liệu.

Ngoài ra còn có một lớp XmlNodeList là một danh sách có thứ tự các nút. Đây là một danh sách sống, và bất kì thay đổi nào
cũng sẽ cập nhật ngay vào danh sách. XmlNodeList hỗ trợ truy cập chỉ mục và truy cập lặp. Còn có một lớp ảo khác,
XmlCharacterData, nó là lớp mở rộng của XmlLinkedNode, và cung cấp các phương thức sử dụng văn bản cho các lớp khác.

Các lớp XmlNode và XmlNodeList trang điểm cho thực thi DOM trong .NET Framework. Đây là một danh sách các lớp cơ sở
trong XmlNode:

Class Name Description

XmlLinkedNode Trả về nút trước và sau nút hiện tại. Thêm các thuộc tính NextSibling và PreviousSibling
vào XmlNode.

XmlDocument Miêu tả toàn bộ tài liệu. Thực thi các đặc điểm kĩ thuật DOM Level 1 và Level 2.

XmlDocumentFragment Miêu tả mô hình cây thư mục của tài liệu.

XmlAttribute Một đối tượng thuộc tính của một đối tượng XmlElement.

XmlEntity Phân tách và tổ hợp các nút.


Class Name Description

XmlNotation Chứa cách chú thích trong môt DTD hoặc sơ đồ.

Các lớp sau mở rộng XmlCharacterData:

Class Name Description

XmlCDataSection Một đối tượng mô tả một đoạn CData section của một tài liệu.

XmlComment Mô tả một đối tượng ghi chú XML.

XmlSignificantWhitespace Mô tả một nút với khoảng trắng. Các nút chỉ được tạo khi cờ PreserveWhiteSpace
là true.

XmlWhitespace Miêu tả whitespace trong một thành phần ghi chú. Các nút được tạo chỉ khi cờ
PreserveWhiteSpace là true.

XmlText Ghi chú dạng văn bản cảu một thành phần hoặc thuộc tính.

Cuối cùng, tập các lớp tiếp theo mở rộng lớp XmlLinkedNode:

Class Name Description

XmlDeclaration Miêu tả nút khai báo (<?xml version='1.0'...>)

XmlDocumentType Quan hệ dữ liệu với khai báo kiểu tài liệu

XmlElement Một đối tượng thành phần XML

XmlEntityReferenceNode Mô tả một nút tham chiếu tồn tại

XmlProcessingInstruction Chứa một cấu trúc xử lí XML

Như bạn thấy, .NET tạo sẽ một lớp phù hợp với bất kì kiểu XML nào mà bạn bắt gặp. Bởi vậy, bạn sẵn có một bộ các công cụ
mạnh mẽ và mềm dẻo. Chúng ta sẽ không đi vào chi tiết các lớp, nhưng chúng ta sẽ dùng một số ví dụ. Sau đây là sơ đồ các lớp:
Sử dụng lớp XmlDocument
XmlDocument và lớp xuất phát XmlDataDocument (chúng ta sẽ xem xét sau trong chương) là các lớp bạn sẽ dùng để miêu tả
DOM trong .NET. Không giống như XmlReader và XmlWriter, XmlDocument cho cung cấp cho bạn khả năng đọc và viết như
truy xuất ngẫu nhiên cây DOM . XmlDocument tương tự như thực thi DOM trong MSXML. Nếu bạn đã từng lập trình với
MSXML bạn sẽ cảm thấy tiện lợi khi dùng XmlDocument.

Lấy một ví dụ về tạo một đối tượng XmlDocument, load một tài liệu từ đĩa, và load các tiêu đề vào một listbox. Nó giống như
mọt ví dự mà chúng ta đã tạo trong phần XmlReader. Cái khác ở đây là chúng ta sẽ chọn các nút mà chúng ta sẽ làm việc thay vì
duyệt qua các nút trong tài liệu như đã từng làm.

Đây là mã. Hãy xem cách làm việc của nó và so sánh với ví dụ XmlReader (file có thể tìm thấy trong thư mục DOMSample1):

private void button1_Click(object sender, System.EventArgs e)


{
// doc is declared at the module level
// change path to match your path structure
doc.Load("..\\..\\..\\books.xml");
// get only the nodes that we want
XmlNodeList nodeLst=doc.GetElementsByTagName("title");
// iterate through the XmlNodeList

foreach(XmlNode node in nodeLst) listBox1.Items.Add(node.InnerText);


}

Chú ý rằng chúng ta cũng có thể thêm khai báo sau vào mô module này:

private XmlDocument doc=new XmlDocument();

Nếu chúng ta chỉ muốn làm như vậy thì dùng XmlReader sẽ tốt hơn vì chỉ cần duyệt qua tài liệu một lần duy nhất để load dữ liệu
cho the listbox. XmlReader được thiết kế cho mục đích này. Nếu bạn muốn tham chiếu lại một nút, thì dùng XmlDocument là
cách tốt nhất. Mở rộng ví dụ trên bằng cách thêm một quản lí sự kiện khác (đây là DOMSample2):

private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e)


{
//create XPath search string
string srch="bookstore/book[title='" + listBox1.SelectedItem.ToString()
+ "']";
//look for the extra data
XmlNode foundNode = doc.SelectSingleNode(srch);
if(foundNode != null)
MessageBox.Show(foundNode.InnerText);
else
MessageBox.Show("Not found");
}

Trong ví dụ này, chúng ta load các tựa trong tài liệu books.xml vào listbox, giống như trong ví dụ trên. Khi chúng ta click vào
listbox, khi đó sự kiện SelectedIndexChanged() được phát ra. Trong trường hợp này, chúng ta tạo mọt văn bản của đối tượng
được tạo trong listbox (tựa của sách) tạo một XPath statement và truyền nó cho phương phương thức SelectSingleNode() của đối
tượng doc. Nó trả về sách có tựa được tìm thấy. Sau đó chúng ta biểu diễn InnerText của nút trong một message box. Chúng ta có
thể giữ clicking trên mục chọn trong listbox, tài liệu đã được load lên sẽ không thay đổi cho đến khi chúng ta thả tay ra.

Một ghi chú nhanh có liên quan đến phương thức SelectSingleNode(). Đó là thực thi XPath trong lớp XmlDocument. Có hai
phương thức SelectSingleNode() và SelectNodes(). Cả hai phương thức này đều được định nghĩa trong XmlNode, là lớp xuất
phát của XmlDocument. SelectSingleNode() trả về một XmlNode và SelectNodes() trả về một XmlNodeList. Tất nhiên, không
gian System.Xml.XPath chứa một thực thi XPath mạnh, và sẽ xem xét trong chương sau.

Inserting Nodes

Như chúng ta đã biết trong một ví dụ sử dụng XmlTextWriter trước đây tạo một tài liệu mới. Giới hạn là nó không thể chèn một
mục mới vào tài liệu hiện tại. Với lớp XmlDocument chúng ta có thể làm điều đó. Thay đổi trình quản lí sự kiện button1_Click()
trong ví dụ trên như sau (DOMSample3 trong mã download):

private void button1_Click(object sender, System.EventArgs e)


{
//change path to match your structure
doc.Load("..\\..\\..\\books.xml");
//create a new 'book' element
XmlElement newBook=doc.CreateElement("book");
//set some attributes
newBook.SetAttribute("genre","Mystery");
newBook.SetAttribute("publicationdate","2001");
newBook.SetAttribute("ISBN","123456789");
//create a new 'title' element
XmlElement newTitle=doc.CreateElement("title");
newTitle.InnerText="The Case of the Missing Cookie";
newBook.AppendChild(newTitle);
//create new author element
XmlElement newAuthor=doc.CreateElement("author");
newBook.AppendChild(newAuthor);
//create new name element
XmlElement newName=doc.CreateElement("name");
newName.InnerText="C. Monster";
newAuthor.AppendChild(newName);
//create new price element
XmlElement newPrice=doc.CreateElement("price");
newPrice.InnerText="9.95";
newBook.AppendChild(newPrice);
//add to the current document
doc.DocumentElement.AppendChild(newBook);
//write out the doc to disk
XmlTextWriter tr=new XmlTextWriter("..\\..\\..\\booksEdit.xml",null);
tr.Formatting=Formatting.Indented;
doc.WriteContentTo(tr);
tr.Close();
//load listBox1 with all of the titles, including new one
XmlNodeList nodeLst=doc.GetElementsByTagName("title");
foreach(XmlNode node in nodeLst)
listBox1.Items.Add(node.InnerText);
}

Sau khi thực thi mã này, bạn sẽ có như khả năng như ví dụ trên, nhưng ở đây đã thêm một sách mới vào listbox, The Case of the
Missing Cookie. Clicking trên tựa bạn sẽ chỉ ra tất cả các thông tin của các tựa khác. Đầu tiên chúng ta tạo một thành phần sách
mới:

XmlElement newBook = doc.CreateElement("book");


Phương thức CreateElement() có ba quá tải cho phép bạn lựa chọn:

• element name
• name và không gian tên URI

• prefix, localname, and namespace

Khi một thành phần được tạo ra chúng ta cần phải thêm các thuộc tính sau:

newBook.SetAttribute("genre","Mystery");
newBook.SetAttribute("publicationdate","2001");
newBook.SetAttribute("ISBN","123456789");

Bây giờ chúng ta có các thuộc tính được tạo chúng ta cần thêm các thuộc tính khác của một cuốn sách:

XmlElement newTitle = doc.CreateElement("title");


newTitle.InnerText = "The Case of the Missing Cookie";
newBook.AppendChild(newTitle);

Trước tiên chúng ta tạo một đối tượng mới xuất phát từ XmlElement. Sau đó chúng ta gán thuộc tính InnerText của tựa sách như
đã làm, và thêm vào như một thành phần con của book. Chúng ta lặp lại cho đên khi hết các thành phần. Chú ý răng chúng ta
thêm thành phần name như một thành phần con của author. Nó cho chúng ta mối quan hệ thích hợp với các thành phần book
khác.

Cuối cùng, chúng ta gán thành phần newBook vào mục doc.DocumentElement. Nó cùng cấp với tất cả các thành phần book
khác. Bây giờ chúng ta cập nhật một tài liệu có sẵn với một thành phần mới.

Thao tác cuối cùng là ghi tài liệu mới lên đĩa. Trong ví dụ này chúng ta tạo một XmlTextWriter mới, và truyền nó cho phương
thức WriteContentTo(). WriteContentTo() và WriteTo() cả hai đều tạo ra mọt XmlTextWriter như là một tham số.
WriteContentTo() lưu nút hiện tại và tất cảc các nút con của nó vào XmlTextWriter, ngược lại WriteTo() chỉ lưu nút hiện tại. Bởi
vì doc là một đối tượng xuất phát từ XmlDocument, nó mô tả toàn bộ tài liệu và nhưng gì nó đã lưu. Chúng ta cũng sử dụng
phương thức Save(). Nó sẽ luôn lưu toàn bộ tài liệu. Save() có bốn quá tải. Bạn có thể chỉ ra một chuỗi với tên file và đường dẫn,
một đối tượng xuất phát từ Stream, một đối tượng xuất phát từ TextWriter, một đối tượng xuất phát từ XmlWriter.

Chúng ta cũng gọi phương thức Close() trên XmlTextWriter để đẩy vào vùng nhớ đệm nội và đóng file.

Đây là những gì chúng ta sẽ cho khi chạy ví dụ này. Chú ý rằng mục đầu tiên nằm ở đáy của danh sách:

Nếu muốn tạo một tài liệu từ chúng, chúng ta có thể dùng XmlTextWriter như đã thấy trong phần trên. Chúng ta cũng có thể
dùng XmlDocument. Tại sao dùng một trong số chúng? Nếu bạn muốn chuyển dữ liệu thành XML là sẵn có và sẵn sàng để ghi,
trong trường hợp này lớp XmlTextWriter là lựa chọn tốt nhất. Và khi bạn cần tạo tài liệu XML từng chút một, và chèn các nút
vào những vị trí khác nhau, sau đó tạo tài liệu khi đó lớp XmlDocument có thể là lựa chọn tốt nhất. Chúng ta có thể hoàn tất nó
bằng thay đổi dòng sau:

doc.Load("..\\..\\..\\books.xml");

thành (mã này nằm trong ví dụ DOMSample4):

//create the declaration section


XmlDeclaration newDec = doc.CreateXmlDeclaration("1.0",null,null);
doc.AppendChild(newDec);
//create the new root element
XmlElement newRoot = doc.CreateElement("newBookstore");
doc.AppendChild(newRoot);

Trước tiên, chúng ta tạo một XmlDeclaration mới. Các tham số là phiên bản (hiện tại là 1.0), mã hóa, và cờ standalone. Tham số
mã hoá có thể được gán bởi một chuỗi là thành phần của lớp System.Text.Encoding nếu null không được dùng . null mặc định là
UTF-8. Cờ standalone có thể là yes, no, hoặc null. Nếu nó là null thì thuộc tính không được dùng và sẽ không được mô tả trong
tài liệu.

Bước kế tiếp tạo ra DocumentElement. Trong trường hợp này chúng ta gọi newBookstore vì vậy bạn có thể nhận thấy khác biệt.
Mục đích của mã giống như ví dụ trước, và hoạt động theo cùng một cách. Đây là booksEdit.xml được tạo ra từ mã:

<?xml version="1.0"?>
<newBookstore>
<book genre="Mystery" publicationdate="2001" ISBN="123456789">
<title>The Case of the Missing Cookie</title>
<author>
<name>C. Monster</name>
</author>
<price>9.95</price>
</book>
</newBookstore>

Chúng ta sẽ không xem xét kĩ lớp XmlDocument, cũng như các lớp khác giúp tạo mô hình DOM trong .NET. Tuy nhiên, chúng
ta đã xem xét khả năng và tính mềm dẻo mà thực thi DOM trong .NET hỗ trợ. Bạn sẽ muốn sử dụng lớp XmlDocument khi bạn
muốn truy xuất ngẫu nhiên đến tài liệu, hoặc các lớp xuất phát từ XmlReader khi bạn muốn một mô hình kiểu stream thay thế.
Nhớ rằng cái giá phải trả cho tính mềm dẻo của XmlNode-based XmlDocument đó là bộ nhớ yêu cầu cao và việc đọc tài liệu
không tốt bằng XmlReader. Vì vậy hãy suy nghĩ kĩ phương thức trước khi chọn.

Sử dụng XPath và XSLT trong .NET


Trong phần này, chúng ta sẽ xem xét hỗ trợ cho XPath và XSL Transforms (XSLT) trong .NET Framework. XPath được hỗ trợ
thông qua không gian tên System.Xml.XPath, và XSLT được hỗ trợ thông qua không gian tên System.Xml.Xsl. Lí do xem xét cả
hai lớp trên là vì lớp XPathNavigator của không gian tên System.XPath cung cấp cách rất thực tế để thực thi các biến đổi XSL
trong .NET.

XPath là một ngôn ngữ truy vấn cho XML. Bạn sẽ dùng XPath để chọn một bộ con các các yếu tố giá trị văn bản hoặc các giá trị
thuộc tính. XSLT được dụng để thay đổi một tài liệu gốc sang một tài liệu với một cấu trúc kiểu khác.

Trước tiên chúng ta sẽ xem xét System.XPath và trình bày cách dùng các lớp System.Xml.Xsl.

Không gian tên System.XPath


Không gian tên System.XPath được xây dựng dựa trên yếu tố tốc độ. Nó cung cấp một thể hiện chỉ đọc cho tài liệu XML của
bạn, vì vậy không có vấn đề soạn thảo ở đây. Các lớp trong không gian này được xây dựng đẻ thực thi lặp nhanh và lựa chon trên
tài liệu XML trong một con trỏ.

Đây là một bảng liệt kê các lớp trong System.XPath, và một giải thích ngắn vì hỗ trợ của mỗi lớp:

Class Name Description

XPathDocument Một view của tài liệu XML . Chỉ đọc.

XPathNavigator Cung cấp khả năng điều hướng cho một XPathDocument.

XPathNodeIterator Cung cấp khả năng truy xuất trực tiếp các nút. XPath trang bị một bộ nút trong Xpath.

XPathExpression Một biên dịch phương thức XPath. Được dùng bởi SelectNodes, SelectSingleNodes, Evaluate,
và Matches.

XPathException XPath exception class.

XPathDocument
XPathDocument tương thích cho bất kì thao tác nào của lớp XmlDocument. Nếu bạn cần khả năng soạn thảo, XmlDocument
được lựa chọn khi bạn sử dụng ADO.NET, XmlDataDocument (chúng ta sẽ xem xét ở phần sau của chương). Dĩ nhiên, khi cần
thao tác nhanh bạn có thể sử dụng XPathDocument. Nó có bốn quá tải cho phép bạn một một tài liệu XML từ một file và một
đường dẫn, một đối tượng TextReader, một đối tượng XmlReader hoặc một đối tượng Stream-based.

XPathNavigator

XPathNavigator chứa tât cả các phương thức di chuyển, lựa chọn mà bạn cần. Sau đây là một số phương thức được định nghĩa
trong lớp:

Method Name Description

MoveTo() Cần một tham số XPathNavigator. Di chuyển vị trí hiện tại đến vị trí được truyền trong
XPathNavigator.

MoveToAttribute() Di chuyển tới thuộc tính được chọn. Cần một tên thuộc tính và không gian làm tham số.

MoveToFirstAttribute() Di chuyển đến thuộc tính đầu tiên của mục hiện tại. Trả về giá trị true nếu thành công.

MoveToNextAttribute() Di chuyển đến thuộc tính tiếp theo của mục hiện tại. Trả về true nếu thành công.

MoveToFirst() Di chuyển đến mẫu đầu tiên cùng loại với mẫu hiện tại. Trả về true nếu thành công,
ngược lại trả về false.

MoveToLast() Di chuyển đến mẫu cuối cùng cùng loại với mẫu hiện tại. Trả về true nếu thành công.

MoveToNext() Di chuyển đến mẫu tiếp theo cùng loại với mẫu hiện tại. Trả về true nếu thành công.

MoveToPrevious() Di chuyển đến mẫu trước cùng loại với mẫu hiện tại. Trả về true nếu thành công.

MoveToFirstChild() Di chuyển đến mẫu con đầu tiên của mẫu hiện tại. Trả về true nếu thành công.

MoveToId() Di chuyển đếu mẫu có ID được truyền qua tham số. Nó cần một sơ đồ tài liệu, và một
kiểu dữ liệu cho là kiểu của ID.

MoveToParent() Di chuyển đến cha của mẫu hiện tại. Trả về true nếu thành công.

MoveToRoot() Di chuyển nếu nút gốc của tài liệu.

Có một vài phương thức Select() khác nhau hỗ trợchọn một tập các nút để làm việc. Tất cả các phương thức Select() trả về một
đối tượng XPathNodeIterator.

Ngoài ra có thể sử dụng các phương thức SelectAncestors() và SelectChildren(). Cả hai phương thức đều trả về một
XPathNodeIterator. Trong khi Select() cần một tham số XPath, thì các phương thức này cần một tham số XPathNodeType.

Bạn có thể dùng XPathNavigator để lưu trữ như là một file hệ thống hoặc Registry thay cho một XPathDocument.

XPathNodeIterator

XPathNodeIterator có thể coi là một trang bị NodeList hoặc một NodeSet trong XPath. Đối tượng này có ba thuộc tính và hai
phương thức:

• Clone – Tạo một copy mới của chính nó


• Count – Số các nút trong đối tượng XPathNodeIterator

• Current – Trả về một XPathNavigator trỏ đến nút hiện tại

• CurrentPosition() – Trả về một số integer chứa vị trí hiện tại

• MoveNext() – Di chuyển đến nút tiếp theo thường thấy trong biểu thức XPath dùng để tạo một XpathNodeIterator
Sử dụng các lớp trong không gian tên XPath

Các tốt nhất để chỉ ra cách làm việc của các ví dụ này là thông qua ví dụ. Hãy load tài liệu books.xml xem qua chúng bạn sẽ thấy
được định hướng của công việc. Trước tiên, chúng ta tạo một tham chiếu đến các không gian tên System.Xml.Xsl và
System.Xml.XPath:

using System.Xml.XPath;
using System.Xml.Xsl;

Trong ví dụ này chúng ta sử dụng file booksxpath.xml. Nó giống như books.xml mà chúng ta đã dùng, ngoại trừ có một hai
quyến sách được thêm vào. Bạn có thể tìm thấy mã nguồn trong thư mục XPathXSLSample1:

private void button1_Click(object sender, System.EventArgs e)


{

//modify to match your path structure


XPathDocument doc=new XPathDocument("..\\..\\..\\booksxpath.xml");
//create the XPath navigator
XPathNavigator nav=doc.CreateNavigator();
//create the XPathNodeIterator of book nodes
// that have genre attribute value of novel
XPathNodeIterator iter=nav.Select("/bookstore/book[@genre='novel']");

while(iter.MoveNext())
{
LoadBook(iter.Current);
}
}

private void LoadBook(XPathNavigator lstNav)


{
//We are passed an XPathNavigator of a particular book node
//we will select all of the descendents and
//load the list box with the names and values

XPathNodeIterator iterBook=lstNav.SelectDescendants
(XPathNodeType.Element,false);
while(iterBook.MoveNext())
listBox1.Items.Add(iterBook.Current.Name + ": "
+ iterBook.Current.Value);
}

Đầu tiên trong phương thức button1_Click() chúng ta tạo một XPathDocument (gọi là doc), và truyền cho nó đường dẫn đến tài
liệu mà bạn muốn mở. Trong dòng tiếp theo chúng ta tạo ra một XPathNavigator:

XPathNavigator nav = doc.CreateNavigator();

Trong ví dụ này, chúng ta sử dụng phương thức Select() để lấy bộ các nút có thuộc tính loại là novel (tiểu thuyết). Sau đó chúng
ta sử dụng phương thức MoveNext() để duyệt qua các tiểu thuyết trong danh mục sách.

Để load dữ liệu vào một listbox, chúng ta sử dụng thuộc tính XPathNodeIterator.Current. Nó sẽ tạo ra một đối tượng
XPathNavigator mới dựa trên nút mà XPathNodeIterator đang trỏ đến. Trong trường hợp này, chúng ta tạo một XPathNavigator
cho một mục sách trong tài liệu.

Phương thức LoadBook() nhận XPathNavigator này và tạo ra một XPathNodeIterator khác bằng cách phát ra một kiểu khác của
phương thức select, phương thức SelectDescendants(). Nó sẽ cho chúng ta một XPathNodeIterator của các nút con và các nút con
của các nút con này của mục sách mà chúng ta đã truyền cho phương thức LoadBook().

Sau đó chúng ta tạo một vòng lặp MoveNext() khác trên XPathNodeIterator và load các thành phần tên và giá trị vào listbox.

Đây là màn hình sau khi chạy mã. Nhớ rằng ở đây chỉ có các tiểu thuyết mới được liệt kê:
Nếu muốn thêm giá cho những quyển sách này thì phải làm sao? XPathNavigator cung cấp phương thức Evaluate() để hỗ trợ
những thao tác kiểu như thế này. Evaluate() có ba quá tải. Phương thức thứ nhất chứa một chuỗi mà hàm XPath gọi. Phương thức
thứ hai sử dụng một tham số đối tượng XPathExpression, phương thức thứ ba sử dụng các tham số XPathExpression
XPathNodeIterator. Các thay đổi được in đậm (mã có thể được tìm thấy trong XPathXSLSample2):

private void button1_Click(object sender, System.EventArgs e)


{
//modify to match your path structure
XPathDocument doc = new XPathDocument("..\\..\\..\\booksxpath.XML");
//create the XPath navigator
XPathNavigator nav = doc.CreateNavigator();
//create the XPathNodeIterator of book nodes
// that have genre attribute value of novel
XPathNodeIterator iter = nav.Select("/bookstore/book[@genre='novel']");
while(iter.MoveNext())
{
LoadBook(iter.Current.Clone());
}
//add a break line and calculate the sum
listBox1.Items.Add("========================");
listBox1.Items.Add("Total Cost = "
+ nav.Evaluate("sum(/bookstore/book[@genre='novel']/price)"));
}

Bây giờ, trong listbox có giá các quyển sách:

Không gian tên System.Xml.Xsl


Không gian tên System.Xml.Xsl chứa các lớp được .NET Framework sử dụng để hỗ trợ các biến đổi XSL. Không gian được
tham chiếu sẵn trong các lưu trữ có thực thi giao diện IXPathNavigable.Trong .NET Framework, sẵn chứa XmlDocument,
XmlDataDocument, và XPathDocument. Một lần nữa XPath là cách lưu trữ khôn ngoan nhất. Nếu bạn muốn tạo một lưu trữ tùy
chọn, như file hệ thống, và bạn muốn có thể thay đổi, hãy bảo đảm rằng lớp của bạn đã thực thi giao diện IXPathNavigable.
XSLT dựa trên mô hình kéo. Bởi vậy, bạn có thể gọp các thay đổi khác nhau lại. Nếu muốn bạn có thể áp dụng một reader tùy
chọn giữa các biến đổi. Nó cho tạo ra một khả năng mềm dẻo trong thiết kế.

Biến đổi XML

Ví dụ đầu tiên mà chúng ta xem xét lấy tài liệu books.xml và thể hiện dưới dạng HTML ví dụ sử dụng file XSLT: books.xsl. (mã
có thể tìm thấy trong thư mục XPathXSLSample3.) Chúng ta phải thêm các dòng using sau:

using System.IO;
using System.Xml.Xsl;
using System.Xml.XPath;

Đây là mã thực thi:

private void button1_Click(object sender, System.EventArgs e)


{
//create the new XPathDocument
XPathDocument doc = new XPathDocument("..\\..\\..\\booksxpath.xml");
//create a new XslTransForm
XslTransform transForm = new XslTransform();
transForm.Load("..\\..\\..\\books.xsl");
//this FileStream will be our output
FileStream fs=new FileStream("..\\..\\..\\booklist.html",
FileMode.Create);
//Create the navigator
XPathNavigator nav = doc.CreateNavigator();
//Do the transform. The output file is created here
transForm.Transform(nav, null, fs);
}

Chúng ta tạo một đối tượng xuất phát từ XPathDocument và một đối tượng xuất phát từ XslTransform. Load file booksxpath.xml
vào XPathDocument, và books.xsl vào XslTransform.

Trong ví dụ này, chúng ta cũng tạo một đối tượng FileStream để ghi tài liệu HTML mới lên đĩa. Nếu đây là một ứng dụng
ASP.NET, chúng ta có thể dùng một đối tượng TextWriter và truyền nó cho đối tượng HttpResponse. Nếu chúng ta đang thay đổi
một tài liệu XML khác, chúng ta có thể dùng một đối tượng XmlWriter.

Sau khi đã chuẩn bị các đối tượng XPathDocument và XslTransform, chúng ta tạo XPathNavigator trên XPathDocument, và
truyền XPathNavigator và FileStream cho phương thức Transform() của đối tượng XslTransform. Transform() có một vài quá
tải, truyền và kến nối các điều hướng, XsltArgumentList (trình bày sau), Và IO streams. Tham số đều hướng có thể là
XPathNavigator, hoặc bất kì đối tượng nào thực thi giao diện IXPathNavigable. IO streams có thể là một TextWriter, Stream,
hoặc một đối tượng XmlWriter.

Tài liệu books.xsl giống như sau:

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">
<html>
<head>
<title>Price List</title>
</head>
<body>
<table>
<xsl:apply-templates/>
</table>
</body>
</html>

</xsl:template>

<xsl:template match="bookstore">
<xsl:apply-templates select="book"/>
</xsl:template>

<xsl:template match="book">
<tr><td>
<xsl:value-of select="title"/>
</td><td>
<xsl:value-of select="price"/>
</td></tr>
</xsl:template>

</xsl:stylesheet>

Sử dụng XsltArgumentList

Chúng ta đã sớm đề cập đến XsltArgumentList. Đây là cách bạn kết nối một đối tượng với các phương thức và một không gian
tên. Khi hoàn tất, bạn có thẻ triệu gọi các phương thức trong quá trình biến đổi. Hãy xem ví dụ dưới đây và chỉ ra cách hoạt động
của chúng (mã có thể tìm thấy trong XPathXSLSample4).

private void button1_Click(object sender, System.EventArgs e)


{
//new XPathDocument
XPathDocument doc=new XPathDocument("..\\..\\..\\booksxpath.xml");
//new XslTransform
XslTransform transForm=new XslTransform();
transForm.Load("..\\..\\..\\booksarg.xsl");
//new XmlTextWriter since we are creating a new XML document
XmlWriter xw=new XmlTextWriter("..\\..\\..\\argSample.xml",null);
//create the XsltArgumentList and new BookUtils object
XsltArgumentList argBook=new XsltArgumentList();
BookUtils bu=new BookUtils();
//this tells the argumentlist about BookUtils
argBook.AddExtensionObject("urn:ProCSharp",bu);
//new XPathNavigator
XPathNavigator nav=doc.CreateNavigator();
//do the transform
transForm.Transform(nav,argBook,xw);
xw.Close();
}

//simple test class

public class BookUtils


{
public BookUtils(){}

public string ShowText()


{
return "This came from the ShowText method!";
}
}

Đây là file đã biến đổi (argSample.xml):

<?xml version="1.0"?>
<books>
<discbook>
<booktitle>The Autobiography of Benjamin Franklin</booktitle>
<showtext>This came from the ShowText method!</showtext>
</discbook>
<discbook>
<booktitle>The Confidence Man</booktitle>
<showtext>This came from the ShowText method!</showtext>
</discbook>
<discbook>
<booktitle>The Gorgias</booktitle>
<showtext>This came from the ShowText method!</showtext>
</discbook>
<discbook>
<booktitle>The Great Cookie Caper</booktitle>
<showtext>This came from the ShowText method!</showtext>
</discbook>
<discbook>
<booktitle>A Really Great Book</booktitle>
<showtext>This came from the ShowText method!</showtext>
</discbook>
</books>
Trong ví dụ này, chúng ta định nghĩa một lớp mới, BookUtils. Trong lớp này chúng ta có một phương thức chỉ trả về chuỗi "This
came from the ShowText method!". Trong sự kiện button1_Click(), chúng ta tạo ra XPathDocument và XslTransform như chúng
ta đã từng làm với hai ngoại lệ. Trong lúc chúng ta tạo một tài liệu XML, vì thế chúng ta dùng XmlWriter thay cho FileStream
mà chúng ta đã từng dùng. Các thay đổi khác:

XsltArgumentList argBook=new XsltArgumentList();


BookUtils bu=new BookUtils();
argBook.AddExtensionObject("urn:ProCSharp",bu);

Chúng ta tạo một XsltArgumentList. Chúng ta tạo một thể hiện của đối tượng BookUtils,và gọi phương thức
AddExtensionObject(). Khi gọi Transform(), chúng ta truyền trong các đối tượng XsltArgumentList (argBook) với
XPathNavigator và XmlWriter mà chúng ta đã tạo.

Đây là tài liệu booksarg.xsl:

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:bookUtil="urn:ProCSharp">
<xsl:output method="xml" indent="yes"/>

<xsl:template match="/">
<xsl:element name="books">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>

<xsl:template match="bookstore">
<xsl:apply-templates select="book"/>
</xsl:template>

<xsl:template match="book">
<xsl:element name="discbook">
<xsl:element name="booktitle">
<xsl:value-of select="title"/>
</xsl:element>
<xsl:element name="showtext">
<xsl:value-of select="bookUtil:ShowText()"/>
</xsl:element>
</xsl:element>
</xsl:template>

</xsl:stylesheet>

Hai dòng quan trọng được in đậm. Đầu tiên chúng ta thêm không gian tên mà chúng ta đã tạo khi thêm đối tượng
XsltArgumentList. Sau đó khi muốn gọi, chúng ta sử dụng không gian các kí hiệu chuẩn trong không gian tên XSLT .

Chúng ta còn có một cách khác đó là sử dụng XSLT scripting. Bạn có thể nhúng mã C#, VB, và JavaScript trong một stylesheet.
Không giống như thực thi phi .NET kịch bản sẽ được thực thi khi XslTransform.Load() được gọi; cách này thực thi các kịch bản
đã được biên dịch, giống như cách mà ASP.NET làm.

Hãy sửa đổi file XSLT trên theo cách này. Trước tiên chúng ta thêm kịch bản vào stylesheet. bạn có thể thấy các thay đổi trong
booksscript.xsl dưới đây:

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:user="http://wrox.com">

<msxsl:script language="C#" implements-prefix="user">

string ShowText()
{
return "This came from the ShowText method!";

}
</msxsl:script>

<xsl:output method="xml" indent="yes"/>


<xsl:template match="/">
<xsl:element name="books">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
<xsl:template match="bookstore">
<xsl:apply-templates select="book"/>
</xsl:template>
<xsl:template match="book">
<xsl:element name="discbook">
<xsl:element name="booktitle">
<xsl:value-of select="title"/>
</xsl:element>
<xsl:element name="showtext">
<xsl:value-of select="user:ShowText()"/>
</xsl:element>
</xsl:element>
</xsl:template>

</xsl:stylesheet>

Chúng ta cài không gian tên scripting, và thêm mã (đã được biên dịch bởi Visual Studio .NET IDE), và tạo một lời gọi đến
stylesheet. File xuất giống hệt ví dụ trên.

Tóm lại, sử dụng XPathDocument nếu bạn muốn soạn thảo, XmlDataDocument nếu bạn muốn lấy dữ liệu từ ADO.NET, và
XmlDocument nếu bạn muốn có thể thao tác dữ liệu.

Tóm tắt
Trong chương này chúng ta kham phát rát nhiều khía cạnh của không gian tên System.Xml namespace trong .NET Framework.
Chúng ta đã xem xét cách đọc ghi các tài liệu XML sử dụng các lớp truy xuất nhanh XmlReader và XmlWriter. Chúng ta xem
xét cách hoạt động của DOM trong .NET, và cách dùng cách khả năng của DOM. Chúng ta thấy rằng XML và ADO.NET có
quan hệ rất gần. Một DataSet và một tài liệu XML chỉ là hai thể hiện khác nhau của cùng một công nghệ. Và dĩ nhiên chúng ta
xem qua XPath và XSL Transforms.

XML sẽ là một phần quan trong trong các ứng dụng trong một vài năm tới. .NET Framework có sẵn các công cụ mạnh mẽ hỗ trợ
các thao tác trên XML. Để hiểu hơn về XML trong C#, hãy tìm đọc tác phẩm "Data-Centric .NET Programming with C#" (Wrox
Press, ISBN 1-861005-92-x).

Trong chương tới chúng ta sẽ xem xét cách quản một file và Registry bằng các lớp C#.

Chương 12: File and Registry Operations


Tổng quan
Trong chương này chúng ta sẽ khảo sát làm thế nào để thực hiện các nhiệm vụ như đọc từ file và viết ra file và hệ thống đăng ký
(Registry) trong C#. Cụ thể chúng ta sẽ được học.

• Cấu trúc thư mục, tìm kiếm file và folder hiện diện và kiểm tra thuộc tính của chúng.
• Di chuyển, sao chép, huỷ các file và folder

• Đọc và ghi văn bản trong các file

• Đọc và ghi các khoá trong Registry

Microsoft cung cấp rất nhiều mô hình đối tượng trực giác mà chúng ta sẽ được khào sát trong chương này, chúng ta cũng biết
cách sử dụng các lớp cơ bản của .NET để thực hiện các nhiệm vụ được đề cập ở trên.

Di chuyển, Sao chép, Huỷ File


Chúng ta vừa mới đế cập di chuyển và huỷ files hoặc folders bằng phương thức MoveTo() và Delete() của lớp FileInfo và
DirectoryInfo. Các phương thức tương đương nhau trên các lớp File và Directory là Move() và Delete(). Lớp FileInfo và File
cũng có cách thức thực hiện tương tự, CopyTo() and Copy(). Không có fương thức nào copy các folder, tuy nhiên bạn có thể
copy từng file trong folder.

Tất cả các phương thức này đều hoàn toàn trực giác, bạn có thể tìm kiếm chi tiết trong help MSDN. Trong phần này chúng ta
sẽ học cách gọi các phương thức tỉnh Move(), Copy(), và Delete() trong lớp File.Để thực hiện chúng ta dùng bài học phần trước
FileProperties làm ví dụ, FilePropertiesAndMovement. Ví dụ này ta sẽ thêm tính năng mỗi lần thuộc tính của một file được hiển
thị, ứng dụng này sẽ cho ta chọn lựa thêm xoá file hoặc di chuyển hoặc sao chép nó đến vị trí khác

Ví dụ: FilePropertiesAndMovement

Ví dụ mới như sau :

Từ hình trên chúng ta có thể thấy nó rất giống nhau trong lần xuất hiện ở ví dụ FileProperties, Ngoại trừ chúng có thêm một
nhóm gồm ba nút button và một textbox tại phía dưới cửa xổ. Các điều khiển này chỉ hiện khi ví dụ hiển thị thuộc tính của một
file- Lần khác chúng bị ẩn, Khi thuộc tính của file được hiển thị ,FilePropertiesAndMovement tự động đặt tên đầy đủ đường dẫn
của file đó ở cuối của xổ trong textbox. User có thể nhấn bất kỳ buttons để thực hiện phép toán thích hợp. Khi chương trình chạy
một message box tương ứng được hiển thị xác nhận hành động.

Để mã hoá chúng ta cần thêm các điều khiển thích hợp, giống như thêm các sự kiện điều khiển cho ví dụ FileProperties .
Chúng ta tạo các controls mới với tên buttonDelete, buttonCopyTo, buttonMoveTo, và textBoxNewPath.

Chúng ta sẽ thấy sự kiện điều kiện nhận được khi user nhấn vào Delete button;

protected void OnDeleteButtonClick(object sender, EventArgs e)


{
try
{
string filePath = Path.Combine(currentFolderPath,
textBoxFileName.Text);
string query = "Really delete the file\n" + filePath + "?";
if (MessageBox.Show(query,
"Delete File?", MessageBoxButtons.YesNo) == DialogResult.Yes)
{
File.Delete(filePath);
DisplayFolderList(currentFolderPath);
}
}
catch(Exception ex)
{
MessageBox.Show("Unable to delete file. The following exception"
+ " occurred:\n" + ex.Message, "Failed");
}
}

Đoạn code thực hiện phương thức này sẽ có phần nắm bắt lỗi, thông báo sẽ lỗi không thể xoá được nếu file xoá đang thực hiện
trên một tiến trình khác. Chúng ta xây dựng đường dẫn của file của file bị xoá từ trường CurrentParentPath , Nơi nó chứa dường
dẫn thư mục cha, và tên file trong textBoxFileName textbox:

Phương thức di chuyển và sao chép file được cấu trúc :

protected void OnMoveButtonClick(object sender, EventArgs e)


{
try
{
string filePath = Path.Combine(currentFolderPath,
textBoxFileName.Text);
string query = "Really move the file\n" + filePath + "\nto "
+ textBoxNewPath.Text + "?";
if (MessageBox.Show(query,
"Move File?", MessageBoxButtons.YesNo) == DialogResult.Yes)
{
File.Move(filePath, textBoxNewPath.Text);
DisplayFolderList(currentFolderPath);
}
}
catch(Exception ex)
{
MessageBox.Show("Unable to move file. The following exception"
+ " occurred:\n" + ex.Message, "Failed");
}
}

protected void OnCopyButtonClick(object sender, EventArgs e)


{
try
{
string filePath = Path.Combine(currentFolderPath,
textBoxFileName.Text);
string query = "Really copy the file\n" + filePath + "\nto "
+ textBoxNewPath.Text + "?";
if (MessageBox.Show(query,
"Copy File?", MessageBoxButtons.YesNo) == DialogResult.Yes)
{
File.Copy(filePath, textBoxNewPath.Text);
DisplayFolderList(currentFolderPath);
}
}
catch(Exception ex)
{
MessageBox.Show("Unable to copy file. The following exception"
+ " occurred:\n" + ex.Message, "Failed");
}
}

Chúng ta cũng tạo buttons và textbox mới được đánh dấu enabled và disabled ở thời điểm thích hợp để enable chúng khi chúng
ta hiển thị nội dung của file, chúng ta cần thêm đoạn code sau:

protected void DisplayFileInfo(string fileFullName)


{
FileInfo theFile = new FileInfo(fileFullName);
if (!theFile.Exists)
throw new FileNotFoundException("File not found: " + fileFullName);

textBoxFileName.Text = theFile.Name;
textBoxCreationTime.Text = theFile.CreationTime.ToLongTimeString();
textBoxLastAccessTime.Text = theFile.LastAccessTime.ToLongDateString();
textBoxLastWriteTime.Text = theFile.LastWriteTime.ToLongDateString();
textBoxFileSize.Text = theFile.Length.ToString() + " bytes";

// enable move, copy, delete buttons


textBoxNewPath.Text = theFile.FullName;
textBoxNewPath.Enabled = true;
buttonCopyTo.Enabled = true;
buttonDelete.Enabled = true;
buttonMoveTo.Enabled = true;
}

Chúng ta cũng cần thay đổi DisplayFolderList:

protected void DisplayFolderList(string folderFullName)


{
DirectoryInfo theFolder = new DirectoryInfo(folderFullName);
if (!theFolder.Exists)
throw new DirectoryNotFoundException("Folder not found: " +
folderFullName);

ClearAllFields();
DisableMoveFeatures();
textBoxFolder.Text = theFolder.FullName;
currentFolderPath = theFolder.FullName;

// list all subfolders in folder


foreach(DirectoryInfo nextFolder in theFolder.GetDirectories())
listBoxFolders.Items.Add(NextFolder.Name);

// list all files in folder


foreach(FileInfo nextFile in theFolder.GetFiles())
listBoxFiles.Items.Add(NextFile.Name);
}

DisableMoveFeatures là một hàm tiện ích nhỏ nó disables các controls mới:

void DisableMoveFeatures()
{
textBoxNewPath.Text = "";
textBoxNewPath.Enabled = false;
buttonCopyTo.Enabled = false;
buttonDelete.Enabled = false;
buttonMoveTo.Enabled = false;
}

Chúng ta cần thêm phương thức ClearAllFields() để xoá các textbox thêm vào:

protected void ClearAllFields()


{
listBoxFolders.Items.Clear();
listBoxFiles.Items.Clear();
textBoxFolder.Text = "";
textBoxFileName.Text = "";
textBoxCreationTime.Text = "";
textBoxLastAccessTime.Text = "";
textBoxLastWriteTime.Text = "";
textBoxFileSize.Text = "";
textBoxNewPath.Text = "";
}

Đến đây đoạn mã chúng ta đã hoàn thành.Bạn có thể tham khảo chương trình kèm theo sách download

Đọc và viết vào File


Đọc và viết vào files nói chung rất đơn giản; tuy nhiên , Điều này không phải bắt buộc biết các
đối tượng DirectoryInfo hoặc FileInfo mà chúng ta vừa khảo sát.Thay vào đó chúng ta
phải biết một số lớp trình bày nội dung chung gọi là stream, Điều này chúng ta sẽ khảo
sát sau đây.

Streams
Đọc và viết dữ liệu sẽ được thực hiện thông qua lớp stream. Stream là dòng dữ liệu chảy đi. Đây là một thực thể (entity) có khả
năng nhận được hoặc tạo ra một "nhúm" dữ liệu. System.IO.Stream là một lớp abstract định nghĩa một số thành viên chịu hỗ trợ
việc đọc/viết đồng bộ (synchronus) hoặc không đồng bộ (asynchronous) đối với khối trữ tin (nghĩa là một tập tin trên đĩa hoặc
tập tin trên ký ức).

Vì Stream là một lớp abstract, nên bạn chỉ có thể làm việc với những lớp được dẫn xuất từ Stream. Các hậu duệ của Stream
tượng trưng dữ liệu như là một dòng dữ liệu thô dạng bytes (thay vì dữ liệu dạng văn bản). Ngoài ra, các lớp được dẫn xuất từ
Stream hỗ trợ việc truy tìm (seek) nghĩa là một tiến trình nhận lấy và điều chỉnh vị trí trên một dòng chảy. Trước khi tìm hiểu
những chức năng mà lớp Stream cung cấp, bạn nên xem qua các thành viên của lớp Stream.

Ý tưởng của stream đã có từ lâu. Một stream là một đối tượng dùng để chuyển dữ liệu. Dữ liệu có thể được truyền theo hai
hướng:

• Nếu dữ liệu được truyền từ nguồn bên ngoài vào trong chương trình của bạn, ta gọi là đọc dữ liệu

• Nếu dữ liệu được truyền từ chương trình của bạn ra nguồn bên ngoài , ta gọi là viết dữ liệu

Thường thì nguồn bên ngoài sẽ là một file, ngoài ra nó còn bao gồm cả trường hợp sau:

• Đọc hoặc ghi dữ liệu trên mạng dùng giao thức mạng
• Đọc hoặc ghi đến một đường ống chỉ định

• Đọc hoặc ghi đến một vùng của bộ nhớ

Các lớp có mối liên hệ trong namespace System.IO như hình sau:

Làm việc với Binary Files


Reading and writing to binary files thường được làm việc với lớp FileStream .

Làm việc với FileStream


Lớp FileStream đem lại việc thi công cho những thành viên của lớp abstract Stream theo một thể thức thích hợp đối với các file-
base streaming giống như các lớp DirectoryInfo và FileInfo, lớp FileStream cho phép mở những tập tin hiện hữu cũng như tạo
mới file. Khi tạo tập tin , lớp FileStream thường dùng những enum FileMode, FileAccess và FileShare

// tạo một tập tin mới trên thư mục làm việc
FileStream myFStream = new FileStream("test.dat",FileMode.OpenOrCreate, FileAccess.ReadWrite);

The FileStream Class


FileStream được sử dụng đọc và viết dữ liệu vào hoặc từ một file. Để khởi tạo một FileStream, bạn cần 4 phần sau:

• file bạn muốn truy xuất .


• mode, cho biết bạn muốn mở file như thế nào.

• access, cho biết bạn muốn truy xuất file như thế nào – bạn định đọc hoặc viết file hoặc cả hai.

• share access – khả năng truy xuất file.


Enumeration Values

FileMode Append, Create, CreateNew, Open, OpenOrCreate, or Truncate

FileAccess Read, ReadWrite, or Write

FileShare Inheritable, None, Read, ReadWrite, or Write

Làm việc với BufferedStream


Khi bạn triệu gọi hàm Read() thì một công tác đọc dữ liệu cho đầy buffer từ đĩa được tiến hành. Tuy nhiên, để cho có hiệu năng,
hệ điều hành thường phải đọc trong một lúc một khối lượng lớn dữ liệu tạm thời trữ trên bufer. Buffer hoạt động như mọt kho
hàng.

Một đối tượng Bufered stream cho phép hệ điều hành tạo buffer riêng cho mình dùng, rồi đọc dữ liệu vào hoặc viết dữ liệu lên ổ
đĩa theo một khối lượng dữ liệu nào đó mà hệ điều hành thấy là có hiệu năng. Tuy nhiên, bạn xũng có thể ấn định chiều dài khối
dữ liệu. Nhưng bạn nhớ cho là buffer sẽ chiêmd chỗ trong ký ức chứ không phải trên đĩa từ. Hiệu quả sử dụng đến buffer là ciệc
xuất nhập dữ liệu chạy nhanh hơn.

Một đối tượng BufferedStream được hình thành xung quanh một đối tượng Stream mà bạn đã tạo ra trước đó. Muốn sử dụng
đến một BufferedStream bạn bắt đầu tạo một đối tượng Stream thông thường như trong thí dụ :

stream inputstream = File.OpenRead(@"C;\test\source\folder3.cs ");

stream outputstream = File.Openwrite(@"C:test\source\folder3.bak");

Một khi bạn đã có stream bình thường, bạn trao đối tượng này cho hàm constructor của buffere stream:

BufferedStrream bufInput = new BufferedStream(inputstream);

BufferedStream bufOutput =new BufferedStream(outputstream);

Sau đó, bạn sử dụng BufferedStream như là một stream bình thường, bạn triệu gọi hàm Read() hoặc Write() như bạn đã làm
trước kia. Hệ điều hành lo việc quản lý vùng đêm:

while ((bytesRead = bufInput.Read(buffer, 0, SIZE_BUFF))>0)

{ bufOutput.Write(buffer, 0, bytesRead);

Chỉ có một khác biệt mà bạn phải nhớ cho là phải tuôn ghi (flush) nội dung của buffer khi bạn muốn bảo đảm là dữ liệu được
ghi lên đĩa.

bufOutput.Flush();

Lệnh trên bảo hệ điều hành lấy toàn bộ dữ liệu trên buffer cho tuôn ra ghi lên tập tin trên đĩa.

Làm việc với những tập tin văn bản


Nếu bạn biết file bạn đang làm việc (đọc/viết) thuộc loại văn bản nghĩa là dữ liệu kiểu string, thì bạn nên nghĩ đến việc sử
dụng đến các lớp StreamReader và StreamWriter. Cả hai lớp theo mặc nhiên làm việc với ký tự Unicode. Tuy nhiên bạn có thể
thay đổi điều này bằng cách cung cấp một đối tượng quy chiếu được cấu hình một cách thích hợp theo System.Text.Reference.
Nói tóm lại hai lớp này được thiết kế để thao tác dễ dàng các tập tin loại văn bản.

Lớp StreamReader được dẫn xuất từ một lớp abstract mang tên TextReader cũng giống như String Reader. Lớp cơ bản
TextReader cung cấp một số chức năng hạn chế cho mỗi hậu duệ, đặc biệt khả năng đọc và "liếc nhìn" (peek) lên một dòng ký tự
(character stream).

Lớp StreamWriter và StringWriter cũng được dẫn xuất từ một lớp abstract mang tên TextWriter; lớp này định nghĩa những thành
viên cho phép các lớp dẫn xuât viết những dữ liệu văn bản lên một dòng văn bản nào đó
Các thành viên của lớp TextWriter

Tên thành viên Ý nghĩa

Close() Cho đóng lại các writer và giải phóng mọi nguồn lực chiếm dụng
Flush() Cho xoá sạch tất cả các buffer đối với writer hiện hành
NewLine Thuộc tính này dùng làm hằng sang hằng
Write() Viết một hằng lên text stream không có newline constant
WriteLine() Viết một hằng lên text stream có newline constant

Ví dụ đọc, viết một tập tin văn bản:


Ví dụ ReadWriteText trình bày cách sử dụng của lớp StreamReader và StreamWriter. Nó trình bày file được đọc vào và hiển thị
Nó cũng có thể lưu file. Nó sẽ lưu bất kỳ file ở định dạng Unicode .

Màn hình trình bày ReadWriteText được dùng hiển thị file CivNegotiations. Chúng ta có thể đọc được ở nhiều định dạng file
khác.

Chúng ta nhìn vào đoạn mã sau. Trước tiên ta thêm câu lệnh using , Từ đây bên cạnh System.IO, chúng ta sử dụng lớp
StringBuilder từ System.Text namespace để xây dựng chuỗi trong textbox:

using System.IO;
using System.Text;

Tiếp theo chúng ta thêm các trường cho lớp main form

public class Form1 : System.Windows.Forms.Form


{
private OpenFileDialog chooseOpenFileDialog = new OpenFileDialog();
private string chosenFile;

Chúng ta cũng cần thêm vài chuẩn mã Windows Forms để thực hiện điều khiển cho menu và hộp thoại:

public Form1()
{
InitializeComponent();
menuFileOpen.Click += new EventHandler(OnFileOpen);
chooseOpenFileDialog.FileOk += new
CancelEventHandler(OnOpenFileDialogOK);
}
void OnFileOpen(object Sender, EventArgs e)
{
chooseOpenFileDialog.ShowDialog();
}
void OnOpenFileDialogOK(object Sender, CancelEventArgs e)
{
chosenFile = chooseOpenFileDialog.FileName;
this.Text = Path.GetFileName(chosenFile);
DisplayFile();
}

Từ đây chúng ta thấy mỗi khi người sử dụng nhấn OK để chọn một file trong hộp thoại, chúng ta gọi phương thức DisplayFile(),
dùng để đọc file.
void DisplayFile()
{
int nCols = 16;
FileStream inStream = new FileStream(chosenFile, FileMode.Open,
FileAccess.Read);
long nBytesToRead = inStream.Length;
if (nBytesToRead > 65536/4)
nBytesToRead = 65536/4;
int nLines = (int)(nBytesToRead/nCols) + 1;
string [] lines = new string[nLines];
int nBytesRead = 0;
for (int i=0 ; i<nLines ; i++)
{
StringBuilder nextLine = new StringBuilder();
nextLine.Capacity = 4*nCols;
for (int j = 0 ; j<nCols ; j++)
{
int nextByte = inStream.ReadByte();
nBytesRead++;
if (nextByte < 0 || nBytesRead > 65536)
break;
char nextChar = (char)nextByte;
if (nextChar < 16)
nextLine.Append(" x0" + string.Format("{0,1:X}",
(int)nextChar));
else if
(char.IsLetterOrDigit(nextChar) ||
char.IsPunctuation(nextChar))
nextLine.Append(" " + nextChar + " ");
else
nextLine.Append(" x" + string.Format("{0,2:X}",
(int)nextChar));
}
lines[i] = nextLine.ToString();
}
inStream.Close();
this.textBoxContents.Lines = lines;
}

Như vậy chúng ta đã mở được file nhờ phương thức DisplayFile(). bây giờ chúng ta xử lý cách để lưu file chúng ta thêm đoạn mã
SaveFile(). Bạn nhìn vào phương thức SaveFile() chúng ta viết mỗi dòng ra textbox, bằng stream StreamWriter

void SaveFile()
{
StreamWriter sw = new StreamWriter(chosenFile, false,
Encoding.Unicode);
foreach (string line in textBoxContents.Lines)
sw.WriteLine(line);
sw.Close();
}

Bây giờ ta xem xét làm thế nào file được đọc vào. Trong quá trình xử lý thực sự chúng ta không biết có bao nhiêu dòng sẽ được
chứa (cũng có nghĩa là có bao nhiêu ký tự (char)13(char)10 tuần tự trong file đến khi nào kết thúc file) Chúng ta giải quyết vấn
đề này bằng cách ban đầu đọc file vào trong lớp đại diện StringCollection, được nằm trong System.Collections.Specialized
namespace. Lớp này được thiết kế để giữ một bộ của chuỗi có thể được mở rộng một cách linh hoạt. Nó thực thi hai phương
thức : Add(), nó thêm một chuỗi vào bộ chọn lựa (collection) , và CopyTo(), nó sao chép string collection vào trong một mảng.
Mỗi thành phần của đối tượng StringCollection object sẽ giữ 1 hàng của file.

Bây giờ chúng ta sẽ xem xét phương thức ReadFileIntoStringCollection() . Chúng ta sử dụng StreamReader để đọc trong mỗi
hàng. Khó khăn chính là cần đếm ký tự đọc để chắc chúng ta không vượt quá khả năng chứa đựng của textbox:

StringCollection ReadFileIntoStringCollection()
{
const int MaxBytes = 65536;
StreamReader sr = new StreamReader(chosenFile);
StringCollection result = new StringCollection();
int nBytesRead = 0;
string nextLine;
while ( (nextLine = sr.ReadLine()) != null)
{
nBytesRead += nextLine.Length;
if (nBytesRead > MaxBytes)
break;
result.Add(nextLine);
}
sr.Close();
return result;
}

Đến đây đoạn mã được hoàn thành.

Bạn có thể tham khảo chương trình kèm theo sách ReadWriteText

Tóm tắt
Trong chương này đã khảo sát làm thế nào sử dụng các lớp cơ bản .NET để truy xuất Registry và file hệ thống từ mã C# .
Chúng ta cũng thấy được trong cả hai trường hợp các lớp cơ bản trình bày đơn giản nhưng hiệu quả , mô hình đối tượng rất đơn
giản để thực hiện hầu hết các hành động trong vùng của chúng. Đối với Registry, chúng ta tạo, chỉnh sửa, hoặc đọc các khoá, và
đối với file hệ thống sao chép file, di chuyển, tạo, huỷ file và folder và đọc và viết file văn bản.

Trong chương này chúng ta sẽ thử chạy đoạn code C# từ tài khoản có quyền hạn truy xuất đến bất kỳ đoạn code cần thực hiện.
Tất nhiên vấn đề bảo mật là quan trọng nơi file truy xuất có liên quan. Và chúng ta sẽ khảo sát vùng bảo mật trong .NET
Security.

Chương 13: Làm việc với Active Directory


Tổng quan
1 đặc tính chính ( có lẽ là quan trọng nhất ) được giới thiệu trong Win 2000 là Active Directory. Active Directory là 1 dịch vụ
thư mục ( directory service), mà cung cấp cách lưu trữ thông tin của người dùng, tài nguyên mạng, các dịch vụ .. có cấu trúc,tập
trung.

Ví dụ ,Microsoft's Exchange Server 2000 dùng Active Directory lưu các thư mục chung và các mục khác

Trước khi có Active Directory, Exchange Server sử dụng cách lưu trữ riêng các đối tượng của nó.Người quản trị hệ thống phải
cấu hình 2 ID user cho một người dùng: 1 tài khoản user trong domain Window NT để có thể đăng nhập,và 1 user trong
Exchange Directory.Điều này là cần thiết bởi vì thông tin thêm của người dùng được yêu cầu ( như địa chỉ e-mail, số điện
thoại, ..) và thông tin người dùng trong domain Window NT không được mở rộng để ta đặt các thông tin đòi hỏi ở đó.Bây giờ
người quản trị hệ thống chỉ cần cấu hình 1 user cho 1 người dùng trong Active Directory,thông tin của đối tượng người dùng có
thể được mở rộng để phù hợp với các yêu cầu của Exchange Server.Ta cũng có thể mở rộng thông tin này.

Trong chương này, ta sẽ tìm hiểu cách dùng .NET framework để truy xuất và thao tác dữ liệu trong 1 dịch vụ thư mục bằng cách
dùng các lớp từ namespace System.DirectoryServices

Để dùng các ví dụ trong chương này ta cần có Window 2000 Server đã cài Active Directory.Các lớp của namespace
System.DirectoryServices cũng có thể được dùng trong các dịch vụ thư mục của Novell và Window NT 4

Trong chương này ta sẽ tìm hiểu các phần sau :

• Kiến trúc của Active Directory -các đặc tính và khái niệm cơ bản
• Một vài công cụ cho người quản trị Active Directory , và lợi ích của chúng trong lập trình

• Cách để đọc và cập nhật dữ liệu trong Active Directory

• Tìm kiếm các đối tượng trong Active Directory


• Sau khi thảo luận kiến trúc và cách lập trình trên Active Directory ta sẽ tạo 1 ứng dụng Window mà có thể đặc tả các
thuộc tính và bộ lọc để tìm kiếm các đối tượng người dùng

Các công cụ quản trị cho Active Directory


Nhà quản trị hệ thống có nhiều công cụ để tạo dữ liệu mới ,cập nhật dữ liệu, và cấu hình Active directory:
- Active Directory Users and Computers MMC snap-in dùng để tạo người dùng mới và cập nhật dữ liệu người dùng.
- Active Directory Sites and Services MMC snap-in dùng để cấu hình các site trong domain và sao chép giữa các site
- Active Directory Domains and Trusts MMC snap-in dùng để xây dựng các mối quan hệ tin cậy giữa các domain trong cây
- ADSI Edit là trình biên tập của Active Directory, nơi đối tượng được xem và chỉnh sửa
Máy tính và người dùng Active Directory
- Active Directory Users and Computers snap-in là công cụ được sử dụng chính bởi nhà quản trị hệ thống để quản lý ngưòi
dùng. chọn Start|Programs| AdministrativeTools| Active Directory Users and Computers:

Với công cụ này ta có thể thêm người dùng mới, nhóm ,contact,máy in , thư mục chia sẻ, máy tính....hình kế tiếp ta có thể thấy
các thuộc tính mà có thể được nhập cho đối tưọng user : office,số điện thoại,địa chỉ email,trang web thông tin tổ chức,...

Active Directory Users and Computers cũng có thể được dùng trong các công ty lớn với hàng triệu đối tượng. không cần phải
xem hết toàn bộ đối tượng ,vì ta có thể lọc để chỉ một số đối tượng được trình bày.ta cũng có thể truy vấn LDAP để tìm các đối
tượng trong công ty.
ADSI Edit
ADSI Edit là trình biên tập của Active Directory.công cụ này không được cài tự động. trên CD Win2000 server ta tìm thư mục
Supporting Tools. sau khi cài đặt ta vào Start | Programs | Windows 2000 Support Tools | Tools | ADSI Edit.
ADSI Edit điều khiển tốt hơn Active Directory Users and Computers tool, với ADSI Edit mọi thứ đều có thể đuợc cấu hình, và ta
cũng có thấy Schema và cấu hình . công cụ này dễ dàng sử dụng nhưng rất dễ nhập dữ liệu sai :
Mở cửa sổ properties của đối tượng ta có thể xem và thay đổi mỗi thuộc tính của 1 đối tượng trong Active Directory.ta thấy các
thuộc tính bắt buộc và tuỳ chọn với kiểu và giá trị:

ADSI viewer
Ta nên cài đặt Active Directory Browser , 1 phần của Microsoft Platform SDK.Microsoft Platform SDK không phải là 1 phần
của Visual Studio .NET .ta có thể download nó từ MSDN web.sau khi cài đặt ta vào :Start | Programs | Microsoft Platform SDK |
Tools | ADSI Viewer.
ADSI viewer có hai mô hình . với File|New ta có thể bắt đầu 1 truy vấn hay sử dụng Object Viewer để trình bày mà cập nhật
thuộc tính của đối tượng. sau khi bắt đầu Object Viewer ta có thể chỉ định 1 đường dẫn LDAP, cũng như username và password
để mở đối tưọng.ví dụ :
ta chỉ định : LDAP://OU=Wrox Press, DC=eichkogelstrasse, DC=local

Nếu đối tượng ta chỉ định với đường dẫn và username và password đúng ta có màn hình Object Viewer nơi ta có thể xem và cập
nhật thuộc tính của đối tượng và các đối tượng con của nó :
Active Directory Service Interfaces ( ADSI)
ADSI là 1 giao diện lập trình cho dịch vụ thư mục.ADSI định nghĩa vài giao diện COM được thực thi bởi provider ADSI. nghĩa
là client có thể dùng các dịch vụ thư mục khác với cùng các giao diện lập trình . các lớp trong .NET frameWork trong namespace
System.DirectoryServices tạo cách dùng các giao diện ADSI
Trong hình sau ta thấy vài provider ADSI ( LDAP,WinNT,NDS) thi hành các giao diện COM như IADs và IUnknown.
ASsembly System.DirectoryServices tạo cách dùng provider ADSI:

Tìm kiếm các đối tượng người dùng


Trong phần này ta sẽ xây dựng 1 Windows Form gọi là UserSearch.trong ứng dụng ta có thể nhập domain controller, uesrname
,password để truy nhập Active Directory. ta sẽ truy nhập vào schema của Active Directory để lấy các thuộc tính của đối tượng
user. người dùng có thể nhập chuỗi tìm các đối tưọng user trong domain. ngoài ra ta cũng có thể thiết lập các thuộc tính của đối
tượng user nên được trình bày.
Giao diện người dùng
Các bước sử dụng :
1. Nhập Username ,PasswordmDomain controller. tất cả đều tuỳ chọn . nếu không nhập domain controller thì kết nối làm việc
với liên kết không server ( serverless binding).nếu không nhập username thì ngữ cảnh của người dùng hiện tại được lấy.
2. 1 nút nhấn cho phép tất cả các tên thuộc tính của đối tượng user được tải vào trong listbox listboxProperties
3. Sau khi tên thuộc tính được tải ta có thể lựa chọn các thuộc tính . selectioinmode của listbox được đặt là MultiSimple.
4. Nhập vào filter để giới hạn tìm kiếm. giá trị mặc định là (objectClass=user)
5. Bắt đầu tìm kiếm
Ngữ cảnh tên Schema ( Schema Naming Context)
Chương trình có 2 phương thức xử lý : đầu tiên là nút nhấn để tải các thuộc tính, thứ 2 là nút bắt đầu tìm kiếm trong domain.phần
đầu ta đọc các thuộc tính trong schema để trình bày nó.
Trong phương thức buttonLoActive DirectoryProperties_Click() ,SetLogonInformation() đọc username,password và hostname từ
hộp thoại và lưu chúng trong các biến thành viên của lớp.sau đó phương thức SetNamingContext() đặt tên LDAP của Schema và
tên LDAP của ngữ cảnh mặc định .tên Schema LDAP này được dùng để đặt các thuộc tính trong listbox : SetUserProperties():

private void buttonLoadProperties_Click(object sender, System.EventArgs e)


{
try
{
SetLogonInformation();
SetNamingContext();

SetUserProperties(schemaNamingContext);
}
catch (Exception ex)
{
MessageBox.Show("Check your inputs! " + ex.Message);
}
}
protected void SetLogonInformation()
{
username = (textBoxUsername.Text == "" ? null : textBoxUsername.Text);
password = (textBoxPassword.Text == "" ? null : textBoxPassword.Text);
hostname = textBoxHostname.Text;
if (hostname != "") hostname += "/";
}

Hổ trợ cho phương thức SetNamingContext(), ta dùng gốc của cây thư mục để lấy các thuộc tính của server, ta chỉ quan tâm đến
giá trị 2 thuộc tính : schemaNamingContext và defaultNamingContext :

protected string SetNamingContext()


{
using (DirectoryEntry de = new DirectoryEntry())
{
string path = "LDAP://" + hostname + "rootDSE";
de.Username = username;
de.Password = password;
de.Path = path;
schemaNamingContext = de.Properties["schemaNamingContext"][0].ToString();
defaultNamingContext =
de.Properties["defaultNamingContext"][0].ToString();
}
}

Lấy các tên thuộc tính của lớp User


Ta có tên LDAP để truy nhập schema . ta có thể dùng tên này để truy nhập thư mục và đọc các thuộc tính. ta không chỉ quan tâm
đến lớp user mà còn các lớp cơ sở khác như : Organizational-Person, Person, and Top. trong chương trình này tên của các lớp cơ
sở là hard-coded ( mã chỉ định sẵn ) . nếu muốn đọc ta dùng thuộc tính subclassof .GetSchemaProperties() trả về 1 mảng chuỗi
với tên tất cả thuộc tính của kiểu đối tượng đặc tả .tất cả tên thuộc tính được thu thập vào thuộc tính StringCollection :
protected void SetUserProperties(string schemaNamingContext)
{
StringCollection properties = new StringCollection();
string[] data = GetSchemaProperties(schemaNamingContext, "User");
properties.AddRange(GetSchemaProperties(schemaNamingContext,
"Organizational-Person"));
properties.AddRange(GetSchemaProperties(schemaNamingContext, "Person"));
properties.AddRange(GetSchemaProperties(schemaNamingContext, "Top"));
listBoxProperties.Items.Clear();
foreach (string s in properties)
{
listBoxProperties.Items.Add(s);
}
}

Trong GetSchemaProperties() ta truy nhập Active Directory lại, lần này rootDSE không được dùng.thuộc tính
SystemMayContain giữ 1 tập tất cả các thuộc tính mà được cho phép trong lớp objectType:

protected string[] GetSchemaProperties(string schemaNamingContext,


string objectType)
{
string[] data;
using (DirectoryEntry de = new DirectoryEntry())
{
de.Username = username;
de.Password = password;

de.Path = "LDAP://" + hostname + "CN=" + objectType + "," +


schemaNamingContext;

DS.PropertyCollection properties = de.Properties;


DS.PropertyValueCollection values = properties["systemMayContain"];

data = new String[values.Count];


values.CopyTo(data, 0);
}
return data;
}

Tìm kiếm các đối tượng user


Nút tìm kiếm gọi pương thức FillReult():

private void buttonSearch_Click(object sender, System.EventArgs e)


{
try
{
FillResult();
}
catch (Exception ex)
{
MessageBox.Show("Check your input: " + ex.Message);
}
}

Ta thiết lập trong FillResult như sau : SearhcScope thiết đặt là subtree,Filter là chuỗi lấy từ textbox và các thuộc tính được tải vào
cache được chọn dựa vào listbox:
protected void FillResult()
{
using (DirectoryEntry root = new DirectoryEntry())
{
root.Username = username;
root.Password = password;
root.Path = "LDAP://" + hostname + defaultNamingContext;

using (DirectorySearcher searcher = new DirectorySearcher())


{
searcher.SearchRoot = root;
searcher.SearchScope = SearchScope.Subtree;
searcher.Filter = textBoxFilter.Text;
searcher.PropertiesToLoad.AddRange(GetProperties());

SearchResultCollection results = searcher.FindAll();


StringBuilder summary = new StringBuilder();
foreach (SearchResult result in results)
{
foreach (string propName in
result.Properties.PropertyNames)
{
foreach (string s in result.Properties[propName])
{
summary.Append(" " + propName + ": " + s + "\r\n");
}
}
summary.Append("\r\n");
}
textBoxResults.Text = summary.ToString();
}
}
}
Kết quả cho ta tất cả các đối tượng được lọc :

Code for Download :

UserSearch

Tóm tắt

Trong chương này ta đả xem xét kiến trúc của Active Directory: Các khái niệmquan trọng như domain,tree,forest.Ta có thể truy
xuất thông tin trong các tổ chức hoàn chỉnh ( complete enterprise).Viết các ứng dụng mà truy xuất Active Directory,ta phải nhậ
nthức rằng dữ liệu ta đọc không thể đưọc cập nhật bởi replication latency.

Các lớp trong namespace System.DirectoryServices cho chúng ta các cách dễ dàng để truy xuất Active Directory bằng cách gói
các provider ADSI. lớp DirecoryEntry có thể đọc và viết các đối tượng 1 cách trực tiếp vào nơi lưu trữ dữ liệu

Với lớp DirectorySearcher ta có thi hành các tìm kiếm phức tạp và định nghĩa các bộ lọc,timeouts,thuộc tính để tải và phạm
vi.Dùng Global Catalog ta có thể tăng tốc độ tìm kiếm các đối tượng trong các tổ chức hoàn chỉnh,bởi vì nó lưu các phiên bản chỉ
đọc của tất cả các đối tượng trong forest

You might also like