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

Frohe Weihnachten! Und hier auch gleich mein kleines Weihnachtsgeschenk:

Ein immer wiederkehrendes Problem mit eigenen Anwendungen basierend auf der Workflow Foundation, ist die eigene Implementierung eines Workflowdesigners. Dafür stellt das .Net Framework einige Klassen zur Verfügung, die auf MSDN kurz erklärt werden.

Eigentlich ist es nutzlos, für jede Anwendung ein neues WPF-Projekt für den Workflow-Designer zu erstellen, weil sich eigentlich kaum etwas ändert. Zusätzlich muss man sich immer noch um eine Lösung für Speichern/Laden (Datenbank, Filesystem …) Gedanken machen, was ja auch immer gleich ist. Sinnvoll wäre es eine dynamische Standardanwendung zu haben, die man auf den jeweiligen Anwendungsfall noch ein bisschen anpassen kann. Als Lösung für diese immer wiederkehrende nervige Sache bietet sich eine modulare Implementierung an. Dabei sollte es möglich sein frei zu entscheiden wie flexibel man sein möchte, also wie viel Logik man aus der Infrastruktur übernimmt.

Also, genug rumgeschwafelt, jetzt kommt meine Implementierung. Ich hab mir gedacht, weil es ein größeres Projekt ist ein paar Artikel dazu zu schreiben. In diesem Artikel soll es zunächst um die grobe Struktur und um die „Core-Module“ meiner Implementierung gehen, bevor wir dann später zum einzelnen optionalen Aufgaben kommen. Vorher aber trotzdem mal ein Screenshot:

dynamischer Workflowdesigner
Workflowdesigner

Meine Implementierung basiert auf Prism, obwohl ich eigentlich nur den RegionManager davon verwende. Mein Ziel, was ich damit erreicht habe, war es nicht vorzugeben, dass die Toolbox links ist oder so, sondern eben einfach eine Toolbox da ist, die dann der RegionManager an eine beliebige Stelle platziert. Damit sind die Kernkomponenten von meinem kleinen Framework ganz schlank und schlicht. Es gibt:

  • Ein zentrales ViewModel, das die Klasse WorkflowDesigner zur Verfügung stellt und sich um Dinge wie das Aktualisieren der Designfläche, oder Designerfehler usw. kümmert
  • Zwei zentrale Views (Toolbox, Designfläche und PropertyInspector) mit diesem ViewModel als DataContext
  • Ein weiteres zentrales ViewModel für die Toolbox (dazu später mehr)
  • Eine zentrale View mit diesem ViewModel als DataContext

Die Views beinhalten ledeglich jeweils ein Content-Control, dass an die jeweilige Property von der WorkflowDesigner Instanz bzw. eben an die Toolbox gebunden ist. Damit ist der WorkflowDesigner schön modular und nicht mehr alles in einer Klasse zentriert. Hier mal als Beispiel einen Ausschnitt vom StandardDesignerViewModel (DesignView und PropertyInspector) mit der Initialisierungslogik:

public class StandardDesignerViewModel : INotifyPropertyChanged, IDesignerViewModel
{
    public WorkflowDesigner Designer
    {
        get;
        set;
    }

    public void ReloadDesigner(Activity root)
    {
        this.Designer = new WorkflowDesigner();
        (new DesignerMetadata()).Register();
        this.Designer.Load(root);
    }
}

Die Views sehen alle gleich aus, z. B. so die Design-View:

<UserControl x:Class="RehostedDesigner.Designer.ViewModule.StandardView"
             ...>
    <ContentControl Content="{Binding Designer.View}"/>
</UserControl>

Die einzige größere Herausvorderung war hier das Laden der Bildchen für die Toolbox, dazu vielleicht mal in einem anderen Artikel. Grundsätzlich kann man über ein Interface steuern, welche Elemente gerade in der Toolbox angezeigt werden und mein ViewModel versucht dann die Icons automatisch zu finden (wer möchte schaut sich einfach den Quellcode an).

Zusammensetzen kann man das ganze dann mit einem Prism-Bootstrapper. Der Name klingt gewaltiger als was dahintersteckt: Eine kleine Klasse, die anstatt des MainWindows von der App.xaml aufgerufen wird:

protected override void OnStartup(StartupEventArgs e)
{
    new Bootstrapper().Run();
}

Die müssen wir von UnityBootstrapper ableiten und setzen damit den ganzen Apparat von Prism in Gang. Die Implementierung ist sehr standardmäßig, wenn ich mir meinen Bootstrapper so anschaue kommt es mir fast so vor, als hätte ich ihn irgendwo von der MSDN Hilfe kopiert, ist zwar absolut korrekt, aber absolut nicht hilfreich/aufschlussreich/sinnvoll? … naja ich kopiers trotzdem mal rein:

public class Bootstrapper : UnityBootstrapper
{
    protected override DependencyObject CreateShell()
    {
        MainWindow shell = new MainWindow();
        shell.Show();

        return shell;
    }

    protected override IModuleCatalog CreateModuleCatalog()
    {
        return new ConfigurationModuleCatalog();
    }

    protected override void ConfigureContainer()
    {
        base.ConfigureContainer();
        ((UnityConfigurationSection)ConfigurationManager.GetSection("unity")).Configure(this.Container);
    }
}

Natürlich brauchen wir jetzt noch unser MainWindow, in dem wir die Regions für den RegionManager definieren. Eigentlich ziemlich selbsterklärend (Standardanordung Toolbox links, PropertyInspector rechts):

<Window x:Class="Selen.WorkflowDesigner.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Selen.WorkflowDesigner"
        xmlns:regions="clr-namespace:Microsoft.Practices.Prism.Regions;assembly=Microsoft.Practices.Prism" 
        Title="Workflow Designer" Height="350" Width="525" WindowState="Maximized">
    <DockPanel>
        <Menu DockPanel.Dock="Top" regions:RegionManager.RegionName="StorageRegion"/>
        <StatusBar DockPanel.Dock="Bottom" Height="25">
            <StatusBarItem HorizontalAlignment="Right">
                <Image Source="logo.png" HorizontalAlignment="Right"/>
            </StatusBarItem>
        </StatusBar>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="4*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>
            <ContentControl Grid.Column="0" Grid.Row="0" 
                            regions:RegionManager.RegionName="ToolboxRegion" Margin="5"/>
            <ContentControl Grid.Column="1" Grid.Row="0" 
                            regions:RegionManager.RegionName="DesignerViewRegion" Margin="5"/>
            <ContentControl Grid.Column="2" Grid.Row="0" 
                            regions:RegionManager.RegionName="DesignerPropertyInspectorViewRegion" Margin="5"/>
            <GridSplitter Grid.Column="0" HorizontalAlignment="Right" .../>
            <GridSplitter Grid.Column="1" HorizontalAlignment="Right" .../>
        </Grid>
        <DockPanel.Background>
            <LinearGradientBrush>
                <GradientStop Color="Black" Offset="0"/>
                <GradientStop Color="Gray" Offset="0.5"/>
                <GradientStop Color="Black" Offset="0.7"/>
                <GradientStop Color="Gray" Offset="1"/>
            </LinearGradientBrush>
        </DockPanel.Background>
    </DockPanel>
</Window>

Das wars dann für den ersten Teil. Als nächstes stehen weitere Module auf dem Plan, die optionale Funktionalität übernehmen können (Speichern u.ä.), inzwischen gibts aber schonmal den gesamten Quellcode des Projekts zum Download: Download hier

Veröffentlicht von

Winfried

Student TU Chemnitz