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