Erstellen von Workflows, Aktivitäten und Ausdrücken mit imperativem Code

Eine Workflowdefinition ist eine Struktur konfigurierter Aktivitätsobjekte. Diese Struktur mit Aktivitäten kann auf viele verschiedene Arten definiert werden, z. B. durch manuelle Bearbeitung des XAML-Codes oder mit dem Workflow-Designer, um XAML-Daten zu erzeugen. Die Verwendung von XAML ist jedoch keine Voraussetzung. Sie können Workflowdefinitionen auch programmgesteuert erstellen. Dieses Thema bietet eine Übersicht über das Erstellen von Workflowdefinitionen, Aktivitäten und Ausdrücken mithilfe von Code. Beispiele für die Arbeit mit XAML-Workflows mithilfe von Code finden Sie unter Serialisieren von Workflows und Aktivitäten in und aus XAML.

Erstellen von Workflowdefinitionen

Sie können eine Workflowdefinition erstellen, indem Sie eine Instanz eines Aktivitätstyps instanziieren und die Eigenschaften des Aktivitätsobjekts konfigurieren. Für Aktivitäten, die keine untergeordneten Aktivitäten enthalten, erreichen Sie dies mit einigen Codezeilen.

Activity wf = new WriteLine
{
    Text = "Hello World."
};

WorkflowInvoker.Invoke(wf);

Hinweis

In den Beispielen dieses Themas wird WorkflowInvoker zum Ausführen der Beispielworkflows verwendet. Weitere Informationen zum Aufrufen von Workflows, zum Übergeben von Argumenten und zu den verschiedenen verfügbaren Hostingoptionen finden Sie unter Verwenden von WorkflowInvoker und WorkflowApplication.

In diesem Beispiel wird ein Workflow erstellt, der aus einer einzelnen WriteLine-Aktivität besteht. Das WriteLine-Argument der Text-Aktivität wird festgelegt, und der Workflow wird aufgerufen. Wenn eine Aktivität untergeordnete Aktivitäten enthält, ist die Konstruktionsmethode ähnlich. Im folgenden Beispiel wird eine Sequence-Aktivität verwendet, die zwei WriteLine-Aktivitäten enthält.

Activity wf = new Sequence
{
    Activities =
    {
        new WriteLine
        {
            Text = "Hello"
        },
        new WriteLine
        {
            Text = "World."
        }
    }
};

WorkflowInvoker.Invoke(wf);

Verwenden von Objektinitialisierern

In den Beispielen in diesem Thema wird die Syntax zur Objektinitialisierung verwendet. Die Objektinitialisierungssyntax kann eine nützliche Möglichkeit zum Erstellen von Workflowdefinitionen per Code darstellen, da diese eine hierarchische Ansicht der Aktivitäten im Workflow bietet und die Beziehung zwischen den Aktivitäten anzeigt. Die Verwendung der Objektinitialisierungssyntax ist keine Anforderung der programmgesteuerten Erstellung von Workflows. Das folgende Beispiel entspricht von der Funktion her dem vorherigen Beispiel.

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

Weitere Informationen zu Objektinitialisierern finden Sie unter Vorgehensweise: Initialisieren von Objekten ohne Aufruf eines Konstruktors (C#-Programmierhandbuch) und Vorgehensweise: Deklarieren eines Objekts mithilfe eines Objektinitialisierers.

Verwenden von Variablen, literalen Werten und Ausdrücken

Achten Sie beim Erstellen einer Workflowdefinition per Code darauf, welcher Code als Teil der Erstellung einer Workflowdefinition ausgeführt wird und welcher Code als Teil der Ausführung einer Instanz dieses Workflows ausgeführt wird. Der folgende Workflow soll z. B. eine zufällige Zahl generieren und auf die Konsole schreiben.

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

Wenn dieser Workflowdefinitionscode ausgeführt wird, erfolgt der Aufruf von Random.Next, und das Ergebnis wird in der Workflowdefinition als literaler Wert gespeichert. Es können viele Instanzen dieses Workflows aufgerufen werden, und für alle wird jeweils die gleiche Zahl angezeigt. Um die Zufallszahlengenerierung während der Workflowausführung durchführen zu lassen, müssen Sie einen Ausdruck verwenden, der bei jeder Ausführung des Workflows ausgewertet wird. Im folgenden Beispiel wird ein Visual Basic-Ausdruck mit VisualBasicValue<TResult> verwendet.

new Assign<int>
{
    To = n,
    Value = new VisualBasicValue<int>("New Random().Next(1, 101)")
}

Der Ausdruck im vorherigen Beispiel könnte auch mithilfe von CSharpValue<TResult> und einem C#-Ausdruck implementiert werden.

new Assign<int>  
{  
    To = n,  
    Value = new CSharpValue<int>("new Random().Next(1, 101)")  
}  

C#-Ausdrücke müssen kompiliert werden, bevor der Workflow, in dem sie enthalten sind, aufgerufen wird. Wenn die C#-Ausdrücke nicht kompiliert werden, wird eine NotSupportedException mit etwa folgendem Wortlaut ausgelöst, sobald der Workflow aufgerufen wird: Expression Activity type 'CSharpValue`1' requires compilation in order to run. Please ensure that the workflow has been compiled. In den meisten Szenarien, die in Visual Studio erstellte Workflows enthalten, werden die C#-Ausdrücke automatisch kompiliert. In einigen Szenarien wie Codeworkflows müssen die C#-Ausdrücke jedoch manuell kompiliert werden. Ein Beispiel für die Kompilierung von C#-Ausdrücken finden Sie im Abschnitt Verwenden von C#-Ausdrücken in Codeworkflows im Thema C#-Ausdrücke.

VisualBasicValue<TResult> stellt einen Ausdruck in Visual Basic-Syntax dar, der als R-Wert in einem Ausdruck verwendet werden kann, und CSharpValue<TResult> stellt einen Ausdruck in C#-Syntax dar, der als R-Wert in einem Ausdruck verwendet werden kann. Diese Ausdrücke werden jedes Mal ausgewertet, wenn die enthaltende Aktivität ausgeführt wird. Das Ergebnis des Ausdrucks wird der Workflowvariablen n zugewiesen, und diese Ergebnisse werden von der nächsten Aktivität im Workflow verwendet. Um zur Laufzeit auf den Wert der Workflowvariablen n zuzugreifen, ist der ActivityContext erforderlich. Darauf kann mit dem folgenden Lambdaausdruck zugegriffen werden.

Hinweis

Beachten Sie, dass beide Codebeispiele C# als Programmiersprache verwenden. In einem Fall wird jedoch VisualBasicValue<TResult> und im anderen CSharpValue<TResult> verwendet. VisualBasicValue<TResult> und CSharpValue<TResult> können sowohl in Visual Basic- als auch in C#-Projekten verwendet werden. Standardmäßig entsprechen die im Workflow-Designer erstellten Ausdrücke der Programmiersprache des Hostingprojekts. Wenn Workflows im Code erstellt werden, wählt der Workflowautor die verwendete Sprache nach eigenem Ermessen aus.

In diesen Beispielen wird das Ergebnis des Ausdrucks der Workflowvariablen n zugewiesen, und diese Ergebnisse werden von der nächsten Aktivität im Workflow verwendet. Um zur Laufzeit auf den Wert der Workflowvariablen n zuzugreifen, ist der ActivityContext erforderlich. Darauf kann mit dem folgenden Lambdaausdruck zugegriffen werden.

new WriteLine
{
    Text = new InArgument<string>((env) => "The number is " + n.Get(env))
}

Weitere Informationen zu Lambdaausdrücken finden Sie unter Lambdaausdrücke (C#-Referenz) oder Lambdaausdrücke (Visual Basic).

Lambda-Ausdrücke können nicht in das XAML-Format serialisiert werden. Wenn versucht wird, einen Workflow mit Lambda-Ausdrücken zu serialisieren, wird LambdaSerializationException mit der folgenden Meldung ausgelöst: "Dieser Workflow enthält im Code spezifizierte Lambda-Ausdrücke. Diese Ausdrücke sind nicht XAML-serialisierbar. Um den Workflow XAML-serialisierbar zu machen, verwenden Sie entweder VisualBasicValue/VisualBasicReference oder ExpressionServices.Convert(lambda). Dadurch werden die Lambda-Ausdrücke in Ausdrucksaktivitäten konvertiert." Um diesen Ausdruck mit XAML kompatibel zu machen, verwenden Sie ExpressionServices und Convert. Dies wird im folgenden Beispiel veranschaulicht.

new WriteLine
{
    //Text = new InArgument<string>((env) => "The number is " + n.Get(env))
    Text = ExpressionServices.Convert((env) => "The number is " + n.Get(env))
}

Es kann auch ein VisualBasicValue<TResult> verwendet werden. Beachten Sie, dass kein Lambda-Ausdruck erforderlich ist, wenn Sie einen Visual Basic-Ausdruck verwenden.

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()")
}

Zur Laufzeit werden die Visual Basic-Ausdrücke in LINQ-Ausdrücke kompiliert. Beide vorherigen Beispiele sind in XAML serialisierbar, aber wenn die serialisierte XAML im Workflow-Designer angezeigt und bearbeitet werden soll, verwenden Sie VisualBasicValue<TResult> für die Ausdrücke. Serialisierte Workflows, die ExpressionServices.Convert verwenden, können zwar im Designer geöffnet werden, der Wert des Ausdrucks ist jedoch leer. Weitere Informationen zum Serialisieren von Workflows zu XAML finden Sie unter Serialisieren von Workflows und Aktivitäten in und aus XAML.

Literale Ausdrücke und Verweistypen

Literale Ausdrücke werden in Workflows durch die Literal<T>-Aktivität dargestellt. Die folgenden WriteLine-Aktivitäten sind funktional äquivalent.

new WriteLine  
{  
    Text = "Hello World."  
},  
new WriteLine  
{  
    Text = new Literal<string>("Hello World.")  
}  

Es ist nicht zulässig, einen literalen Ausdruck mit einem beliebigen Verweistyp außer String zu initialisieren. Im folgenden Beispiel wird die Assign-Eigenschaft einer Value-Aktivität mit einem literalen Ausdruck mit List<string> initialisiert.

new Assign  
{  
    To = new OutArgument<List<string>>(items),  
    Value = new InArgument<List<string>>(new List<string>())  
},  

Bei der Überprüfung des Workflows, der die Aktivität enthält, wird der folgende Validierungsfehler zurückgegeben: „Literal unterstützt nur Werttypen und den unveränderlichen Typ „System.String“. Der System.Collections.Generic.List`1 [System.String]-Typ kann nicht als Literal verwendet werden." Beim Aufrufen des Workflows wird InvalidWorkflowException ausgelöst, die den Text des Validierungsfehlers enthält. Dies ist ein Validierungsfehler, da durch das Erstellen eines literalen Ausdrucks mit einem Verweistyp keine neue Instanz des Verweistyps für die einzelnen Instanzen des Workflows erstellt wird. Um diesen Fehler zu beheben, ersetzen Sie den literalen Ausdruck durch einen Ausdruck, der eine neue Instanz des Verweistyps erstellt und zurückgibt.

new Assign  
{  
    To = new OutArgument<List<string>>(items),  
    Value = new InArgument<List<string>>(new VisualBasicValue<List<string>>("New List(Of String)"))  
},  

Weitere Informationen zu Ausdrücken finden Sie unter Ausdrücke.

Aufrufen von Methoden für Objekte mithilfe von Ausdrücken und der InvokeMethod-Aktivität

Die InvokeMethod<TResult>-Aktivität kann verwendet werden, um statische und Instanzenmethoden von Klassen in .NET Framework aufzurufen. In einem vorherigen Beispiel dieses Themas wird eine Zufallszahl mithilfe der Random-Klasse generiert.

new Assign<int>
{
    To = n,
    Value = new VisualBasicValue<int>("New Random().Next(1, 101)")
}

Die InvokeMethod<TResult>-Aktivität kann auch verwendet werden, um die Next-Methode der Random-Klasse aufzurufen.

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  
}  

Da Next keine statische Methode ist, wird eine Instanz der Random-Klasse für die TargetObject-Eigenschaft angegeben. In diesem Beispiel wird eine neue Instanz mit einem Visual Basic-Ausdruck erstellt. Diese hätte jedoch auch zuvor erstellt und in einer Workflowvariablen gespeichert werden können. In diesem Beispiel wäre es einfacher, die Assign<T>-Aktivität anstelle der InvokeMethod<TResult>-Aktivität zu verwenden. Wenn der Methodenaufruf, der letztendlich entweder durch die Assign<T>-Aktivität oder durch InvokeMethod<TResult>-Aktivität aufgerufen wird, eine lange Ausführungszeit aufweist, bietet InvokeMethod<TResult> einen Vorteil, da sie über eine RunAsynchronously-Eigenschaft verfügt. Wenn diese Eigenschaft auf true festgelegt ist, wird die aufgerufene Methode im Hinblick auf den Workflow asynchron ausgeführt. Wenn andere Aktivitäten parallel sind, werden sie nicht blockiert, während die Methode asynchron ausgeführt wird. Auch wenn die aufzurufende Methode über keinen Rückgabewert verfügt, stellt InvokeMethod<TResult> die geeignete Möglichkeit zum Aufrufen der Methode dar.

Argumente und dynamische Aktivitäten

Eine Workflowdefinition wird im Code erstellt, indem Aktivitäten in einer Aktivitätsstruktur zusammengestellt und alle Eigenschaften und Argumente konfiguriert werden. Vorhandene Argumente können gebunden werden, aber Aktivitäten können keine neuen Argumente hinzugefügt werden. Dies gilt auch für Workflowargumente, die an die Stammaktivität übergeben werden. In imperativem Code werden Workflowargumente als Eigenschaften eines neuen CLR-Typs angegeben, und in XAML werden sie mithilfe von x:Class und x:Member deklariert. Da kein neuer CLR-Typ erstellt wird, wenn eine Workflowdefinition als Struktur von Objekten im Arbeitsspeicher erstellt wird, können Argumente nicht hinzugefügt werden. Argumente können jedoch einer DynamicActivity hinzugefügt werden. In diesem Beispiel wird eine DynamicActivity<TResult> erstellt, die zwei ganzzahlige Argumente verwendet, diese zusammenfügt und das Ergebnis zurückgibt. Für jedes Argument wird ein DynamicActivityProperty-Element erstellt, und das Ergebnis des Vorgangs wird dem Result-Argument von DynamicActivity<TResult> zugewiesen.

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

Weitere Informationen zu dynamischen Aktivitäten finden Sie unter Erstellen einer Aktivität zur Laufzeit.

Kompilierte Aktivitäten

Dynamische Aktivitäten bieten eine Möglichkeit, eine Aktivität mit Argumenten auf der Basis von Code zu definieren, Aktivitäten können jedoch auch im Code erstellt und in Typen kompiliert werden. Es können einfache Aktivitäten erstellt werden, die von CodeActivity abgeleitet sind, und asynchrone Aktivitäten, die von AsyncCodeActivity abgeleitet sind. Diese Aktivitäten können Argumente und Rückgabewerte aufweisen, und ihre Logik kann mithilfe von imperativem Code definiert werden. Beispiele zum Erstellen dieser Art von Aktivitäten finden Sie unter CodeActivity-Basisklasse und Erstellen asynchroner Aktivitäten.

Aktivitäten, die von NativeActivity abgeleitet werden, können ihre Logik mithilfe von imperativem Code definieren, und sie können auch untergeordnete Aktivitäten enthalten, die die Logik definieren. Sie haben außerdem Vollzugriff auf Funktionen der Laufzeit, z. B. das Erstellen von Lesezeichen. Beispiele zum Erstellen einer NativeActivity-basierten Aktivität finden Sie unter NativeActivity-Basisklasse, Vorgehensweise: Erstellen einer Aktivität und dem Beispiel Benutzerdefinierte Zusammensetzungen mit NativeActivity.

Aktivitäten, die von Activity abgeleitet sind, definieren ihre Logik nur durch die Verwendung untergeordneter Aktivitäten. Diese Aktivitäten werden in der Regel mit dem Workflow-Designer erstellt, können aber auch mithilfe von Code definiert werden. Im folgenden Beispiel wird eine Square-Aktivität definiert, die von Activity<int> abgeleitet wird. Die Square-Aktivität verfügt über ein einzelnes InArgument<T> namens Value und definiert ihre Logik, indem eine Sequence-Aktivität unter Verwendung der Implementation-Eigenschaft angegeben wird. Die Sequence-Aktivität enthält eine WriteLine-Aktivität und eine Assign<T>-Aktivität. Zusammen implementieren diese drei Aktivitäten die Logik der Square-Aktivität.

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

Im folgenden Beispiel wird mit Square eine Workflowdefinition aufgerufen, die aus einer einzelnen WorkflowInvoker-Aktivität besteht.

Dictionary<string, object> inputs = new Dictionary<string, object> {{ "Value", 5}};  
int result = WorkflowInvoker.Invoke(new Square(), inputs);  
Console.WriteLine("Result: {0}", result);  

Wenn der Workflow aufgerufen wird, wird die folgende Ausgabe in der Konsole angezeigt:

Quadrieren des Werts: 5
Ergebnis: 25