Tore Vestues blogs

On a quest for the silver bullet...

Boo AstAttributes explained

Writing extensions for Boo is a very powerful thing. In this post I'm going to explain how to write AstAttributes in Boo. These attributes are much more than normal .net attributes. They are one of the ways you can extend the Boo language.

Before you read on, these posts might be useful to read:

To implement an AstAttribute you have to create a class that inherits the AbstractAstAttribute-class and implements the Apply-method. The class name must be postfixed with "Attribute", the syntax is then "'YourAttributeName'Attribute". Like this:

import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast

class DemoAttribute(AbstractAstAttribute):
    def Apply(type as Node):
        pass

You have now created an AstAttribute named Demo, and you can use it (from another assembly) like this:

[Demo]
class MyTestClass:
    def constructor():
        pass

For the moment our attribute does absolutely nothing. To make it do something we have to do some work in the Apply-method we implemented. The method takes a Node as an argument. A Node can be just about any type, since alot of classes inherit the Node-class. Here the Node is the actual "thing" that you tagged the Attribute with. In our example the Node will be a definition of the MyTestClass. Say you tag a Method you will get the method definition (including all its content) passed into the Apply method. We will examine the possibilities this gives us, but let us first take a look at what sub types the Node-class has, that are relevant for us.

Image Text

The picture shows the relevant subtypes. In other words these subtypes show where you can use your attribute, meaning you can tag classes, method parameters, methods, constructors, fields and properties with an AstAttribute that you create.

To experiment with this, I've made a little solution for you that you, it will print just about any information that is available at compile-time. (I've added it at the bottom)

NotifyPropertyChanged

I want to demonstrate an AstAttribute that I've been using in quite a few of the presentations I've held on Boo, and that I have also mentioned in this blog earlier (What makes boo great): the NotifyPropertyChanged-attribute. I wrote this attribute about half a year ago, and it really blew my mind on all the possibilities Boo offers.

First let's look at how we implement the INotifyPropertyChanged in C#, just to remind us of what parts we want to auto generate.

public class Customer: INotifyPropertyChanged
{
    private string firstName;
    private string lastName;

    public string FirstName
    {
        get
        {
            return firstName;
        }
        set
        {
            firstName = value;
            NotifyPropertyChanged("FirstName");
        }
    }
    public string LastName
    {
        get
        {
            return lastName;
        }
        set
        {
            lastName = value;
            NotifyPropertyChanged("LastName");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

That code is as ugly as always. We basically want to write it like this:

[NotifyPropertyChanged]
class BooCustomer:
    [Property(LastName)]
    lastName as String
    [Property(FirstName)]
    firstName as String

Ok, the first thing we have to do is make sure the attribute is used on a class and not somewhere else. Let's do it like this:

def Apply(target as Node):
    classDef = GetClassDefinition(target)

def GetClassDefinition(target as Node) as ClassDefinition:
    classDef = target as ClassDefinition
    if classDef is null:
        raise Exception("The NotifyPropertyChangedAttribute can only be used on classes")       
    return classDef

We're casting the Node to a ClassDefinition, which is the type it must be if the attribute is used on a class. Then we must add the INotifyPropertyChanged to the class definition. Notice that our BooCustomer hasn't implemented it, since it's expecting us (the attribute) to do it.

Ok, lets add the code for that:

def Apply(target as Node):
    classDef = GetClassDefinition(target)
    AddInterface(classDef)

def AddInterface(classDef as ClassDefinition):
    classDef.BaseTypes.Add(SimpleTypeReference("System.ComponentModel.INotifyPropertyChanged"))

That was simple! Next we need to add the actual Event:

def Apply(target as Node):
    classDef = GetClassDefinition(target)
    AddInterface(classDef)
    AddEvent(classDef)

def AddEvent(classDef as ClassDefinition):
    notifyEvent = Event()
    notifyEvent.Type = SimpleTypeReference("System.ComponentModel.PropertyChangedEventHandler")
    notifyEvent.Name = "PropertyChanged"
    classDef.Members.Add(notifyEvent)

Here we create an event, set the right type, set the name, and simply add it to our classdefinition.

The last thing to do is to find all the properties, and add some code to the setter of those properties. This is how we find all the properties:

def AddToAllProperties(classDef as ClassDefinition):
    for member in classDef.Members:
        property = member as Property
        continue if property is null
        AddNewBody(property)

And to conclude it, we must implement the AddNewBody-method:

def AddNewBody(property as Property):
    oldBody as Block = property.Setter.Body
    property.Setter.Body = [|
        $oldBody
        if (PropertyChanged is not null):
            PropertyChanged(self,System.ComponentModel.PropertyChangedEventArgs($(property.Name)))
    |]

Ok, here's something new. The [|...|]-tags. You probably can understand what is happening here, but not really why. The syntax is easy to both read and write. It's called quasi quotation. I will describe it further in another post.

That concludes our attribute! Here's the complete code:

import System
import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast

class NotifyPropertyChangedAttribute(AbstractAstAttribute):

    def Apply(target as Node):
        classDef = GetClassDefinition(target)
        AddInterface(classDef)
        AddEvent(classDef)
        AddToAllProperties(classDef)

    def GetClassDefinition(target as Node) as ClassDefinition:
        classDef = target as ClassDefinition
        if classDef is null:
            raise Exception("The NotifyPropertyChangedAttribute can only be used on classes")
        return classDef

    def AddInterface(classDef as ClassDefinition):
        classDef.BaseTypes.Add(SimpleTypeReference("System.ComponentModel.INotifyPropertyChanged"))

    def AddEvent(classDef as ClassDefinition):
        notifyEvent = Event()
        notifyEvent.Type = SimpleTypeReference("System.ComponentModel.PropertyChangedEventHandler")
        notifyEvent.Name = "PropertyChanged"
        classDef.Members.Add(notifyEvent)

    def AddToAllProperties(classDef as ClassDefinition):
        for member in classDef.Members:
            property = member as Property
            continue if property is null
            AddNewBody(property)

    def AddNewBody(property as Property):
        oldBody as Block = property.Setter.Body
        property.Setter.Body = [|
            $oldBody
            if (PropertyChanged is not null):
                PropertyChanged(self,System.ComponentModel.PropertyChangedEventArgs($(property.Name)))
        |]

Using that code, you can now write classes like this that actually implement the complete INotifyPropertyChanged-behaviour:

[NotifyPropertyChanged]
class BooCustomer:
    [Property(LastName)]
    lastName as String
    [Property(FirstName)]
    firstName as String

Try it yourself and look at the code through reflector to see what is really going on here. Remember to put the attribute and the classes using it in different assemblies.

Any comments on how to improve or clarify any of this will be appreciated.

The solution that prints alot of info on your attribute, and shows where you can place it: Boo AttributeExplorer-solution

- Tore Vestues

(This post have been migrated from my old blog, so sadly the old comments are gone)

blog comments powered by Disqus