INotifyPropertyChanged with custom targets
Wouldn’t it be nice to have a simple attribute instead of backing field madness in Silverlight/WPF? Just like this:
public class MainViewModel : ViewModelBase { [NotifyPropertyChanged] public string Title { get; set; } }
You can use PostSharp for that, you should at least use lambda expressions instead of strings.
Here’s how to do it without 3rd party software:
1.
In your project declare base view model:
public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged = delegate { }; protected void OnPropertyChanged(string propertyName) { this.PropertyChanged( this, new PropertyChangedEventArgs(propertyName)); } };
2.
Declare the attribute
It will be used to mark properties that should raise the PropertyChanged event.
[AttributeUsage(AttributeTargets.Property)] public class NotifyPropertyChangedAttribute : Attribute { }
3.
Create new MSBuildTasks library project (it can be in different solution)
Add references to:
- Mono.Cecil.dll
- Mono.Cecil.Pdb.dll (this needed so Cecil can updated pdb file, which is need for debugging the modified assembly)
- MSBuild assemblies
4.
Create new MSBuild task
It will inject PropertyChanged event invocation on properties marked with NotifyPropertyChanged attribute.
using System; using System.IO; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Mono.Cecil; using Mono.Cecil.Cil; namespace MSBuildTasks { public class NotifyPropertyChangedTask : Task { public override bool Execute() { InjectMsil(); return true; } private void InjectMsil() { var assemblyDefinition = AssemblyDefinition.ReadAssembly( AssemblyPath, new ReaderParameters { ReadSymbols = true }); var module = assemblyDefinition.MainModule; foreach (var type in module.Types) { foreach (var prop in type.Properties) { foreach (var attribute in prop.CustomAttributes) { string fullName = attribute.Constructor.DeclaringType.FullName; if (fullName.Contains("NotifyPropertyChanged")) { InjectMsilInner(module, type, prop); } } } } assemblyDefinition.Write( this.AssemblyPath, new WriterParameters { WriteSymbols = true }); } private static void InjectMsilInner( ModuleDefinition module, TypeDefinition type, PropertyDefinition prop) { var msilWorker = prop.SetMethod.Body.GetILProcessor(); var ldarg0 = msilWorker.Create(OpCodes.Ldarg_0); MethodDefinition raisePropertyChangedMethod = FindRaisePropertyChangedMethod(type); if (raisePropertyChangedMethod == null) throw new Exception( "RaisePropertyChanged method was not found in type " + type.FullName); var raisePropertyChanged = module.Import( raisePropertyChangedMethod); var propertyName = msilWorker.Create( OpCodes.Ldstr, prop.Name); var callRaisePropertyChanged = msilWorker.Create( OpCodes.Callvirt, raisePropertyChanged); msilWorker.InsertBefore( prop.SetMethod.Body.Instructions[ prop.SetMethod.Body.Instructions.Count - 1], ldarg0); msilWorker.InsertAfter(ldarg0, propertyName); msilWorker.InsertAfter( propertyName, callRaisePropertyChanged); } private static MethodDefinition FindRaisePropertyChangedMethod( TypeDefinition type) { foreach (var method in type.Methods) { if (method.Name == "RaisePropertyChanged" && method.Parameters.Count == 1 && method.Parameters[0].ParameterType.FullName == "System.String") { return method; } } if (type.BaseType.FullName == "System.Object") return null; return FindRaisePropertyChangedMethod( type.BaseType.Resolve()); } [Required] public string AssemblyPath { get; set; } } }
5.
Compile the task assembly,
and copy it to “$(SolutionDir)/../lib/MSBuild/MSBuildTasks.dll” folder along with Mono.Cecil.dll and Mono.Cecil.Pdb.dll assemblies.
4.
Finally modify your Silverlight/WPF project (.csproj file):
<project> ... <usingTask TaskName="MSBuildTasks.NotifyPropertyChangedTask" AssemblyFile="$(SolutionDir)..libMSBuildMSBuildTasks.dll" /> <target Name="AfterCompile"> <msbuildTasks.NotifyPropertyChangedTask AssemblyPath="$(ProjectDir)obj$(Configuration)$(TargetFileName)" /> </target> </project>
Voila! Enjoy!
Here’s the source code of the MSBuildTasks project:
MSBuildTasks
May 24th, 2015 at 12:03
I started with something similar when I began this
http://code.google.com/p/notifypropertyweaver/
Here are some pointers
-“$(ProjectDir)obj$(Configuration)$(TargetFileName)” can be replaced with “@(IntermediateAssembly)”
-You probably don’t need to insert Nops. The compiler places them in to make it easier for the debugger to set breakpoints. Since u have no source code they are not necessary
-You have not handled signed assemblies
-module.Types does not return nested types
-should only attempt to weave types that are instance classes for perf reasons
-you don’t skip abstract, static or readonly properties
-since INPC is for databinding you should ignore properties with no get
-you don’t skip properties that already call OnNotifyPropertyChanged
-because you not define a custom IAssemblyResolver you will have problems if you try to weave Silverlight or windows phone 7. The reason is because you are running in a .net 4 context when you call Resolve it may incorrectly resolve to a .net 4 assembly rather than a SL or WP7 assembly
-I suspect you will have a bug with generic types
-you assume RaisePropertyChanged is virtual and force a callvirt
Hope this helps
May 24th, 2015 at 12:14
@Simon
Thanks!
> module.Types does not return nested types
Don’t even remember when I declared one.
> – you don’t skip abstract, static or readonly properties
> – since INPC is for databinding you should ignore properties with no get
Remember that I have control on which property I apply NotifyPropertyChanged attribute
> -you don’t skip properties that already call OnNotifyPropertyChanged
This is on purpose. I have control on which property I apply NotifyPropertyChanged attribute
> -you assume RaisePropertyChanged is virtual and force a callvirt
Yep.
May 24th, 2015 at 12:44
yeah. sorry. I forgot you were only going the attribute approach.