GIÁO TRÌNH

Giáo trình ngôn ngữ lập trình C#

Science and Technology

Cơ chế sự kiện

Tác giả: Khuyet Danh

Sự kiện

Trong môi trường giao diện đồ họa, bất cứ thành phần nào cũng có thể đưa ra sự kiện. Ví dụ, khi chúng ta kích vào một nút lệnh, nó có thể đưa ra sự kiện Click. Khi chúng ta thêm một mục vào danh sách, nó sẽ đưa ra sự kiện ListChanged.

Cơ chế publishing và subscribing

Trong ngôn ngữ C#, bất cứ đối tượng nào cũng có thể publish một tập hợp các sự kiện để cho các lớp khác có thể đăng ký. Khi một lớp publish đưa ra một sự kiện, thì tất cả các lớp đã đăng ký sẽ được nhận sự cảnh báo.

Tác giả Gamma (Addison Wesley, 1995) mô tả cơ chế này như sau: “Định nghĩa một đến nhiều sự phụ thuộc giữa những đối tượng do đó khi một đối tượng thay đổi trạng thái, tất cả các đối tượng khác phụ thuộc vào nó sẽ được cảnh báo và cập nhật một cách tự động”.

Với cơ chế này, đối tượng của chúng ta có thể nói rằng “Ở đây có những thứ mà tôi có thể thông báo cho bạn” và những lớp khác có thể đăng ký đáp rằng “Vâng, hãy báo cho tôi biết khi chuyện đó xảy ra”.

Một nút lệnh có thể cảnh báo cho bất cứ thành phần nào khi nó được nhấn. Nút lệnh này được gọi là publisher bởi vì nó phân phát sự kiện Click và những lớp khác là các lớp subscriber vì chúng đăng ký nhận sự kiện Click này.

Sự kiện và delegate

Những sự kiện trong C# được thực thi với những delegate. Lớp publisher định nghĩa một delegate và những lớp subscriber phải thực thi. Khi một sự kiện xuất hiện thì phương thức của lớp subscriber được gọi thông qua delegate.

Một phương thức được dùng để xử lý các sự kiện thì được là trình xử lý sự kiện (event handler). Chúng ta có thể khai báo trình xử lý sự kiện này như chúng ta đã làm với bất cứ delegate khác.

Theo quy ước, những trình xử lý sự kiện trong .NET Framework trả về giá trị void và lấy hai tham số. Tham số đầu tiên là nguồn dẫn đến sự kiện, đây chính là đối tượng publisher. Và tham số thứ hai là đối tượng dẫn xuất từ lớp EventArgs. Yêu cầu chúng ta phải thực hiện trình xử lý sự kiện theo mẫu như trên.

EventArgs là lớp cơ sở cho tất cả các dữ liệu về sự kiện, lớp EventArgs thừa kế tất cả các phương thức của nó từ Object, và thêm vào một trường public static empty thể hiện một sự kiện không có trạng thái (cho phép sử dụng hiệu quả những sự kiện không trạng thái). Lớp dẫn xuất từ EventArgs chứa những thông tin về sự kiện.

Sự kiện là thuộc tính của lớp phát ra sự kiện. Từ khóa event điều khiển cách thuộc tính sự kiện được truy cập bởi các lớp subscriber. Từ khóa event được thiết kế để duy trì cho cách thể hiện publish/ subscribe.

Giả sử chúng ta muốn tạo một lớp Clock dùng những sự kiện để cảnh báo những lớp subscriber bất cứ khi nào đồng hồ hệ thống thay đổi giá trị trong một giây. Gọi sự kiện này là OnSecondChange. Chúng ta khai báo sự kiện và kiểu delegate xử lý sự kiện của nó như sau:

[attributes] [modifiers] event type

member- name

Ví dụ khai báo như sau:

public event SecondChangeHandler OnSecondChange;

Trong ví dụ này ta không dùng thuộc tính, modifier ở đây là abstract, new, override, static, virtual, hay là một trong bốn access modifier, trong trường hợp này public. Modifier được theo sau bởi từ khóa event.

Trường type trong trường hợp ví dụ này là delegate mà chúng ta muốn liên hệ với sự kiện, ở đây là SecondChangeHandler.

Tên thành viên là tên của sự kiện, trong trường hợp này là OnSecondChange. Thông thường, tên sự kiện bắt đầu với từ On.

Tóm lại, trong sự khai báo này OnSecondChange là sự kiện được thực thi bởi delegate có kiểu là SecondChangeHandler.

Ta có khai báo cho delegate này như sau:

public delegate void SecondChangeHandler( object clock, TimeInfoEventArgs timeInformation);

Như đã nói trước đây, theo quy ước một trình xử lý sự kiện phải trả về giá trị void và phải lấy hai tham số: nguồn phát ra sự kiện (trong trường hợp này là clock) và một đối tượng dẫn xuất từ EventArgs, là TimeInfoEventArgs. Lớp TimeInfoEventArgs được định nghĩa như sau:

public class TimeInfoEventArgs : EventArgs

{

public TimeInfoEventArgs(int hour, int minute, int second)

{

this.hour = hour;

this.minute = minute;

this.second = second;

}

public readonly int hour;

public readonly int minute;

public readonly int second;

}

Đối tượng TimeInfoEventArgs sẽ có thông tin về giờ phút giây hiện thời. Nó định nghĩa một bộ khởi tạo, ba phương thức, một biến nguyên readonly.

Ngoài việc thêm vào một sự kiện và delegate, lớp đối tượng Clock có ba biến thành viên là :hour, minute, và second. Cuối cùng là một phương thức Run():

public void Run()

{

for(;;)

{

// ngừng 10 giây

Thread.Sleep( 10 );

// lấy thời gian hiện hành

System.DateTime dt = System.DateTime.Now;

// nếu giây thay đổi cảnh báo cho

subscriber if ( dt.Second != second)

{

// tạo TimeInfoEventArgs để truyền

// cho subscriber

TimeInfoEventArgs timeInformation =

new TimeInfoEventArgs( dt.Hour, dt.Minute, dt.Second);

// nếu có bất cứ lớp nào đăng ký thì cảnh báo

if ( OnSecondChange != null)

{

OnSecondChange( this, timeInformation);

}

}

// cập nhật trạng thái

this.second = dt.Second;

this.minute = dt.Minute;

this.hour = dt.Hour;

}

}

Phương thức Run tạo vòng lặp vô hạn để kiểm tra định kỳ thời gian hệ thống. Nếu thời gian thay đổi từ đối tượng Clock hiện hành, thì nó sẽ cảnh báo cho tất cả các subscriber và sau đó cập nhật lại những trạng thái của nó.

Bước đầu tiên là ngừng 10 giây:

Thread.Sleep(10);

Ở đây chúng ta sử dụng phương thức tĩnh của lớp Thread từ System.Threading của .NET. Sử dụng phương thức Sleep() để kéo dài khoảng cách giữa hai lần thực hiện vòng lặp.

Sau khi ngừng 10 mili giây, phương thức sẽ kiểm tra thời gian hiện hành:

System.DateTime dt = System.DateTime.Now;

Cứ khoảng 100 lần kiểm tra, thì một giây sẽ được gia tăng. Phương thức ghi nhận sự thay đổi

và cảnh báo đến những subscriber của nó. Để làm được điều này, đầu tiên phải tạo ra một đối tượng TimeInfoEventArgs:

if ( dt.Second != second)

{

// tạo TimeInfoEventArgs để truyền cho các subscriber

TimeInfoEventArgs timeInformation =new TimeInfoEventArgs( dt.Hour, dt.Minute, dt.Second);

}

Và để cảnh báo cho những subscriber bằng cách kích hoạt sự kiện OnSecondChange:

// cảnh báo cho các subscriber

if ( OnSecondChange != null)

{

OnSecondChange( this, timeInformation);

}

Nếu một sự kiện không có bất cứ lớp subscriber nào đăng ký thì nó ước lượng giá trị là null. Phần kiểm tra bên trên xác định giá trị của sự kiện có phải là null hay không, để đảm bảo rằng có tồn tại lớp đăng ký nhận sự kiện trước khi gọi sự kiện OnSecondChange.

OnSecondChange lấy hai tham số: nguồn phát ra sự kiện và đối tượng dẫn xuất từ lớp EventArgs. Ở đây chúng ta có thể thấy rằng tham chiếu this của lớp clock được truyền bởi vì clock là nguồn phát ra sự kiện. Tham số thứ hai là đối tượng TimeInfo- EventArgs được tạo ra ở dòng lệnh bên trên.

Một sự kiện được phát ra thì sẽ gọi bất cứ phương thức nào được đăng ký với lớp Clock thông qua delegate, chúng ta sẽ kiểm tra điều này sau.

Một khi mà sự kiện được phát ra, chúng ta sẽ cập nhật lại trạng thái của lớp Class:

this.second = dt.Second;

this.minute = dt.Minute;

this.hour = dt.Hour;

Sau cùng là chúng ta xây dựng những lớp có thể đăng ký vào các sự kiện này. Chúng ta sẽ tạo hai lớp. Lớp đầu tiên là lớp DisplayClock. Chức năng chính của lớp này không phải là lưu giữ thời gian mà chỉ để hiển thị thời gian hiện hành ra màn hình console. Để đơn giản chúng ta chỉ tạo hai phương thức cho lớp này. Phương thức thứ nhất có tên là Subscribe, phương thức chịu trách nhiệm đăng ký một sự kiện OnSecondChange của lớp Clock. Phương thức thứ hai được tạo ra là trình xứ lý sự kiện TimeHasChanged:

public class DisplayClock

{

public void Subscrible(Clock theClock)

{

theClock.OnSecondChange +=new Clock.SecondChangeHandler(TimeHasChanged);

}

public void TimeHasChanged( object theClock, TimeInfoEventArgs ti)

{

Console.WriteLine("Current Time: {0]:{1}:{2}", ti.hour.ToString(), ti.minute.ToString(),

ti.Second.ToString());

}

}

Khi phương thức đầu tiên Subscribe được gọi, nó sẽ tạo ra một delegate SecondChange- Handler mới, và truyền vào phương thức xử lý sự kiện TimeHasChanged của nó. Sau đó nó sẽ đăng ký delegate với sự kiện OnSecondChange của Clock.

Lớp thứ hai mà chúng ta tạo cũng sẽ đáp ứng sự kiện này, tên là LogCurrentTime. Thông thường lớp này ghi lại sự kiện vào trong tập tin, nhưng với mục đích minh họa của chúng ta,nó sẽ ghi ra màn hình console:

public class LogCurrentTime

{

public void Subscribe(Clock theClock)

{

theClock.OnSecondChange +=new Clock.SecondChangeHandler(WriteLogEntry);

}

// thông thường phương thức này viết ra file

// nhưng trong minh họa này chúng ta chỉ xuất

// ra màn hình console mà thôi

public void WriteLogEntry( object theClock, TimeInfoEventArgs ti)

{

Console.WriteLine("Logging to file: {0}:{1}:{2}", ti.hour.ToString(),

ti.minute.ToString(), ti.second.ToString());

}

}

Những sự kiện được thêm vào bằng cách sử dụng toán tử +=. Điều này cho phép những sự kiện mới được thêm vào sự kiện OnSecondChange của đối tượng Clock mà không có phá hủy bất cứ sự kiện nào đã được đăng ký. Khi LogCurrentTime đăng ký một sự kiện OnSecondChange, chúng ta không muốn việc đăng ký này làm mất đi sự đăng ký của lớp DisplayClock trước đó.

Tất cả phần còn lại cần thực hiện là tạo ra một lớp Clock, tạo mộ đối tượng DisplayClock và bảo nó đăng ký sự kiện. Sau đó chúng ta tạo ra một đối tượng LogCurrentTime và cũng đăng ký sự kiện tương tự. Cuối cùng thì thực thi phương thức Run của Clock. Tất cả phần trên được trình bày trong ví dụ sau.

Làm việc với những sự kiện.

-----------------------------------------------------------------------------

namespace Programming_Csharp

{

using System;

using System.Threading;

// lớp lưu giữ thông tin về sự kiện, trong trường hợp

// này nó chỉ lưu giữ những thông tin có giá trị lớp clock

public class TimeInfoEventArgs : EventArgs

{

public TimeInfoEventArgs(int hour, int minute, int second)

{

this.hour = hour;

this.minute = minute;

this.second = second;

}

public readonly int hour;

public readonly int minute;

public readonly int second;

}

// khai báo lớp Clock lớp này sẽ phát ra các sự kiện

public class Clock

{

// khai báo delegate mà các subscriber phải thực thi

public delegate void SecondChangeHandler(object clock, TimeInfoEventArgs timeInformation);

// sự kiện mà chúng ta đưa ra

public event SecondChangeHandler OnSecondChange;

// thiết lập đồng hồ thực hiện, sẽ phát ra mỗi sự kiện trong mỗi giây

public void Run()

{

for(;;)

{

// ngừng 10 giây

Thread.Sleep( 10 );

// lấy thời gian hiện hành

System.DateTime dt = System.DateTime.Now;

// nếu giây thay đổi cảnh báo cho subscriber

if ( dt.Second != second)

{

// tạo TimeInfoEventArgs để truyền

// cho subscriber

TimeInfoEventArgs timeInformation =new TimeInfoEventArgs( dt.Hour, dt.Minute, dt.Second);

// nếu có bất cứ lớp nào đăng ký thì cảnh báo

if ( OnSecondChange != null)

{

OnSecondChange( this, timeInformation);

}

}

// cập nhật trạng thái

this.second = dt.Second;

this.minute = dt.Minute;

this.hour = dt.Hour;

}

}

private int hour;

private int minute;

private int second;

}

// lớp DisplayClock đăng ký sự kiện của clock.

// thực thi xử lý sự kiện bằng cách hiện thời gian hiện hành

public class DisplayClock

{

public void Subscrible(Clock theClock)

{

theClock.OnSecondChange +=new Clock.SecondChangeHandler(TimeHasChanged);

}

public void TimeHasChanged( object theClock, TimeInfoEventArgs ti)

{

Console.WriteLine("Current Time: {0}:{1}:{2}", ti.hour.ToString(),

ti.minute.ToString(), ti.second.ToString());

}

}

// lớp đăng ký sự kiện thứ hai

public class LogCurrentTime

{

public void Subscribe(Clock theClock)

{

theClock.OnSecondChange +=new Clock.SecondChangeHandler(WriteLogEntry);

}

// thông thường phương thức này viết ra file

// nhưng trong minh họa này chúng ta chỉ xuất

// ra màn hình console mà thôi

public void WriteLogEntry( object theClock, TimeInfoEventArgs ti)

{

Console.WriteLine("Logging to file: {0}:{1}:{2}", ti.hour.ToString(),

ti.minute.ToString(), ti.second.ToString());

}

}

// lớp Test minh họa sử dụng sự kiện public class Test

{

public static void Main()

{

// tạo ra đối tượng clock

Clock theClock = new Clock();

// tạo đối tượng DisplayClock đăng ký

// sự kiện và xử lý sự kiện

DisplayClock dc = new DisplayClock();

dc.Subscribe(theClock);

// tạo đối tượng LogCurrent và yêu cầu đăng

// ký và xử lý sự kiện

LogCurrentTime lct = new LogCurrentTime();

lct.Subscribe(theClock);

// bắt đầu thực hiện vòng lặp và phát sinh sự kiện

// trong mỗi giây đồng hồ theClock.Run();

}

}

}

-----------------------------------------------------------------------------

Kết quả thực hiện có thể như sau:

Current Time: 11:54:20

Logging to file: 11:54:20

Current Time: 11:54:21

Logging to file: 11:54:21

Current Time: 11:54:22

Logging to file: 11:54:22

-----------------------------------------------------------------------------

Điều quan trọng chính của ví dụ minh họa trên là việc tạo ra hai lớp đối tượng DisplayClock và lớp LogCurrentTime. Cả hai lớp này đều đăng ký một sự kiện Clock.OnSecondChange của lớp thứ ba là lớp Clock

Lợi ích của cơ chế publish/subscribe là bất cứ lớp nào cũng có thể được cảnh báo khi một sự kiện xuất hiện. Những lớp subscriber không cần biết cách mà Clock làm việc, và Clock cũng không cần biết cách mà các lớp subscriber đáp ứng với sự kiện mà nó đưa ra.

Publisher và subscriber được phân tách bởi delegate, đây là một sự mong đợi cao, nó làm cho mã lệnh linh họat và mạnh mẽ hơn. Lớp Clock có thể thay đổi cách dò thời gian mà không làm ảnh hưởng đến bất cứ lớp subscriber nào. Các lớp subscriber có thể thay đổi cách mà chúng đáp ứng với sự thay đổi của thời gian mà không tác động với Clock. Cả hai lớp này hoạt động độc lập với nhau, và làm cho đoạn chương trình dễ duy trì hơn.

Câu hỏi và trả lời

Tóm tắt những nét cơ bản về uỷ quyền?

Ủy quyền là một kiểu dữ liệu tham chiếu đươc dùng để đóng gói phương thức với các tham số và kiểu trả về xác định. Ủy quyền cũng tương tự như con trỏ hàm trong ngôn ngữ C++. Tuy nhiên, trong ngôn ngữ C# ủy quyền là kiểu dữ liệu hướng đối tượng, an toàn và bảo mật.

Con trỏ hàm là gì?

Trong ngôn ngữ như C hay C++, có một chức năng gọi là con trỏ hàm. Một con trỏ hàm được sử dụng để thiết lập cùng một nhiệm vụ như một ủy quyền. Tuy nhiên, con trỏ hàm trong C/C++ đơn giản không phải là một đối tượng. Còn ủy quyền trong C# là kiểu dữ liệu an toàn, được dùng để tham chiếu đến những phương thức, ủy quyền còn được sử dụng bởi những sự kiện.

Câu hỏi thêm

Có thể sử dụng ủy quyền như một thuộc tính hay không? Nếu có thể thì sử dụng như thế nào? Cho biết ý nghĩa?

Nếu có một số hoạt động cần được thực hiện theo một thứ tự nhất định thì ta phải làm thế nào để khi cần thực hiện thì gọi lần lượt thực hiện các hoạt động đó?

Công dụng của việc khai báo ủy quyền tĩnh? Khi nào thì nên khai báo ủy quyền tĩnh khi nào thì không nên?

Một ủy quyền có thể gọi được nhiều hơn một phương thức hay không? Chức năng nào trong C# hỗ trợ ủy quyền này?

Có phải tất cả các ủy quyền đều là ủy quyền Multicast hay không? Điều kiện để trở thành ủy quyền Multicast?

Các toán tử nào có thể dùng để thực hiện việc Multicast các ủy quyền?

Sự kiện là gì? Trong hệ thống ứng dụng nào thì sự kiện được sử dụng nhiều?

Những sự kiện trong C# được thực hiện thông qua cái gì?

Hãy tóm lược quá trình tạo một sự kiện và giải quyết sự kiện thông qua cơ chế ủy quyền trong C#?

Bài tập

Viết chương trình minh họa sử dụng ủy quyền để thực hiện việc sắp xếp các số nguyên trong một mảng?

Viết chương trình minh họa sử dụng ủy quyền để thực hiện việc chuyển các ký tự thường thành ký tự hoa trong một chuỗi?

Viết chương trình kết hợp giữa delegate và sự kiện để minh họa một đồng hồ điện tử thể hiện giờ hiện hành trên màn hình console.