Разработка рабочих процессов, действий и выражений с помощью императивного кода
Определение рабочего процесса представляет собой дерево настроенных объектов действий. Это дерево действий можно определить многими способами, включая редактирование XAML вручную и использование конструктора рабочих процессов для создания XAML. Но использование XAML не является обязательным. Определения рабочих процессов могут быть также созданы вручную. В этом разделе представлены общие сведения о создании определений, действий и выражений рабочего процесса с помощью кода. Примеры работы с рабочими процессами XAML с помощью кода см. в статье сериализация рабочих процессов и действий из XAML.
Создание определений рабочих процессов
Определение рабочего процесса можно создать путем создания экземпляра типа действия и настройки свойств объекта действия. Для действий, которые не содержат дочерних действий, определение можно создать программно с помощью нескольких строк кода.
Activity wf = new WriteLine
{
Text = "Hello World."
};
WorkflowInvoker.Invoke(wf);
Примечание.
В примерах этого раздела для запуска рабочих процессов образцов используется WorkflowInvoker. Дополнительные сведения о вызове рабочих процессов, передаче аргументов и различных доступных вариантах размещения см. в разделе Using WorkflowInvoker и WorkflowApplication.
В этом примере создается рабочий процесс, состоящий из одного действия WriteLine. Задается аргумент WriteLine действия Text и выполняется вызов рабочего процесса. Если в действии содержатся дочерние действия, то используется аналогичный метод построения. В следующем примере используется действие Sequence, которое содержит два действия WriteLine.
Activity wf = new Sequence
{
Activities =
{
new WriteLine
{
Text = "Hello"
},
new WriteLine
{
Text = "World."
}
}
};
WorkflowInvoker.Invoke(wf);
Использование инициализаторов объектов
В примерах в этом разделе используется синтаксис инициализации объектов. Синтаксис инициализации объектов можно использовать для создания определений рабочих процессов в коде, поскольку он обеспечивает иерархическое представление действий в рабочем процессе и показывает связи между действиями. При программном создании рабочих процессов нет необходимости использовать синтаксис инициализации объектов. Следующий пример функционально эквивалентен предыдущему примеру.
WriteLine hello = new WriteLine();
hello.Text = "Hello";
WriteLine world = new WriteLine();
world.Text = "World";
Sequence wf = new Sequence();
wf.Activities.Add(hello);
wf.Activities.Add(world);
WorkflowInvoker.Invoke(wf);
Дополнительные сведения о инициализаторах объектов см. в статье "Практическое руководство. Инициализация объектов без вызова конструктора (руководство по программированию на C#) и практическое руководство. Объявление объекта с помощью инициализатора объектов.
Работа с переменными, литеральными значениями и выражениями
При создании определения рабочего процесса с помощью кода необходимо помнить, какая часть кода выполняется как часть создания определения рабочего процесса и какая часть кода выполняется как часть выполнения экземпляра рабочего процесса. Например, следующий рабочий процесс должен создавать случайное число и записывать его в консоль.
Variable<int> n = new Variable<int>
{
Name = "n"
};
Activity wf = new Sequence
{
Variables = { n },
Activities =
{
new Assign<int>
{
To = n,
Value = new Random().Next(1, 101)
},
new WriteLine
{
Text = new InArgument<string>((env) => "The number is " + n.Get(env))
}
}
};
После выполнения кода определения рабочего процесса происходит вызов Random.Next
и результат сохраняется в определении рабочего процесса как литеральное значение. Можно вызвать несколько экземпляров этого рабочего процесса, при этом все они отобразят одно и то же число. Чтобы обеспечить создание случайного числа во время выполнения рабочего процесса, необходимо использовать выражение, которое вычисляется при каждом запуске рабочего процесса. В следующем примере выражение Visual Basic используется вместе с VisualBasicValue<TResult>.
new Assign<int>
{
To = n,
Value = new VisualBasicValue<int>("New Random().Next(1, 101)")
}
Выражение из предыдущего примера также могло быть реализовано с помощью выражения CSharpValue<TResult> и выражения C#.
new Assign<int>
{
To = n,
Value = new CSharpValue<int>("new Random().Next(1, 101)")
}
Выражения C# необходимо компилировать до того, как будет вызван рабочий процесс, содержащий их. Если выражения C# не компилируются, NotSupportedException возникает при вызове рабочего процесса с сообщением, аналогичным следующему: Expression Activity type 'CSharpValue`1' requires compilation in order to run. Please ensure that the workflow has been compiled.
В большинстве сценариев, связанных с рабочими процессами, созданными в Visual Studio, выражения C# компилируются автоматически, но в некоторых сценариях, таких как рабочие процессы кода, выражения C# должны быть скомпилированы вручную. Пример компиляции выражений C# см . в разделе "Использование выражений C# в рабочих процессах кода" раздела "Выражения C#".
VisualBasicValue<TResult> представляет выражение в синтаксисе Visual Basic, которое может быть использовано в качестве правостороннего значения в выражении, а CSharpValue<TResult> представляет выражение в синтаксисе Visual C#, которое может быть использовано в качестве правостороннего значения в выражении. Значения этих выражений вычисляются каждый раз при выполнении содержащего их действия. Результат выражения назначается переменной рабочего процесса n
, и эти результаты используются следующим действием в рабочем процессе. Для доступа к значению переменной n
рабочего процесса во время ActivityContext выполнения требуется. Доступ может быть получен с помощью следующего лямбда-выражения.
Примечание.
Обратите внимание, что в обоих примерах используется код на языке C#, но в одном используется VisualBasicValue<TResult>, а во втором - CSharpValue<TResult>. VisualBasicValue<TResult> и CSharpValue<TResult> могут использоваться в проектах Visual Basic и C#. По умолчанию выражения, созданные в конструкторе рабочих процессов, соответствуют языку проекта для размещения. При создании рабочих процессов в коде выбор языка остается за автором рабочего процесса.
В этих примерах результат выражения назначается переменной рабочего процесса n
и эти результаты используются следующим действием в рабочем процессе. Для доступа к значению переменной n
рабочего процесса во время ActivityContext выполнения требуется. Доступ может быть получен с помощью следующего лямбда-выражения.
new WriteLine
{
Text = new InArgument<string>((env) => "The number is " + n.Get(env))
}
Дополнительные сведения о лямбда-выражениях см. в лямбда-выражениях (справочнике по C#) или лямбда-выражениям (Visual Basic).
Лямбда-выражения не могут быть сериализованы в формат XAML. При попытке сериализовать рабочий процесс лямбда-выражениями возникает исключение LambdaSerializationException со следующим сообщением: «Этот рабочий процесс содержит указанные в коде лямбда-выражения. Эти выражения XAML-несериализуемы. Чтобы сделать рабочий процесс XAML-сериализуемым, используйте или VisualBasicValue/VisualBasicReference, или ExpressionServices.Convert(lambda). Это преобразует лямбда-выражения в действия выражений". Чтобы сделать это выражение совместимым с XAML, используйте ExpressionServices и Convert, как показано в следующем примере.
new WriteLine
{
//Text = new InArgument<string>((env) => "The number is " + n.Get(env))
Text = ExpressionServices.Convert((env) => "The number is " + n.Get(env))
}
Можно также использовать VisualBasicValue<TResult>. Обратите внимание, что при использовании выражений Visual Basic можно обойтись без лямбда-выражений.
new WriteLine
{
//Text = new InArgument<string>((env) => "The number is " + n.Get(env))
//Text = ExpressionServices.Convert((env) => "The number is " + n.Get(env))
Text = new VisualBasicValue<string>("\"The number is \" + n.ToString()")
}
Во время выполнения выражения Visual Basic компилируются в выражения LINQ. Оба приведенных примера сериализуются в языке XAML, однако, если сериализованный язык XAML должен просматриваться и изменяться в конструкторе рабочих процессов, используйте для выражений VisualBasicValue<TResult>. Сериализованные рабочие процессы, использующие ExpressionServices.Convert
, могут быть открыты в конструкторе, но значение выражения будет пустым. Дополнительные сведения о сериализации рабочих процессов в XAML см. в статье сериализация рабочих процессов и действий из XAML.
Литеральные выражения и ссылочные типы
Литеральные выражения представляются действием Literal<T> в рабочем процессе. Следующие действия WriteLine функционально эквивалентны.
new WriteLine
{
Text = "Hello World."
},
new WriteLine
{
Text = new Literal<string>("Hello World.")
}
Недопустимо инициализировать литеральное выражение любым ссылочным типом, за исключением String. В следующем примере свойство Assign действия Value инициализируется литеральным выражением с помощью List<string>
.
new Assign
{
To = new OutArgument<List<string>>(items),
Value = new InArgument<List<string>>(new List<string>())
},
Когда рабочий процесс, содержащий это действие, проверяется, возникает следующая ошибка проверки: «Литералы поддерживают лишь типы значений и неизменяемый тип System.String. Тип System.Collections.Generic.List'1[System.String] не может использоваться в качестве литерала". Если вызывается рабочий процесс, создается исключение, InvalidWorkflowException содержащее текст ошибки проверки. Это ошибка проверки, поскольку при создании литерального выражения со ссылочным типом не создается новый экземпляр ссылочного типа для каждого из экземпляров рабочего процесса. Чтобы устранить эту проблему, замените литеральное выражение таким, которое создает и возвращает новый экземпляр ссылочного типа.
new Assign
{
To = new OutArgument<List<string>>(items),
Value = new InArgument<List<string>>(new VisualBasicValue<List<string>>("New List(Of String)"))
},
Дополнительные сведения о выражениях см. в разделе "Выражения".
Вызов методов в объектах с помощью выражений и действия InvokeMethod
Действие InvokeMethod<TResult> можно использовать для вызова статических методов и методов экземпляров классов в платформе .NET Framework. В предыдущем примере этого раздела случайное число было создано с помощью класса Random.
new Assign<int>
{
To = n,
Value = new VisualBasicValue<int>("New Random().Next(1, 101)")
}
Действие InvokeMethod<TResult> может также использоваться для вызова метода Next класса Random.
new InvokeMethod<int>
{
TargetObject = new InArgument<Random>(new VisualBasicValue<Random>("New Random()")),
MethodName = "Next",
Parameters =
{
new InArgument<int>(1),
new InArgument<int>(101)
},
Result = n
}
Поскольку Next не статический метод, предоставляется экземпляр класса Random для свойства TargetObject. В этом примере создается новый экземпляр класса с использованием выражения Visual Basic, но он мог быть также создан ранее и сохранен в переменной рабочего процесса. В этом примере будет проще использовать действие Assign<T> вместо действия InvokeMethod<TResult>. Если вызов метода, в конечном счете осуществляемый действием Assign<T> или действием InvokeMethod<TResult>, является долго выполняющимся, преимущество имеет InvokeMethod<TResult> за счет наличия свойства RunAsynchronously. Если это свойство имеет значение true
, вызванный метод работает асинхронно относительно рабочего процесса. Если параллельно работают другие действия, они не будут заблокированы, пока метод выполняется асинхронно. Кроме того, если вызываемый метод не имеет возвращаемого значения, то для его вызова подойдет InvokeMethod<TResult>.
Аргументы и динамические действия
Определение рабочего процесса создается в коде путем сборки действий в дерево действий и настройки всех свойств и аргументов. Для существующих аргументов можно выполнить привязку, но новые аргументы нельзя добавить к действиям. Сюда относятся аргументы рабочего процесса, передаваемые корневому действию. В императивном коде аргументы рабочего процесса указываются как свойства для нового CLR-типа, а в XAML они объявляются с помощью x:Class
и x:Member
. Поскольку при создании определения рабочего процесса как дерева объектов, содержащихся в памяти, не создается нового CLR-типа, аргументы не могут быть добавлены. Однако аргументы могут быть добавлены к DynamicActivity. В этом примере создается действие DynamicActivity<TResult>, которое принимает и складывает два целочисленных аргумента и возвращает результат. Для каждого аргумента создается свойство DynamicActivityProperty, результат операции присваивается аргументу Result действия DynamicActivity<TResult>.
InArgument<int> Operand1 = new InArgument<int>();
InArgument<int> Operand2 = new InArgument<int>();
DynamicActivity<int> wf = new DynamicActivity<int>
{
Properties =
{
new DynamicActivityProperty
{
Name = "Operand1",
Type = typeof(InArgument<int>),
Value = Operand1
},
new DynamicActivityProperty
{
Name = "Operand2",
Type = typeof(InArgument<int>),
Value = Operand2
}
},
Implementation = () => new Sequence
{
Activities =
{
new Assign<int>
{
To = new ArgumentReference<int> { ArgumentName = "Result" },
Value = new InArgument<int>((env) => Operand1.Get(env) + Operand2.Get(env))
}
}
}
};
Dictionary<string, object> wfParams = new Dictionary<string, object>
{
{ "Operand1", 25 },
{ "Operand2", 15 }
};
int result = WorkflowInvoker.Invoke(wf, wfParams);
Console.WriteLine(result);
Дополнительные сведения о динамических действиях см. в разделе "Создание действия в среде выполнения".
Скомпилированные действия
Динамические действия - это один из способов определить действие, которое содержит аргументы в коде, но действия также можно создавать в коде и компилировать в типы. Можно создавать простые действия, производные от класса CodeActivity, и асинхронные действия, производные от класса AsyncCodeActivity. Эти действия могут иметь аргументы и возвращаемые значения и задают логику с помощью императивного кода. Примеры создания этих типов действий см. в разделе "Базовый класс CodeActivity" и "Создание асинхронных действий".
Действия, производные от NativeActivity, могут определять свою логику с помощью императивного кода и содержать дочерние действия, определяющие логику. Кроме того, они имеют полный доступ к возможностям среды выполнения, таким как создание закладок. Примеры создания действия на основе см. в разделе "Базовый NativeActivityкласс NativeActivity", "Практическое руководство. Создание действия" и "Настраиваемое составное" с помощью примера "Машинное действие".
Действия, производные от Activity, задают логику исключительно с помощью дочерних действий. Эти действия обычно создаются с помощью конструктора рабочих процессов, а также могут быть заданы с помощью кода. В следующем примере определяется действие Square
, являющееся производным от Activity<int>
. Действие Square
содержит класс InArgument<T> с именем Value
и задает его логику, указав действие Sequence с помощью свойства Implementation. Действие Sequence содержит действие WriteLine и действие Assign<T>. Совместно эти три действия реализуют логику действия Square
.
class Square : Activity<int>
{
[RequiredArgument]
public InArgument<int> Value { get; set; }
public Square()
{
this.Implementation = () => new Sequence
{
Activities =
{
new WriteLine
{
Text = new InArgument<string>((env) => "Squaring the value: " + this.Value.Get(env))
},
new Assign<int>
{
To = new OutArgument<int>((env) => this.Result.Get(env)),
Value = new InArgument<int>((env) => this.Value.Get(env) * this.Value.Get(env))
}
}
};
}
}
В следующем примере вызывается определение рабочего процесса, состоящего из одного действия Square
, с помощью метода WorkflowInvoker.
Dictionary<string, object> inputs = new Dictionary<string, object> {{ "Value", 5}};
int result = WorkflowInvoker.Invoke(new Square(), inputs);
Console.WriteLine("Result: {0}", result);
При вызове рабочего процесса на консоль выводятся следующие данные:
Квадратное значение: 5
Результат: 25