суббота, 14 августа 2010 г.

INotifyPropertyChanged strikes back

This short post was inspired by this question on StackOverflow. Questioner asks for some language-specific features that can simplify tracking of changes in objects. This task can be perfectly solved by language that supports compile-time metaprogramming, unfortunately F# doesn’t have such features… maybe only in some distant future. Let’s demonstrate the solution using language that already has such capabilities – Nemerle.

Macros in Nemerle are programs that are executed in compile-time, consumes and produces AST.

Macro code

using Nemerle;
using Nemerle.Assertions;
using Nemerle.Collections;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Compiler.Typedtree;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using SCG = System.Collections.Generic;
using System.Linq;
using System.ComponentModel;

namespace ComponentModelHelpers
{
[MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Class, Inherited = false, AllowMultiple = false)]
public macro ImplementsNotifyPropertyChanged(tb : TypeBuilder)
{
NPCHelper.ImplementInterface(tb);
}

[MacroUsage(MacroPhase.WithTypedMembers, MacroTargets.Class, Inherited = false, AllowMultiple = false)]
public macro ImplementsNotifyPropertyChanged(tb : TypeBuilder)
{
NPCHelper.FixProperties(tb)
}

[MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Property, Inherited = false, AllowMultiple = false)]
public macro IgnoreProperty(tb : TypeBuilder, property : ParsedProperty)
{
NPCHelper.RegisterIgnored(tb, property)
}

module NPCHelper
{
private ignoredProperties : SCG.HashSet[TypeBuilder * string] = SCG.HashSet();

public RegisterIgnored(tb : TypeBuilder, property : ClassMember.Property) : void
{
ignore(ignoredProperties.Add(tb, property.Name));
}

public FixProperties(tb : TypeBuilder) : void
{
def properties = tb
.GetProperties()
.Filter(p => !ignoredProperties.Contains(tb, p.Name));

foreach(p is PropertyBuilder in properties)
{
def setter = (p.GetSetter() :> MethodBuilder);
when (setter != null)
{
setter.Body = <[
$(setter.Body);
RaisePropertyChanged($(p.Name : string));
]>;
}
}
}

public ImplementInterface(tb : TypeBuilder) : void
{
def handlerFieldName = Macros.NewSymbol("PropertyChanged");
def fieldDecl = <[ decl:
private mutable $(handlerFieldName.Id : usesite) : PropertyChangedEventHandler;
]>;

def modifyEvent(modifier)
{
<[
mutable tmp;
mutable h = $(handlerFieldName.Id : usesite);
do
{
tmp = h;
def newHandler = $(modifier)(tmp, value) :> PropertyChangedEventHandler;
h = System.Threading.Interlocked.CompareExchange(ref $(handlerFieldName.Id : usesite), newHandler, tmp);
} while(h != tmp);
]>
}
def eventDecl = <[decl:
public event PropertyChanged : PropertyChangedEventHandler
{
add { $(modifyEvent(<[ Delegate.Combine ]>)); }
remove { $(modifyEvent(<[ Delegate.Remove ]>)); }
}
]>;

def raisePropertyChangedMethodDecl = <[ decl:
protected RaisePropertyChanged(propertyName: string) : void
{
def h = $(handlerFieldName.Id : usesite);
when (h != null)
h(this, PropertyChangedEventArgs(propertyName));
}
]>;

tb.Define(fieldDecl);
tb.Define(eventDecl);
tb.Define(raisePropertyChangedMethodDecl);
tb.AddImplementedInterface(<[INotifyPropertyChanged]>);
}
}
}

Notes:

  • ImplementsNotifyPropertyChanged macro adds implementation of INotifiedPropertyChanged and patches properties so RaisePropertyChanged will be called in every setter
  • IgnoreProperty registers ignored property.
  • <[ ]> – denotes a quotation. Basically quotation produces PExpr (expression), so we need to mark explicitly if quotation contains declaration with decl: prefix
  • $(…) – splice in quotation
  • Macros in Nemerle are hygienic, so x variable introduced in macros won’t clash with the variable of the same name. If we need to suppress hygiene – this is done with :usesite directive.

Client code

using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Console;
using System.Linq;
using System.ComponentModel;

using ComponentModelHelpers;

module Program
{
Main() : void
{
def c = MegaComponent();
Test(c);
c.X = 100;
c.Y = "123";
}

Test(npc : INotifyPropertyChanged) : void
{
npc.PropertyChanged += (_, e) => WriteLine($"$(e.PropertyName) changed")
}
}

[ImplementsNotifyPropertyChanged]
class MegaComponent
{
private mutable y : string;

public X : int {get;set;};

[IgnoreProperty]
public Y : string
{
get { y }
set
{
y = value;
RaisePropertyChanged("Y");
}
}
}

/*
X changed
Y changed
*/

Notes:

  • Macro is made accessible in source code by opening containing namespace: ComponentModelHelpers
  • RaisePropertyChanged method is generated by macro so source code can use it

Decompiled class

internal class MegaComponent : INotifyPropertyChanged
{
// Fields
private PropertyChangedEventHandler _N_PropertyChanged_2784;
[CompilerGenerated, DebuggerBrowsable(DebuggerBrowsableState.Never)]
private int _N_X_3169;
private string y;

// Events
public event PropertyChangedEventHandler PropertyChanged
{
add
{
PropertyChangedEventHandler a = null;
PropertyChangedEventHandler handler2 = this._N_PropertyChanged_2784;
do
{
a = handler2;
PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler) Delegate.Combine(a, value);
handler2 = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this._N_PropertyChanged_2784, handler3, a);
}
while (handler2 != a);
}
remove
{
PropertyChangedEventHandler source = null;
PropertyChangedEventHandler handler2 = this._N_PropertyChanged_2784;
do
{
source = handler2;
PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler) Delegate.Remove(source, value);
handler2 = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this._N_PropertyChanged_2784, handler3, source);
}
while (handler2 != source);
}
}

// Methods
protected void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this._N_PropertyChanged_2784;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}

// Properties
public int X
{
[CompilerGenerated]
get
{
return this._N_X_3169;
}
[CompilerGenerated]
set
{
this._N_X_3169 = value;
this.RaisePropertyChanged("X");
}
}

public string Y
{
get
{
return this.y;
}
set
{
this.y = value;
this.RaisePropertyChanged("Y");
}
}
}

3 комментария:

  1. It is utterly counterproductive and downright dangerous to use Nemerle in production just so you can get INPC injected. If really necessary, AOP via PostSharp or even a good old-fashioned dynamic proxy will do the job. No need to drag additional languages into the mix.

    Nice academic exercise, though :)

    ОтветитьУдалить
  2. The idea of post was not advertisement of Nemerle but demonstration how this problem can be solved with language that natively supports compile-time metaprogramming. I hope that this feature will be supported in F# (maybe in a distant future in a galazy far-far away):)

    ОтветитьУдалить
  3. Using Nemerle in production might be dangerous, but certainly is not counterproductive.

    ОтветитьУдалить

 
GeekySpeaky: Submit Your Site!