Summarize
If you use the C# language, its events and delegates themselves are the basic implementation of the observer pattern . In addition, property modification notifications and property dependencies are also one of the uses of the observer pattern. In WinForm or WPF, a collection class control is usually bound to a collection. When the collection data changes, the bound control Can be notified and can automatically refresh the interface.
Using the observer mode in C#, in addition to the common event events, the event stream mode can also be implemented through the two interfaces IObservable<T> and IObserver<T>, which seems a bit complicated, but this method has many advantages, And it's easy to work with the Reactive Extensions framework. Finally, the observable collections bindinglist<t> and observablecollection<t>, which are not thread safe, need to pay attention to the problem of multi-threaded reading and writing when they are used. These two collections are usually followed by supporting collection backed controls are used for binding, in this way, when the collection data changes, the corresponding interface can be automatically reflected
The observer pattern, in simple terms, is that a component can notify other components when the state changes. The observer pattern is very useful. For example, in the UI interface, when the bound data object sends changes, the UI will send the changes accordingly. The observer pattern is a very common and necessary pattern, and the designers of C# simplified the use of the observer pattern through the keyword event. Its basic usage is to first use the event keyword to define an event, and then register the event callback method EventHandler. The callback method usually contains two parameters, a sender of object type and a parameter inherited from EventArgs, which carries some necessary triggering events. information.
event events are actually wrappers for delegates, just like Action and Func are wrappers for lamda expressions. The packaging of event becomes EventHandler, which has generic and non-generic versions, and generics are mainly types inherited from EventArgs.
Here's an example: Suppose a person is sick and needs to see a doctor. The first thing to do is to define the information to be provided when seeing a doctor, in this case only the address of the doctor needs to be given (assuming that in a developed capitalist country, the doctor visits the door).
First define a class that inherits from EventArgs to store basic information.
class FallsIllEventArgs : EventArgs { public string Address; }
Then implement the Person class:
class Person { public event EventHandler<FallsIllEventArgs> FallsIll; public void CatchACold() { FallsIll?.Invoke(this, new FallsIllEventArgs { Address = "123 Zhong shan Road" }); } }
As you can see, the generic delegate of EventHandler is used here, and then the event keyword is used to encapsulate it. When the CatchACold method is called, check if there is a registration for the FallsIll event, and if so, fire the event. In the Main function, use the following:
static void Main(string[] args) { Person person = new Person(); person.FallsIll += CallDoctor; person.CatchACold(); } private static void CallDoctor(object sender, FallsIllEventArgs e) { Console.WriteLine($"A doctor has been called to {e.Address}"); }
This is the simplest observer pattern. When the CatchACode method of Person is called, it will check whether anyone has registered the FallsIll event, and if so, trigger the callback of the event, which is the CallDoctor method here, and the additional information of the delegate will be stored in the second parameter.
An event can be subscribed by multiple objects, just simply use +=, or use -= to unsubscribe. When all subscriptions have been cancelled, the object's FallsIll field will be set to null.
weak event pattern
.NET programs can also generate memory leaks when a reference to an object is held longer than necessary. For example, setting an object to null, but it is still alive, such as the following example:
Define a Button object that contains the Clicked event:
class Button { public event EventHandler Clicked; public void Fire() { Clicked?.Invoke(this, EventArgs.Empty); } }
Now, suppose there is this Button in the Windows object. For simplicity, pass in the Button object in the Windows constructor:
class Windows { public Windows(Button btn) { btn.Clicked += ButtonOnClick; } private void ButtonOnClick(object sender, EventArgs e) { Console.WriteLine("Button clicked (windows handler)"); } ~Windows() { Console.WriteLine("Window finalized"); } }
The above code looks boring. Here we first instantiate a Button and Windows, then set windows to null and perform garbage collection, so now the windows object is still alive?
var btn = new Button(); var windows = new Windows(btn); WeakReference windowsRef = new WeakReference(windows); btn.Fire(); windows = null; FireGC(); Console.WriteLine($"Is windows alive after GC?{windowsRef.IsAlive}"); btn = null; FireGC(); Console.WriteLine($"Is windows alive after GC?{windowsRef.IsAlive}"); FireGC Methods as below: private static void FireGC() { Console.WriteLine("Starting GC"); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Console.WriteLine("GC is done!"); }
The results of the operation are as follows:
Button clicked (windows handler) Starting GC GC is done! Is windows alive after GC? True Button clicked (windows handler) Starting GC Window finalized GC is done! Is windows alive after GC?False
As you can see, after setting windows to null, the object is still alive after GC. Because the button object is referenced in windows and the Clicked event of the button object is registered, the event source will hold a strong reference to the object where the event handler is located, preventing the GC from recycling it, so the life cycle of the event handler object is affected by the event source object. .
Next, if the button is set to null again, and then GC is executed, windows will be released.
There is also an easier way, that is to use the WeakEventManager class provided in System.Windows to associate events and callback methods.
public Windows(Button btn) { WeakEventManager<Button, EventArgs>.AddHandler(btn, "Clicked", ButtonOnClick); }
Other places remain unchanged, run the above results again, you can see that the object is released the first time you can see it.
It should be noted that WeakEventManager is introduced in the WPF framework and is not currently in .NET Core. If you want to use this method, you need to set the project to .NET Framework 4.5 and above. In addition, the above experiments need to be performed in the Release environment. The Debug environment is in order to Debugging, the object will not be recycled.
property observer
Attributes will change, how to notify when attributes change? Let's take the Age property of the Persion object as an example. Suppose we define the following properties.
class Person { public int Age { get; set; } }
When Age changes, we need to be notified. There is a way to check whether there is a change every 100 milliseconds, but this method is too stupid and not smart enough.
What we need to do is actually when setting Age, if the value of Age changes, an event will be triggered, so we need to define a private age field, and then do some judgment and processing in the attribute set.
class Person { private int age; public int Age { get { return age; } set { //todo:something here age = value; } } }
Determine whether the new value is equal to the old age, if not, trigger a reminder. In addition to manually hardcoding your own implementation, there is a ready-made interface INotifyPropertyChanged, which has only one event.
public interface INotifyPropertyChanged { event PropertyChangedEventHandler PropertyChanged; }
So, in Person, we only need to trigger the PropertyChanged event. A method can be added to simplify and wrap this event trigger.
class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
The attribute [callermembername] is used here, which calls the compiler to assign the current member name to the propertyname field when compiling (there is also a call in the code such as methodbase. Getcurrentmethod() to get the current method name) The method of use is as follows:
public int Age { get { return age; } set { if (age == value) return; age = value; OnPropertyChanged(); } }
In the Main function, call as follows:
static void Main(string[] args) { var p = new Person(); p.PropertyChanged += (sender, eventArgs) => { Console.WriteLine($"{eventArgs.PropertyName} changed"); }; p.Age = 15; p.Age++; }
After running, you can see that the event is triggered twice, the first time Age is changed from 0 to 15, and the second time is changed from 15 to 16.
dependency problem
In Excel, if a function in a cell references other cells, when the value of the referenced cell changes, the function of the cell will recalculate to get the latest value.
This situation also exists in properties. Some classes send a change to a certain property, but this change will affect other parts, and will also trigger the corresponding property change event. Unlike Excel, which has a built-in recalculation mechanism, in .NET We need to deal with this problem ourselves. To illustrate the problem, now assume that only age 16 or older has voting rights, and we need to be notified when the user's voting rights change.
public bool CanVote => Age >= 16;
You can see that the CanVote property is read-only, so it is only triggered when Age is set, so the code becomes as follows:
public int Age { get { return age; } set { if (age == value) return; var oldCanVote = CanVote; age = value; OnPropertyChanged(); if (oldCanVote == CanVote) { OnPropertyChanged(nameof(CanVote)); } } }
The code is first recorded in the oldCanVote before the Age is modified. After modifying the Age, compare whether the latest CanVote and the previous oldCanVote are equal. If they are not equal, the OnPropertyChanged event is triggered, and the parameter is the CanVote property. This code can meet the requirements, but the code is too ugly and full of Bad Smell. Now the Age property only affects CanVote, what if it also affects other properties, the above code is very unfriendly to extensions.
One way is to create a new base class to handle all reminders. We define a Dictionary here to hold properties, as well as other properties that are affected.
class PropertyNotificationSupport : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private readonly Dictionary<string, HashSet<string>> affectedBy = new Dictionary<string, HashSet<string>>(); }
Next, implement the OnPropertyChanged method to trigger the PropertyChanged event.
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); foreach (var affect in affectedBy.Keys) { if (affectedBy[affect].Contains(propertyName)) { OnPropertyChanged(affect); } } }
Now, not only the currently changing property is fired, but also all other properties that depend on the current property. Now the question is, how to describe CanVote's dependency on Age and how to fill affectedBy structure? Need extra manual filling? This approach requires too much work. One way here is to use expression trees, which I've written a few articles about before C# Expression Trees: Basic Usage,Build dynamic queries,Replacing reflection with expression trees , you can refer to.
If we define the read-only property of CanVote as an expression tree, then we can traverse the expression tree and automatically discover all dependencies on other properties, which is definitely better than the previous stupid method.
We can create a new method named Property in the PropertyNotificationSupport base class, the parameters are the property name and an expression tree Expression<Func<T>>, this expression tree is used to calculate the value of the current property based on other properties.
protected Func<T> Property<T>(String name, Expression<Func<T>> expr) { Console.WriteLine($"Creating computed property for expression {expr} "); var visitor = new MemberAccessVisitor(GetType()); visitor.Visit(expr); if (visitor.PropertyNames.Any()) { if (!affectedBy.ContainsKey(name)) { affectedBy.Add(name, new HashSet<string>()); } foreach (var propName in visitor.PropertyNames) { if (propName != name) { affectedBy[name].Add(propName); } } } return expr.Compile(); }
The purpose of this method is not only to compile the expression tree into an executable form, it also uses the MemberAccessVisitor class to traverse the expression tree, find all other dependent attributes, and add them to the affectedBy dictionary. This MemberAccessVisitor itself is a visitor pattern and can be implemented as a private inner class as follows:
class MemberAccessVisitor : ExpressionVisitor { private readonly Type declaringType; public IList<string> PropertyNames = new List<string>(); public MemberAccessVisitor(Type declaringType) { this.declaringType = declaringType; } public override Expression Visit(Expression expr) { if (expr != null && expr.NodeType == ExpressionType.MemberAccess) { var memberExpr = (MemberExpression)expr; if (memberExpr.Member.DeclaringType == declaringType) { PropertyNames.Add(memberExpr.Member.Name); } } return base.Visit(expr); } }
This method is to traverse the expression, then find all the members, such as the Age attribute, and then add the attribute name to the List to return.
Finally, the usage is as follows:
class Person : PropertyNotificationSupport { private int age; public int Age { get { return age; } set { if (age == value) return; age = value; OnPropertyChanged(); } } private readonly Func<bool> canVote; public bool CanVote => canVote(); public Person() { canVote = Property(nameof(CanVote), () => Age >= 16); } }
Re-run the Main function, you can see that whenever the Age property changes, CanVote will "change". CanVote is not the real "value" that changes, but needs to be "recalculated". The above approach may seem overly complicated, but this approach solves the property dependency problem with very little performance impact. The only problem is that there are problems with "interdependencies" such as A depends on B, which depends on A, or "circular dependencies".
event stream
When it comes to the Observer pattern, you will notice that two interfaces have been added to the latest .NET Framework: IObserver<T> and IObservable<T>, which happen to be released along with Reactive Extensions(RX). , which means they originally existed to handle response streams. Reactive Extension is not discussed here (for details, please see what I wrote before Getting Started with Reactive Extension s series of articles), but these two interfaces are worth examining.
The IObservable<T> interface is very similar to ordinary .NET events. The difference is that ordinary .NET events use += to subscribe, but the IObservable<T> interface requires us to implement the Subscribe() method. The only parameter of this method is IObserver<T> interface. It should be noted that IObserver<t> is an interface. Unlike events or delegates, it does not implement a storage mechanism and can be handled very flexibly.
Another thing to note is that the IObservable<T> interface provides a method to unsubscribe. The Subscribe() method returns an IDisposable type, which is usually a type that implements the memo pattern and has a Dispose() method that can be used to unsubscribe.
The second is the IObserver<T> interface. It is designed to implement push-based notifications, and it has three methods:
- OnNext(T), this method will be fired when a new event occurs.
- OnCompleted(), fired when no data is generated from the event source.
- OnError(), fired when the event source generates an error.
Once again, it should be noted that this is just an interface, how to implement it is entirely up to us, for example, the OnCompleted() and OnError() methods can be completely ignored.
Using these two interfaces, the above example of seeing a doctor when sick can be rewritten. First of all, we need to encapsulate the event subscription, we need to implement the object of the memo mode and implement the IDisposable interface, which can handle the scenario of unsubscription.
class Subscription : IDisposable { private Person person; public IObserver<Event> Observer; public Subscription(Person pers, IObserver<Event> observer) { person = pers; Observer = observer; } public void Dispose() { person.subscriptions.Remove(this); } }
This Subscription is an inner class of the Person class, and it will complicate the code to support the event stream pattern. Now looking at the Person class, it needs to implement the IObservable<T> interface. What type is T? Unlike traditional events, there is no requirement to inherit from EventArgs. Here we first define a base class Event, which does nothing in it, and then define a subclass FallIllEvent, which has an Address field, indicating that the doctor needs to be notified when sick. the address to arrive at .
class Event { } class FallIllEvent : Event { public string Address; }
Now, you can make Person implement the IObservable<Event> interface of this type, and receive the IObserver<Event> interface parameter in the Subscribe() method of the interface. The specific implementation of Person is as follows:
class Person: IObservable<Event> { private readonly HashSet<Subscription> subscriptions = new HashSet<Subscription>(); public IDisposable Subscribe(IObserver<Event> observer) { var subscription = new Subscription(this, observer); subscriptions.Add(subscription); return subscription; } public void CatchACode() { foreach (var sub in subscriptions) { sub.Observer.OnNext(new FallIllEvent() { Address = "123 Zhongsan Road" }); } } class Subscription : IDisposable { private Person person; public IObserver<Event> Observer; public Subscription(Person pers, IObserver<Event> observer) { person = pers; Observer = observer; } public void Dispose() { person.subscriptions.Remove(this); } } }
As you can see, this version of the code is much more complex than the previous simple event pattern implemented using the event keyword. But this way of event streaming has many advantages. For example, we can use our own strategy to deal with repeated subscriptions . For example, when a subscriber tries to repeatedly subscribe to an event, we can consider whether to trigger multiple times or only trigger once. It should be noted that the HashSet<Subscription> here is not thread-safe, which means that if called at the same time The Subscribe() method and the CatchACode method may involve thread safety issues. It can be implemented using locks, or using simpler methods such as ImmutableList, which are skipped here.
The "trouble" is not over yet. It should be noted that the subscriber also needs to implement the IObserver<Event> interface to handle the OnNext, OnCompleted, and OnError callbacks.
class Program : IObserver<Event> { public Program() { var person = new Person(); var sub = person.Subscribe(this); person.CatchACode(); } static void Main(string[] args) { new Program(); Console.ReadLine(); } public void OnNext(Event value) { if (value is FallIllEvent args) { Console.WriteLine($"A doctor has been called to {args.Address}"); } } public void OnError(Exception error) { //Ignore } public void OnCompleted() { //Ignore } }
One more word here, we can use the Observable.Subscribe() static method to simplify event subscription, but the Observable (without T) class is the implementation in the Reactive Extensions class library. If you want to implement it, you need to install the class library.
The above is that we do not use the event keyword, but use the IObservable<T> and IObserver<T> that come with .NET to manually implement the observer pattern. The biggest advantage is that this event flow pattern generated by IObservable is easy to combine with some operations in the ReactiveExtension (Rx) framework. For example, using System.Reactive, the entire above Program can be simplified to the following, which is very convenient.
var person = new Person(); var sub = person.Subscribe(this); person.OfType<FallsIllEvent>() .Subscribe(args => WriteLine($"A doctor has been called to {args.Address}"));
Observable collection
If you bind a List<T> to a ListBox in WinForm or WPF, the UI will not update when the List sends changes, this is because the List<T> does not support the Observer pattern. Of course if a single object changes, there is no event for the entire List to notify its contents that the change was sent. It is true that we can wrap the List and add event notifications in the Add or Remove methods. It is possible but not necessary. There are Observable collections in WinForm and WPF. , they are BindingList<T> and ObservableCollection<T> respectively.
These two collection types behave just like Collection<T>, but provide additional notifications. For example, for UI components, the UI is automatically updated when changes are sent to the bound collection. ObservableCollection<T> implements INotifyCollectionChanged interface, so he has CollectionChanged event. This time will tell what has changed in the collection, and will provide the old and new elements, as well as the position of the old and new elements in the collection, in other words, according to the event we can correctly repaint the ListBox or other collection components .
It should be noted that BindingList<T> and ObservableCollection<T> are not thread-safe collections, so you need to pay special attention when reading and writing collections in multiple threads. One way is to inherit from the Observable collection and then lock some write operations. Another method is to inherit from ConcurrentBag<T>, and then implement the INotifyCollectionChanged interface. Relatively speaking, the first method is simpler.