Fun with Bookmarks: Implementing Continue and Break


A few people have noticed in the Workflow Foundation Beta 2 forums that the looping constructs provided in System.Activities.Statements such as While activity, ForEach activity, and ParallelForEach lack certain features found in their C# counterparts - there is no break or continue statement. This seemed like an interesting problem - how can we implement these missing features ourselves? Does the framework give us the amount of power we need to define our own control structures that go beyond the built-in? The answer is yes! It's all possible by using bookmarks (System.Activities.Bookmark).


What is a bookmark, exactly? I first ran across the idea of bookmarks under a different name: Continuations. (Probably it was while reading something by Paul Graham.) The concept is a little weird, so let me try to explain it as simply as possible - a continuation is like a magical goto that can jump to even a different function executing on the stack. It can even jump to a different 'branch' of the stack from the current one - if the language supports it by keeping old stack frames hanging around. Doesn't that sound crazy? Yup. It's craziness is both general and powerful. You can use continuations as a low-level device to implement co-routines, iterators, goto, and all sorts of program flow where you control the scheduling. (Or implement massively multiple player online games in Stackless Python.)


So what is a bookmark again? It's a way of explicitly controlling program flow.


Basic Implementation Idea


We're going to implement the WhileContinueBreak activity, which will be just like the System.Activities.Statements.While, except that this time we are going to add support for continue and break. We'll implement the Continue and Break statements by creating new activitiy classes also. When we are done we will be able to drop an instance of a Break activity into a Sequence, itself contained in a WhileContinueBreak, which will look like this, and it will work just like we expect.



<WhileContinueBreak>
   <Sequence>
     <If Condition="True" >
       <If.Then><Break></If.Then>
       <If.Else><WriteLine Text="Nah!"/><If.Else>
     </If>
     <Continue>
   </Sequence>
</WhileContinueBreak>


Continue and Break will be implemented to work using bookmarks. The Continue and Break activities know what bookmarks there are available to be resumed not by passing the bookmarks using Arguments or Variables, but by making the bookmarks available by using the idea of scopes, as in a previous post [link]. This runtime scoping ought to let our activities behavior mirror the syntactic scoping semantics of C#.


But First, a Designer


Before we code any activities let's whip up a quick WhileContinueBreak designer, which will look kinda like the designer for While Activity, so that we can play with our activities. I'm going to create a new Activity Designer inside a new Activity Library project. Inside the <ActivityDesigner>tags I'll be lazy and use panels for layout, rather than a grid.


    <StackPanel>


        <WrapPanel>


        <Label>Condition:</Label>


        <sapv:ExpressionTextBox


            Expression="{Binding Path=ModelItem.Condition}"


            ExpressionType="s:Boolean"


            OwnerActivity="{Binding Path=ModelItem}"


            HintText="Condition (VB)"


            Width="200"


            />


        </WrapPanel>


        <Label>Body:</Label>


        <sap:WorkflowItemPresenter


            Item="{Binding Path=ModelItem.Body}"


            HintText="Body (Activity)" MinWidth="100" MinHeight="50"/>


    </StackPanel> 



One control that I haven't mentioned previously is the ExpressionTextBox. Every time we use an ExpressionTextBox there are three properties specific to ExpressionTextBox that we will definitely set on it: Expression, ExpressionType, and ActivityOwner. The rest is just to make it look better. For more info see Cathy Dumas's Expression Text Box 101.


The Activities


Starting with our class skeleton for WhileContinueBreak which looks like this:


    [ContentProperty("Body")]


    [Designer(typeof(CustomWhileDesigner))]


    public sealed class WhileContinueBreak : NativeActivity


    {


        [DefaultValue(null)]


        public Activity<bool> Condition { get; set; }


 


        [DefaultValue(null)]


        public Activity Body { get; set; }


 


        protected override bool CanInduceIdle


        {


            get { return true; } //Required by the WF runtime because we create bookmarks


        }


    }


 


Next we are going to override Execute(). Here we need to do two things - first, create the bookmarks which will be activated by the Break activity and the Continue activity. Second, we schedule one of our child activities for execution (otherwise nothing much would happen).


 


        protected override void Execute(NativeActivityContext context)


        {


            //Interesting note: can't create bookmarks while there are any children scheduled.


            Bookmark continueBookmark = context.CreateBookmark(OnContinue,


                BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);


            Bookmark breakBookmark = context.CreateBookmark(OnBreak,


                BookmarkOptions.NonBlocking);


            context.Properties.Add("ContinueBookmark", continueBookmark);


            context.Properties.Add("BreakBookmark", breakBookmark);


 


            //Schedule the first Condition test


            context.ScheduleActivity<bool>(Condition, ConditionCompletion);


        }


The continueBookmark is MultipleResume because a continue statement can potentially be executed many times. I don't expect Break to be executed more than once, so I'll leave it off there. The bookmarks are also NonBlocking. What that means is that our WhileContinueBreak activity should complete when all its children have finished executing, regardless of whether anyone closed the bookmarks or not. (The default is a blocking bookmark, which would block our activity from completing until all its bookmarks are closed.) Once we create the bookmarks, we added them as ExecutionProperties named "ContinueBookmark" and "BreakBookmark" to the current execution context. Finally, our While loop is ready to start looping, so we schedule the first piece of work in the loop: the Conditional test.


Now we can figure out what goes in Continue.Execute() and Break.Execute(), which look exactly the same except for those two words:


        protected override void Execute(NativeActivityContext context)


        {


            Bookmark breakProperty = (Bookmark)context.Properties.Find(BreakBookmarkName);


            Bookmark willNeverBeResumed = context.CreateBookmark(); //Blocking bookmark


            var result = context.ResumeBookmark(breakProperty, null);


        }


The steps are: 1) Retrieve the bookmark stored in the execution property, 2) Create a bookmark to block this activity from ever completing (a hack), 3) Resume the bookmark retrieved in step 1.


Step 2 requires some explanation: If we don't block this activity from completing by having an blocking bookmark, the next thing that the workflow runtime executes is not going to be the bookmark we just resumed. It's going to be whatever else was ready in the scheduler queue, which is e.g. the next activity in the sequence...


Now we need to finish implementing the execution logic for our WhileContinueBreak activity, so here it is:



        private void ConditionCompletion(NativeActivityContext context, ActivityInstance completedInstance, bool result)


        {


            if (completedInstance.State == ActivityInstanceState.Closed && result)


            {


                //Condition returned true: start executing body


                context.ScheduleActivity(Body, BodyCompletion);


            }


        }


 


        private void BodyCompletion(NativeActivityContext context, ActivityInstance completedInstance)


        {


            if (completedInstance.State == ActivityInstanceState.Closed)


            {


                //Body completed successfully without cancellation: loop to condition


                context.ScheduleActivity<bool>(Condition, ConditionCompletion);


            }


        }


 


        private void OnContinue(NativeActivityContext context, Bookmark bookmark, object value)


        {


            context.CancelChildren();


            context.ScheduleActivity<bool>(Condition, ConditionCompletion);


        }


 


        private void OnBreak(NativeActivityContext context, Bookmark bookmark, object value)


        {


            context.CancelChildren();


        }


Tada! All done.


Questions:




  • What happens if we try to use the Break activity outside of a While loop?


  • Could we use the same Break and Continue activities inside of a different control structure? What would need to be done?


  • When we call ExecutionPropertiesAdd, should we call the overload with parameter onlyVisibleToPublicChildren set to true?

Comments (0)

Skip to main content