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

时间:2012-07-17 05:51:30

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

简短版本:

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


长版:

我很难从Visual Studio创建的XAML文件加载WF4工作流程。 我的方案是我想把这个文件放到数据库中,以便能够集中修改它,而无需重新编译Workflow调用程序。

我目前正在使用此代码:

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文件中定义的参数?

4 个答案:

答案 0 :(得分: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无法执行),所以我最终得到了一个自己的活动类型,它在内部将一种类型转换为另一种类型。如果您想查看我的结果,请阅读this article的最后几部分。

答案 1 :(得分:4)

我有一个项目可以执行此操作 - 程序集也存储在数据库中。

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

  1. 将程序集从数据库下载到缓存位置
  2. 创建一个新的AppDomain,将汇编路径传递给它。
  3. 从新的AppDomain加载每个程序集 - 您可能还需要加载托管环境所需的程序集。
  4. 我不需要乱用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.Activities
    • System.Core程序
    • 的System.Xml

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

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

答案 2 :(得分:1)

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

您提到需要将文件保存在数据库中。您是否还可以在同一数据库中存储有关所需程序集的位置或元数据信息,并在调用工作流之前使用Reflection以递归方式加载它们?

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

更改动态加载程序集的代码
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

方法

答案 3 :(得分:1)

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

//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");
        }
    }