(WF4, Toolbox) Updating the WF4 Toolbox Icon FAQ (for Rehosting and Custom Activities)
Introit - call it whatever, but today I’m recycling an old post. And I have one more little confession. When I started writing a new rehosted app today, I found out my old toolbox FAQ was fairly useless. It was even getting to the point where someone had to post this in the forum:
“I found a number of posts online about how to populate a toolbox with the standard activities/icons. Unfortunately they are all wrong in one way or another. Some add internal non user tools, some get the wrong icons, and most are fragile.”
Ouch! (I’m kind of curious what the internal non-user tools bit is talking about, by the way… anyone know?)
Anyway, I actually really appreciated this post from Frank Holden. Criticism is good, but constructive criticism is great. Especially considering that by posting code with real solutions to the problem, he’s unselfishly helping out the whole WF4 community. This is something I think we want to encourage in the forum – reusable code in posts benefits everyone.
As well as today’s rehosting exertions and Frank’s post, there was one other event pushing me in the direction of updating this FAQ. Even developers at MS are resorting to using private Reflection to get Toolbox Icons working in rehosted apps. The saddest part of this is that the private reflection solution to the problem, while completely unsupportable, just looks much quicker and pain-free than the WPF-ness I am about to do today.
While I’m at it, it’s time to start encouraging people to use icons from the DLL which is legal, and where possible encourage use of vector graphics over bitmaps (just for the sake of pretty apps). So anwyay I felt like finally a new post instead of an update to the old is justified, and we can also make it a better FAQ by readdressing the most frequent question first! (Also there is an answer about creating custom toolbox controls which I have been meaning to add for a long time. Tada!)
The WF4 Toolbox Icon and Custom Activity Designer Icon FAQ [Version 4]
Question: How do I make the out-of-box activities have their standard icons in my rehosted designer app? (Without incurring legal issues around redistributing Microsoft.Activities.* dlls which are installed with VS)
Answer (for ease of explanation, this is broken into multiple parts):
Part 1
Extract icons in DrawingBrush form from the .Net assembly System.Activities.Presentation. (Please don’t extract them from Microsoft.VisualStudio.Activities.dll. Legal thing.)
internal static class ToolboxIconExtraction { static ResourceDictionary iconsDict;
/// <summary> /// Loads the standard activity icons from System.Activities.Presentation. /// Accepts various forms of input including 'short name' i.e. toolbox display name, and 'Type.FullName' - and tries to fix them automatically. /// </summary> public static object ExtractIconResource(string iconOrtypeName) { iconsDict = iconsDict ?? new ResourceDictionary { Source = new Uri("pack://application:,,,/System.Activities.Presentation;component/themes/icons.xaml") };
string resourceKey = GetResourceName(iconOrtypeName); object resource = iconsDict.Contains(resourceKey) ? iconsDict[resourceKey] : null;
if (!(resource is DrawingBrush)) { resource = iconsDict["GenericLeafActivityIcon"]; }
return resource; }
public static object ExtractIconResource(Type type) { string typeName = type.IsGenericType ? type.GetGenericTypeDefinition().Name : type.Name; return ExtractIconResource(typeName); }
static string GetResourceName(string typeName) { string resourceKey = typeName; resourceKey = resourceKey.Split('.').Last(); resourceKey = resourceKey.Split('`').First();
if (resourceKey == "Flowchart") { // it appears that themes/icons.xaml has a typo here resourceKey = "FlowChart"; }
if (!resourceKey.EndsWith("Icon")) { resourceKey += "Icon"; }
return resourceKey; } } |
[Credit to Brannon King for pointing out the icons can be found in the SAP assembly, and showing how to converting DrawingBrush to Bitmap.]
Part 2
Teach your toolbox control look for the extracted icon resources. Here is one possible way relying only on public APIs, by restyling the ToolboxControl:
<sapt:ToolboxControl x:Name="toolbox"> <sapt:ToolboxControl.Style> <Style TargetType="{x:Type sapt:ToolboxControl}"> <Style.Resources> <local:ToolboxItemConverter x:Key="IconConverter"/> <DataTemplate x:Key="IconTemplate"> <Rectangle Width="18" Height="18" Fill="{Binding}"/> </DataTemplate> <DataTemplate x:Key="ToolTemplate" DataType="{x:Type sapt:ToolboxItemWrapper}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*" SharedSizeGroup="toolLabel"/> </Grid.ColumnDefinitions> <ContentControl Grid.Column="0" Content="{Binding Path=DisplayName, Converter={StaticResource IconConverter}}" ContentTemplate="{StaticResource IconTemplate}"/> <TextBlock Grid.Column="1" Text="{Binding Path=DisplayName}" Margin="5,1,5,0"/> </Grid> </DataTemplate> </Style.Resources> <Setter Property="ToolTemplate" Value="{StaticResource ToolTemplate}" /> </Style> </sapt:ToolboxControl.Style> </sapt:ToolboxControl> |
That XAML-ified toolbox style above involves one piece of custom code which is the ToolboxItemConverter class
internal class ToolboxItemConverter : IValueConverter { public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (!(value is string)) { return null; }
DrawingBrush b = ToolboxIconExtraction.ExtractIconResource((string)value) as DrawingBrush; return b; }
public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new System.NotImplementedException(); } } |
Part 3
Fill up your toolbox! Because the above XAML template will look up icons based on the DisplayName property of ToolboxItemWrapper, make sure the display name matches the resource name, e.g. ‘ForEach’ not ‘ForEachWithBodyFactory’. (Or add hacks , like the ‘FlowChart’ hack in the top section.)
void PopulateToolbox() { ToolboxCategory c = new ToolboxCategory("Control Flow"); c.Add(new ToolboxItemWrapper(typeof(DoWhile))); c.Add(new ToolboxItemWrapper(typeof(ForEachWithBodyFactory<>), "ForEach")); c.Add(new ToolboxItemWrapper(typeof(Parallel))); c.Add(new ToolboxItemWrapper(typeof(ParallelForEachWithBodyFactory<>), "ParallelForEach")); c.Add(new ToolboxItemWrapper(typeof(PickWithTwoBranchesFactory), "Pick")); … toolbox.Categories.Add(c); } |
Question: How can I use DrawingBrush to create my own icons for custom activities?
Example:
Answer [from old FAQ]:
The class ActivityDesigner (System.Activities.Presentation) has a property called Icon.
... public DrawingBrush Icon { get; set; }
|
You can set it from XAML:
<sap:ActivityDesigner x:Class="WorkflowConsoleApplication6.ActivityDesigner1" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"> <sap:ActivityDesigner.Icon> <DrawingBrush> <DrawingBrush.Drawing> <GeometryDrawing Brush="Coral"> <GeometryDrawing.Geometry> <GeometryGroup> <EllipseGeometry RadiusX="5" RadiusY="5"/> </GeometryGroup> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingBrush.Drawing> </DrawingBrush> </sap:ActivityDesigner.Icon> </sap:ActivityDesigner>
|
or your C# code beside (here loading a DrawingBrush resource from an assembly)
public ActivityDesigner1() { this.InitializeComponent(); Uri uri = new Uri( "WorkflowConsoleApplication6;component/Dictionary1.xaml", System.UriKind.Relative); ResourceDictionary rd = (ResourceDictionary)Application.LoadComponent(uri); this.Icon = rd["CoralIcon"] as DrawingBrush; }
|
Question: How can I show my Custom activity with no Icon or Display Name?
Answer [recycled,updated]:
There are multiple options.
1) Subclass WorkflowViewElement instead of ActivityDesigner. (This is also what you would often do creating custom designers for objects which don’t inherit Activity (like PickBranch)).
2) WPF magic – override the default Style or ContentTemplate of ActivityDesigner.
(Note: WorkflowViewElement is quite the blank slate - it even has null content/background by default, which makes it hard to interact with until you provide something.)
Question: Can I create my own toolbox, instead of using ToolboxControl, and still drag-drop stuff into the designer?
Answer [new]:
Yes! All you need to do is use the correct drag+drop item format. You can do this with some code like this in a mouse event handler:
{ DataObject data = new DataObject(System.Activities.Presentation.DragDropHelper.WorkflowItemTypeNameFormat, typeof(Sequence).AssemblyQualifiedName); DragDrop.DoDragDrop(this/*ToolboxControlIWrote?*/, data, DragDropEffects.All); }
|
Question: I’m creating a custom activity designer in VS and I want to add my custom bitmap icon for the Activity in Visual Studio’s toolbox. How do I do that?
Answer [old]:
The only way I know so far to customize the Visual Studio toolbox icons is using System.Drawing.ToolboxBitmapAttribute on your activity class .
Notes:
-if you want to use bitmaps from resources, rather than installing actual bitmap files, it seems that you must set the .bmp file build action to ‘Embedded Resource’, which is the only kind System.Drawing.ToolboxBitmapAttribute understands.
-When using the below code, I have had no luck using anything other than 16x16 pixels bitmaps.
-Hot pink seems to be interpreted as transparent by VS.
Example code:
[System.Drawing.ToolboxBitmap(typeof(CodeActivity4), "Heart.bmp")] public sealed class CodeActivity4 : CodeActivity { protected override void Execute(CodeActivityContext context) {
} }
|
Question: How can I load those same BMP bitmaps that I used in VS toolbox (which are compiled into my assembly as ‘Embedded Resource’) and reuse them for the ActivityDesigner Icon?
Answer [old]:
Since the only resources that System.Drawing.ToolboxBitmapAttribute understands are EmbeddedResource, which isn’t easy to get at from WPF XAML, the best way to go is probably writing some code in the code-beside C# file to extract the resource, and update the ActivityDesigner’s Icon property.
public ActivityDesigner1() { this.InitializeComponent(); Stream manifestResourceStream = typeof(ActivityDesigner1) .Module.Assembly.GetManifestResourceStream( typeof(ActivityDesigner1), "Heart.bmp"); var bmpframe = BitmapFrame.Create(manifestResourceStream); this.Icon = new DrawingBrush { Drawing = new ImageDrawing { Rect = new System.Windows.Rect(0, 0, 16, 16), ImageSource = bmpframe } }; }
|
Question: How do I convert DrawingBrush to Bitmap type (for e.g. creating .bmp icon files, or using the VS toolbox)?
Answer:
Here’s code which helps convert DrawingBrush to Bitmap objects in memory. Saving to disk is just a matter of using the appropriate encoder on the BitmapSource and saving to a FileStream.
public static BitmapSource BitmapSourceFromBrush(Brush drawingBrush, int size = 32, int dpi = 96) { var pixelFormat = PixelFormats.Pbgra32; RenderTargetBitmap rtb = new RenderTargetBitmap(size, size, dpi, dpi, pixelFormat); var drawingVisual = new DrawingVisual(); using (DrawingContext context = drawingVisual.RenderOpen()) { context.DrawRectangle(drawingBrush, null, new Rect(0, 0, size, size)); } rtb.Render(drawingVisual); return rtb; }
public static System.Drawing.Bitmap BitmapFromBitmapSource(BitmapSource source, System.Drawing.Imaging.PixelFormat pixelFormat) { var bmp = new System.Drawing.Bitmap(source.PixelWidth, source.PixelHeight, pixelFormat); BitmapData data = bmp.LockBits(new System.Drawing.Rectangle(System.Drawing.Point.Empty, bmp.Size), ImageLockMode.WriteOnly, pixelFormat); source.CopyPixels(Int32Rect.Empty, data.Scan0, data.Height * data.Stride, data.Stride); bmp.UnlockBits(data); return bmp; }
|
Question: Can I override the default activity icons globally?
Answer:
Yes, in at least one way – see this blog post from Kushal where he modifies the default ActivityDesigner appearance/behavior using WPF styles.
[Thanks to Notre for pointing me at that in comments on the original post.]
(If you just want to change the default icon for activities that don’t have a special icon out of the box, a hacky way is to override the default activity designer registration.
var atb = new AttributeTableBuilder();
atb.AddCustomAttributes(typeof(Activity),
new DesignerAttribute(typeof(StyleableDefaultActivityDesigner)));
MetadataStore.AddAttributeTable(atb.CreateTable());
)
Question: Does WF Designer toolbox control support subcategories, or functioning as an arbitrary treeview?
Answer:
No. In comments to the original of this FAQ, ‘Evan’ suggested one alternative, which is to use multiple toolbox controls, where each toolbox control is a whole subcategory on its own.
If you need a fully-blown TreeView with deep nested categories then you are probably better off creating a custom toolbox control using the above technique for drag+drop integration.
Question: Why can’t I significantly modify the toolbox appearance via ToolboxItemWrapper? (And how can I add tooltips?)
A. You might notice that ToolboxCategory and ToolboxItemWrapper represent the items in your toolbox, but they are not actually Visuals, i.e. not part of the WPF visual tree. Their role is just to act as the ViewModel (MVVM) for the toolbox, and supply data which WPF can bind the Visuals/Controls to.
The Toolbox class, on the other hand, is part of the visual tree and it exposes customization points that you can use to drastically modify the toolbox item templated appearance. Those customization points are the properties CategoryItemStyle, CategoryTemplate, ToolboxItemStyle, and ToolTemplate. ( This is used above.)
Here is a neat example of using this to add tooltips to the rehosted toolbox from Yuanji Wang via the forums.
Got more toolbox/icon questions? Ask in the comments, or forums by all means. (Experience suggests that comments are kind of broken and you might have to post multiple times until you see a success indicator. Comments are moderated to remove spam and don’t appear right away.)
Comments
Anonymous
August 11, 2011
Excellent article, thanks. Please also consider trolling /questions/tagged/workflow-foundation-4 on StackOverflow for good subjects for articles. I'd suggest looking for questions that have upvotes and no answers, as they usually indicate the difficult ones.Anonymous
August 22, 2011
Note - for readers of Japanese and/or VisualBasic, there is a translation/summarization into both those languages by a reader here: blogahf.blogspot.com/.../wf_22.htmlAnonymous
September 13, 2011
The comment has been removedAnonymous
February 03, 2012
Tim - you could also see how I've done this by having a look at my post... blogs.msdn.com/.../using-standard-icons-a-rehosted-workflow-designer.aspx Cheers, MorganAnonymous
August 28, 2012
It looks like in .NET 4.5, the icons are now in icons.windowsapp.xaml instead of icons.xaml.Anonymous
February 15, 2013
.NET 4.5 also appears to correct the typo for "Flowchart". So if you want to support both machines with and without 4.5, you need to check both locations in System.Activities.Presentation, and correct (or not) the case of "Flowchart".Anonymous
February 09, 2015
Just as a note for others Icons.WindowsApp.xaml gives the newer flat windows app style icons. For the older style icons use Icons.Default.xaml instead.