Dynamischer Rehosted Workflowdesigner für die WF 4 – Teil 3

Rückblick auf Teil 2: Das Storage Module kümmert sich um

  1. Erzeugen eines neuen Workflows
  2. Speichern eines Workflows
  3. Laden eines vorhandenen Workflows
  4. Löschen eines vorhandenen Workflows

abhängig vom ausgewählten Workflowtyp.

In diesem Teil wollen wir uns damit beschäftigen eigene Activities zu bauen, wie das auch der Visual-Studio-Designer macht. Im VS-Designer werden für alle Workflows eigene Typen kompiliert, die dann auch als Black-Boxes in anderen Workflows wiederverwendet werden können bzw. denen Argumente und ein spezielles Aussehen verpasst werden können (wie z.B. bei allen Standardactivities wie Assign, For, Foreach und so weiter). Wir werden dann die Interfaces vom vorigen Teil mit dieser Logik implementieren und damit einen ersten Workflowtyp haben. Dann müssen wir als  letztes noch einen ToolboxService schreiben, der unsere eigenen Activities in die Toolbox lädt. Der fertige Activity-Designer sieht so aus:

Eigene Activities als Root und Child

Es gibt in der WF zwei Klassen zur Definition dynamischer Activities. Die ActivityBuilder Klasse kann als Root-Activity im Designer verwendet werden. Wenn man diese dann abspeichert erhält man das übliche Activity-XAML („<Activity …“), wie es auch vom Visual Studio generiert wird.

Wenn man allerdings dieses XAML dann wieder lädt, erhält man eine DynamicActivity, die sich richtig wie eine Activity anfühlt, also in den Workflowbaum eingebaut werden kann, ausgeführt werden kann, deren Argumente mit Werten befüllt werden können … Leider kann man diese Activity aber überhaupt nicht abspeichern (Beim Versuch fliegt eine Exception).

Damit ergibt sich unser erstes Problem. Wir könnten also problemlos eine Activity mit dem ActivityBuilder zusammenstellen und als XAML abspeichern. Wir könnten ein ToolboxItem erstellen, was dieses XAML als DynamicActivity zurückgibt und dann diese DynamicActivity in einen anderen Workflow einbauen und diesen Workflow sogar ausführen. Aber wir könnten diesen Workflow nicht speichern. Eine Möglichkeit wäre es einen richtigen Typ zu kompilieren (wie VisualStudio). Allerdings könnte man diesen Typ dann nicht mehr als XAML öffnen und es ist sicherlich auch nicht ganz einfach aus dem XAML eine richtige Typedefinition zu bekommen. Die zweite Möglichkeit wäre, die DynamicActivity zu erweitern. Leider können wir nicht von ihr ableiten, weil sie sealed ist, aber im Internet findet sich der Quellcode, den man dazu benutzen kann eine eigene DynamicActivity zu entwickeln, die bei mir jetzt PlaceholderActivity heißt. Im Wesentlichen habe ich alles wie bei der DynamicActivity gemacht und nur den folgenden Code hinzugefügt:

public PlaceholderActivity(DynamicActivity dynamicActivity)
    : this()
{
    this.ApplyDynamicActivity(dynamicActivity);
}

public PlaceholderActivity()
    : base()
{
    this.typeDescriptor = new PlaceholderActivityTypeDescriptor(this);
}

private void ApplyDynamicActivity(DynamicActivity dynamicActivity)
{
    this.DisplayName = dynamicActivity.Name;

    foreach (var item in dynamicActivity.Attributes)
    {
        this.Attributes.Add(item);
    }

    foreach (var item in dynamicActivity.Constraints)
    {
        this.Constraints.Add(item);
    }

    this.Implementation = dynamicActivity.Implementation;
    this.Name = dynamicActivity.Name;

    foreach (var item in dynamicActivity.Properties)
    {
        this.Properties.Add(item);
    }
}

[Browsable(false)]
public string XAML
{
    get
    {
        var activityBuilder = new ActivityBuilder();

        foreach (var item in this.Attributes)
        {
            activityBuilder.Attributes.Add(item);
        }

        foreach (var item in this.Constraints)
        {
            activityBuilder.Constraints.Add(item);
        }

        activityBuilder.Implementation = this.Implementation != null ? this.Implementation() : null;
        activityBuilder.Name = this.Name;

        foreach (var item in this.Properties)
        {
            activityBuilder.Properties.Add(item);
        }

        var sb = new StringBuilder();
        var xamlWriter = ActivityXamlServices.CreateBuilderWriter(
                         new XamlXmlWriter(new StringWriter(sb), new XamlSchemaContext()));
        XamlServices.Save(xamlWriter, activityBuilder);

        return sb.ToString();
    }
    set
    {
        this.ApplyDynamicActivity(ActivityXamlServices.Load(new StringReader(value)) as DynamicActivity);
    }
}

Zusätzlich habe ich alle anderen Properties aus der Serialisierung beim Speichern ausgeschlossen:

...
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ...

Die Idee dahinter ist einfach: Wir erstellen ganz normal einen ActivityBuilder und speichern dessen XAML ab. Wenn wir die Activity im Designer wieder bearbeiten möchten, können wir, indem wir nur die XamlServices zum Laden verwenden wieder einen ActivityBuilder erzeugen. Wenn wir die Activity aber  jetzt in einen anderen Workflow einbauen wollen, so bauen wir nicht direkt die zurückgelieferte DynamicActivity ein, sondern erzeugen eine neue Instanz der PlaceholderActivity, der wir im Konstruktor die DynamicActivity übergeben. Da ja die PlaceholderActivity ganau die gleichen Properties, wie die DynamicActivity hat, reicht es diese einfach zu kopieren. Wenn wir nun den neuen Workflow speichern, wird nur die XAML-Property der PlaceholderActivity serialisiert (weil ja die anderen alle ausgeschlossen sind). Diese Property erzeugt intern wieder einen ActivityBuilder und dadurch kann die Activity wieder als XAML abgespeichert werden. Beim Laden wird dann von der WF-Runtime die XAML Property gesetzt und es kann wieder eine DynamicActivity gelesen werden, deren Properties wir wieder kopieren können. Damit haben wir eine DynamicActivity, die sich abspeichern lässt.

Die Implementierung der Daten-Interfaces

Zuerst implementieren wir die Daten-Interfaces (IWorkflowDescription und IDesignerModel). Die erste Implementierung fällt ganz simpel aus, wir brauchen nur die Properties auszuschreiben:

public class WorkflowDescription : IWorkflowDescription
{
    public WorkflowDescription(object key)
    {
        if (key == null) throw new ArgumentNullException("key");
        this.Key = key;
    }

    public string WorkflowName { get; set; }

    public string Description { get; set; }

    public object Key { get; private set; }
}

Das zweite Interface besteht aus der Root-Activity und einem Objekt für die vom Workflowtyp abhängigen Daten. Diese zusätzlichen Daten werden im zweiten PropertyGrid angezeigt und können bearbeitet werden. Ich habe jetzt einfach nur die Möglichkeit bereitgestellt die Beschreibung zu ändern. Außerdem enthält diese Klasse noch die Logik, die den Key generiert (in diesem Fall der Name der Activity).

public class DesignerModel : IDesignerModel
{
    private string originalKey;

    public DesignerModel(object rootActivity, ActivityEntity activityEntity)
    {
        if (rootActivity == null) throw new ArgumentNullException("rootActivity");
        if (activityEntity == null) throw new ArgumentNullException("activityEntity");

        this.RootActivity = rootActivity;
        this.ActivityEntity = activityEntity;
        this.ApplyKey();
    }

    public object Key { get { return (this.RootActivity as ActivityBuilder).Name; } }

    public bool HasKeyChanged
    {
        get
        {
            return (string)this.Key != originalKey;
        }
    }

    public object RootActivity { get; private set; }

    public object PropertyObject { get { return this.ActivityEntity; } }

    public ActivityEntity ActivityEntity { get; private set; }

    public void ApplyKey()
    {
        this.originalKey = (string)this.Key;
    }
}
public class ActivityEntity
{
    [Category("General")]
    [Description("Activity description")]
    public string Description { get; set; }
}

Die Implementierung der Logik-Interfaces

Das wichtigste Logik-Interface ist der StorageAdapter. Ich habe mich dafür entschieden die Activities als DLLs abzuspeichern. In jeder dieser DLLs ist jeweils ein Typ mit dem Namen CompiledActivity enthalten, der von IActivityTemplateFactory ableitet. Diesen Typ erzeuge ich anhand einer Vorlage und ändere dann nur den Wert von einigen privaten Werten:

private readonly string xaml = @"{0}";
private readonly string description = @"{1}";
private readonly string activityName = @"{2}";

Diese Werte werden dann über Properties wieder nach außen gestellt, sodass sie später wieder ausgelesen werden können (z.B. zum Bearbeiten einer Activitiy im Designer). Der eigentliche Vorteil ist aber, dass der erzeugte Typ sofort an die Toolbox übergeben werden kann. Die WF-Runtime erkennt das Interface und ruft dann die Create() Methode auf, die es uns ermöglicht unsere PlaceholderActivity zu erstellen und zurückzugeben:

public Activity Create(DependencyObject target)
{
    DynamicActivity dynamicActivity = ActivityXamlServices.Load(new StringReader(this.xaml)) as DynamicActivity;
    return new PlaceholderActivity(dynamicActivity);
}

Die eigentlichen Speicher-/Ladevorgänge basieren auf den (Activity)XamlServices:

Laden (XamlServices liefern ActivityBuilder zurück):

var workflow = XamlServices.Load(ActivityXamlServices.CreateBuilderReader
    (new XamlXmlReader(new StringReader(instance.XAML))));

Speichern:

var sb = new StringBuilder();
var xamlWriter = ActivityXamlServices.CreateBuilderWriter
    (new IgnorableXamlXmlWriter(new StringWriter(sb), new XamlSchemaContext()));
XamlServices.Save(xamlWriter, model.RootActivity);
string xaml = sb.ToString();

Beim Speichern benutze ich noch einen speziellen XamlXmlWriter (IgnorableXamlXmlWriter), der dafür sorgt, dass vor allen Design-Einstellungen (Position von Activities im Flowchart ..) ein „ignorable“ steht. Das hat den Vorteil, dass zum Ausführen der Workflows nicht immer eine Referenz auf System.Activities.Presentation/System.Activities.C0re.Presentation vorhanden sein muss.

Die DLLs werden zusätzlich noch zwischen temporären Verzeichnissen und zwei Unterverzeichnissen (eins für die Toolbox und eins zum Bearbeiten) hin- und herkopiert um Kollisionen zwischen Speicher-/Ladevorgängen und den Zugriffen der Toolbox zu vermeiden. Wichtig sind diese beiden Ordner im Verzeichnis der Designer-Exe:

  • activities: Diese Activities können geöffnet und bearbeitet werden
  • activityModules: Diese Activities werden in der Toolbox angezeigt

Immer beim Neuladen der Toolbox werden die Verzeichnisse durch den ToolboxCreatorService synchronisiert:

var loader = new CompiledActivityLoader();
loader.LoadActivities();

Download: Source Code

Veröffentlicht von

Winfried

Student TU Chemnitz