Creazione di flussi di lavoro, attività ed espressioni tramite codice imperativo

Una definizione del flusso di lavoro è un albero degli oggetti attività configurati che può essere definita in diversi modi, ad esempio modificando manualmente il codice XAML o usando l'utilità di progettazione del flusso di lavoro per produrre codice XAML. L'utilizzo di XAML tuttavia non è un requisito. Le definizioni del flusso di lavoro possono essere create anche a livello di codice. In questo argomento viene fornita una panoramica sulla creazione di definizioni del flusso di lavoro, di attività e di espressioni tramite codice. Per alcuni esempi di utilizzo dei flussi di lavoro XAML tramite codice, vedere Serializzazione di flussi di lavoro e attività da e verso XAML.

Creazione di definizioni del flusso di lavoro

È possibile creare una definizione del flusso di lavoro creando un'istanza di un tipo di attività e configurando le proprietà dell'oggetto attività. Per attività che non contengono attività figlio, questa operazione può essere completata usando alcune righe di codice.

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

WorkflowInvoker.Invoke(wf);

Nota

Negli esempi del presente argomento viene usato WorkflowInvoker per l'esecuzione dei flussi di lavoro di esempio. Per altre informazioni sul richiamo dei flussi di lavoro, il passaggio di argomenti e le varie opzioni di hosting disponibili, vedere Uso di WorkflowInvoker e WorkflowApplication.

In questo esempio viene creato un flusso di lavoro che è costituito da una singola attività WriteLine. L'argomento WriteLine dell'attività Text viene impostato e il flusso di lavoro viene richiamato. Se un'attività contiene attività figlio, il metodo di costruzione è simile. Nell'esempio seguente viene usata un'attività Sequence che contiene due attività WriteLine.

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

WorkflowInvoker.Invoke(wf);

Uso di inizializzatori di oggetti

Negli esempi di questo argomento viene usata la sintassi di inizializzazione oggetti che può essere utile per creare definizioni del flusso di lavoro in codice in quanto fornisce una visualizzazione gerarchica delle attività nel flusso di lavoro e consente di illustrare la relazione tra le attività. Non è previsto alcun requisito per usare la sintassi di inizializzazione oggetti quando si creano flussi di lavoro a livello di codice. L'esempio seguente rappresenta l'equivalente funzionale dell'esempio precedente.

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

Per altre informazioni sugli inizializzatori di oggetti, vedere Procedura: Inizializzare oggetti senza chiamare un costruttore (Guida per programmatori C#) e Procedura: Dichiarare un oggetto usando un inizializzatore di oggetto.

Uso di variabili, valori letterali ed espressioni

Quando si crea una definizione del flusso di lavoro tramite codice, prestare attenzione a quale parte di codice viene eseguita per la creazione della definizione del flusso di lavoro e a quale invece per l'esecuzione di un'istanza di quel flusso di lavoro. Ad esempio il flusso di lavoro seguente viene usato per generare un numero casuale e scriverlo nella console.

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

Quando viene eseguito questo codice di definizione del flusso di lavoro, viene effettuata la chiamata all'oggetto Random.Next e il risultato viene archiviato nella definizione del flusso di lavoro come valore letterale. È possibile richiamare molte istanze di questo flusso di lavoro e tutte vengono visualizzate con lo stesso numero. Per la generazione di numeri casuali durante l'esecuzione del flusso di lavoro, è necessario usare un'espressione che viene valutata ad ogni esecuzione del flusso di lavoro. Nell'esempio seguente riportato di seguito viene usata un'espressione di Visual Basic con un elemento VisualBasicValue<TResult>.

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

L'espressione nell'esempio precedente poteva essere distribuita anche usando un elemento CSharpValue<TResult> e un'espressione C#.

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

Le espressioni C# devono essere compilate prima che il flusso di lavoro che le contiene venga richiamato. Se le espressioni C# non vengono compilate, viene generata una NotSupportedExceptionquando il flusso di lavoro viene richiamato con un messaggio simile al seguente: Expression Activity type 'CSharpValue`1' requires compilation in order to run. Please ensure that the workflow has been compiled. Nella maggior parte degli scenari che implicano i flussi di lavoro creati in Visual Studio, le espressioni C# vengono compilate automaticamente, ma in alcuni scenari, ad esempio i flussi di lavoro di codice, le espressioni C# devono essere compilate manualmente. Per un esempio di compilazione di espressioni C#, vedere la sezione Utilizzo delle espressioni C# nei flussi di lavoro di codice dell'argomento Espressioni C#.

VisualBasicValue<TResult> rappresenta un'espressione nella sintassi di Visual Basic che può essere usata come r-value in un'espressione e CSharpValue<TResult> rappresenta un'espressione nella sintassi di C# che può essere usata come r-value in un'espressione. Queste espressioni vengono valutate ad ogni esecuzione dell'attività che le contiene. Il risultato dell'espressione viene assegnato alla variabile n del flusso di lavoro e questi risultati vengono utilizzati dalla successiva attività nel flusso di lavoro. Per accedere al valore della variabile n del flusso di lavoro in fase di esecuzione, è necessario l'oggetto ActivityContext. al quale è possibile accedere tramite l'espressione lambda seguente.

Nota

Si noti che entrambi i codici di esempio usano C# come linguaggio di programmazione, ma uno usa un oggetto VisualBasicValue<TResult> e l'altro usa un oggetto CSharpValue<TResult>. VisualBasicValue<TResult> e CSharpValue<TResult> possono essere usati sia nei progetti C# che in progetti Visual Basic. Per impostazione predefinita, le espressioni create nella finestra di progettazione del flusso di lavoro corrispondono al linguaggio del progetto di hosting. Durante la creazione dei flussi di lavoro nel codice, il linguaggio usato è a discrezione dell'autore del flusso di lavoro.

In questi esempi il risultato dell'espressione viene assegnato alla variabile n del flusso di lavoro e questi risultati vengono usati dalla successiva attività nel flusso di lavoro. Per accedere al valore della variabile n del flusso di lavoro in fase di esecuzione, è necessario l'oggetto ActivityContext. al quale è possibile accedere tramite l'espressione lambda seguente.

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

Per altre informazioni sulle espressioni lambda, vedere Espressioni lambda (Riferimenti per C#) e Espressioni lambda (Visual Basic).

Le espressioni lambda non sono serializzabili nel formato XAML. Se viene effettuato un tentativo di serializzare un flusso di lavoro con espressioni lambda, viene generata un'eccezione LambdaSerializationException con il messaggio seguente: "Il flusso di lavoro contiene le espressioni lambda specificate nel codice. Queste espressioni non sono serializzabili in XAML. Per rendere il flusso di lavoro serializzabile in XAML, usare VisualBasicValue/VisualBasicReference o ExpressionServices.Convert(lambda). In questo modo le espressioni lambda verranno convertite in attività di espressione." Per rendere questa espressione compatibile con XAML, usare l'oggetto ExpressionServices e il metodo Convert, come illustrato nell'esempio seguente.

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

Si può usare anche un oggetto VisualBasicValue<TResult>. Si noti che le espressioni lambda non sono obbligatorie quando si usa un'espressione di 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()")
}

In fase di runtime, le espressioni di Visual Basic vengono compilate nelle espressioni LINQ. Entrambi gli esempi precedenti sono serializzabili in XAML, ma se si intende visualizzare e modificare il codice XAML serializzato nella finestra di progettazione del flusso di lavoro, usare VisualBasicValue<TResult> per le espressioni. I flussi di lavoro serializzati che usano ExpressionServices.Convert possono essere aperti nella finestra di progettazione, ma il valore dell'espressione sarà vuoto. Per altre informazioni sulla serializzazione di flussi di lavoro verso XALM, vedere Serializzazione di flussi di lavoro e attività da e verso XAML.

Espressioni letterali e tipi riferimento

Le espressioni letterali sono rappresentate nei flussi di lavoro dall'attività Literal<T>. Le seguenti attività WriteLine sono equivalenti a livello funzionale.

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

Non è valido inizializzare un'espressione letterale con qualsiasi tipo di riferimento eccetto String. Nell'esempio riportato di seguito, la proprietà Assign di un'attività Value viene inizializzata con un'espressione letterale usando List<string>.

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

Quando il flusso di lavoro che contiene questa attività viene convalidato, viene restituito il seguente errore di convalida: “Il valore letterale supporta solo tipi di valore e il tipo non modificabile System.String. Impossibile usare System.Collections.Generic.List`1[System.String] come valore letterale". Se il flusso di lavoro viene richiamato, viene generata un'eccezione InvalidWorkflowException contenente il testo dell'errore di convalida. Si tratta di un errore di convalida perché la creazione di un'espressione letterale con un tipo di riferimento non crea una nuova istanza del tipo di riferimento per ogni istanza del flusso di lavoro. Per risolvere il problema, sostituire l'espressione letterale con una che crea e restituisce una nuova istanza del tipo di riferimento.

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

Per ulteriori informazioni sulle espressioni, vedere Espressioni.

Chiamata di metodi su oggetti usando le espressioni e l'attività InvokeMethod

L'attività InvokeMethod<TResult> può essere usata per richiamare i metodi statici e di istanza di classi di .NET Framework. In un esempio precedente in questo argomento, un numero casuale è stato generato usando la classe Random.

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

Si sarebbe anche potuto usare l'attività InvokeMethod<TResult> per chiamare il metodo Next della classe 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  
}  

Poiché Next non è un metodo statico, un'istanza della classe Random viene fornita per la proprietà TargetObject. In questo esempio viene creata una nuova istanza usando un'espressione di Visual Basic, ma avrebbe potuto essere creata in precedenza ed essere archiviata in una variabile del flusso di lavoro. In questo esempio, sarebbe più semplice usare l'attività Assign<T> anziché l'attività InvokeMethod<TResult>. Se la chiamata al metodo richiamato di recente dalla attività Assign<T> o InvokeMethod<TResult> ha un'esecuzione prolungata, InvokeMethod<TResult> ha un vantaggio poiché dispone di una proprietà RunAsynchronously. Se questa proprietà è impostata su true, il metodo richiamato verrà eseguito in modo asincrono rispetto al flusso di lavoro. Se vi sono altre attività in parallelo, queste non verranno bloccate durante l'esecuzione asincrona del metodo. Inoltre, se il metodo da richiamare non ha valore restituito, InvokeMethod<TResult> rappresenta la soluzione appropriata per richiamare il metodo.

Argomenti e attività dinamiche

Una definizione del flusso di lavoro viene creata in codice assemblando le attività in un albero delle attività e configurando qualsiasi proprietà e argomento. Gli argomenti esistenti possono essere associati, ma quelli nuovi non possono essere aggiunti alle attività, inclusi gli argomenti del flusso di lavoro passati all'attività radice. In codice imperativo, gli argomenti del flusso di lavoro vengono specificati come proprietà in un nuovo tipo CLR e in XAML vengono dichiarati tramite gli oggetti x:Class e x:Member. Poiché non è disponibile alcun nuovo tipo CLR creato quando una definizione del flusso di lavoro viene creata come albero degli oggetti in memoria, gli argomenti non possono essere aggiunti. Tuttavia, gli argomenti possono essere aggiunti a un oggetto DynamicActivity. In questo esempio viene creato un oggetto DynamicActivity<TResult> che accetta due argomenti Integer, li somma e restituisce il risultato. Viene creato un oggetto DynamicActivityProperty per ogni argomento e il risultato dell'operazione viene assegnato all'argomento Result dell'oggetto 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);

Per altre informazioni sulle attività dinamiche, vedere Creazione di un'attività in fase di esecuzione.

Attività compilate

Le attività dinamiche sono un modo per definire tramite codice un'attività che contiene argomenti, ma le attività possono anche essere create nel codice e compilate nei tipi. È possibile creare attività semplici che derivano da CodeActivitye attività asincrone che derivano da AsyncCodeActivity. Queste attività possono avere argomenti, valori restituiti e definiscono la relativa logica usando il codice imperativo. Per esempi di creazione di questi tipi di attività, vedere Classe di base CodeActivity e Creazione di attività asincrone.

Le attività che derivano da NativeActivity possono definire la relativa logica usando il codice imperativo possono inoltre contenere attività figlio che definiscono la logica. Dispongono inoltre dell'accesso completo alle funzionalità del runtime come la creazione dei segnalibri. Per esempi di creazione di un'attività basata su NativeActivity, vedere Classe di base NativeActivity, Procedura: Creare un'attività e l'esempio Attività composita personalizzata tramite l'oggetto NativeActivity.

Le attività che derivano da Activity definiscono la relativa logica unicamente attraverso l'uso di attività figlio. Queste attività vengono in genere create tramite la finestra di progettazione del flusso di lavoro, ma possono essere definite anche tramite codice. Nell'esempio seguente viene definita un'attività Square che deriva da Activity<int>. L'attività Square ha un unico InArgument<T> denominato Value e definisce la relativa logica specificando un'attività Sequence che usa la proprietà Implementation. L'attività Sequence contiene un'attività WriteLine e un'attività Assign<T>. Insieme, queste tre attività implementano la logica dell'attività 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))  
                }  
            }  
        };  
    }  
}  

Nell'esempio riportato di seguito, una definizione del flusso di lavoro costituita da un'unica attività Square viene richiamata usando WorkflowInvoker.

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

Quando il flusso di lavoro viene richiamato, l'output seguente viene visualizzato nella console:

Elevazione al quadrato del valore: 5
Risultato: 25