Misadventures in CacheMetadata – wrapping an inner activity, in code

Let’s dig deeper into CacheMetadata (intro post).

We’ll do it with a toy problem – I want to wrap the Delay activity and customize it so that I can override the delay argument in code. I’m going to jump straight in and do it in NativeActivity (without thinking about whether this is a good implementation choice or not).

From the outside, I want this activity to look just like a Delay activity – it will have one InArgument<TimeSpan> called Duration. But (for a testing scenario) I’m going to be able to override the Duration InArgument by queuing up some override values before I run the workflow. Here’s a first draft which is not quite all there yet:

public sealed class HackableDelayActivity : NativeActivity

    {

        //static

        private static Queue<TimeSpan> overrideDurations = new Queue<TimeSpan>();

        public static Queue<TimeSpan> OverrideDurations { get { return overrideDurations; } }

        //inargument

        public InArgument<TimeSpan> Duration { get; set; }

        //implementation

        public Delay InnerDelay { get; set; }

        protected override void Execute(NativeActivityContext context)

        {

            TimeSpan configuredDuration = context.GetValue(Duration);

            if (OverrideDurations.Count > 0)

            {

                configuredDuration = OverrideDurations.Dequeue();

            }

            context.ScheduleActivity(InnerDelay);

        }

   }

 

 

I want to override the value of the InArgument on HackableDelayActivity, and pass it through to InnerDelay, but how can we do that? I also have a couple other problems:

  1. InnerDelay is never initialized.
  2. InnerDelay is public.

You’ll agree that never being initialized could be a problem. But why is InnerDelay being public a problem?

Well, if we define InnerDelay as public, then InnerDelay is going to show up in the XAML:

 

<local:HackableDelayActivity Duration="{x:Null}" InnerDelay="{x:Null}" />

 

And, even if we were to change to initialize InnerDelay in the constructor of HackableDelayActivity, if we load up ugly XAML like the above, InnerDelay is going to get overwritten right back to null and our workflow will be broken.This scenario is no fun at all. One more problem with InnerDelay being public is that InnerDelay.Duration is also public and settable by users of the activity… argh.

 

So, let’s update our code. We want to change our code to make InnerDelay private, initialize InnerDelay, and set up the InArgument: InnerDelay.Duration. But how can we do the last of these?

 

Does this work?

 

    //public noargs ctor

    public HackableDelayActivity()

    {

    InnerDelay = new Delay

    {

    Duration = new InArgument<TimeSpan>(this.Duration)

    };

    }

 

Um, no. There’s two huge problems. Problem one: in our constructor, this.Duration is still null. Public InArguments are configurable by the user after the object has been constructed, and may be null for a long time. Problem two: the code doesn’t compile, because there is no InArgument<T>(InArgument<T>) constructor.

 

So the problem remains - how do we pass data from InArgument A to InArgument B?

 

Can we use a Variable?

 

There are a bunch of other constructors for InArgument<T>, including InArgument<T>(Variable<T>), which kind of suggests we might be able to use a Variable to work out our problem with referencing arguments which don’t exist yet.

 

OMG it compiles!

 

And we haven’t even touched CacheMetadata yet, we are still using the default implementation.

 

    public sealed class HackableDelayActivity : NativeActivity

    {

        //static

        private static Queue<TimeSpan> overrideDurations = new Queue<TimeSpan>();

        public static Queue<TimeSpan> OverrideDurations { get { return overrideDurations; } }

        //public noargs ctor

        public HackableDelayActivity()

        {

            DurationVariable = new Variable<TimeSpan>();

            InnerDelay = new Delay()

            {

                Duration = new InArgument<TimeSpan>(DurationVariable),

       };

        }

        //inargument

        public InArgument<TimeSpan> Duration { get; set; }

        //implementation

        private Delay InnerDelay { get; set; }

        private Variable<TimeSpan> DurationVariable { get; set; }

        protected override void Execute(NativeActivityContext context)

        {

            TimeSpan configuredDuration = context.GetValue(Duration);

            if (OverrideDurations.Count > 0)

            {

                configuredDuration = OverrideDurations.Dequeue();

            }

            context.SetValue(DurationVariable, configuredDuration);

            context.ScheduleActivity(InnerDelay);

        }

    }

 

But does it run?

 

System.InvalidOperationException was unhandled
  Message=Variable '' of type 'System.TimeSpan' cannot be used. Please make sure it is declared in an Activity or SymbolResolver.
  Source=System.Activities
  StackTrace:
       at System.Activities.WorkflowApplication.Invoke(Activity activity, IDictionary`2 inputs, WorkflowInstanceExtensionManager extensions, TimeSpan timeout)
       at System.Activities.WorkflowInvoker.Invoke(Activity workflow, TimeSpan timeout, WorkflowInstanceExtensionManager extensions)
       at System.Activities.WorkflowInvoker.Invoke(Activity workflow)
       at WorkflowConsoleApplication5.Program.Main(String[] args) in Program.cs:line 13

 

 

The runtime doesn’t know our DurationVariable exists. Why? It isn’t declared in CacheMetadata. OK, so let’s declare it then. Here follows a little dialogue between activity author and workflow runtime:

 

        protected override void CacheMetadata(NativeActivityMetadata metadata)

        {

            base.CacheMetadata(metadata);

            metadata.AddVariable(this.DurationVariable);

        }

System.InvalidOperationException was unhandled
  Message=Activity '1: HackableDelayActivity' cannot access this variable because it is declared at the scope of activity '1: HackableDelayActivity'.  An activity can only access its own implementation variables.
  Source=System.Activities
  StackTrace:
       at System.Activities.WorkflowApplication.Invoke(Activity activity, IDictionary`2 inputs, WorkflowInstanceExtensionManager extensions, TimeSpan timeout)
       at System.Activities.WorkflowInvoker.Invoke(Activity workflow, TimeSpan timeout, WorkflowInstanceExtensionManager extensions)
       at System.Activities.WorkflowInvoker.Invoke(Activity workflow)
       at WorkflowConsoleApplication5.Program.Main(String[] args) in Program.cs:line 13

        protected override void CacheMetadata(NativeActivityMetadata metadata)

        {

            base.CacheMetadata(metadata);

            metadata.AddImplementationVariable(this.DurationVariable);

        }

 

System.ArgumentException was unhandled
  Message=The provided activity was not part of this workflow definition when its metadata was being processed.  The problematic activity named 'Delay' was provided by the activity named 'HackableDelayActivity'.
Parameter name: activity
  Source=System.Activities
  ParamName=activity
  StackTrace:
       at System.Activities.WorkflowApplication.Invoke(Activity activity, IDictionary`2 inputs, WorkflowInstanceExtensionManager extensions, TimeSpan timeout)
       at System.Activities.WorkflowInvoker.Invoke(Activity workflow, TimeSpan timeout, WorkflowInstanceExtensionManager extensions)
       at System.Activities.WorkflowInvoker.Invoke(Activity workflow)
       at WorkflowConsoleApplication5.Program.Main(String[] args) in Program.cs:line 13

        protected override void CacheMetadata(NativeActivityMetadata metadata)

        {

            base.CacheMetadata(metadata);

            metadata.AddImplementationVariable(this.DurationVariable);

            metadata.AddChild(this.InnerDelay);

        }

 

System.Activities.InvalidWorkflowException was unhandled
  Message=The following errors were encountered while processing the workflow tree:
'VariableValue<TimeSpan>': The referenced Variable object (Name = '') is not visible at this scope.  There may be another location reference with the same name that is visible at this scope, but it does not reference the same location.
  Source=System.Activities
  StackTrace:
       at System.Activities.Validation.ActivityValidationServices.ThrowIfViolationsExist(IList`1 validationErrors)
       at System.Activities.Hosting.WorkflowInstance.ValidateWorkflow(WorkflowInstanceExtensionManager extensionManager)
       at System.Activities.Hosting.WorkflowInstance.RegisterExtensionManager(WorkflowInstanceExtensionManager extensionManager)
       at System.Activities.WorkflowApplication.EnsureInitialized()
       at System.Activities.WorkflowApplication.RunInstance(WorkflowApplication instance)
       at System.Activities.WorkflowApplication.Invoke(Activity activity, IDictionary`2 inputs, WorkflowInstanceExtensionManager extensions, TimeSpan timeout)
       at System.Activities.WorkflowInvoker.Invoke(Activity workflow, TimeSpan timeout, WorkflowInstanceExtensionManager extensions)
       at System.Activities.WorkflowInvoker.Invoke(Activity workflow)
       at WorkflowConsoleApplication5.Program.Main(String[] args) in Program.cs:line 13

        protected override void CacheMetadata(NativeActivityMetadata metadata)

        {

            base.CacheMetadata(metadata);

            metadata.AddImplementationVariable(this.DurationVariable);

            metadata.AddImplementationChild(this.InnerDelay);

        }

 

And victory! We finally have a workflow that runs.

What did we learn from all those CacheMetadata errors about what the workflow runtime expects?

  • There are two different spaces inside an activity. A public space, and a private implementation space . Variables and child Activities must be declared in CacheMetadata to belong to one or the other.

  • Child Activities in the public space cannot see variables in the implementation space . Further experimentation proves that child Activities in the implementation space cannot see variables in the public space either .

  • An activity can modify its own implementation variables, but not its own public variables.

Basically, the implementation of an activity is cut off from the outside world. Where does this distinction come into play?

 

To try and understand that, let’s look at how we could have implemented our HackableDelayActivity in the designer.

 

image

I left out the special logic for adjusting the duration which was the whole point of creating HackableDelay in the first place, but it has exactly the same idea of an implementation child activity . Only in this case, it’s a subclass of Activity, and it has a property called Implementation – which contains all of it’s implementation Children.

Visually, when we are editing the activity’s XAML definition in designer, then we are looking at its implementation space. When we are using the activity in a different XAML workflow, then we should see only what the activity exposes from its public space . Which, in our case, is basically nothing (except arguments if you like to think of them as public):

image

OK, so that’s it for today, except for a bonus point – there was actually a simple way we could initialize argument A to refer to argument B:

InnerDelay = new Delay()
            {
                Duration = new InArgument<TimeSpan>((context) => context.GetValue(this.Duration)),
            };

I don’t think it will round-trip to XAML though, so it is better to use for private bits which won’t ever be saved to XAML.