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

Tags: , ,

3 Responses to “INotifyPropertyChanged with custom targets”

  1. Simon Says:

    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

  2. Limilabs support Says:

    @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.

  3. Simon Says:

    yeah. sorry. I forgot you were only going the attribute approach.

Questions?

Consider using our Q&A forum for asking any questions.