Neue Version vom Rehosted Workflow Designer

Die neue Version von meinem dynamischen Rehosted Workflow Designer steht auf codeproject zum Download. Die wichtigsten Neuerungen sind neben einigen Bug Fixes und Refraktorisierungen vor allem UI Verbesserungen.

Select Workflow Type Dialog

Den Dialog zur Typauswahl habe ich komplett überarbeitet. Der Style ist dunkler geworden und die ListBox an der linken Seite hat ein schickes TreeView ersetzt. Damit ist es jetzt möglich die Workflowtypen in Gruppen einzuteilen und so anzuzeigen. IWorkflowType leitet dazu von dem Interface IAppTreeDataProvider ab, welches relevante Daten für das TreeView bereitstellt.

public interface IAppTreeDataProvider
{
     string Name { get; }
     string Path { get; }
     // ..
}

Fehleranzeige

Die Fehleranzeige für ungültiges Xaml habe ich überarbeitet. Neben der Anzeige von Zeilennummern in der TextBox kann sich der Balken zum Anzeigen der Exceptions an längere Texte anpassen und in der Größe variieren.

Download: Hier nochmal der Link zum codeproject Artikel, oder hier der direkte Download von meinem FTP  Server.

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:

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

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

Rückblick auf das Ziel von Teil 1: Die Vorteile der Prism-Architektur sind:

  • Möglichkeit die Prism-Module in eine bestehende Anwendung einzubauen
  • Möglichkeit nur die benötigten Module zu verwenden
  • Möglichkeit die Module beliebig anzuordnen

Nachdem es also das letzte Mal um die grundsätzliche auf Prism-Modulen basierende Infrastruktur ging, möchte ich nun ein spezielleres Modul vorstellen, das sich um Speicher- und Ladevorgänge kümmert. Natürlich soll man hier dynamisch die Datenquelle ändern können, also z. B. entscheiden können, ob man den Workflow aus einer Datenbank oder aus einer Datei liest. Sinnvoll ist es natürlich auch mehrere solche Speicheroptionen zur Verfügung zu stellen. Ich habe mich dafür entschieden ein Dialogfeld zu implementieren, was die Speicheroptionen anzeigt und aus dem heraus man neue Workflows erstellen oder andere laden bzw. löschen kann. Zu bedenken ist auch, dass beim Erstellen eines neuen Workflows verschiedene Root-Activities verwendet werden können sollen und damit auch die Toolbox abhängig vom „Workflow-Typ“ aufgebaut werden muss. Es ergibt sich damit das folgende Interface für  unsere Workflow-Typdefinition:

public interface IWorkflowType
{
    IToolboxCreatorService ToolboxCreatorService { get; }
    IStorageAdapter StorageAdapter { get; }
    string DisplayName { get; }
    string Description { get; }
}

Neben einer Beschreibung  und  einem Namen verweist der Workflowtyp auf zwei andere Interfaces, die Toolboxaufbau und Datenzugriffe kapseln. Das Interface für den Toolboxaufbau ist recht einfach, es liefert einfach eine Liste der Kategorien, die anzuzeigen sind. Im vorigen Artikel hatte ich ja schon das ToolboxModul erwähnt, das sich um das Laden der Toolbox kümmert. Dieses Modul nimmt später einfach wieder den ToolboxCreatorService entgegen.

Das Interface für die Datenzugriffe lässt sich in 2 Bereiche einteilen:

  • Abfrage von Name + Beschreibung aller im Datenspeicher vorhandenen Workflows
  • Abfragen der RootActivity und weiterer typspezifischer Daten

Der Grund für die Trennung ist natürlich die Performance. Es würde ewig dauern für alle Workflows ständig den Activity-Baum zu laden, zumal dieser ja zunächst noch aus XAML erzeugt werden muss. Workflow Description (Name + Beschreibung) und DesignerModel (RootActivity + weitere Daten) lassen sich über einen eindeutigen Schlüssel zuordnen, der abhängig von den Daten im DesignerModel ermittelt werden kann (wird z.B. dann beim Speichern ausgelesen).

public interface IStorageAdapter
{
    IEnumerable<IWorkflowDescription> GetAllWorkflowDescriptions();

    IDesignerModel CreateNewDesignerModel();
    bool CanCreateNewDesignerModel { get; }
    IDesignerModel GetDesignerModel(object key);
    bool SaveDesignerModel(IDesignerModel designerModel);
    bool DeleteDesignerModel(object key);
}

Hier mal ein Screenshot vom dazugehörigen Dialog:

Zusammengehalten und gesteuert wird das Ganze von einem eigenen Modul, was das Menüband oben erweitert. Durch WPF-Commands werden die Aktionen in zwei zentrale ViewModels weitergeleitet, die nach einigen Validierungen wieder die Interfaces aufrufen. Im nächsten Teil geht es dann um eine konkrete Implementierung der Interfaces.

Sourcecode:

Den kompletten (weiterentwickelten) Quellcode gibt es wieder hier.

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

Flowdenken – Einführung in WF 4

Der Streit um gute Architekturen war schon immer  ein Kernthema. Die Art, wie Software aufgebaut ist, soll  möglichst felxibel, erweiterbar, leicht verständlich usw. sein. Es gibt inzwischen viele Ansätze, wobei inzwischen einige darauf basieren Software als eine Art Fluss oder Flow, also als Ablauf zu sehen. Sehr viele Prozesse lassen sich als Ablauf darstellen, zum Beispiel ist die Fertigung von einem Produkt in einer Firma ein Ablauf, der sich über mehrere Maschinen und Arbeitsschritte hinzieht. Natürlich hat diese Vorstellungweise den großen Vorteil, dass man sie einfach grafisch darstellen kann. Ein Ablauf ist in grafischer Ansicht nachvollziehbar, auch für Betrachter, die keine Programmiersprache beherrschen. Dabei soll es aber in diesem Artikel zunächst nicht um den Nutzen des Flows für die gesamte Architektur einer Software gehen (hier wäre z.B. EBC ein Beispiel), sondern um ein Framework, das es uns ermöglicht solche Abläufe in unsere Software einzubinden, also zunächst nur Teile unserer Software als Flow zu betrachten. Es geht um die Workflow Foundation von Microsoft. Workflows sind Arbeitsabläufe, die aus dem eigenen Programm heraus angesteuert werden können und grafisch erstellt werden. Fangen wir aber einmal ganz von vorne an und öffnen einfach Visual Studio 2010, wählen ein neues Workflow-Console Projekt und geben ihm den Namen  UserLogin:

Flowdenken — Einführung in WF 4 weiterlesen

WF 4 – Cache Metadata for Custom Activities / Variables Bug?

Heute mal was ganz anderes.. Hauptsächlich wegen meinem Praktikum hab ich mich in den letzten Wochen ziemlich intensiv wieder mit einem sehr interressanten Framework beschäftigt, was eigentlich viel zu wenig Beachtung findet, nämlich der Workflow Foundation. Ab dem .Net Framework 4 macht die so richtig was her. Hier mal ein Screenshot aus einem Beispielprogramm:

Für die Leute die sich auch damit beschäftigen, will ich jetzt mal meine neuste Erfahrung damit aufschreiben, weil ich denke das könnte dem einen oder anderen helfen.. Es geht um die NativeActivities. WF 4 — Cache Metadata for Custom Activities / Variables Bug? weiterlesen