13

简洁版本:

如何从 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”。由于其保护级别,它可能无法访问。, 源 = 10: VisualBasicValue, PropertyName = , IsWarning = False }

这可以通过使用此处描述的技术来解决,该技术基本上将所有使用类型的程序集和命名空间添加到某个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”。'Pin' 未声明。由于其保护级别,它可能无法访问。, 源 = 61: VisualBasicValue, PropertyName = , IsWarning = False }

至今没有解决办法。
问题 2:如何告诉 WF4 使用 XAML 文件中定义的参数?

4

4 回答 4

11

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

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

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

于 2012-07-25T20:58:41.147 回答
4

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

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

  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
  • 系统
  • 系统活动
  • 系统核心
  • 系统文件

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

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

于 2012-07-21T02:21:28.490 回答
1

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

方法。

于 2012-07-19T13:42:09.427 回答
1

这是我将 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");
        }
    }
于 2016-05-18T06:51:29.137 回答