【问题标题】:How to correctly load a WF4 workflow from XAML?如何从 XAML 正确加载 WF4 工作流?
【发布时间】:2012-07-17 05:51:30
【问题描述】:

短版:

如何从 XAML 加载 WF4 工作流? 重要细节:加载工作流的代码不需要事先知道工作流中使用了哪些类型。


长版:

我很难从 Visual Studio 创建的 XAML 文件加载 WF4 工作流。 我的场景是我想把这个文件放到数据库中,以便能够在不重新编译工作流调用程序的情况下集中修改它。

我目前正在使用此代码:

var xamlSchemaContext = new XamlSchemaContext(GetWFAssemblies());
var xmlReaderSettings = new XamlXmlReaderSettings();
xmlReaderSettings.LocalAssembly = typeof(WaitForAnySoundActivity).Assembly;
var xamlReader = ActivityXamlServices.CreateBuilderReader(
                     new XamlXmlReader(stream, xmlReaderSettings), 
                     xamlSchemaContext);

var activityBuilder = (ActivityBuilder)XamlServices.Load(xamlReader);
var activity = activityBuilder.Implementation;
var validationResult = ActivityValidationServices.Validate(activity);

这给了我很多错误,分为两类:

第 1 类:
我的程序集的类型未知,尽管我向XamlSchemaContext 的构造函数提供了正确的程序集。

ValidationError { Message = 编译器错误遇到处理表达式“GreetingActivationResult.WrongPin”。 未声明“GreetingActivationResult”。由于其保护级别,它可能无法访问。 , Source = 10: VisualBasicValue, PropertyName = , IsWarning = False }

这可以通过使用here 描述的技术来解决,该技术基本上将所有使用类型的程序集和命名空间添加到一些VisualBasicSettings 实例中:

var settings = new VisualBasicSettings();
settings.ImportReferences.Add(new VisualBasicImportReference
{
    Assembly = typeof(GreetingActivationResult).Assembly.GetName().Name,
    Import = typeof(GreetingActivationResult).Namespace
}); 
// ...
VisualBasic.SetSettings(activity, settings);
// ... Validate here

这可行,但会使工作流的整个“动态加载”部分成为一个笑话,因为代码仍然需要知道所有使用的命名空间。
问题 1:还有其他方法可以得到无需事先知道使用了哪些命名空间和程序集即可消除这些验证错误?

第 2 类:
我所有的输入参数都是未知的。我可以在 activityBuilder.Properties 中很好地看到它们,但我仍然收到验证错误,说它们是未知的:

ValidationError { Message = 遇到编译器错误处理表达式 “别针”。 'Pin' 未声明。由于其保护级别,它可能无法访问。 , Source = 61: VisualBasicValue, PropertyName = , IsWarning = False }

目前没有解决方案。
问题 2:如何告诉 WF4 使用 XAML 文件中定义的参数?

【问题讨论】:

    标签: c# .net workflow-foundation-4 workflow-foundation


    【解决方案1】:

    问题 2: 您不能执行 ActivityBuilder,它只是用于设计。您必须加载 DynamicActivity(仅通过 ActivityXamlServices)。它应该以这种方式工作(不使用特殊的 XamlSchemaContext),但您必须提前加载所有使用的程序集(将它们放在 bin 目录中也应该有效,到目前为止,关于问题 1,DynamicActivity 可能会使事情变得更容易一些):

    var dynamicActivity = ActivityXamlServices.Load(stream) as DynamicActivity;
    WorkflowInvoker.Invoke(dynamicActivity);
    

    总的来说,我的印象是您正在尝试实现自己的“ActivityDesigner”(如 VS)。我自己尝试过,处理 DynamicActivity 和 ActivityBuilder 非常困难(因为 DynamicActivity 不可序列化但 ActivityBuilder 无法执行),所以我最终得到了一个自己的活动类型,它在内部将一种类型转换为另一种类型。如果你想看看我的结果,请阅读this article 的最后几节。

    【讨论】:

    • 感谢您的回答。几天前我已经偶然发现了ActivityXamlServices.Load,但其他错误掩盖了这样一个事实,即这是从 XAML 加载活动的正确做法。屏蔽错误之一是ActivityBuilder.Implementation 等同于从 XAML 加载的动态活动。我实际上使用了与您的代码项目文章中描述的代码类似的代码,将加载的 DynamicActivity 转换为 ActivityBuilder。问题是我在加载后立即执行此操作,并在需要 Activity 时使用ab.Implementation
    • 但是,我现在遇到的问题是它只适用于 VS 创建的 XAML。它不适用于由XamlServices.Save 创建的 XAML,如您的文章中所述...我仍然收到我在问题中显示的相同错误消息...有什么想法吗?
    • 我为它创建了一个新问题:stackoverflow.com/questions/11663896/…。如果你能看一下就好了。
    【解决方案2】:

    我有一个这样做的项目 - 程序集也存储在数据库中。

    当需要实例化工作流实例时,我会执行以下操作:

    1. 将程序集从数据库下载到缓存位置
    2. 创建一个新的 AppDomain,将程序集路径传入其中。
    3. 从新的 AppDomain 加载每个程序集 - 您可能还需要加载托管环境所需的程序集。

    我不需要弄乱 VisualBasic 设置 - 至少就我所见,我快速浏览了我的代码,但我确定我在某处看到过它......

    在我的情况下,虽然我不知道输入名称或类型,但调用者应该构建一个包含输入名称和值(作为字符串)的请求,然后通过反射帮助器将其转换为正确的类型类。

    此时我可以实例化工作流。

    我的 AppDomain 初始化代码如下所示:

            /// <summary>
            /// Initializes a new instance of the <see cref="OperationWorkflowManagerDomain"/> class.
            /// </summary>
            /// <param name="requestHandlerId">The request handler id.</param>
            public OperationWorkflowManagerDomain(Guid requestHandlerId)
            {
                // Cache the id and download dependent assemblies
                RequestHandlerId = requestHandlerId;
                DownloadAssemblies();
    
                if (!IsIsolated)
                {
                    Domain = AppDomain.CurrentDomain;
                    _manager = new OperationWorkflowManager(requestHandlerId);
                }
                else
                {
                    // Build list of assemblies that must be loaded into the appdomain
                    List<string> assembliesToLoad = new List<string>(ReferenceAssemblyPaths);
                    assembliesToLoad.Add(Assembly.GetExecutingAssembly().Location);
    
                    // Create new application domain
                    // NOTE: We do not extend the configuration system
                    //  each app-domain reuses the app.config for the service
                    //  instance - for now...
                    string appDomainName = string.Format(
                        "Aero Operations Workflow Handler {0} AppDomain",
                        requestHandlerId);
                    AppDomainSetup ads =
                        new AppDomainSetup
                        {
                            AppDomainInitializer = new AppDomainInitializer(DomainInit),
                            AppDomainInitializerArguments = assembliesToLoad.ToArray(),
                            ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
                            PrivateBinPathProbe = null,
                            PrivateBinPath = PrivateBinPath,
                            ApplicationName = "Aero Operations Engine",
                            ConfigurationFile = Path.Combine(
                                AppDomain.CurrentDomain.BaseDirectory, "ZenAeroOps.exe.config")
                        };
    
                    // TODO: Setup evidence correctly...
                    Evidence evidence = AppDomain.CurrentDomain.Evidence;
                    Domain = AppDomain.CreateDomain(appDomainName, evidence, ads);
    
                    // Create app-domain variant of operation workflow manager
                    // TODO: Handle lifetime leasing correctly
                    _managerProxy = (OperationWorkflowManagerProxy)Domain.CreateInstanceAndUnwrap(
                        Assembly.GetExecutingAssembly().GetName().Name,
                        typeof(OperationWorkflowManagerProxy).FullName);
                    _proxyLease = (ILease)_managerProxy.GetLifetimeService();
                    if (_proxyLease != null)
                    {
                        //_proxyLease.Register(this);
                    }
                }
            }
    

    下载程序集代码很简单:

            private void DownloadAssemblies()
            {
                List<string> refAssemblyPathList = new List<string>();
                using (ZenAeroOpsEntities context = new ZenAeroOpsEntities())
                {
                    DbRequestHandler dbHandler = context
                        .DbRequestHandlers
                        .Include("ReferenceAssemblies")
                        .FirstOrDefault((item) => item.RequestHandlerId == RequestHandlerId);
                    if (dbHandler == null)
                    {
                        throw new ArgumentException(string.Format(
                            "Request handler {0} not found.", RequestHandlerId), "requestWorkflowId");
                    }
    
                    // If there are no referenced assemblies then we can host
                    //  in the main app-domain
                    if (dbHandler.ReferenceAssemblies.Count == 0)
                    {
                        IsIsolated = false;
                        ReferenceAssemblyPaths = new string[0];
                        return;
                    }
    
                    // Create folder
                    if (!Directory.Exists(PrivateBinPath))
                    {
                        Directory.CreateDirectory(PrivateBinPath);
                    }
    
                    // Download assemblies as required
                    foreach (DbRequestHandlerReferenceAssembly dbAssembly in dbHandler.ReferenceAssemblies)
                    {
                        AssemblyName an = new AssemblyName(dbAssembly.AssemblyName);
    
                        // Determine the local assembly path
                        string assemblyPathName = Path.Combine(
                            PrivateBinPath,
                            string.Format("{0}.dll", an.Name));
    
                        // TODO: If the file exists then check it's SHA1 hash
                        if (!File.Exists(assemblyPathName))
                        {
                            // TODO: Setup security descriptor
                            using (FileStream stream = new FileStream(
                                assemblyPathName, FileMode.Create, FileAccess.Write))
                            {
                                stream.Write(dbAssembly.AssemblyPayload, 0, dbAssembly.AssemblyPayload.Length);
                            }
                        }
                        refAssemblyPathList.Add(assemblyPathName);
                    }
                }
    
                ReferenceAssemblyPaths = refAssemblyPathList.ToArray();
                IsIsolated = true;
            }
    

    最后是 AppDomain 初始化代码:

            private static void DomainInit(string[] args)
            {
                foreach (string arg in args)
                {
                    // Treat each string as an assembly to load
                    AssemblyName an = AssemblyName.GetAssemblyName(arg);
                    AppDomain.CurrentDomain.Load(an);
                }
            }
    

    您的代理类需要实现 MarshalByRefObject 并充当您的应用与新应用域之间的通信链接。

    我发现我能够毫无问题地加载工作流并获取根活动实例。

    29/07/12 编辑 **

    即使您只将 XAML 存储在数据库中,您也需要跟踪引用的程序集。您的引用程序集列表将按名称在附加表中进行跟踪,或者您必须上传(并且显然支持下载)工作流引用的程序集。

    然后您可以简单地枚举所有引用程序集并将所有公共类型的所有命名空间添加到 VisualBasicSettings 对象 - 就像这样...

                VisualBasicSettings vbs =
                    VisualBasic.GetSettings(root) ?? new VisualBasicSettings();
    
                var namespaces = (from type in assembly.GetTypes()
                                  select type.Namespace).Distinct();
                var fullName = assembly.FullName;
                foreach (var name in namespaces)
                {
                    var import = new VisualBasicImportReference()
                    {
                        Assembly = fullName,
                        Import = name
                    };
                    vbs.ImportReferences.Add(import);
                }
                VisualBasic.SetSettings(root, vbs);
    

    最后不要忘记从环境程序集中添加命名空间 - 我从以下程序集中添加命名空间:

    • mscorlib
    • 系统
    • System.Activity
    • System.Core
    • System.Xml

    总之:
    1. 跟踪用户工作流引用的程序集(因为您将重新托管工作流设计器,这将是微不足道的)
    2. 构建将导入命名空间的程序集列表 - 这将是默认环境程序集和用户引用程序集的联合。
    3. 使用命名空间更新 VisualBasicSettings 并重新应用到根 Activity。

    您需要在执行工作流实例的项目和重新托管工作流设计器的项目中执行此操作。

    【讨论】:

    • 非常感谢您的回答。您在这里谈论的是程序集,而不是 XAML。您能否澄清一下工作流以哪种格式存储?它是否以纯文本 (XAML) 形式存储在数据库中?还是包含在其中一个程序集中?如果存储为纯文本,能否请您显示加载此 XAML 的代码?
    • 在我的例子中,我将编译后的工作流程序集存储在数据库中,尽管您提醒我在哪里看到 VisualBasic 设置的东西 - 它在重新托管 WF 设计器的代码中!
    • 在我的情况下,我想将 XAML 存储在数据库中,而不是编译的程序集,因为在第二步中,我想重新托管 WF 设计器并让用户创建自己的工作流...
    • 编辑了我的原始答案,以包含有关初始化命名空间列表和跟踪引用程序集的更多信息。
    【解决方案3】:

    Team Foundation 2010 的构建系统是我知道的与您尝试做的工作相同的系统。在控制器上执行自定义构建工作流时,需要将构建控制器指向 TFS 中保存自定义程序集的路径。然后,控制器在开始处理工作流时递归地从该位置加载所有程序集。

    您提到您需要将文件保存在数据库中。您是否也不能将有关所需程序集的位置或元数据信息存储在同一个数据库中,并在调用工作流之前使用反射递归地加载它们?

    然后,您可以有选择地从此路径添加/删除程序集,而无需更改使用

    动态加载程序集的代码
    var settings = new VisualBasicSettings();
    settings.ImportReferences.Add(new VisualBasicImportReference
    {
        Assembly = typeof(GreetingActivationResult).Assembly.GetName().Name,
        Import = typeof(GreetingActivationResult).Namespace
    }); 
    // ...
    VisualBasic.SetSettings(activity, settings);
    // ... Validate here
    

    接近。

    【讨论】:

    • 感谢您的回答。问题是,恕我直言,所有信息都已包含在 XAML 中。为什么在加载代码时需要在代码中重复大量此类信息?此外,不幸的是,您的回答没有回答问题 2。但是这个问题具有更高的优先级,因为到目前为止我还没有找到解决方案......
    • 您说的很对,所有信息都已经存储在 XAML 本身中,因此您可能希望先将其作为纯 XML 文件读取,在开始之前解析命名空间并加载相关的 DLL WF 执行。至于问题 2,您将不得不详细说明一下。请问你的意图是什么?
    • 我不想编写 WF XAML 解析器。我很确定存在一种“正确”的方法来做到这一点。关于问题 2:嗯,我正在尝试验证或运行我的工作流程,但由于错误,这是不可能的。问题是,当它们在 VB 表达式的活动中使用时,它无法识别定义为 InArgumentOutArgument 的属性......
    • 您不需要完整的 WF XAML 解析器。您可以使用 XElement.Parse,然后遍历 XML 树并收集所有 Xmlns 命名空间。
    • @TimLovell-Smith:这无关紧要,因为问题 2 没有解决。
    【解决方案4】:

    这是我将 xaml 嵌入资源(默认工作流)加载到工作流设计器的方式:

    //UCM.WFDesigner is my assembly name, 
    //Resources.Flows is the folder name, 
    //and DefaultFlow.xaml is the xaml name. 
     private const string ConstDefaultFlowFullName = @"UCM.WFDesigner.Resources.Flows.DefaultFlow.xaml";
          private void CreateNewWorkflow(object param)
        {
    
            //loading default activity embeded resource
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ConstDefaultFlowFullName))
            {
                StreamReader sReader = new StreamReader(stream);
                string content = sReader.ReadToEnd();
                //createion ActivityBuilder from string
                ActivityBuilder activityBuilder = XamlServices.Load( ActivityXamlServices
                    .CreateBuilderReader(new XamlXmlReader(new StringReader(content)))) as ActivityBuilder;
                //loading new ActivityBuilder to Workflow Designer
                _workflowDesigner.Load(activityBuilder);
                OnPropertyChanged("View");
            }
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多