Dnes si ukážeme jak snadně naimplementovat INotifyPropertyChanged.
Všichni co dělali z wpf to určitě znají dlouhý a špatně přehledný kód modelů, který vypadá nějak takto:
class User : INotifyPropertyChanged
{
private string firstName;
private string lastName;
private string phone;
public event PropertyChangedEventHandler PropertyChanged;
public string FirstName
{
get { return firstName; }
set
{
if (value != firstName)
{
firstName = value;
OnPropertyChanged();
OnPropertyChanged("FullName");
}
}
}
public string LastName
{
get { return lastName; }
set
{
if (value != lastName)
{
lastName = value;
OnPropertyChanged();
OnPropertyChanged("FullName");
}
}
}
public string FullName
{
get { return string.Format("{0} {1}", this.FirstName, this.LastName); }
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Rozhraní INotifyPropertyChanged se dá velmi pohodlně naimplementovat pomocí PostSharpu.
Implementace INotifyPropertyChanged
Vytvoříme třídu s požadovanými vlastnostmi:
internal class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName
{
get { return string.Format("{0} {1}", this.FirstName, this.LastName); }
}
}
Obdobně jako v případě přidání logování klikneme na název třídy a rozbalíme nabídku možností (CTRL+.). Zvolíme možnost “Implement INotifyPropertyChanged”
Při první implementaci INotifyPropertyChanged v projektu potřebuje PostSharp stáhnout balíček PostSharp.Patterns.Model. O tom nás informuje jednoduchým dialogem.
Ke třídě se připojí atribut NotifyPropertyChanged:
[NotifyPropertyChanged]
internal class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName
{
get { return string.Format("{0} {1}", this.FirstName, this.LastName); }
}
}
No a tím máme hotovo. Ovšem má to malou záludnost. Wpf binding funguje správně, ale nemůžeme se navázat na event PropertyChanged ze C# kódu, jednoduše proto, že ho tam v době kompilace nemáme. PostSharp ho tam doplní až po kompilaci. Pokud potřebujeme někde v kódu reagovat na událost PropertyChanged, musíme použít přetypování na INotifyPropertyChanged.
User user = new User { FirstName = "John", LastName = "Doe" };
((INotifyPropertyChanged)user).PropertyChanged += user_PropertyChanged;
To se ovšem nezdá Visual Studiu a reaguje hláškou:
PostSharp má pro podobné situace metodu Post.Cast<SourceType, TargetType>. Hlavní výhodou použití téhle metody o proti klasickému přetypování je, že v případě kdy nebude přetypování možné, vyhodí PostSharp chybu při kompilaci, kdežto klasické přetypování jí vyhodí až za běhu programu.
User user = new User { FirstName = "John", LastName = "Doe" };
Post.Cast<User, INotifyPropertyChanged>(user).PropertyChanged += user_PropertyChanged;
Post kompilace
Už jsme si ukázali jak pomocí PostSharpu logovat, jak naimplementovat INotifyPropertyChanged, ale neukázali jsme si jak funguje. Jak už jsem zmínil výše, aspekty se připojují až po kompilaci. Projekt se nejdříve zkompiluje, postsharp post processor pak vezme vygenerovanou assembly, pomocí MSIL injection připojí aspekty a vygeneruje novou výslednou assembly.
Pojďme se podívat, co generuje PostSharp. Použiju jednoduchý aspekt podobný jako byl v prvním dílu.
[Serializable]
public sealed class TraceInfoAttribute : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Trace.WriteLine(string.Format("Entering {0} with parametr {1}", GetMethodFullName(args), ArgumentsToString(args)));
}
public override void OnException(MethodExecutionArgs args)
{
Trace.WriteLine(string.Format("Exception {0} on method {1}", args.Exception, GetMethodFullName(args)));
}
public override void OnSuccess(MethodExecutionArgs args)
{
Trace.WriteLine(string.Format("Success {0}", GetMethodFullName(args)));
}
public override void OnExit(MethodExecutionArgs args)
{
Trace.WriteLine(string.Format("Leaving {0}", GetMethodFullName(args)));
}
private static string ArgumentsToString(MethodExecutionArgs args)
{
ParameterInfo[] methodParams = args.Method.GetParameters();
List paramsList = new List();
for (int x = 0; x < methodParams.Count(); x++)
{
paramsList.Add(string.Format("{0}: {1} = {2}",
methodParams[x].ParameterType.FullName,
methodParams[x].Name,
args.Arguments[x]));
}
return string.Join(", ", paramsList);
}
private static string GetMethodFullName(MethodExecutionArgs args)
{
return string.Format("{0}.{1}.{2}",
args.Method.DeclaringType.Namespace,
args.Method.DeclaringType.Name,
args.Method.Name);
}
}
[TraceInfo]
private static void SayHello(string who, int howManyTimes)
{
for (int i = 0; i < howManyTimes; i++)
{
Console.WriteLine("Hello, {0}.", who);
}
}
Když metodu SayHello, k níž je aspekt připojen, dekompilujem Reflectorem nebo jiným dekompilátorem, uvidíme:
private static void SayHello(string who, int howManyTimes)
{
Arguments<string, int> arguments = new Arguments<string, int> {
Arg0 = who,
Arg1 = howManyTimes
};
MethodExecutionArgs args = new MethodExecutionArgs(null, arguments) {
Method = <>z__a_1._2
};
<>z__a_1.a0.OnEntry(args);
try
{
for (int i = 0; i < howManyTimes; i++)
{
Console.WriteLine("Hello, {0}.", who);
}
<>z__a_1.a0.OnSuccess(args);
}
catch (Exception exception)
{
args.Exception = exception;
<>z__a_1.a0.OnException(args);
throw;
}
finally
{
<>z__a_1.a0.OnExit(args);
}
}
Pojďme si kód trošku rozebrat. Proměnná methodExecutionArgs obsahuje informace o metodě. Konstruktoru se předává null, protože první parametr je instance objektu na kterém je metoda vyvolána. Druhým jsou parametry metody, pokud je metoda bezparametrická a nebo aspekt paremetry nepotřebuje, bude i druhý paramter null. Do vlastnosti Method proměnné methodExecutionArgs se vloží informace o metodě jako je jmený prostor, název atd... Vlastnost je typu MethodExecutionArgs.
<>z__a_1 je třída, ve které si PostSharp udržuje informace o třídách a metodách a instance atributů. Pro každou třídu a každou metodu je zde statická properity s informacemi. Například <>z__a_1._2 jsou informace o metodě SayHello a <>z__a_1.a0 je instance TraceInfoAttribute použitá v metodě SayHello.
[CompilerGenerated, DebuggerNonUserCode]
internal sealed class <>z__a_1
{
// Fields
internal static MethodBase _2;
internal static readonly TraceInfoAttribute a0;
// Methods
[CompilerGenerated]
static <>z__a_1();
}
Optimalizace aspektů
PostSharp je naštěstí dost chytrý na to, aby dělal jen to co je opravdu potřeba. Pokud zjednodušíme aspekt.
[Serializable]
public sealed class TraceInfoAttribute : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Trace.WriteLine("Entry");
}
}
Bude kód metody SayHello takový to:
private static void SayHello(string who, int howManyTimes)
{
<>z__a_1.a0.OnEntry(null);
for (int i = 0; i < howManyTimes; i++)
{
Console.WriteLine("Hello, {0}.", who);
}
}
Všiměte si že zmizela počáteční inicializace Arguments i MethodExecutionArgs a blok try catch, protože nejsou pro aktuální aspekt potřeba. Bohužel PostSharp provádí optimalizaci až od verze Professional. Ve verzi express se musíme spokojit s voláním prázdných metod.