EventWzór.11 Wzorzec projektowy Obserwator polega na tym,że jeden komponent informuje drugi, że coś się wydarzyło. Ten wzorzec jest używany wszędzie. W aplikacjach WPF i Windows Forms, jak i ASP.NET Web Forms i wszelkiej formie UI w platformie .NET mamy system zdarzeniowy.
Ten system do program wysyła obiekty określający jakie zmiany zaszły przy interakcji z użytkownikiem.
Wzorzec Obserwator jest popularny i potrzebnym wzorcem w wielu miejscach w aplikacji nawet bez twojego kodu.
Nic dziwnego, że twórcy C# postanowili zaszyć ten wzorzec do języka w postaci słowa kluczowego event
Oto następująca konwencja dla słowa kluczowego event i zdarzeń:
- Zdarzenia mogą być składowymi klasy i określasz je słowem kluczowym event
- Event Handlers : to metody, które zostaną wywołane, gdy dane zdarzenie zajdzie
- Dodajesz metodę do zdarzenia używasz operatora +=
- Usuwasz metodę z zdarzenia używasz operatora -=
- Event Handlers przyjmą dwa argument w swoich metodach.
- Referencje do obiektu, który wywołał dane zdarzenie
- Obiekt, który dziedziczy po EventArgs. On zawiera dodatkowe informacje o zdarzeniu, które zaszło
Dokładny typem zdarzenia jest zazwyczaj delegata. Tak jak delegaty Action/Func potrafią by wyrażone lambdą w C# tak delegaty są opakowane w słowa kluczowe event.
Zdarzenie więc można traktować jak delegatę, która przyjmuje w parametrach obiekt, jak i coś co dziedziczy po EventArgs,
Zademonstrujmy to na prostym przykładzie:
public class GoinToVacationEventArgs : EventArgs
{
public string DefaultEmailResponse;
public DateTime WhenItBegan;
public DateTime WhenItEnd;
}
Oto argument do zdarzenia pójścia na wakacje
public class Person
{
public void GoingToSopot()
{
GotoVacationEvent?.Invoke(this,
new GoinToVacationEventArgs
{
Address = "Orzechowa 21",
DefaultEmailResponse = "Jestem na urlopie",
WhenItBegan = DateTime.Now,
WhenItEnd = DateTime.Now.AddDays(10),
});
}
public event EventHandler<GoinToVacationEventArgs> GotoVacationEvent;
}
Jak widzisz zdarzenie pójścia na wakacje jest publiczne, tak aby każdy inna klasa mogła się do niego podpiąć. Metoda GoinToSopot() jest używana do odpalania tego zdarzenia.
Zobacz, że korzystam z operatora "?", aby wcześniej sprawdzić, czy w ogóle ktoś jest podpięty pod moje zdarzenie. Jeśli moje zdarzenie nie ma żadnych subskrybentów to otrzymam w takim wypadku NullReferenceException
Pokażmy jak ten scenariusz działa z inną klasą podpinająca się pod te zdarzenie.
Zdarzenia w Visual Studio mają swoją własną ikonę pioruna.
internal class Main
{
internal static async Task Work(string[] args)
{
Person p = new Person();
p.GotoVacationEvent += SendEmailToTheBoss;
p.GoinToSopot();
}
private static void SendEmailToTheBoss(object sender,
GoinToVacationEventArgs eventArgs)
{
Console.WriteLine("Poszedł na urlop:");
Console.WriteLine(eventArgs.DefaultEmailResponse);
Console.WriteLine($"Od Kiedy {eventArgs.WhenItBegan}");
Console.WriteLine($"Do Kiedy {eventArgs.WhenItEnd}");
}
}
Zdarzenie może obsłużyć metoda, lokalna funkcja czy wrażenie lambda. To twój wybór.
p.GotoVacationEvent += (s, e) =>
{
};
Każde zdarzenie też może mieć wiele podpiętych funkcji do siebie. Pamiętaj jednak, że przy usunięciu wszystkich podpiętych metod sama instancja zdarzenia staje się NULL
Zdarzenia i wycieki pamięci
Z zdarzeniami pojawia się też problem specyficznego wycieku pamięci w .Netcie. Możesz ustawić zmienną wskazującą na referencje do pewnego obiektu na NULL i mimo to on nadal będzie żył w pamięci. Jak to?
Ten problem jest nasilony przy frameworkach jak : WPF, Windows Form, Xamarin
Oto przykład okna i przycisku.
public class GridWindow
{
public GridWindow(LinkButton button)
{
button.Clicked += ButtonOnClicked;
}
private void ButtonOnClicked(object sender,
EventArgs eventArgs)
{
Console.WriteLine("Button clicked (Window handler)");
}
}
public class LinkButton
{
public event EventHandler Clicked;
public void Fire()
{
Clicked?.Invoke(this, EventArgs.Empty);
}
}
Okno jest podłączone do zdarzenia przycisku. Gdy okno przestanie istnieć to logicznej jest, że samo podpięcie do zdarzenie też powinno zniknąć. Oczywiście tak się nie dzieje.
Dopóki okno jest subskrybentem zdarzenia przycisku tak długo ono będzie istnieć w pamięci, nawet jeśli wszystkie zmienne odwołujące się do niego już nie istnieją.
Domyślnie to tak działa. Program myśli, że jest to dla Ciebie istotne.
var btn = new LinkButton();
var grid = new GridWindow(btn);
var gridRef = new WeakReference(grid);
grid = null;
GC.Collect();
GC.WaitForFullGCComplete();
Console.WriteLine($"Is window alive after GC? {gridRef.IsAlive}");
// True
Możesz usunąć ten problem na dwa sposoby . Przed usunięciem referencji do obiektu, usuń z niego wszystkie odwołania do zdarzeń innych klas.
Drugi sposób polega na skorzystaniu z implementacji WeakEventManager w zależności od frameworka, w którym piszesz (WPF,Windows Forms, Xamarin, UWP)
https://referencesource.microsoft.com/#windowsbase/Base/System/Windows/WeakEventManager.cs
Zdarzenie opakowane przez ten typ generyczny daje Ci zachowanie, jakie będziesz chciał mieć w swojej aplikacji.
public class StackWindow
{
public StackWindow(LinkButton button)
{
WeakEventManager<LinkButton, EventArgs>
.AddHandler(button, "Clicked", ButtonOnClicked);
}
private void ButtonOnClicked(object sender,
EventArgs eventArgs)
{
Console.WriteLine("Button clicked (Window handler)");
}
}
Domyślnie ten mechanizm nie jest używany dla wszystkich zdarzeń ze względu małe ryzyko wystąpienia. Warto to jednak o tym pamiętać, zwłaszcza jeśli tworzysz ciągle nowe instancję okienek w jakimś frameworku w .NET.
Obserwatory właściwości, kiedy coś zmieni
Na giełdzie dane ciągle się zmieniają. Wczoraj dana akcja giełdowa może mieć inną wartość niż dziś.
public class StockExchange
{
public double CDProjectRed { get; set; }
public double NBP { get; set; }
}
Jak jednak możemy wiedzieć, KIEDY te właściwości się zmienią?
Można by było sprawdzać te właściwości co 1000 milisekund i patrzeć czy istnieje różnica pomiędzy obecną wartością a poprzednią.
To by działało, ale nie byłoby wydajne. Zwłaszcza gdy będziemy mieć takich operacji dużo więcej. To nie będzie się skalować.
public class StockExchange
{
private double _cdprojectRed;
public double CDProjectRed
{
get { return _cdprojectRed; };
set {
//tutaj dodajemy coś
_cdprojectRed = value;
}
}
Mógłbyś podpiąć się settera właściwości i jakoś powiadomić o jego zmianie? Nie musisz tworzyć swoich typów w .NET istnieje już interfejs INotifyPropertyChanged, który spełnia tę funkcjonalność
public class StockExchange : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
//jeżeli masz ReSharpera do dostaniejsz jesze taką dekoracje
//[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged
([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
Do twojej klasy powinno dołączyć zdarzenie obsługujący moment, gdy jedna z jego właściwości ulega zmianie.
Atrybut [CallerMemeberName] automatycznie przypiszę do zdarzenie nazwę właściwości do zdarzenia tak byś ty tego nie musiał robić.
Teraz możesz wywołać zdarzenie przy setterze właściwości.
private double _cdprojectRed;
public double CDProjectRed
{
get { return _cdprojectRed; }
set
{
OnPropertyChanged();
_cdprojectRed = value;
}
}
Tylko zaraz może powinniśmy wyłapywać momenty, gdy do właściwość próbuje się przypisać tę samą wartość. Nie mówiąc o tym, że zdarzenie powinniśmy odpalać po przypisaniu nowej wartości, a nie przed.
private double _cdprojectRed;
public double CDProjectRed
{
get { return _cdprojectRed; }
set
{
if (value == _cdprojectRed)
return;
_cdprojectRed = value;
OnPropertyChanged();
}
}
Pamiętaj, że logika porównywania powinna być bardziej złożona, gdy porównujesz typy referencyjne.
Łańcuch zdarzeń przy zmianie właściwości
Co, jeśli chciałbyś mieć właściwość zależną od inne właściwości . Jak wtedy te obserwatory zmiany by wyglądał w kodzie? Czy będzie to ładne, czy zobaczymy za chwilę bałagan?
public bool ShouldIBuyStocksOfCDProjectRed
{
get
{
return CDProjectRed > 20.00;
}
}
Teraz chcielibyśmy też obserwować właściwość "ShouldIBuyStocksOfCDProjectRed", ale zaraz tam nie mamy settera. Wygląda na to, że musimy ją obsłużyć we właściwości CdProjectRed
public double CDProjectRed
{
get { return _cdprojectRed; }
set
{
if (value == _cdprojectRed)
return;
var oldShould =
ShouldIBuyStocksOfCDProjectRed;
_cdprojectRed = value;
OnPropertyChanged();
if (oldShould !=
ShouldIBuyStocksOfCDProjectRed)
OnPropertyChanged
(nameof(ShouldIBuyStocksOfCDProjectRed));
}
}
Jak widzisz jednak kod jest już zakręcony, a gdybyś chciał mieć jeszcze więcej właściwości zależnych od siebie to ten setter by jeszcze bardziej urósł.
Jednym z rozwiązań tego problemu jest stworzenie słownika, który będzie zawierał jakie właściwości powinny być odświeżone pod wpływem innych
public class StockExchange : INotifyPropertyChanged
{
private readonly Dictionary<string, HashSet<String>> affectedBy
= new Dictionary<string, HashSet<string>>();
public event PropertyChangedEventHandler PropertyChanged;
Teraz musimy zmienić implementację "OnPropertyChanged". Korzystając ze słownika musimy poinformować jakie inne właściwości także się zmienią.
protected virtual void OnPropertyChanged
([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
foreach (string item in affectedBy.Keys)
{
if (affectedBy[item].Contains(propertyName))
{
OnPropertyChanged(item);
}
}
}
Potem dodać do konstruktora słownik właściwości, które wzajemnie na siebie wpływają.
public StockExchange()
{
affectedBy.Add("ShouldIBuyStocksOfCDProjectRed",
new HashSet<string>()
{
"CDProjectRed"
});
}
Czy można to zrobić lepiej?
Można skorzystać z wyrażeń drzewiastych, ale poziom skomplikowania kodu wtedy rośnie, że aż głowa boli. Zaczynamy od klasy bazowej.
public class PropertyNotificationBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged
([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
foreach (string item in affectedBy.Keys)
{
if (affectedBy[item].Contains(propertyName))
{
OnPropertyChanged(item);
}
}
}
Potem tworzymy wyrażenie drzewiaste, które będzie kalkulować jaką właściwość ma wpływa na jaką.
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();
}
Samo wyrażenie drzewiaste nie wystarczy. Trzeba jeszcze stworzyć klasę, która będzie odwiedzać elementy wrażenia według wzoru Visitor. Tutaj stworzę listę zależnych właściwości.
private 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);
}
}
Kompletna klasa bazowa wygląda tak:
public class PropertyNotificationBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged
([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
foreach (string item in affectedBy.Keys)
{
if (affectedBy[item].Contains(propertyName))
{
OnPropertyChanged(item);
}
}
}
private readonly Dictionary<string, HashSet<String>> affectedBy
= new Dictionary<string, HashSet<string>>();
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();
}
private 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);
}
}
}
Teraz gdy jesteśmy tak uzbrojeni to możemy zautomatyzować obserwowanie każdej właściwości zależnej w taki sposób.
public class StockExchange : PropertyNotificationBase
{
private readonly Func<bool> _shouldIBuyStocksOfCDProjectRed;
public StockExchange()
{
_shouldIBuyStocksOfCDProjectRed =
property(nameof(ShouldIBuyStocksOfCDProjectRed),
() => CDProjectRed > 20.00);
}
public bool ShouldIBuyStocksOfCDProjectRed
{
get
{
return
_shouldIBuyStocksOfCDProjectRed();
}
}
private double _cdprojectRed;
public double CDProjectRed
{
get { return _cdprojectRed; }
set
{
if (value == _cdprojectRed)
return;
_cdprojectRed = value;
OnPropertyChanged();
}
}
public double NBP { get; set; }
}
Co tutaj się dzieje:
- Klasa StockExchange dziedziczy po PropertyNotificationBase
- Pojawiło się pole prywatne z delegatą Func
- W konstruktorze wywołujemy nasze wyrażenie drzewiaste Expression<Func<bool>> i uzupełniamy tą delegatę nim
- Właściwość ShouldIBuyStocksOfCDProjectRed zwraca działanie delegaty Func<bool>
Rozwiązanie jest kosmiczne i wiesz mi powstało przy pomocy StackOverflow i książek jak zawsze. Dodatkowo te rozwiązanie głupieje przy cyklicznych zależnościach pomiędzy właściwościami więc może lepiej jest ręcznie napisać, która właściwość jest zależna od której.
Ktoś mógł powiedzieć, że to koniec wzorca obserwatora w .NET, ale nie. Mamy jeszcze przecież kolekcję IObservable<T>. Jak jej nazwa wskazuję ma chyba ona coś wspólnego z tym wzorcem projektowym.