Context-aware Tasks

This article shows an useful way how to combine the Spicelib Task Framework with Parsley. This way you can access everything from the context during the execution of a Task:

“The Task Framework is a general abstraction for asynchronous operations. It allows nesting / grouping of Tasks in TaskGroup instances for concurrent or sequential execution” [source: Parsley Documentation]

One way to access the Context from within a Task would be to define it during the Parsley Configuration (Context). But this would mean, that the Task will not be removed until the context gets destroyed. Because a Task is only active during a limited time, this doesn’t make much sense.

Another approach would be, not to define the Task in the Context but passing in all the necessary dependencies within the constructor. Depending on the amount of required dependencies, the constructor can become quite big. Also Parsley features like Messaging are not directly supported anymore.

Solution

That’s why this solution utilizes the Dynamic Object feature of Parsley. It allows to add any instance to the Context during runtime and also removing it again, if not required anymore. For a Task this means that it needs to be added before the start() is called, and removed after the complete() has been triggered.

This adding/removing happens through the Context interface of Parsley. It should happen automatically in the Task to reduce the amount of code to write. In order to not have to pass the Context to each Task it could just be set once in the parent TaskGroup. The child Task then accesses it via the parent getter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SequentialContextTaskGroup extends
    SequentialTaskGroup implements IContextProvider
{
    private var _context:Context;

    public function SequentialContextTaskGroup(context:Context,
        name:String = null)
    {
        _context = context;
       
        super(name);
    }

    public function get context():Context
    {
        return _context;
    }
}

And this is how the IContextProvider interface looks like:

1
2
3
4
public interface IContextProvider
{
    function get context():Context;
}

The following AbstractContextTask is then responsible for adding itself to the Context as soon as it is started. It also removes itself as soon as the complete(), cancel() or skip() has been called. Furthermore it provides a doStartContext() method (read more in the next paragraph).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class AbstractContextTask extends Task
{
    private var _dynamicObject:DynamicObject;
   
    protected override function doStart():void
    {
        _dynamicObject = IContextProvider(parent)
            .context.addDynamicObject(this);

        doStartContext();
        super.doStart();
    }

    protected function doStartContext():void
    {
        /* base implementation does nothing */
    }

    protected override function doCancel():void
    {
        cleanContext();
        super.doCancel();
    }

    protected override function doSkip():void
    {
        cleanContext();
        super.doSkip();
    }

    protected override function complete():Boolean
    {
        cleanContext();
        return super.complete();
    }

    private function cleanContext():void
    {
        _dynamicObject.remove();
    }
}

The following class is a concrete implementation of AbstractContextTask. That’s why it gets the HintComponent injected (line 4). Please also note that this Task overrides the method doStartContext() (line 13). This way we ensure that the class already has been added to the Context.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ShowHintTask extends AbstractContextTask
{
    [Inject]
    public var hint:HintComponent;
   
    private var type:HintType;

    public function ShowHintTask(type:HintType)
    {
        this.type = type;
    }
       
    protected override function doStartContext():void
    {
        hint.showHint(type);
       
        complete();
    }
}

The following code shows how everything can be glued together. Note, that you don’t have to pass the Context to each task.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TaskController
{
    [Inject]
    public var context:Context;
   
    [Init]
    public function initialize():void
    {
        var taskGroup:SequentialContextTaskGroup
            = new SequentialContextTaskGroup(context);

        taskGroup.addTask(new ShowHintTask(HintType.ATTENTION));
        taskGroup.addTask(new PointToTask(new Point(3, 5)));
        // add more tasks here...

        taskGroup.start();
    }
}

Summary

This approach of Context-aware Tasks ensures that you don’t have a lot of Tasks at the same time in the Context. The adding and removing from the Context happens in an encapsulated class. The Task itself can directly access injected objects during execution time.

Links

  1. Jens says:

    Nice one. This was indeed an omission in Parsley 2, mainly due to historical reasons (the Task Framework is much older than Parsley Commands).

    But in Parsley 3 you’ll get that out of the box with the new integration for Spicelib Commands (which are a replacement of the Task Framework which will fade out).

    See: http://www.spicefactory.org/news/news-2011-05-01.php

  2. Mattes says:

    Hi Jens,

    thanks for letting me know! I just read through your roadmap of spicelib 3 and really look forward to it. It contains pretty much everything we ever struggled with in the current task implementation. Very good work!

    I’m curios when the first stable version will be available.

  3. Jens says:

    Well, as of today I haven’t even started yet, but then again, the hard part was the spec, implementing it is quite straightforward. I can never set fixed deadlines, but a rough estimate would be that Spicelib Commands is out in June/July and then Parsley 3 that integrates with it about 2 months later.

  4. adi b says:

    Awesome article. I was looking into integrating Tasks into a project today and ran into the same issue with accessing the Context in a clean manner. We’re going to use these classes with full credit for your work. Many thanks!

  5. Mattes says:

    @adi Glad I could help. Thanks for the feedback!

  6. Chris says:

    This doesn’t work as you seem to be using 2.3 of Parsley and you haven’t defined the IContextProvider interface. I’m trying to get this working with Parsley 2.4.

  7. Mattes says:

    Thanks Chris for your feedback. I added the IContextProvider interface to the blog post. This was really forgotten.

    Regarding your Parsley 2.4 question. I don’t know any reason why it should not run with 2.4. Do you get an error? We have it running with 2.4-M2 which is not the final version. But we had other reasons for this.

  8. Chris says:

    Sorry, I was referring to the fact the I couldn’t find IContextProvider in 2.4. It’s working now that I added in the interface IContextProvider.

    Is it possible for SequentialContextTaskGroup to determine the context instead of explicitly injecting it?

    Also, shouldn’t the error function in AbstractContextTask also be overridden to clean up the context?

    And for beginners, I think if would be best for doStartContext to throw an error if hasn’t been overridden with a message saying that doStartContext needs to be overridden and that doStart needs to be made ‘final’ to prevent people from overridding it.

  9. Mattes says:

    Hi Chris,

    sorry for the delay. My answers in order:

    1) I wouldn’t know how to determent it unless you use something like a singleton. I think dependency injection is better in this case.

    2) Good catch. I didn’t check this yet but you might be right.

    3) Very good ideas! Thanks for sharing :)

  1. There are no trackbacks for this post yet.

Leave a Reply

*