locked
Add Activities to toolbox in run-time RRS feed

  • Question

  • Hello.

    In my rehosted Designer I'd like to store a part from the Workflow and add it to the toolbox for re-use (non-generic, with values etc). The ToolboxItemWrapper seems to accept generic types only.

    ModelItem activity = this.ModelItem;
    ToolboxCategory cat = new ToolboxCategory("Custom");
                cat.Add(new ToolboxItemWrapper(activity.ItemType, "CustomSeq"));
    MainWindow.tbCtrl.Categories.Add(cat);

    From the designer I already get the ModelItem. How can I store it as an Activity to the toolbox?

    Some posts say it is necessary to compile it first, but some say it isn't. Anyway, it is not clear to me how to archieve that.

    Thank you for you help!

    Regards
    Lyndon

    Monday, December 3, 2012 9:57 AM

Answers

  • No idea what is wrong. Let me post the whole sample again that works for me.

    1. DynamicActivityTemplateFactory is used as an activity templates.

    public abstract class DynamicActivityTemplateFactory : IActivityTemplateFactory
    {
    	public virtual string GetActivity()
    	{
    		return null;
    	}
    
    	public Activity Create(DependencyObject target)
    	{
    		return XamlServices.Load(new StringReader(GetActivity())) as Activity;
    	}
    }

    2. DynamicActivityGenerator is used to generate dynamic activities

    public class DynamicActivityGenerator { private const string _templateTypePrefix = ""; private AssemblyBuilder _ab; private ModuleBuilder _mb; private readonly string _fileName; private readonly string _moduleName; private Dictionary<string, Type> _types; public DynamicActivityGenerator(string name) { _types = new Dictionary<string, Type>(); _fileName = String.Format("{0}.dll", name); _moduleName = name; _ab = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName(_moduleName), AssemblyBuilderAccess.RunAndSave); _mb = _ab.DefineDynamicModule(_moduleName, _fileName); } private string GetTypeName(string prefix, string workflowName) { var baseName = String.Format("{0}{1}", prefix, workflowName); var name = baseName; var index = 1; while (_types.ContainsKey(name)) { name = String.Format("{0}{1}", baseName, index); index++; } return name; } public Type AppendSubWorkflowTemplateFromFile(string workflowName, string path) { return AppendSubWorkflowTemplate(workflowName, LoadXamlFromFile(path)); }

    public Type AppendSubWorkflowTemplate(string workflowName, Activity activity)
    {
    return AppendSubWorkflowTemplate(workflowName, XamlServices.Save(activity));
    }

    public Type AppendSubWorkflowTemplate(string workflowName, string xaml) { var name = GetTypeName(_templateTypePrefix, workflowName); var tb = _mb.DefineType(name, TypeAttributes.Public | TypeAttributes.Class, typeof(DynamicActivityTemplateFactory)); var methodb = tb.DefineMethod("GetActivity", MethodAttributes.Public | MethodAttributes.Virtual, typeof(string), null); var msil = methodb.GetILGenerator(); msil.Emit(OpCodes.Ldstr, xaml); msil.Emit(OpCodes.Ret); var t = tb.CreateType(); _types.Add(name, t); return t; } public void Load() { _ab.Save(_fileName); Assembly.Load(new AssemblyName(_moduleName)); } public DynamicActivityDescriptor[] Activities { get { return _types.Select(descriptor => new DynamicActivityDescriptor(descriptor.Key, descriptor.Value)).ToArray(); } } private string LoadXamlFromFile(string path) { var xml = new XmlDocument(); xml.Load(path); if (xml.DocumentElement == null) throw new InvalidOperationException("Unable to load Component. Component has invalid content."); return xml.DocumentElement.InnerXml; } } public class DynamicActivityDescriptor { public DynamicActivityDescriptor(string name, Type type) { Name = name; Type = type; } public string Name { get; set; } public Type Type { get; set; } }

    3. Use above API in Workflow Designer

    var category = new ToolboxCategory("Custom activities"); var generator = new DynamicActivityGenerator("Test.Activities.DynamicActivities");
    var path = @"Test.xaml";
    generator.AppendSubWorkflowTemplateFromFile("Activity from File", path);
    generator.AppendSubWorkflowTemplate("Activity from Xaml", GenerateXaml("Dynamic Activity from Xaml"));
    generator.AppendSubWorkflowTemplate("Activity from Code", new Sequence() { DisplayName = "Dynamic Activity" });
    generator.Load();

    foreach (var descriptor in generator.Activities) { category.Add(new ToolboxItemWrapper(descriptor.Type, descriptor.Name)); } Toolbox.Categories.Add(category);

    4. It's also possible to generate an activity in a design time. For example, add selected activity from the Workflow Designer as an activity template to toolbox.

    - Get selected activity from the Rehosted Designer

    WorkflowDesigner.Context.Items.GetValue<Selection>().PrimarySelection.GetCurrentValue() as Activity

    or use ModelService to find required activity

    var modelService = WorkflowDesigner.Context.Services.GetService<ModelService>();
    IEnumerable<ModelItem> mc = modelService.Find(modelService.Root, typeof(Activity));
    var activity = mc.First().GetCurrentValue() as Activity;

    - Generate assembly with custom activity and add it to Toolbox

    var generator = new DynamicActivityGenerator("Test.Activities.DynamicActivities1");
    generator.AppendSubWorkflowTemplate("New Activity", XamlServices.Save(activity));
    generator.Load();
    
    foreach (var descriptor in generator.Activities)
    {
    	Toolbox.Categories[0].Add(new ToolboxItemWrapper(descriptor.Type, descriptor.Name));
    }

    Note: This solution generates assembly and loads it into current process and there is no simple way to unload this assembly. Therefor if you need to regenerate activity, the new assembly with a new name should be generated. It's possible that at the end we will have many "dynamic" assemblies loaded, but this is not a problem, because these assemblies are used in design time only and are not used in run-time.

    Also, if it's required to persist generated activities for future use, it's better to store them as XAML and generate assembly when Workflow Desisgner is loaded.

    • Marked as answer by lyndon23 Wednesday, December 19, 2012 10:47 AM
    • Edited by Alex Mrynsky Friday, December 21, 2012 8:50 AM Solution Adjusted
    Wednesday, December 19, 2012 8:40 AM
  • I have adjusted my solution to provide additional use case for generating Activities from ModelItem. Also please review the Note at the end of the answer.

    As for you problem with loading XAML. Probably you save it incorrectly. Try to use XmlDocument.DocumentElement.InnerXml only as in my example.

    If you post me the XAML, I could provide more concrete answer.

    Hope it helps.

    Regards
    Alex

    • Marked as answer by lyndon23 Friday, December 21, 2012 11:08 AM
    Friday, December 21, 2012 9:01 AM

All replies

  • Hello

    You can export your activty into an external assembly and load it again to be adde to your toolbox

    Wednesday, December 5, 2012 8:42 AM
  • Ok, do you have sample code that shows how to export it to an external assembly in run-tme?
    Wednesday, December 5, 2012 9:34 AM
  • You could use IActivityTemplateFactory to achive it.

    The idea is to generate Activity Template in rum-time using Reflection.

    1. Define base class for template

    public abstract class DynamicActivityTemplateFactory : IActivityTemplateFactory
    {
    	public virtual string GetActivity()
    	{
    		return null;
    	}
    
    	public Activity Create(DependencyObject target)
    	{
    		return XamlServices.Load(new StringReader(GetActivity())) as Activity;
    	}
    }

    2. Build "dynamic" assembly that will contain new activities

    private static ModuleBuilder GenerateAssembly(string name)
    {
    	var moduleName = String.Format("{0}Module", name);
    	var fileName = String.Format("{0}.dll", moduleName);
    
    	var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
    		new AssemblyName(moduleName), AssemblyBuilderAccess.RunAndSave);
    	ModuleBuilder mb = ab.DefineDynamicModule(moduleName, fileName);
    	return mb;
    }

    3. Generate activities based on XAML

    private static Type AppendType(ModuleBuilder mb, string typeName, string xaml)
    {
    	var tb = mb.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class, typeof (DynamicActivityTemplateFactory));
    	var methodb = tb.DefineMethod("GetActivity", MethodAttributes.Public | MethodAttributes.Virtual, typeof (string), null);
    
    	var msil = methodb.GetILGenerator();
    	msil.Emit(OpCodes.Ldstr, xaml);
    	msil.Emit(OpCodes.Ret);
    	var t = tb.CreateType();
    
    	return t;
    }

    Then you could use it as following

    ModuleBuilder mb = GenerateAssembly("DynamicActivities");
    Type activity1 = AppendType(mb, "DynamicActivity1", xaml1);
    Type activity2 = AppendType(mb, "DynamicActivity2", xaml2);
    Result types could be added to WD Toolbox.

    Tuesday, December 18, 2012 11:39 AM
  • Thank you very much so far!
    It seems to work, however I receive an exception when I'm trying add the activity to the toolbox (translated):

    "Could not find the file or assembly "DynamicActivitiesModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" or one of its dependencies."

    What am I missing?

    Regards,
    Lyndon

    Tuesday, December 18, 2012 2:06 PM
  • Forgot to mention. You need to load generated assembly in order to use it.

    ab.Save(fileName);
    Assembly.Load(new AssemblyName(moduleName));

    Tuesday, December 18, 2012 2:32 PM
  • OK, the generated assembly is loaded. But now I get the following Message:

    "Could not load type "DynamicActivity1" in the Assembly "DynamicActivitiesModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null".

    Any idea?
    Thanks in advance.

    Wednesday, December 19, 2012 7:06 AM
  • No idea what is wrong. Let me post the whole sample again that works for me.

    1. DynamicActivityTemplateFactory is used as an activity templates.

    public abstract class DynamicActivityTemplateFactory : IActivityTemplateFactory
    {
    	public virtual string GetActivity()
    	{
    		return null;
    	}
    
    	public Activity Create(DependencyObject target)
    	{
    		return XamlServices.Load(new StringReader(GetActivity())) as Activity;
    	}
    }

    2. DynamicActivityGenerator is used to generate dynamic activities

    public class DynamicActivityGenerator { private const string _templateTypePrefix = ""; private AssemblyBuilder _ab; private ModuleBuilder _mb; private readonly string _fileName; private readonly string _moduleName; private Dictionary<string, Type> _types; public DynamicActivityGenerator(string name) { _types = new Dictionary<string, Type>(); _fileName = String.Format("{0}.dll", name); _moduleName = name; _ab = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName(_moduleName), AssemblyBuilderAccess.RunAndSave); _mb = _ab.DefineDynamicModule(_moduleName, _fileName); } private string GetTypeName(string prefix, string workflowName) { var baseName = String.Format("{0}{1}", prefix, workflowName); var name = baseName; var index = 1; while (_types.ContainsKey(name)) { name = String.Format("{0}{1}", baseName, index); index++; } return name; } public Type AppendSubWorkflowTemplateFromFile(string workflowName, string path) { return AppendSubWorkflowTemplate(workflowName, LoadXamlFromFile(path)); }

    public Type AppendSubWorkflowTemplate(string workflowName, Activity activity)
    {
    return AppendSubWorkflowTemplate(workflowName, XamlServices.Save(activity));
    }

    public Type AppendSubWorkflowTemplate(string workflowName, string xaml) { var name = GetTypeName(_templateTypePrefix, workflowName); var tb = _mb.DefineType(name, TypeAttributes.Public | TypeAttributes.Class, typeof(DynamicActivityTemplateFactory)); var methodb = tb.DefineMethod("GetActivity", MethodAttributes.Public | MethodAttributes.Virtual, typeof(string), null); var msil = methodb.GetILGenerator(); msil.Emit(OpCodes.Ldstr, xaml); msil.Emit(OpCodes.Ret); var t = tb.CreateType(); _types.Add(name, t); return t; } public void Load() { _ab.Save(_fileName); Assembly.Load(new AssemblyName(_moduleName)); } public DynamicActivityDescriptor[] Activities { get { return _types.Select(descriptor => new DynamicActivityDescriptor(descriptor.Key, descriptor.Value)).ToArray(); } } private string LoadXamlFromFile(string path) { var xml = new XmlDocument(); xml.Load(path); if (xml.DocumentElement == null) throw new InvalidOperationException("Unable to load Component. Component has invalid content."); return xml.DocumentElement.InnerXml; } } public class DynamicActivityDescriptor { public DynamicActivityDescriptor(string name, Type type) { Name = name; Type = type; } public string Name { get; set; } public Type Type { get; set; } }

    3. Use above API in Workflow Designer

    var category = new ToolboxCategory("Custom activities"); var generator = new DynamicActivityGenerator("Test.Activities.DynamicActivities");
    var path = @"Test.xaml";
    generator.AppendSubWorkflowTemplateFromFile("Activity from File", path);
    generator.AppendSubWorkflowTemplate("Activity from Xaml", GenerateXaml("Dynamic Activity from Xaml"));
    generator.AppendSubWorkflowTemplate("Activity from Code", new Sequence() { DisplayName = "Dynamic Activity" });
    generator.Load();

    foreach (var descriptor in generator.Activities) { category.Add(new ToolboxItemWrapper(descriptor.Type, descriptor.Name)); } Toolbox.Categories.Add(category);

    4. It's also possible to generate an activity in a design time. For example, add selected activity from the Workflow Designer as an activity template to toolbox.

    - Get selected activity from the Rehosted Designer

    WorkflowDesigner.Context.Items.GetValue<Selection>().PrimarySelection.GetCurrentValue() as Activity

    or use ModelService to find required activity

    var modelService = WorkflowDesigner.Context.Services.GetService<ModelService>();
    IEnumerable<ModelItem> mc = modelService.Find(modelService.Root, typeof(Activity));
    var activity = mc.First().GetCurrentValue() as Activity;

    - Generate assembly with custom activity and add it to Toolbox

    var generator = new DynamicActivityGenerator("Test.Activities.DynamicActivities1");
    generator.AppendSubWorkflowTemplate("New Activity", XamlServices.Save(activity));
    generator.Load();
    
    foreach (var descriptor in generator.Activities)
    {
    	Toolbox.Categories[0].Add(new ToolboxItemWrapper(descriptor.Type, descriptor.Name));
    }

    Note: This solution generates assembly and loads it into current process and there is no simple way to unload this assembly. Therefor if you need to regenerate activity, the new assembly with a new name should be generated. It's possible that at the end we will have many "dynamic" assemblies loaded, but this is not a problem, because these assemblies are used in design time only and are not used in run-time.

    Also, if it's required to persist generated activities for future use, it's better to store them as XAML and generate assembly when Workflow Desisgner is loaded.

    • Marked as answer by lyndon23 Wednesday, December 19, 2012 10:47 AM
    • Edited by Alex Mrynsky Friday, December 21, 2012 8:50 AM Solution Adjusted
    Wednesday, December 19, 2012 8:40 AM
  • Thank you very much, it's working.
    If I don't want to load the xaml from a file, but use the xaml directly from a (custom) activity in my workflow, how do I do this?

    I am accessing my activities by ModelItem. What is the approach to get the xaml out of it, by serialization?

    Wednesday, December 19, 2012 10:56 AM
  • Not sure that I understood you right, but you could get XAML from existing activities using the following code

    XamlServices.Save(new Sequence() { DisplayName = "Hello" });			
    

    Wednesday, December 19, 2012 12:15 PM
  • What I meant are custom Activities that already have a user input. If you save the whole Workflow by

    WorkflowDesigner.Flush()

    you can keep your progess. What I need is not the whole workflow, but let's say the selected Activity (that I want to add to the toolbox).

    The other thing... I'm still struggeling with loading the xaml. It is added to the toolbox, but when dropping it at the Workflow, I get exceptions at this line:

    return XamlServices.Load(new StringReader(GetActivity())) as Activity;

    If I save the workflow and try to generate an Activity, it is just not accepted. There are a lot of prefixes declared in the header, I guess I need all of them?

    Thank you and regards
    Lyndon

    Thursday, December 20, 2012 1:25 PM
  • I have adjusted my solution to provide additional use case for generating Activities from ModelItem. Also please review the Note at the end of the answer.

    As for you problem with loading XAML. Probably you save it incorrectly. Try to use XmlDocument.DocumentElement.InnerXml only as in my example.

    If you post me the XAML, I could provide more concrete answer.

    Hope it helps.

    Regards
    Alex

    • Marked as answer by lyndon23 Friday, December 21, 2012 11:08 AM
    Friday, December 21, 2012 9:01 AM
  • It is working now. Thank you very much!
    It seems to be a quite complicated solution, particularly because you need to create multiple dlls..
    surprises me that wf 4.0 does not support an easier way.
    Wednesday, January 2, 2013 8:09 AM