基础内容

支持本地通信的工作流服务

Matt Milner

下载代码示例

在前一列 (请参见 “ 流通信 ” 中 2007 骞? 9 鏈? 颁发的 msdn.microsoft.com/magazine/cc163365.aspx 处的 MSDN Magazine),编写有关核心通信体系结构 Windows 流基础 3 (WF3) 中。 没有未涵盖的主题是本地的通信活动是一个抽象 (此通信体系结构的顶部。 如果您看.net Framework 4 Beta 1,您将注意到没有 HandleExternalEvent 活动。 在事实的方式数据表与 WF4,通信活动,包括已内置 Windows Communication Foundation (WCF)。 这个月我介绍如何使用 WCF 工作流和 Windows 流基础 3 中的为宿主应用程序之间的通信。 获得此知识应该使用 WF3 您开发成果有关的帮助,并准备您 WF4,WCF 其中附带框架的队列 (称为 “ 书签 ” WF4 中) 上是唯一的抽象。 (有关 WF3 中的工作流服务的基本信息,请参阅我地基列中的 msdn.microsoft.com/magazine/cc164251.aspx 处的 MSDN Magazine Visual Studio 2008 启动问题。

概甘

宿主应用程序和工作流之间的通信证明具有挑战性的某些开发人员,因为它们可以轻松地忽略这一事实,在不同的线程上经常执行工作流和主机。 通信体系结构的设计被为了使开发人员不必担心管理线程上下文封送处理数据和其他低级别的详细信息。 通过在 WF 排队体系结构的一个抽象是.NET Framework Framework的版本 3.5 中引入该 WCF 消息传递集成。 大多数示例和实验室显示活动和扩展到 WCF 可以如何使用公开给宿主进程的外部的客户端的工作流,但此相同的通讯框架可用于在同一进程内进行通信。

实现此通信涉及几个步骤,但不是到很多金额工作,并不比您将不得不使用本地通信活动做更多。

您可以执行其他操作之前,您需要定义 (或最小日志开始迭代的方法中定义) 的通信使用 WCF 服务合同合同。 接下来,您需要使用您的工作流中的那些合同,模型通信点,在逻辑中。 最后,一起挂钩,工作流和其他服务需要托管作为 WCF 服务的终结点配置。

建模通信

第一步中建模的通信是以定义您的宿主应用程序和工作流之间合同。 WCF 服务使用合同定义组成服务和发送和接收的邮件的操作的集合。 在本例中将通信从该工作流主机和工作流,以便主机,因为您需要定义两个服务合同和相关的数据协定 的 图 1 所示。

图 1 合同的通信

[ServiceContract(
    Namespace = "urn:MSDN/Foundations/LocalCommunications/WCF")]
public interface IHostInterface
{
[OperationContract]
void OrderStatusChange(Order order, string newStatus, string oldStatus);
}

[ServiceContract(
    Namespace="urn:MSDN/Foundations/LocalCommunications/WCF")]
public interface IWorkflowInterface
{
    [OperationContract]
    void SubmitOrder(Order newOrder);

    [OperationContract]
    bool UpdateOrder(Order updatedOrder);
}

[DataContract]
public class Order
{
    [DataMember]
    public int OrderID { get; set; }
    [DataMember]
    public string CustomerName { get; set; }
    [DataMember]
    public double OrderTotal { get; set; }
    [DataMember]
    public string OrderStatus { get; set; }
    }

与就地合同,建模工作流使用发送和接收活动的工作象为远程通信。 这是关于 WCF 美丽的事情之一:远程或本地编程模型是相同的。 作为简单的示例 的 图 2 显示了具有两个接收活动和建模工作流和主机之间通信的一次发送活动的工作流。 接收活动配置有 IWorkflowInterface 服务合同,发送活动使用 IHostInterface 合同。

目前为止为本地通信使用 WCF 不是从远程通信使用 WCF 得不同,非常类似于使用本地通信活动和服务。 主要区别就是如何宿主代码写入启动工作流,并处理来自工作流的通信。

图 2 根据合同建模工作流

宿主服务

因为我们希望流动使用 WCF 这两种方法的通信,我们需要承载两个服务 — 工作流服务运行在宿主应用程序接收来自该工作流的邮件中的工作流和服务。 在我的示例中我构建一个简单的 Windows Presentation Foundation (WPF) 应用程序,使其作为主机,用来管理该主机的 App 类已经 OnStartup 和 OnExit 方法。 您第一斜率可能创建 WorkflowServiceHost 类和打开它右 OnStartup 方法中。 由于打开主机后,Open 方法将不会阻止,您可以继续处理、 加载用户界面并开始工作流进行交互。 因为 WPF (和其他客户端技术) 使用单个线程来处理,这将很快导致问题因为服务和客户端调用不能使用相同的线程以使客户端超时。 要避免出现这在 WorkflowServiceHost 上创建另一个线程使用该 ThreadPool 中所示 图 3

图 3 驻留工作流服务

ThreadPool.QueueUserWorkItem((o) =>
{

//host the workflow
workflowHost = new WorkflowServiceHost(typeof(
    WorkflowsAndActivities.OrderWorkflow));
workflowHost.AddServiceEndpoint(
    "Contracts.IWorkflowInterface", LocalBinding, WFAddress);
try
{
    workflowHost.Open();
}
catch (Exception ex)
{
    workflowHost.Abort();
    MessageBox.Show(String.Format(
        "There was an error hosting the workflow as a service: {0}",
    ex.Message));
}
});

下次您会遇到的挑战选择适当的绑定为本地通讯。 目前,没有中-内存或进程绑定,它是非常轻量为这些类型的方案。 轻量的频道的最佳选择是使用具有安全性已关闭的该 NetNamedPipeBinding。 遗憾的是,濡傛灉灏濊瘯作为服务使用此绑定和主机工作流您将得到通知主机需要与上下文通道存在绑定,因为服务协定可能要求在会话错误。 进一步,是附带只有三个上下文绑定,.NET Framework Framework中包含没有 NetNamedPipeContextBinding:BasicHttpContextBinding、 NetTcpContextBinding 和 WSHttpContextBinding。 幸运的是,您可以创建您自己自定义绑定到包含上下文通道。 图 4 显示了一个自定义绑定从 NetNamedPipeBinding 类派生,并在 ContextBindingElement 注入绑定的。 在两个方向上的通信可以使用不同的地址现在终结点注册中使用此绑定。

图 4 的 NetNamedPipeContextBinding

public class NetNamedPipeContextBinding : NetNamedPipeBinding
{
    public NetNamedPipeContextBinding() : base(){}

    public NetNamedPipeContextBinding(
        NetNamedPipeSecurityMode securityMode):
        base(securityMode) {}

    public NetNamedPipeContextBinding(string configurationName) :
        base(configurationName) {}

    public override BindingElementCollection CreateBindingElements()
    {
        BindingElementCollection baseElements = base.CreateBindingElements();
        baseElements.Insert(0, new ContextBindingElement(
            ProtectionLevel.EncryptAndSign,
            ContextExchangeMechanism.ContextSoapHeader));

        return baseElements;
    }
}

与此新的绑定可以在该 WorkflowServiceHost 上创建一个终结点和打开主机有没有更多的错误。 工作流已准备就绪,可从使用服务合同的主机接收数据。 若要发给该数据您需要创建一个代理和调用该操作中 的 图 5 所示。

图 5 的 宿主代码以开始工作流

App a = (App)Application.Current;
    IWorkflowInterface proxy = new ChannelFactory<IWorkflowInterface>(
    a.LocalBinding, a.WFAddress).CreateChannel();

    proxy.SubmitOrder(
        new Order
        {
            CustomerName = "Matt",
            OrderID = 0,
            OrderTotal = 250.00
        });

因为共享该合同,没有代理类这样您不必使用 ChannelFactory < TChannel > 创建客户端代理。

在工作流正在宿主并准备好接收邮件,它仍然需要配置为将消息发送到主机。 最重要工作流需要能够使用发送活动时获取客户端终结点。 发送活动允许您指定通常是到配置文件中命名的终结点映射该终结点名称。 尽管将终结点信息放在配置文件中工作,但也可以使用 ChannelManagerService (如在 msdn.microsoft.com/magazine/cc721606.aspx 我 2008 年八月列所述) 存放由您发送的活动工作流程中使用的客户端终结点。 图 6 显示创建该服务、 提供与一个已命名终结点,并将其添加到该 WorkflowServiceHost 中承载 WorkflowRuntime 宿主代码。

图 6 添加到运行库 ChannelManagerService

ServiceEndpoint endpoint = new ServiceEndpoint
(
    ContractDescription.GetContract(typeof(Contracts.IHostInterface)),
        LocalBinding, new EndpointAddress(HostAddress)
);
endpoint.Name = "HostEndpoint";

WorkflowRuntime runtime =
    workflowHost.Description.Behaviors.Find<WorkflowRuntimeBehavior>().
WorkflowRuntime;

ChannelManagerService chanMan =
    new ChannelManagerService(
        new List<ServiceEndpoint>
        {
            endpoint
        });

runtime.AddService(chanMan);

让工作流服务承载提供能力从工作流的主机发送的邮件,但邮件回到主机,您需要一个 WCF 服务,可以从该工作流中接收消息。 此服务是标准 WCF 服务应用程序中 self-hosted。 因为该服务不是工作流服务,您可以使用标准 NetNamedPipeBinding 或重用以前所示 NetNamedPipeContextBinding。 最后,从该工作流中调用此服务,因为它可以被承载进行与用户界面元素的交互更简单的 UI 线程上。 图 7 显示服务的宿主代码。

图 7 驻留主机服务

ServiceHost appHost = new ServiceHost(new HostService());
appHost.AddServiceEndpoint("Contracts.IHostInterface",
LocalBinding, HostAddress);

try
{
    appHost.Open();
}
catch (Exception ex)
{
    appHost.Abort();
    MessageBox.Show(String.Format(
        "There was an error hosting the local service: {0}",
    ex.Message));
}

这两种在寄宿的服务与您现在可以运行工作流、 发送消息和接收回一条消息。 但是,如果试图发送第二个使用此代码以第二个邮件接收工作流中的活动,您会收到有关上下文错误。

处理实例相关

处理上下文问题的一种方法是对服务的每个调用均使用相同的客户端代理。 这使客户端代理来管理上下文标识符 (使用该 NetNamedPipeContextBinding) 和发送回与后续请求服务。

在某些方案中 ’s 不可以使周围的所有请求相同的代理。 请考虑这种情况在何处启动工作流、 保持到数据库并关闭客户端应用程序。 客户端应用程序启动时再次,您需要一种通过将另一条消息发送到该特定实例恢复工作流的方式。 其他常见用例是时要使用单一客户端代理,但需要与几个具有唯一的标识符的每个工作流实例进行交互。 渚嬪用户界面提供了每个都有一个相应工作流的订单的列表,当用户调用上所选订单的动作时, 需要将消息发送到工作流实例。 让管理上下文标识符绑定将不起作用鍦 ㄨ 繖绉嶆儏鍐典笅因为它将始终使用最后一个工作流与之交互的标识符。

对于第一个方案 — — 每个调用中使用新的代理 — 您需要手动到上下文中使用 IContextManager 界面设置工作流标识符。 IContextManager 访问通过 GetProperty < TProperty > 方法 IClientChannel 接口上。 之后该 IContextManager 可以使用它来获取或设置上下文。

上下文本身是其中最重要的是 instanceId 值名称-值对的字典。 下面的代码演示如何检索 ID 从上下文中以便它可以存储由您的客户端应用程序,以便以后时需要相同的工作流实例进行交互。 本示例在 ID 被显示在客户端用户界面,而不是存储在数据库中:

IContextManager mgr = ((IClientChannel)proxy).GetProperty<IContextManager>();
      
string wfID = mgr.GetContext()["instanceId"];
wfIdText.Text = wfID;

一旦使第一个调用到工作流服务,上下文是自动填充该工作流的实例 ID 由上下文绑定,在服务终结点上。

使用新创建的代理服务器进行通信与先前创建的工作流实例时, 可以使用类似的方法以确保您的邮件被路由到正确的工作流实例,如下所示上下文中设置该标识符:

IContextManager mgr = ((IClientChannel)proxy).GetProperty<IContextManager>();
  mgr.SetContext(new Dictionary<string, string>{
    {"instanceId", wfIdText.Text}
  });

当您第一次有新创建的代理好此代码的工作原理,但不是如果试图设置上下文用于调用另一个工作流实例第二次。 您收到该错误会告诉您启用自动上下文管理后您不能更改上下文。 实质上是,您是告诉您 can’t 有您的蛋糕,并且它太吃。 濡傛灉鎮 ㄦ 兂来自动管理上下文您 can’t 手动操作。 遗憾的是,如果想手动管理上下文失败获取意味着您不能从上下文中检索工作流实例 ID,如我先前介绍的自动管理。

若要处理此不匹配,分别处理每种情况。 对于要工作流与初始呼叫,使用新的代理,但对于现有的工作流实例的所有后续调用,使用单个客户端代理和手动管理上下文。

为初始呼叫您应该使用单个 ChannelFactory < TChannel > 创建所有代理。 这导致更好的性能,因为该 ChannelFactory 的创建都有一些不想为每个第一个调用重复的开销。 使用在前面所示 的 图 5 类似的代码可以使用单个 ChannelFactory < TChannel > 创建初始的代理。 在您调用的代码中使用该代理后应当按照调用 Close 方法以释放该代理的最佳做法。

这是用于创建使用通道工厂方法您代理的标准 WCF 代码。 因为绑定是上下文绑定,您将得到意味着您可以从上下文中提取工作流实例标识符进行该工作流在第一次调用之后的默认自动上下文管理。

为使后续调用,您需要自己,管理上下文和这必然伴 WCF 客户端代码而不是经常使用的开发人员使用。 若要手动设置上下文,需要使用一个 OperationContextScope 自己创建该 MessageContextProperty。 在 MessageContextProperty 设置邮件上它正在发送,这等效于使用该 IContextManager 设置上下文与直接使用该属性适用即使上下文管理禁用该异常。 图 8 显示了代码以创建使用该同一 ChannelFactory < TChannel > 用于初始的代理服务器的代理。 不同之处在于在这种情况下在 IContextManager 用来禁用自动上下文管理功能和使用缓存的代理而不是创建一个新的每个请求上。

图 8 禁用自动上下文管理

App a = (App)Application.Current;

if (updateProxy == null)
{
    if (factory == null)
        factory = new ChannelFactory<IWorkflowInterface>(
            a.LocalBinding, a.WFAddress);

        updateProxy = factory.CreateChannel();
        IContextManager mgr =
            ((IClientChannel)updateProxy).GetProperty<IContextManager>();
        mgr.Enabled = false;
        ((IClientChannel)updateProxy).Open();
}

一旦在创建该代理需要创建一个 OperationContextScope 和传出的邮件属性,在作用域添加在 MessageContextProperty。 这使作用域的持续时间的过程中将包括在传出邮件上的属性。 图 9 显示了代码以创建和使用该 OperationContextScope 消息属性设置。

图 9 使用 OperationContextScope

using (OperationContextScope scope =
    new OperationContextScope((IContextChannel)proxy))
{
    ContextMessageProperty property = new ContextMessageProperty(
        new Dictionary<string, string>
        {
            {“instanceId”, wfIdText.Text}
        });

OperationContext.Current.OutgoingMessageProperties.Add(
    "ContextMessageProperty", property);

proxy.UpdateOrder(
    new Order
        {
            CustomerName = "Matt",
            OrderID = 2,
            OrderTotal = 250.00,
            OrderStatus = "Updated"
        });
}

这可能看起来相当多的位的只是要与主机和工作流之间的工作。 好消息是大部分此逻辑和管理的标识符可以封装在几个类中。 但是,它涉及编写您的客户端代码以确保为这些情况下,需要将多个消息发送到工作流实例正确管理上下文特定的方式。 在本文中在代码下载我已经包含一个示例主机为使用本地通信试图封装在复杂性的很多的工作流和示例应用程序演示如何使用主机。

关于用户界面交互 Word

将数据从该工作流发送到主机的主要原因之一是您要向用户应用程序界面中显示它。 幸运的是,与此模型有一些选项,以利用在 WPF 中包括数据绑定的用户界面功能。 作为简单的示例如果您希望您的用户界面,可以使用数据绑定和更新用户界面数据接收来自工作流时您可以在用户界面直接绑定到主机已经服务实例。

为您的窗口作为数据上下文中使用的服务实例的关键是该实例需要作为单一实例为宿主。 时承载作为单一实例服务,您有权访问该实例,并可以在您的 UI 中使用它。 的 图 10 所示的简单主机服务更新属性时它接收从该工作流的信息,并使用该 INotifyPropertyChangedInterface 帮助立即挑选所做的更改,数据绑定基础结构。 请注意 ServiceBehavior 属性,指示此类应作为单一实例为宿主。 如果为 的 图 7 重新查看您可以看到实例化不具有类型,但与类的实例 ServiceHost。

图 10 的 服务实现与 INotifyPropertyChanged

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
internal class HostService : IHostInterface, INotifyPropertyChanged
{
    public void OrderStatusChange(Order order, string newStatus,
        string oldStatus)
    {
        CurrentMessage = String.Format("Order status changed to {0}",
            newStatus);
    }

private string msg;

public string CurrentMessage {
get { return msg; }
set
    {
        msg = value;
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(
                "CurrentMessage"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

要为此值的数据绑定,可以 DataContext 窗口或窗口中的某个特定控件的设置与该实例。 可以通过使用 ServiceHost 类上的 SingletonInstance 属性检索该实例,如下所示:

HostService host = ((App)Application.Current).appHost.SingletonInstance as HostService;
  if (host != null)
    this.DataContext = host;

现在您可以将只需绑定元素在您的窗口中到该对象上的属性与此 TextBlock 所示:

<TextBlock Text="{Binding CurrentMessage}" Grid.Row="3" />

我说这是您可以执行的操作的简单示例。 在实际的应用程序您可能会不直接绑定到该服务实例,但而是将绑定到哪个同时您的窗口和服务实现有访问某些对象。

查找领先于 WF4

WF4 引入了一些功能,可以将本地通信通过 WCF 更加轻松。 主要功能是不依赖于该协议的消息相关。 也就是工作流实例标识符的使用仍将一个选项,但新的选项将启用相关是根据邮件内容的消息。 因此,如果您的邮件的每个包含订单 ID、 客户 ID 或的数据的某些片段,您可以定义这些消息之间的关联,并没有使用支持上下文管理的绑定。

此外,WPF 和 WF 建立在相同的核心 XAML api 在.net Framework 版本 4 上的事实可能打开了一些有趣的可能性,将该技术集成中的新方法。 如我们更接近于发布的.net Framework 4 将在上连同其他内容上 WF4 的内部的工作原理与 WCF 和 WPF,集成 WF 提供更多详细信息。

Matt Milner 是的 Pluralsight,其中他侧重于连接的系统技术 (WCF、 Windows 流基础、 BizTalk、 “ 都柏林,” 和 Azure 服务平台) 在技术人员的成员。 Matt 也是一个独立的顾问 specializing 中 Microsoft.net 应用程序设计和开发。 他定期共享他爱技术通过在本地、 地区和国际会议,如 Tech·Ed 说话。 Microsoft 已识别 Milner 为连接的系统技术周围他团体的贡献的 MVP。 您可以通过在 pluralsight.com/community/blogs/matt 他博客与他联系。