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");
}
}
}
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 :)
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):)
ОтветитьУдалитьUsing Nemerle in production might be dangerous, but certainly is not counterproductive.
ОтветитьУдалить