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:
- InnerDelay is never initialized.
- 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.
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):
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.
Comments
Anonymous
February 26, 2010
Hi Tim, Thanks for this post. The cache metadata seems a bit confusing to me. Your post helps clarify some of the confusion, but I think it'll take some trial and error on my part to completely understand the subtly. It seems a bit strange to me that an activity can modify its implementation variables, but not its public variables. I realize public variables are not 'owned' like implementation variables, but not being able to modify them seems strange. How does an activity author initialize a public variable (or do they not)? Having to use a variable to initialize the argument seems a bit convoluted. I take it your 'bonus point' is an alternate way to achieve this initialization without using a variable. What kind of issues would we see if we used this approach in terms of 'round tripping to XAML'? Finally, I noticed that the override to CacheMetadata calls the base class' CacheMetadata method. I was under the impression from the earlier post that this wasn't a good idea, for performance reasons. Is it necessary to call the base class method for some reason, in this example? Thanks, NotreAnonymous
February 26, 2010
Public variables cannot be modified directly using context.SetValue(), but they can be -initialized to a default value based on an expression -passed to a public child which initializes them using an OutArgument (such as Activity<T> - or Assign) Calling base.CacheMetadata() - it's an easy way to initailize the metadata needed for argument Duration.Anonymous
June 20, 2010
The comment has been removedAnonymous
June 21, 2010
Hi TRex, I'm not really sure whether what you want to do is possible or not. For a composite activity, like you generate by creating a Workflow1.xaml file, it is possible to override CacheMetadata in code-beside file. The main reason I can think of is to add validation logic. But I don't think overriding Activity.CacheMetadata helps. ActivityMetadata limits what you can do, compared to NativeActivityMetadata, and I don't think it gives you any extra wiggle room, My gut feeling is that overriding Activity.CacheMetadata is a dead end because either what you want to do is possible without overriding Activity.CacheMetadata, or it is just impossible (or requires being a NativeActivity is another possibility...) TimAnonymous
September 08, 2010
It appears to me that the inner activity has to be a property, public or private whatsoever. When I it is a private member, I got the same error telling that variables could not be found in given scope....Anonymous
September 09, 2010
@Rieze That is due to the behavior of base.CacheMetadata(), which I am calling in all my code [but you don't have to call it, if it doesn't do what you want]. Calling base.CacheMetadata gives you a 'default' cached metadata based on the public properties of your (current) activity. If you have a property 'public Activity Child { get; set; } then base.CacheMetadata automatically calls metadata.AddChild(). If your property were private you could achieve the same affect by calling metadata.AddChild() yourself, after calling base.CacheMetadata. (Note, if your property is private, when building a designer for your activity, you also won't see it in your model item's model properties.) TimAnonymous
March 03, 2011
Your blog post just helped me out (after reading it many months ago) - thanks! social.msdn.microsoft.com/.../d01d446c-56d9-4f2b-a22b-03c782f70596Anonymous
March 21, 2011
Hi Tim, Your blog post did help me out a lot, but now I've run into a snag for which I hope I can get your expertise. Please see WF question: social.msdn.microsoft.com/.../fa6bb712-c911-4c81-b215-2300c031d6f2 I just updated the WF forum question with what I think is a simple repro scenario. Thank you!Anonymous
November 29, 2014
The comment has been removed