简体   繁体   English

如何从XAML正确加载WF4工作流?

[英]How to correctly load a WF4 workflow from XAML?

Short version: 简洁版本:

How do I load a WF4 workflow from XAML? 如何从XAML加载WF4工作流程? Important detail: The code that loads the workflow shouldn't need to know beforehand which types are used in the workflow. 重要细节:加载工作流的代码无需事先知道工作流中使用的类型。


Long version: 长版:

I am having a very hard time loading a WF4 workflow from the XAML file create by Visual Studio. 我很难从Visual Studio创建的XAML文件中加载WF4工作流。 My scenario is that I want to put this file into the database to be able to modify it centrally without recompiling the Workflow invoker. 我的情况是,我希望将此文件放入数据库中,以便能够在不重新编译工作流调用程序的情况下进行集中修改。

I am currently using this code: 我目前正在使用此代码:

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);

This gives me a whole lot of errors, which fall into two categories: 这给了我很多错误,分为两类:

Category 1: 第1类:
Types from my assemblies are not known, although I provided the correct assemblies to the constructor of XamlSchemaContext . 尽管我向XamlSchemaContext的构造函数提供了正确的程序集,但程序集的类型未知。

ValidationError { Message = Compiler error(s) encountered processing expression "GreetingActivationResult.WrongPin". ValidationError {消息=处理处理表达式“ GreetingActivationResult.WrongPin”时遇到编译器错误。 'GreetingActivationResult' is not declared. 未声明“ GreetingActivationResult”。 It may be inaccessible due to its protection level. 由于其保护级别,它可能无法访问。 , Source = 10: VisualBasicValue, PropertyName = , IsWarning = False } ,来源= 10:VisualBasicValue,PropertyName =,IsWarning = False}

This can be solved by using the technique described here , which basically adds the assemblies and namespaces of all used types to some VisualBasicSettings instance: 这可以通过使用此处描述的技术来解决,该技术基本上将所有使用的类型的程序集和名称空间添加到某些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

This works but makes the whole "dynamic loading" part of the Workflow a joke, as the code still needs to know all used namespaces. 这可以工作,但是使工作流的整个“动态加载”部分成为一个笑话,因为代码仍然需要知道所有使用的名称空间。
Question 1: Is there another way to get rid of these validation errors without the need to know beforehand which namespaces and assemblies are used? 问题1:是否有另一种摆脱这些验证错误的方法,而无需事先知道使用了哪些名称空间和程序集?

Category 2: 类别2:
All my input arguments are unknown. 我所有的输入参数都是未知的。 I can see them just fine in activityBuilder.Properties but I still get validation errors saying they are unknown: 我可以在activityBuilder.Properties看到它们很好,但是仍然收到验证错误,指出它们是未知的:

ValidationError { Message = Compiler error(s) encountered processing expression "Pin". ValidationError {消息=处理表达式“ Pin”时遇到编译器错误。 'Pin' is not declared. 未声明“ Pin”。 It may be inaccessible due to its protection level. 由于其保护级别,它可能无法访问。 , Source = 61: VisualBasicValue, PropertyName = , IsWarning = False } ,来源= 61:VisualBasicValue,PropertyName =,IsWarning = False}

No solution so far. 到目前为止没有解决方案。
Question 2: How to tell WF4 to use the arguments defined in the XAML file? 问题2:如何告诉WF4使用XAML文件中定义的参数?

Question 2: You can´t execute an ActivityBuilder, it´s just for design. 问题2:您无法执行ActivityBuilder,仅用于设计。 You have to load a DynamicActivity (only through ActivityXamlServices). 您必须加载DynamicActivity(仅通过ActivityXamlServices)。 It should work that way (without using a special XamlSchemaContext), but you must have loaded all used assemblies in advance (placing them in the bin directory should also work, so far about Question 1, DynamicActivity might make things a little bit easier): 它应该以这种方式工作(不使用特殊的XamlSchemaContext),但是您必须事先加载所有使用的程序集(将它们放置在bin目录中也应该工作,到目前为止,关于问题1,DynamicActivity可能会使事情变得容易一些):

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

In general, I got the impression that you´re trying to implement your own "ActivityDesigner" (like VS). 总的来说,我给您的印象是您正在尝试实现自己的“ ActivityDesigner”(如VS)。 I tried this myself, and it was quite hard to deal with DynamicActivity and ActivityBuilder (as DynamicActivity is not serializable but ActivityBuilder cannot be executed), so I ended up with an own activity type that internally converts one type into the other. 我自己尝试了一下,因此很难处理DynamicActivity和ActivityBuilder(因为DynamicActivity无法序列化,但无法执行ActivityBuilder),所以我最终得到了自己的活动类型,该类型在内部将一种类型转换为另一种类型。 If you want to have a look at my results, read the last sections of this article . 如果你想看看我的成果,阅读的最后部分这篇文章

I have a project that does this - the assemblies are also stored in a database. 我有一个执行此操作的项目-程序集也存储在数据库中。

When it is time to instantiate a workflow instance I do the following: 当需要实例化工作流实例时,请执行以下操作:

  1. Download the assemblies from the database to a cache location 将程序集从数据库下载到缓存位置
  2. Create a new AppDomain passing the assembly paths into it. 创建一个新的AppDomain,将装配路径传递到其中。
  3. From the new AppDomain load each assembly - you may also need to load assemblies required by your hosting environment too. 从新的AppDomain加载每个程序集-您可能还需要加载托管环境所需的程序集。

I didn't need to mess around with VisualBasic settings - at least as far as I can see having taken a quick look in my code but I'm sure I've seen it somewhere... 我不需要弄乱VisualBasic设置-至少就我所看到的快速浏览我的代码而言,但我敢肯定我已经在某个地方看到它了...

In my case while I don't know the input names or types, the caller is expected to have built a request that contains the input names and values (as strings) which are then converted into the correct types via a reflection helper class. 在我的情况下,虽然我不知道输入名称或类型,但调用方应建立一个包含输入名称和值(作为字符串)的请求,然后再通过反射帮助器类将其转换为正确的类型。

At this point I can instantiate the workflow. 至此,我可以实例化工作流程了。

My AppDomain initialisation code looks like this: 我的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);
                }
            }
        }

The download assemblies code is easy enough: 下载程序集代码非常简单:

        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;
        }

And finally the AppDomain initialisation code: 最后是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);
            }
        }

Your proxy class needs to implement MarshalByRefObject and serves as your communication link between your app and the new appdomain. 您的代理类需要实现MarshalByRefObject,并充当您的应用程序与新的appdomain之间的通信链接。

I find that I am able to load workflows and get the root activity instance without any problem. 我发现我能够加载工作流并获得根活动实例,而没有任何问题。

EDIT 29/07/12 ** 编辑2012年12月29日**

Even if you only store the XAML in the database you will need to track the referenced assemblies. 即使仅将XAML存储在数据库中,也需要跟踪引用的程序集。 Either your list of referenced assemblies will tracked in an additional table by name or you will have to upload (and obviously support download) the assemblies referenced by the workflow. 您引用的程序集列表将按名称在另一个表中跟踪,或者您必须上载(显然支持下载)工作流引用的程序集。

Then you may simply enumerate all the reference assemblies and add ALL namespaces from ALL public types to the VisualBasicSettings object - like this... 然后,您可以简单地枚举所有引用程序集,并将所有公共类型的所有名称空间添加到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);

Finally don't forget to add namespaces from the environment assemblies - I add namespaces from the following assemblies: 最后,不要忘记从环境程序集中添加名称空间-我从以下程序集中添加名称空间:

  • mscorlib mscorlib
  • System 系统
  • System.Activities 系统活动
  • System.Core 系统核心
  • System.Xml 系统文件

So in summary: 因此,总而言之:
1. Track the assembly referenced by the user's workflow (since you will be rehosting the workflow designer this will be trivial) 1.跟踪用户工作流引用的程序集(因为您将重新托管工作流设计器,所以这很简单)
2. Build a list of assemblies from which namespaces will be imported - this will be a union of the default environment assemblies and the user referenced assemblies. 2.建立一个程序集列表,从中导入名称空间-这将是默认环境程序集和用户引用的程序集的并集。
3. Update the VisualBasicSettings with the namespaces and reapply to the root activity. 3.使用名称空间更新VisualBasicSettings,然后重新应用于根活动。

You will need to do this in the project that executes workflow instances and in the project that rehosts the workflow designer. 您将需要在执行工作流实例的项目中以及在重新托管工作流设计器的项目中执行此操作。

One system that I know which does the same job that you are trying to do is the Team Foundation 2010's build system. 我知道可以完成与您要完成的任务相同的一个系统是Team Foundation 2010的构建系统。 When you execute a custom build workflow on a controller, you need to point the build controller to a path in TFS where you keep your custom assemblies. 在控制器上执行自定义构建工作流时,需要将构建控制器指向TFS中保留自定义程序集的路径。 The controller then recursively loads up all the assemblies from that location as it starts processing the workflow. 然后,控制器在开始处理工作流程时从该位置递归加载所有程序集。

You mentioned that you need to keep the file in a database. 您提到需要将文件保存在数据库中。 Can you not also store the location or meta data information about the required assemblies in the same database and use Reflection to load them recursively before you invoke your workflow? 您是否也不能在同一数据库中存储有关所需程序集的位置或元数据信息,并在调用工作流之前使用Reflection递归地加载它们?

You can then selectively add/remove assemblies from this path without having to alter the code that dynamically load assemblies using the 然后,您可以有选择地从此路径添加/删除程序集,而不必更改使用以下代码动态加载程序集的代码:

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

approach. 方法。

This is the way how I load xaml embeded resource (default workflow) to a Workflow Designer: 这是我将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");
        }
    }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM