Rendre une activité plus “sexy” n’est pas forcément un travail compliqué. Mais si on ne part pas sur de bonnes bases on perd vite du temps (parfois même beaucoup de temps). Je profite de cet article pour vous livrer quelques une de mes astuces et recommandations.

Au menu du jour :

  • La customisation d’un ActivityDesigner.
  • L’ajout sans angoisse d’un Icon à une activité.

Pour illustrer mes explications je vais partir d’une architecture que l’on devrait commencer à bien connaitre avec 3 projets:

wf4wpf_sexy00Ma solution de base a cette arborescence :

wf4wpf_sexy01

On y retrouve donc :

  • 3 projets
  • 2 activités
  • 2 designers d’activités
  • Un classe DesignerMetadata pour enregistrer les designers
  • Un dictionnaire de ressources Generic.xaml contenant mes styles de bases

J’en profite pour rappeler que la sortie du projet WHSActivities.Design ne pointe pas vers son répertoire habituel “bin\Debug\” mais celui de WHSActivities “..\WHSActivities\bin\Debug\”

Le fichier Generic.xaml est un dictionnaire xaml que j’ai l’habitude d’utiliser avec Workflow Foundation 4. Il contient le nécessaire pour avoir des activités propres et limiter la réécriture des éléments récurant comme :

  • Les textes habituels “glisser un activité ici”.
  • Les marges entre les textes.
  • Un séparateur d’activité semblable à celui qui existe dans WF4.

Voici son contenu :

<ResourceDictionary 
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation">

    <!-- BackGround de la zone des activités -->
    <SolidColorBrush x:Key="BackGround" Color="White" />
    
    <!-- Style des TextBlock -->
    <Style TargetType="{x:Type TextBlock}">
        <Style.Setters>
            <Setter Property="Margin" Value="0 0 5 0" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style.Setters>
    </Style>
    
    <!-- Style des Border -->
    <Style x:Key="BorderStyle" TargetType="{x:Type Border}">
        <Style.Setters>
            <Setter Property="BorderBrush" Value="LightGray" />
            <Setter Property="BorderThickness" Value="1" />
            <Setter Property="Background" Value="White" />
            <Setter Property="Padding" Value="5" />
        </Style.Setters>
    </Style>

    <!-- Style des Commentaires -->
    <Style x:Key="CommentStyle" TargetType="{x:Type TextBlock}">
        <Style.Setters>
            <Setter Property="FontStyle" Value="Italic" />
            <Setter Property="TextAlignment" Value="Center" />
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style.Setters>
    </Style>

    <!-- Style des WorkflowItemPresenter -->
    <Style TargetType="{x:Type sap:WorkflowItemPresenter}" >
        <Style.Setters>
            <Setter Property="HintText" Value="Glissez une activité ici ..." />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type sap:WorkflowItemPresenter}">
                        <Border Style="{StaticResource ResourceKey=BorderStyle}">
                            <ContentPresenter />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style.Setters>
    </Style>
    
    <!-- Style des WorkflowItemsPresenter -->
    <Style TargetType="{x:Type sap:WorkflowItemsPresenter}">
        <Style.Setters>
            <Setter Property="HintText" Value="Glissez vos activités ici ..." />
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <!-- Oriantation des activités (de haut en bas) -->
                        <StackPanel Orientation="Vertical" />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="SpacerTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <!-- 
                         StackPanel utilisé pour étandre la zone de drag and drop d'activité
                         au dela du simple Polygon
                         -->
                        <StackPanel Background="{StaticResource ResourceKey=BackGround}">
                            <Polygon HorizontalAlignment="Center" Margin="5" Fill="White" Stroke="Gray" Points="0,0 16,0 8,8" />
                        </StackPanel>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type sap:WorkflowItemsPresenter}">
                        <Border Style="{StaticResource ResourceKey=BorderStyle}">
                            <ContentPresenter />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style.Setters>
    </Style>
</ResourceDictionary>

Uniquement en utilisant ce fichier vous obtiendrez un rendu propre sans utiliser trop de Xaml dans vos designers d’activités.

wf4wpf_sexy02

Voila! Cela n’a rien d’exceptionnel mail mon Xaml est se réduit à l’aspect fonctionnel + Binding :

<sap:ActivityDesigner x:Class="WHSActivities.Design.WarningSequenceDesigner"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation">
    <sap:ActivityDesigner.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/WHSActivities.Design;component/Themes/Generic.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </sap:ActivityDesigner.Resources>
    <Grid>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <TextBlock Grid.Row="0" Text="Activities" />
            <sap:WorkflowItemsPresenter Grid.Row="1" Items="{Binding Path=ModelItem.Activities}"/>
        </Grid>
    </Grid>
</sap:ActivityDesigner>

Notez bien le format complet de l’url source du dictionnaire qui contient “pack://application:,,,/WHSActivities.Design;” qui est indispensable quand le projets sont découpés de la sorte.

Maintenant si on veut ajouter un peu de vie à tout cela, nous allons commencer par ruser ;).

Pourquoi donc “Ruser”? Nous allons ruser car l’ActivitDesigner de WF4 a des propriétés qui ne sont pas disponibles aujourd’hui. Le bacground, les border ou ContentTemplate par exemple ne peuvent pas être utilisés. Template est par contre tout à fait opérationnel mais je ne vous encourage pas à l’utiliser car cela sous-entend de refaire le design complet (bandeau contenant le DisplayName de l’activité compris). J’ai voulu cet article simple, nous allons donc partir sur une solution simple.

Vu que l’on est dans Visual studio 2010 on toute l’attitude pour jouer avec WPF. Et on ne vas pas s’ne priver ;)

Pour changer le background et les bordures, je vous propose d’ajouter un Border à l’ActivitDesigner et de jouer avec sa propriété Margin et Padding. Après quelques tests, j’ai constaté qu’il était possible de remplir toute la partie basse de l’activité en utilisant un Margin négatif de 7. J’utiliserai donc -7 comme Margin et je compenserai le décalage du contenu par un Padding de +7. Ce qui donne si on ajoute un peu de couleur et un Icon :

wf4wpf_sexy03

Avec le Xaml suivant :

<sap:ActivityDesigner x:Class="WHSActivities.Design.WarningSequenceDesigner"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation">
    <sap:ActivityDesigner.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/WHSActivities.Design;component/Themes/Generic.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </sap:ActivityDesigner.Resources>
    <sap:ActivityDesigner.Icon>
        <DrawingBrush>
            <DrawingBrush.Drawing>
                <ImageDrawing Rect="0 0 16 16" ImageSource="Images\WarningSequenceDesigner.png"/>
            </DrawingBrush.Drawing>
        </DrawingBrush>
    </sap:ActivityDesigner.Icon>
    <Border Margin="-7" Padding="7"
        BorderBrush="Yellow" BorderThickness="1"
        TextBlock.Foreground="Yellow">
        <Border.Background>
            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                <LinearGradientBrush.GradientStops>
                    <GradientStop Offset="0" Color="Orange" />
                    <GradientStop Offset="1" Color="Red" />
                </LinearGradientBrush.GradientStops>
            </LinearGradientBrush>
        </Border.Background>
        <Grid>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
                <TextBlock Grid.Row="0" Text="Activities" />
                <sap:WorkflowItemsPresenter Grid.Row="1" Items="{Binding Path=ModelItem.Activities}"/>
            </Grid>
        </Grid>
    </Border>
</sap:ActivityDesigner>

L’Icon est une image Png de 16*16 avec transparence que j’ai glissée dans un répertoire images de WHSActivities.Design. Dans l’explorateur du projet, cette image à la propriété « Action de génération» mise sur « Ressource » (mise par défaut).

Attention tout de même aux comportements que peut engendrer votre designer sur ses enfants. Petit exemple : dans cet ActivitDesigner j’ai changer la propriété TextBlock.Foreground="Yellow". Ceci induit une modification de cette propriété sur les activités enfants :

wf4wpf_sexy04

Passons maintenant à la fameuse ToolBox de Visual Studio, qui par défaut est de cette allure :

wf4wpf_sexy05

Pour changer l’Icon d’une activité dans la Toolbox, il faut passer par l’attribut ToolboxBitmap. Celui-ci fait partie du namespace System.Drawing, il faudra donc l’ajouter en référence au projet WHSActivities. Le projet WHSActivities.Design aurait été préférable, mais malheureusement l’association via IRegisterMetadata n’est pas prise en compte dans la Toolbox de Visual Studio (peut-être qu’à l’avenir ce sera possible).

Arrive alors le souci de la manière dont on peut utiliser ToolboxBitmap pour l’ajouter à notre activité (exemple : l’activité WarningSequence).

[ToolboxBitmap(typeof(WarningSequence ))]

Nécessite que l’image ait le même nom que l’activité et qu’elle se trouve dans le même répertoire que celui-ci.

[ToolboxBitmap(typeof(WarningSequence), "Images.WarningSequence.bmp")]

Plus pratique. Cette méthode permet de donner le nom que l’on veut à l’image, et surtout de la mettre dans le répertoire que l’on souhaite. Dans mon exemple, mon image a le nom WarningSequence.bmp et se trouve dans un répertoire Images à la racine du projet WHSActivities.

Autre petit souci possible avec cette ToolboxBitmap, la qualité de l’image et le respect de ses couleurs une foi affichée dans la Toolbox. Pour ça, j’ai une solution qui fonctionne à tous les coups. N’étant pas un virtuose du traitement d’images et de tout le tremblement qui va avec (je laisse cela à mon frère, chacun son métier). Je ne m’en sors jamais vraiment bien avec la gestion des palettes de couleurs (même dansPaint.net). Par contre Visual Studio, lui sait très bien quelle palette il apprécie. La solution la plus simple consiste donc à utiliser Visual studio et son éditeur intégré ;)

Pour arriver à mes fins ma méthode est simple :

  1. Ajouter un bmp via le menu ajouter un nouvelle élément de Visual Studio.
  2. Utiliser l’éditeur de Visual Studio (double click sur le bmp) pour changer les propriétés de cette image pour avoir :
    1. Colors : 24 bits
    2. Height : 16
    3. Width : 16
  3. J’ajoute un fon uni magenta (c’est la couleur le plus souvent utilisée pour la transparence)
  4. Copier le contenu de l’image provenant de ma source (dans mon cas l’image png que j’utilise aussi pour mon ActivitDesigner) Patin.net est pratique dans ce cas, mais Paint peut aussi faire l’affaire.
  5. Ajouter un peu de magenta là où je veux m’assurer de la transparence.
  6. Enregistrer ;)

wf4wpf_sexy06

Ensuite dans l’explorateur de projet, il faut impérativement changer la propriété « Action de génération » pour la passer sur « Ressource incorporée ». Et le tour est joué.

Après avoir fait un premier bmp, on peut le copier et le coller dans l’explorateur de projet cela permet d’avoir un fichier avec les mêmes propriétés (image et génération).

Et tout de suite nos activités ont plus fière allure dans la Toolbox :

wf4wpf_sexy07

Si la Toolbox n’est pas mise à jour, il faut passer par l’explorateur de solution et lancer un nettoyage de la solution puis une compilation.

Voila

PS : oui j’ai un petit projet de rehosting Workflow Foundation 4 dans la console de Vail Clignement d'œil