What can we do in the CacheMetaData Method of Activity
After reading Basic Activity Scheduling and starting to write subclasses of NativeActivity, you may start running into a bunch of really weird or downright incomprehensible error messages. Some samples to whet your appetite:
Activity '1: CodeActivity1' cannot access this variable because it is declared at the scope of activity '1: CodeActivity1'. An activity can only access its own implementation variables.
Variable '' of type 'System.Int32' cannot be used. Please make sure it is declared in an Activity or SymbolResolver.
The following errors were encountered while processing the workflow tree:
'CodeActivity1': The activity with display name 'CodeActivity1' is attempting to reference itself. An activity cannot be its own parent.
The following errors were encountered while processing the workflow tree:
'CodeActivity1': The activity 'CodeActivity1' cannot be referenced by activity 'CodeActivity1' because the latter is not in another activity's implementation. An activity can only be referenced by the implementation of an activity which specifies that activity as a child or import. Activity 'CodeActivity1' is declared by activity 'CodeActivity1'.
Hungry? OK. Now what the heck is going on?
Cache your metadata! (Eat your vegetables!)
All these error messages are the runtime’s heavy-handed way of limiting the way your activities work to something it can understand. How does it understand what your activity does? Well you have to tell it, by providing the runtime with metadata. But what, for example, information are you actually telling the runtime?
Here’s the first and most important thing
- I might want to invoke these child activities, which are somewhat accessible to the outside world so I might not know completely what they do.
- I might want to invoke these child activities, which I totally own - they can’t see anything outside me.
Actually we’ll use special names for these: in the first case such children get called public children. In the second case such children get called implementation children.
You will see a lot of examples of Activities with public children in the WF 4.0 framework, here are a few: Sequence, If, While, Flowchart. What do all these activities have in common? You provide the children.
You should know some examples of Activities with implementation children too: every time you create an activity in XAML using the workflow designer, all the activities you added inside that activity are implementation children. And of course it’s possible familiar activities have some invisible implementation children that we just never know about…
Aside from using CacheMetadata to declare child activities, you can also
- Declare variables (public or implementation)
- Declare arguments
- Implement custom validation rules, so you too can have nifty validation errors show up in the Workflow Designer
- Declare you require an extension
- Register a default extension provider
The default implementation
Do the WF 4.0 built-in activities override CacheMetadata()? Of course! There’s a good reason, which is performance! But do we have to? The Basic Activity Scheduling post (again) didn’t override NativeActivity.CacheMetadata(), and it still worked...
NativeActivity.CacheMetadata does have a default implementation, and it goes something like this:
For each public Property of type Activity on this type, declare it as a public child activity.
For each public Property of type Variable on this type, declare it as a public variable.
For each public Property of type InArgument on this type, declare it as an argument.
All of this code works by reflection on your class. Reflection is slow. This is why the framework always overrides CacheMetadata, and a reason you might need to override CacheMetadata in a 'real world' application.
So if you had to reinvent the wheel, and write your own custom Sequence activity, you probablydon’t want to call base.CacheMetadata():
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
//NOT calling base.CacheMetadata(); even though it might do exactly what we want
foreach(Activity x in this.Children) metadata.AddChild(x);
}
Variables
The real fun starts when you start thinking variables are the way to pass data around your workflow, and you get into variable visibility rules, and all those public/implementation declarations start to have an effect….
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 oneInArgument<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 nulland 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 privateimplementation 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 childactivity. 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 itsimplementation 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.
posted on 2012-06-21 18:35 malaikuangren 阅读(2044) 评论(0) 编辑 收藏 举报