Как правильно загружать рабочий процесс WF4 из XAML?

Краткая версия:

Как загрузить рабочий процесс WF4 из XAML? Важная деталь: код, загружающий рабочий процесс, не обязательно должен знать, какие типы используются в рабочем процессе.


Длинная версия:

Мне очень тяжело загружать рабочий процесс WF4 из файла XAML, создаваемого Visual Studio. Мой сценарий заключается в том, что я хочу поместить этот файл в базу данных, чтобы иметь возможность его централизованно изменять без перекомпиляции Invoke 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}

Это можно решить, используя описанный метод здесь, который в основном добавляет сборки и пространства имен всех используемых типов к экземпляру 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

Это работает, но делает всю "динамическую загрузку" части Workflow шуткой, поскольку код все еще должен знать все используемые пространства имен.
Вопрос 1: Есть ли другой способ избавиться от этих ошибок проверки без необходимости заранее знать, какие пространства имен и сборки используются?

Категория 2:
Все мои входные аргументы неизвестны. Я могу видеть их просто отлично в activityBuilder.Properties, но я все еще получаю ошибки проверки, говоря, что они неизвестны:

ValidationError {Message = ошибка компилятора, с которой встречается выражение обработки "Штырь". "Pin" не объявлен. Он может быть недоступен из-за его уровня защиты., Source = 61: VisualBasicValue, PropertyName =, IsWarning = False}

Пока нет решения.
Вопрос 2: Как сообщить WF4 использовать аргументы, определенные в файле XAML?

Ответ 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 не может быть выполнен), поэтому у меня появился собственный тип активности, который внутренне преобразует один тип в другой. Если вы хотите посмотреть мои результаты, прочитайте последние разделы этой статьи.

Ответ 2

У меня есть проект, который делает это - сборки также хранятся в базе данных.

Когда пришло время создать экземпляр рабочего процесса, я делаю следующее:

  • Загрузите сборки из базы данных в расположение кеша
  • Создайте новый AppDomain, передающий ему пути сборки.
  • Из новой загрузки 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 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 и служит вашей линией связи между вашим приложением и новым appdomain.

Я нахожу, что я могу загружать рабочие процессы и получать экземпляр корневой активности без каких-либо проблем.

РЕДАКТИРОВАТЬ 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 с помощью пространств имен и повторно примените их к корневой активности.

Вам нужно будет сделать это в проекте, который выполняет экземпляры рабочего процесса и в проекте, который переопределяет конструктор рабочего процесса.

Ответ 3

Одна система, которую я знаю, выполняет ту же работу, которую вы пытаетесь сделать, это система сборки 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

подход.

Ответ 4

Вот как я загружаю xaml-встроенный ресурс (рабочий процесс по умолчанию) в конструктор Workflow:

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