Sharing Functionality Between Designers (WF4 EditingContext Intro Part 2)
This part 2 of my 6 part series on the EditingContext.
- Introduction
- Sharing Functionality between Designers (you are here)
- Host provided capabilities
- Providing callbacks for the host
- Subscription/Notification engine
- Inspection, Default Services and Items
Setup
We will need a custom activity, EmptyOne and designer called InteractWithServiceDesigner.
using System.Activities;
using System.ComponentModel;
namespace blogEditingContext
{
[Designer(typeof(InteractWithServicesDesigner))]
public sealed class EmptyOne : CodeActivity
{
// Define an activity input argument of type string
public InArgument<string> Text { get; set; }
// If your activity returns a value, derive from CodeActivity<TResult>
// and return the value from the Execute method.
protected override void Execute(CodeActivityContext context)
{
// Obtain the runtime value of the Text input argument
string text = context.GetValue(this.Text);
}
}
}
What We Will See
The designers for Foo will leverage a new service in order to display a list of database tables. We will also need to publish this service to the editing context, and handle the fact that we don’t know who might publish it (or when it might be published). Note that in VS, there is no way to inject services except by having an activity designer do it. In a rehosted app, the hosting application could publish additional services (see part 4) that the activities can consume. In this case though, we will use the activity designer as our hook.
Publishing a Service
Let’s look at the designer for Foo (as Foo is our generic, and relatively boring activity).
<sap:ActivityDesigner x:Class="blogEditingContext.InteractWithServicesDesigner"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"
xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation">
<StackPanel>
<ListBox Height="100" Name="listBox1" Width="120" />
<Button Name="button1" Click="Button_Click">Publish Service</Button>
</StackPanel>
</sap:ActivityDesigner>
Not much to this, except a drop down list that is currently unbound (but a name is provided). Also note that there is a button that says to “publish the service”. Let’s first look at the code for the button click
private void Button_Click(object sender, RoutedEventArgs e)
{
if (!this.Context.Services.Contains<ISampleService>())
{
this.Context.Services.Publish<ISampleService>(new SampleServiceImpl());
}
}
What are we doing here? We first check if this service is already published using the Contains method. We can do this because ServiceManager implements IEnumerable<Type>.
[update, finishing this sentence.] One could also consume the service using GetService<TResult>. You may also note that there is a GetRequiredService<T>. This is a call that we know won’t return null, as the services we are requesting must be there for the designer to work. Rather than returning null, this will throw an exception. Within the designer, we generally think of one service as required:
Let’s look at the definition of the service. Here you can see that we are using both an interface and then providing an implementation of that interface. You could just as easily use an abstract class, or even a concrete class, there is no constraint on the service type.
using System.Collections.Generic;
namespace blogEditingContext
{
public interface ISampleService
{
IEnumerable<string> GetDropdownValues(string DisplayName);
}
public class SampleServiceImpl : ISampleService
{
public IEnumerable<string> GetDropdownValues(string DisplayName)
{
return new string[] {
DisplayName + " Foo",
DisplayName + " Bar",
"Baz " + DisplayName
} ;
}
}
}
If there is not a service present, we will publish an instance of one. This becomes the singleton instance for any other designer that may request it. Right now, we have a designer that can safely publish a service. Let’s look at consuming one
Consuming a Service
Let’s look at some code to consume the service. There are two parts to this. One is simply consuming it, which we already saw above in discussing GetService and GetRequiredService . The second is hooking into the notification system to let us know when a service is made available. In this case, it’s a little contrived, as the service isn’t published until the button click, but it’s good practice to use the subscription mechanism as we make no guarantees on ordering, or timing of service availability.
Subscribing to Service
Here, using the Subscribe<TServiceType> method, we wait for the service to be available. The documentation summarizes this method nicely:
Invokes the provided callback when someone has published the requested service. If the service was already available, this method invokes the callback immediately.
In the OnModelItemChanged method, we will subscribe and hook a callback. The callback’s signature is as follows:
public delegate void SubscribeServiceCallback<TServiceType>(
TServiceType serviceInstance
)
As you can see, in this callback, the service instance is provided, so we can query it directly. You may ask, “why not in Intialize?” well, there are no guarentees that the editing context will be available at that point. We could either subscribe to context being made available, or just use ModelItemChanged:
protected override void OnModelItemChanged(object newItem)
{
if (!subscribed)
{
this.Context.Services.Subscribe<ISampleService>(
servInstance =>
{
listBox1.ItemsSource = servInstance.GetDropdownValues(this.ModelItem.Properties["DisplayName"].ComputedValue.ToString());
button1.IsEnabled = false;
}
);
subscribed = true;
}
}
This wraps a basic introduction to the ServiceManager type and how to leverage it effectively to share functionality in designers.
Let’s look at a before and after shot in the designer:
Before & After
What about Items?
Items follow generally the same Get, Subscribe, and publish pattern, but rather than publish, there is a SetValue method. If you have “just data” that you would like to share between designers (or between the host and the designer) an Item is the way to go about that. The most commonly used item we’ve seen customers use is the Selection item in order to be able to get or set the currently selected model item.
That’s our tour of basic publish and subscribe with Services and Items.
[updated 12/22/2009 @ 10:23 am to finish an unclear sentence about GetService<>]
[updated 12/22/2009 @ 8:50pm : Link to download sample code is here]
Attachment(s): blogEditingContext.zip
Comments
Anonymous
December 21, 2009
can you give the demo code to download?Anonymous
December 22, 2009
Hi Matt, Thanks for your further explanation on the editing context! Probably I'm missing the obvious, but I'm not sure where you demonstrate consuming the service. I assume this is just a GetService<TResult> call, but I didn't notice it demonstrated. Thanks, NotreAnonymous
December 22, 2009
@Notre, Yes, in this case, I am consuming by subscribing to it being made available (which cheats a bit because that delegate passes the instance to me). I could certainly use GetService<TResult> to get it as well. mattAnonymous
December 22, 2009
Hi Matt, This may be slightly off topic, but can you please explain when OnModelItemChanged is called. The MSDN docs say: Invoked when the underlying model item is changed I don't understand which model item is being refered to by the docs. Thanks, NotreAnonymous
December 22, 2009
Hi Matt, You mentioned that in VS, the only way to inject a service is by using the activity designer. I'm thinking of rehosting the workflow designer in my own Visual Studio editor (using a VS package), possibly in my own VS project system. In this case, I think I'm effectively rehosting the app, in which case I suspect I would be able to publish additional services outside of the activity designer, in a similar manner to what you will be discussing in part 4. Does that sound right? Thanks, NotreAnonymous
December 22, 2009
@Notre, updated the post a little bit, realized I missed the intro to a sentence. thanks for the catch!Anonymous
December 22, 2009
@Notre, Yes, in that case you woudl be controlling the host code (similar to what we do in Microsoft.VisaulStudio.Activities.AddIn.dll (or something similar). Your host code would then be hosted in VS as well :-) mattAnonymous
December 22, 2009
@Notre, Let me try to get this last one about OnModelItemChanged. This is within the designer and it refers to when the property ModelItem on WorkflowViewElement is set, or when it changes. You can use it to effectively initialize because the event will be fired when the ActivityDesigner.ModelItem is set the first time. mattAnonymous
December 22, 2009
Thanks Matt. I think the piece I was missing was that an AcitivtyDesigner is a WorkflowViewElement and a WorkflowViewElement is related to exactly one model item. Your comment and the docs for WorkflowViewElement and ModelItem helped clarify my understanding. NotreAnonymous
December 22, 2009
To those asking about the code, you can download it from post 6, here: http://blogs.msdn.com/mwinkle/archive/2009/12/23/inspection-default-services-and-items-wf4-editingcontext-intro-part-6.aspx