Giới thiệu:
Mọi người học lập trình C# chắc đã biết đến lập trình điều kiển sự kiện. C# thêm vào thế giới lập trình điều kiển sự kiện thông qua events và delegate. Bài viết này sẽ trình bày chính xác về cách định nghĩa và những gì xảy ra khi bạn thêm vào trình đk sự kiện cho các UI Control. Ví dụ như các sự kiện được thêm vào Button khi nó được cài đặt v.v… Giúp bạn hiểu hơn về cách điều kiển sự kiện tự nhiên bằng cách sử dụng multicast delegate.
Delegate
Delegate được hiểu tương tự như là con trỏ hàm trong C và C++. Sử dụng Delegate cho phép lập trình viên tóm lượt 1 tham chiếu tới 1 hàm trong đối tượng Delegate.
Gọi hàm trưc tiếp, ko dùng Delegate
Đây là 1 đoạn code đơn giản và dễ hiểu được dùng trong hầu hết các trường hợp. Tuy nhiên , thình thoảng chúng ta ko muốn gọi hàm trực tiếp - ta muốn có thể bỏ qua nó tới 1 chỗ khác sau đó ta mới gọi nó. Cái này đặc biệt hữu ích trong hệ thống điều kiển sự kiện ví dụ như giao diện người dùng đồ họa, khi chúng ta muốn 1 đoạn code được kích hoạt khi user click vào 1 button, hoặc khi ta muốn ghi lại 1 số thông tin mà ko xác định được khi nào nó được ghi.
Một Delegate đơn giản
Một tính chất hữu ích và thú vị của Delegate là nó ko biết hoặc không quan tâm đến lớp đối tượng mà nó chỉ đến.
Khai báo 1 delegate:
Ví dụ:
Delegate sẽ cho phép ta xác định hàm nào mà ta gọi giống như là ko có xát định hàm nào để gọi. (Đoạn này hơi khó hiều, đọc vd các bạn sẽ hiểu hơn). Khai báo 1 delegate giống như khai báo một hàm.
3 bước để định nghĩa và sử dụng delegate:
Kết quả:
# csc SimpleDelegate1.cs
# SimpleDelegate1.exe
Kết quả:
# csc SimpleDelegate2.cs
# SimpleDelegate2.exe
Gọi một hàm thành viên
Trong ví dụ trên hàm logger() đơn giản là viết ra 1 chuối. 1 hàm khác cần phải ghi thông tin vào 1 file, nhưng để làm điều đó, hàm cần biết file để ghi thông tin.
Dịch và chạy chương trình:
# csc SimpleDelegate3.cs
# SimpleDelegate3.exe
# cat process.log
Đặc biệt ở đây là chúng ta ko cần thay đổi hàm Process(), delegate ko đếm xỉa tới việc nó trỏ tới hàm static hay member
Multicasting.
Trỏ tới 1 hàm thành viên đã là rất tốt rùi nhưng ở đây có nhiều thú vị mà bạn có thể làm với Delegate. Trong C#, delegate là multicast, nghĩa là chúng có thể trỏ đến nhiều hàm cũng 1 lúc (System.MulticastDelegate). Multicase delegate lưu trữ danh sách tất cả các hàm mà nó sẽ được gọi khi thực thi delegate.
Kết quả:
# csc SimpleDelegate4.cs
# SimpleDelegate4.exe
# cat process.log
Event
Mô hình sự kiên trong C# là một mô hình lập trình sự kiên phổ biến trong lập trình không đồng bộ. Điều căn bản trong mô hình này là khái niệm về “người xuất bản và người đăng ký" (publisher and subscribers.). Publisher sẽ làm 1 số việc và xuất bản ra 1 sự kiện, sau đó sẽ gửi chúng tới subscriber, subscriber sẽ mô tả và nhận sự kiện rõ rang.
Trong C#, bất kỳ đối tượng nào mô tả sự kiện thì bất kỳ ứng dụng nào khác cũng có thể mô tả. Khi lớp publisher đưa lên 1 sự kiện thì tất cả các ứng dụng mô tả nó đều nhận ra.
Quy ước:
Sau đây là những quy ước quan trọng khi sử dụng sự kiện:
- Event Handlers trong .NET không có giá trị trả về và có 2 tham số.
+ 1 là nguồn phát sinh sự kiện, tức là đối tượng publisher.
+ 2 là đối tượng thừa kế từ EventArgs.
- Những sự kiện là thuộc tính của lớp phát sinh sự kiện.(publisher)
- Sử dụng từ khóa event để điều khiển cách mà các thuộc tính event được truy cập bởi các lớp mô tả (subscriber)
Simple Event
Kết quả:
# csc SimpleEvent.cs
# SimpleEvent.exe
# cat process.log
The Second Change Event Example
Cung cấp cho bạn các tạo 1 đối tượng với 1 số sự kiện để thông báo đến subscriber bất cứ khi nào thời gian thay đổi mỗi giây.
Kết luận:
Lớp Clock từ ví dụ trên đơn giản là in ra thời gian mỗi khi phát sự kiện (giây thay đổi), vì sao phải sử dụng delegate? Điều tiện lợi của publisher/ subscriber là bất kỳ bao nhiêu lớp đều có thể được thông báo khi có 1 sự kiện được phát ra. Lớp subscriber ko cần biết lớp Clock làm việt như thế nào , và lớp Clock ko cần biết đối tượng nào sẽ tiếp nhận khi có sự kiện. Ví dụ đơn giản là 1 button có thể xuất bản ra 1 sự kiện Onclick , và bất kỳ đối tượng nào cũng có thể mô tả tới sự kiện này, nhận thông báo khi button được click.
Publisher và subscriber được tách riêng ra bởi delegate. Đặc tính này đã làm cho code được linh hoạt và rõ rang hơn. Lớp Clock có thể thay đổi cách mà nó kiểm tra thời gian mà ko ảnh hưởng gì đến lớp subscriber. Và ngược lại, lớp subscriber có thể thay đổi cách mà nó phản hồi lại sự kiên thời giant hay đổi mà ko ảnh hưởng đến Clock. 2 lớp này độc lập với nhau và làm cho code dễ bảo trì.
Mọi người học lập trình C# chắc đã biết đến lập trình điều kiển sự kiện. C# thêm vào thế giới lập trình điều kiển sự kiện thông qua events và delegate. Bài viết này sẽ trình bày chính xác về cách định nghĩa và những gì xảy ra khi bạn thêm vào trình đk sự kiện cho các UI Control. Ví dụ như các sự kiện được thêm vào Button khi nó được cài đặt v.v… Giúp bạn hiểu hơn về cách điều kiển sự kiện tự nhiên bằng cách sử dụng multicast delegate.
Delegate
Delegate được hiểu tương tự như là con trỏ hàm trong C và C++. Sử dụng Delegate cho phép lập trình viên tóm lượt 1 tham chiếu tới 1 hàm trong đối tượng Delegate.
Gọi hàm trưc tiếp, ko dùng Delegate
using System; namespace NoDelegate { public class MyClass { public void Process() { Console.WriteLine("Process() begin"); Console.WriteLine("Process() end"); } } public class Test { static void Main(string[] args) { MyClass myClass = new MyClass(); myClass.Process(); } } }
Đây là 1 đoạn code đơn giản và dễ hiểu được dùng trong hầu hết các trường hợp. Tuy nhiên , thình thoảng chúng ta ko muốn gọi hàm trực tiếp - ta muốn có thể bỏ qua nó tới 1 chỗ khác sau đó ta mới gọi nó. Cái này đặc biệt hữu ích trong hệ thống điều kiển sự kiện ví dụ như giao diện người dùng đồ họa, khi chúng ta muốn 1 đoạn code được kích hoạt khi user click vào 1 button, hoặc khi ta muốn ghi lại 1 số thông tin mà ko xác định được khi nào nó được ghi.
Một Delegate đơn giản
Một tính chất hữu ích và thú vị của Delegate là nó ko biết hoặc không quan tâm đến lớp đối tượng mà nó chỉ đến.
Khai báo 1 delegate:
delegate result-type identifier ([parameters]); * result-type: Kiểu trả về. * identifier: Tên. * parameters: Tham biến/ có thể có hoặc không.
Ví dụ:
// Khai báo 1 delegate không có giá trị trả về public delegate void SimpleDelegate ();
// Khai báo một delegate có trả về giá trị kiểu int public delegate int ButtonClickHandler (object obj1, object obj2);
Delegate sẽ cho phép ta xác định hàm nào mà ta gọi giống như là ko có xát định hàm nào để gọi. (Đoạn này hơi khó hiều, đọc vd các bạn sẽ hiểu hơn). Khai báo 1 delegate giống như khai báo một hàm.
3 bước để định nghĩa và sử dụng delegate:
* Declaration – khai báo * Instantiation – Thuyết minh, hiện thực * Invocation – Gọi, cầu khẩn
using System; namespace BasicDelegate { // Khai báo public delegate void SimpleDelegate(); class TestDelegate { public static void MyFunc() { Console.WriteLine("I was called by delegate …"); } public static void Main() { // Khởi tạo delegate SimpleDelegate simpleDelegate = new SimpleDelegate(MyFunc); // gọi delegate simpleDelegate(); } } }
Kết quả:
# csc SimpleDelegate1.cs
# SimpleDelegate1.exe
I was called by delegate …Gọi Static Functions
using System; namespace SimpleDelegate { public class MyClass { // Khai báo delegate có 1 tham biến kiểu String // và ko có giá trị trả về. public delegate void LogHandler(string message); // Sử dụng delegate giống như khi ta gọi hàm 1 cách trực tiếp // nhưng chúng ta cần kiển tra liệu rằng delegate có null hay ko // (tức là ko trỏ tới hàm nào) trước khi gọi hàm (hay trước khi gọi delegate). public void Process(LogHandler logHandler) { // Thực hiện thao tác kiểm tra null trước khi gọi delegate if (logHandler != null) { logHandler("Process() begin"); // gọi delegate } if (logHandler != null) { logHandler ("Process() end"); } } } // Test Application sử dung delegate đã được định nghĩa public class TestApplication { // Static Function: sẽ được sử dụng trong delegate. Để gọi Process() // function, ta cần định nghĩa logging function: Logger() để được // trỏ đến bởi delegate static void Logger(string s) { Console.WriteLine(s); } static void Main(string[] args) { MyClass myClass = new MyClass(); // Tạo 1 đối tượng delegate chỉ đến hàm logger // delegate này sau đó sẽ truyền cho Process() function. MyClass.LogHandler myLogger = new MyClass.LogHandler(Logger); myClass.Process(myLogger); } } }
Kết quả:
# csc SimpleDelegate2.cs
# SimpleDelegate2.exe
Process() begin Process() end
Gọi một hàm thành viên
Trong ví dụ trên hàm logger() đơn giản là viết ra 1 chuối. 1 hàm khác cần phải ghi thông tin vào 1 file, nhưng để làm điều đó, hàm cần biết file để ghi thông tin.
using System; using System.IO; namespace SimpleDelegate { // Xác định một delegate public class MyClass { // Khai báo delegate có tham số là 1 chuỗi và ko có giá trị trả về public delegate void LogHandler(string message); public void Process(LogHandler logHandler) { if (logHandler != null) { logHandler("Process() begin"); } if (logHandler != null) { logHandler ("Process() end"); } } } // FileLogger class đơn giản là sử dụng file I/O public class FileLogger { FileStream fileStream; StreamWriter streamWriter; // Constructor public FileLogger(string filename) { fileStream = new FileStream(filename, FileMode.Create); streamWriter = new StreamWriter(fileStream); } // Hàm thành viên được sử dụng khi gọi delegate public void Logger(string s) { streamWriter.WriteLine(s); } public void Close() { streamWriter.Close(); fileStream.Close(); } } // Main() is modified so that the delegate points to the Logger() // function on the fl instance of a FileLogger. When this delegate // is invoked from Process(), the member function is called and // the string is logged to the appropriate file. public class TestApplication { static void Main(string[] args) { FileLogger fl = new FileLogger("process.log"); MyClass myClass = new MyClass(); // Crate an instance of the delegate, pointing to the Logger() // function on the fl instance of a FileLogger. MyClass.LogHandler myLogger = new MyClass.LogHandler(fl.Logger); myClass.Process(myLogger); fl.Close(); } } }
Dịch và chạy chương trình:
# csc SimpleDelegate3.cs
# SimpleDelegate3.exe
# cat process.log
Process() begin Process() end
Đặc biệt ở đây là chúng ta ko cần thay đổi hàm Process(), delegate ko đếm xỉa tới việc nó trỏ tới hàm static hay member
Multicasting.
Trỏ tới 1 hàm thành viên đã là rất tốt rùi nhưng ở đây có nhiều thú vị mà bạn có thể làm với Delegate. Trong C#, delegate là multicast, nghĩa là chúng có thể trỏ đến nhiều hàm cũng 1 lúc (System.MulticastDelegate). Multicase delegate lưu trữ danh sách tất cả các hàm mà nó sẽ được gọi khi thực thi delegate.
using System; using System.IO; namespace SimpleDelegate { // Delegate Specification public class MyClass { // Khai báo delegate có một tham số và trả về kiểu void public delegate void LogHandler(string message); // The use of the delegate is just like calling a function directly, // though we need to add a check to see if the delegate is null // (that is, not pointing to a function) before calling the function. public void Process(LogHandler logHandler) { if (logHandler != null) { logHandler("Process() begin"); } if (logHandler != null) { logHandler ("Process() end"); } } } // The FileLogger class merely encapsulates the file I/O public class FileLogger { FileStream fileStream; StreamWriter streamWriter; // Constructor public FileLogger(string filename) { fileStream = new FileStream(filename, FileMode.Create); streamWriter = new StreamWriter(fileStream); } // Member Function which is used in the Delegate public void Logger(string s) { streamWriter.WriteLine(s); } public void Close() { streamWriter.Close(); fileStream.Close(); } } // Test Application which calls both Delegates public class TestApplication { // Static Function which is used in the Delegate static void Logger(string s) { Console.WriteLine(s); } static void Main(string[] args) { FileLogger fl = new FileLogger("process.log"); MyClass myClass = new MyClass(); // Crate an instance of the delegates, pointing to the static // Logger() function defined in the TestApplication class and // then to member function on the fl instance of a FileLogger. MyClass.LogHandler myLogger = null; myLogger += new MyClass.LogHandler(Logger); myLogger += new MyClass.LogHandler(fl.Logger); myClass.Process(myLogger); fl.Close(); } } }
Kết quả:
# csc SimpleDelegate4.cs
# SimpleDelegate4.exe
Process() begin Process() end
# cat process.log
Process() begin Process() end
Event
Mô hình sự kiên trong C# là một mô hình lập trình sự kiên phổ biến trong lập trình không đồng bộ. Điều căn bản trong mô hình này là khái niệm về “người xuất bản và người đăng ký" (publisher and subscribers.). Publisher sẽ làm 1 số việc và xuất bản ra 1 sự kiện, sau đó sẽ gửi chúng tới subscriber, subscriber sẽ mô tả và nhận sự kiện rõ rang.
Trong C#, bất kỳ đối tượng nào mô tả sự kiện thì bất kỳ ứng dụng nào khác cũng có thể mô tả. Khi lớp publisher đưa lên 1 sự kiện thì tất cả các ứng dụng mô tả nó đều nhận ra.
Quy ước:
Sau đây là những quy ước quan trọng khi sử dụng sự kiện:
- Event Handlers trong .NET không có giá trị trả về và có 2 tham số.
+ 1 là nguồn phát sinh sự kiện, tức là đối tượng publisher.
+ 2 là đối tượng thừa kế từ EventArgs.
- Những sự kiện là thuộc tính của lớp phát sinh sự kiện.(publisher)
- Sử dụng từ khóa event để điều khiển cách mà các thuộc tính event được truy cập bởi các lớp mô tả (subscriber)
Simple Event
using System; using System.IO; namespace SimpleEvent { public class MyClass { // Define a delegate named LogHandler, which will encapsulate // any method that takes a string as the parameter and returns no value public delegate void LogHandler(string message); // Define an Event based on the above Delegate public event LogHandler Log; // Instead of having the Process() function take a delegate // as a parameter, we’ve declared a Log event. Call the Event, // using the OnXXXX Method, where XXXX is the name of the Event. public void Process() { OnLog("Process() begin"); OnLog("Process() end"); } // By Default, create an OnXXXX Method, to call the Event protected void OnLog(string message) { if (Log != null) { Log(message); } } } // The FileLogger class merely encapsulates the file I/O public class FileLogger { FileStream fileStream; StreamWriter streamWriter; // Constructor public FileLogger(string filename) { fileStream = new FileStream(filename, FileMode.Create); streamWriter = new StreamWriter(fileStream); } // Member Function which is used in the Delegate public void Logger(string s) { streamWriter.WriteLine(s); } public void Close() { streamWriter.Close(); fileStream.Close(); } } /* ========= Subscriber of the Event ============== */ // It’s now easier and cleaner to merely add instances // of the delegate to the event, instead of having to // manage things ourselves public class TestApplication { static void Logger(string s) { Console.WriteLine(s); } static void Main(string[] args) { FileLogger fl = new FileLogger("process.log"); MyClass myClass = new MyClass(); // Subscribe the Functions Logger and fl.Logger myClass.Log += new MyClass.LogHandler(Logger); myClass.Log += new MyClass.LogHandler(fl.Logger); // The Event will now be triggered in the Process() Method myClass.Process(); fl.Close(); } } }
Kết quả:
# csc SimpleEvent.cs
# SimpleEvent.exe
Process() begin Process() end
# cat process.log
Process() begin Process() end
The Second Change Event Example
Cung cấp cho bạn các tạo 1 đối tượng với 1 số sự kiện để thông báo đến subscriber bất cứ khi nào thời gian thay đổi mỗi giây.
using System; using System.Threading; namespace SecondChangeEvent { /* ======================= Event Publisher =============================== */ // Our subject — it is this class that other classes // will observe. This class publishes one event: SecondChange. // The observers subscribe to that event. public class Clock { // Private Fields holding the hour, minute and second private int _hour; private int _minute; private int _second; // The delegate named SecondChangeHandler, which will encapsulate // any method that takes a clock object and a TimeInfoEventArgs // object as the parameter and returns no value. It’s the // delegate the subscribers must implement. public delegate void SecondChangeHandler ( object clock, TimeInfoEventArgs timeInformation ); // The event we publish public event SecondChangeHandler SecondChange; // The method which fires the Event protected void OnSecondChange( object clock, TimeInfoEventArgs timeInformation ) { // Check if there are any Subscribers if (SecondChange != null) { // Call the Event SecondChange(clock,timeInformation); } } // Set the clock running, it will raise an // event for each new second public void Run() { for(;;) { // Sleep 1 Second Thread.Sleep(1000); // Get the current time System.DateTime dt = System.DateTime.Now; // If the second has changed // notify the subscribers if (dt.Second != _second) { // Create the TimeInfoEventArgs object // to pass to the subscribers TimeInfoEventArgs timeInformation = new TimeInfoEventArgs( dt.Hour,dt.Minute,dt.Second); // If anyone has subscribed, notify them OnSecondChange (this,timeInformation); } // update the state _second = dt.Second; _minute = dt.Minute; _hour = dt.Hour; } } } // The class to hold the information about the event // in this case it will hold only information // available in the clock class, but could hold // additional state information 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; } /* ======================= Event Subscribers =============================== */ // An observer. DisplayClock subscribes to the // clock’s events. The job of DisplayClock is // to display the current time public class DisplayClock { // Given a clock, subscribe to // its SecondChangeHandler event public void Subscribe(Clock theClock) { theClock.SecondChange += new Clock.SecondChangeHandler(TimeHasChanged); } // The method that implements the // delegated functionality public void TimeHasChanged( object theClock, TimeInfoEventArgs ti) { Console.WriteLine("Current Time: {0}:{1}:{2}", ti.hour.ToString(), ti.minute.ToString(), ti.second.ToString()); } } // A second subscriber whose job is to write to a file public class LogClock { public void Subscribe(Clock theClock) { theClock.SecondChange += new Clock.SecondChangeHandler(WriteLogEntry); } // This method should write to a file // we write to the console to see the effect // this object keeps no state public void WriteLogEntry( object theClock, TimeInfoEventArgs ti) { Console.WriteLine("Logging to file: {0}:{1}:{2}", ti.hour.ToString(), ti.minute.ToString(), ti.second.ToString()); } } /* ======================= Test Application =============================== */ // Test Application which implements the // Clock Notifier - Subscriber Sample public class Test { public static void Main() { // Create a new clock Clock theClock = new Clock(); // Create the display and tell it to // subscribe to the clock just created DisplayClock dc = new DisplayClock(); dc.Subscribe(theClock); // Create a Log object and tell it // to subscribe to the clock LogClock lc = new LogClock(); lc.Subscribe(theClock); // Get the clock started theClock.Run(); } } }
Kết luận:
Lớp Clock từ ví dụ trên đơn giản là in ra thời gian mỗi khi phát sự kiện (giây thay đổi), vì sao phải sử dụng delegate? Điều tiện lợi của publisher/ subscriber là bất kỳ bao nhiêu lớp đều có thể được thông báo khi có 1 sự kiện được phát ra. Lớp subscriber ko cần biết lớp Clock làm việt như thế nào , và lớp Clock ko cần biết đối tượng nào sẽ tiếp nhận khi có sự kiện. Ví dụ đơn giản là 1 button có thể xuất bản ra 1 sự kiện Onclick , và bất kỳ đối tượng nào cũng có thể mô tả tới sự kiện này, nhận thông báo khi button được click.
Publisher và subscriber được tách riêng ra bởi delegate. Đặc tính này đã làm cho code được linh hoạt và rõ rang hơn. Lớp Clock có thể thay đổi cách mà nó kiểm tra thời gian mà ko ảnh hưởng gì đến lớp subscriber. Và ngược lại, lớp subscriber có thể thay đổi cách mà nó phản hồi lại sự kiên thời giant hay đổi mà ko ảnh hưởng đến Clock. 2 lớp này độc lập với nhau và làm cho code dễ bảo trì.
9 comments:
Pai viet kha hay, nhung phan event minh can thi lai ko duoc dich
Bạn tham khảo bài viết có nói về Event trong C# xem có thêm được thông tin không nhé!
http://vubka.blogspot.com/2010/08/su-khac-nhau-giua-delegate-va-event.html
Theo mình biết khi thuyết minh một delegate ta phải gán một phương thức cho nó, vậy tại sao mà khi định nghĩa sự kiện ta chỉ cần khai báo và sử dụng dù chưa gán phương thức cho nó.
ex: public delegate void PropertyChanged(object sender,EventAgrs Agrs).
event PropertyChanged propertyChanged;
Sử dụng.
PropertyChanged();
Theo mình biết khi thuyết minh một delegate ta phải gán một phương thức cho nó, vậy tại sao mà khi định nghĩa sự kiện ta chỉ cần khai báo và sử dụng dù chưa gán phương thức cho nó.
ex: public delegate void PropertyChanged(object sender,EventAgrs Agrs).
event PropertyChanged propertyChanged;
Ta vẫn có thể phát sinh sự kiện ngay cả khi nó chưa được gán cho 1 hành động nào trước đó (phương thức).
Khi chưa được gán thì mặc nhiên sự kiện đang có g.trị NULL và sẽ phát sinh lỗi nếu ta có lời gọi đến nó!
Như vậy, cách thông thường mà ta hay làm để phát sinh sự kiện là viết riêng một hàm kiểm tra cho sự kiện đó, ví dụ như sau và bạn hãy chú ý hàm OnEventCallback luôn kiểm tra EventCallback phải khác NULL nếu đúng mới phát sinh EventCallback:
public class VubkaEvent
{
public delegate void ExampleEventHandler();
public event ExampleEventHandler EventCallback;
protected virtual void OnEventCallback()
{
if (EventCallback != null)
EventCallback();
}
public void Fire()
{
OnEventCallback();
}
}
P/s: Bạn cũng tham khảo thêm link mà mình đã đưa ở trên nhé!
Bài viết rất hay
Cảm ơn bạn nhiều
rất dễ hiểu .tks
Bài viết rất hay và tường minh.
Đăng nhận xét