locked
Custom FlowDecision : FlowNode RRS feed

  • Question

  • Hi,

    I have read on these forums that MS does not intend to have the Flowchart Nodes extendable. Every post I've read the team members ask for a reason why this is required. I will give a valid reason.

    I am developing an application that will be used by non-programer end users. These people will not be able to understand VB expressions. The users intend to create a flow of execution to solve various problems using some basic building blocks. It is intended to be very flexible. I decided to use workflow but in a neutered fasion (maybe not intended use). I have developed various custom activties and activity designers in order to shield the end user from the need to write any VB expressions. 99% of all inputs are design time so this works for me. 

    I need to be able to extend or write a new FlowDecesion. My decision would simply take 2 strings and an int. the strings would match variables and the int would corrospond to the operator. Basically all I need is to modify how the Activity<bool> is presented. Instead of an expersion, I'd like to simplfy it.

    I was well on my way of extending FlowNode untill it would not compile becasue I have not overridden all abstract members. When I attempted to do so I found that the members were flagged as internal. WHY? I cant even attempted to extend and I dont understand why this is prevented. I may have to scrap MS Workflow and use some 3rd party opensource (which I'd prefer not to).

    Is there any other way to accomplish what I want to do with out extending FlowNode?

    Tuesday, November 23, 2010 6:40 PM

Answers

  • Hi All,

    I have discovered a very hackish workaround to my situation.

    In theory what I've done is told my designer to use a custom ActivityDesigner in place of the FlowDecision type. This Control simply exposes the designer of the condition activity:

    <sap:ActivityDesigner x:Class="Hach.IIM.Viper.ViperCustomActivities.CustomFlowDecisionDesigner"
      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"
               >
      <Grid Name="DesignerGrid">
        <sap:WorkflowItemPresenter AllowDrop="False" Name="InsideItem" HintText="" Item="{Binding Path=ModelItem.Condition}">
        </sap:WorkflowItemPresenter>
      </Grid>
    </sap:ActivityDesigner>
    

    Then I created a custom activity TrueFalseActivity : CodedActivity<bool>.

    This activity takes my 2 variable names and operator.

    public sealed class TrueFalseActivity : CodeActivity<bool>
      {
        public string Var1 { get; set; }
        public string Var2 { get; set; }
    
        public int Operator { get; set; }
        
        protected override bool Execute(CodeActivityContext context)
        {
          double v1;
          double v2;
    
          if (!ViperWorkflowDataHandler.ContainsKey(Var1))
          {
            if (!double.TryParse(Var1, out v1))
            {
              throw new ViperParameterDoesNotExistsException(Var1);
            }
          }
          else
          {
            v1 = (double)ViperWorkflowDataHandler.GetResult(Var1);
          }
    
    
          if (!ViperWorkflowDataHandler.ContainsKey(Var2))
          {
            if (!double.TryParse(Var2, out v2))
            {
              throw new ViperParameterDoesNotExistsException(Var2);
            }
          }
          else
          {
            v2 = (double)ViperWorkflowDataHandler.GetResult(Var2);
          }
    
          switch(Operator)
          {
            // =
            case 1:
              return v1 == v2;
              
            // <=
            case 2:
              return v1 <= v2;
              
            default:
              throw new ViperParameterDoesNotExistsException("Operator");
    
          }
        }
      }
    

    I created a Designer for this activity that a simple looking form and bound imputes to activity like normal.

    <sap:ActivityDesigner x:Class="Hach.IIM.Viper.ViperCustomActivities.TrueFalseActivityDesigner"
      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:UIStrings="clr-namespace:Hach.IIM.Viper.ViperCustomActivities">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*"/>
          <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
    
        <TextBlock Grid.Column="0" Grid.Row="0" Text="Var1" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="5"/>
        <TextBox Margin="5" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding ModelItem.Var1, Mode=TwoWay}" Width="100" MaxWidth="100"/>
    
        <TextBlock Grid.Column="0" Grid.Row="1" Text="Var2" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="5"/>
        <TextBox Margin="5" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding ModelItem.Var2, Mode=TwoWay}" Width="100" MaxWidth="100"/>
    
        <TextBlock Grid.Column="0" Grid.Row="2" Text="Operator" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="5"/>
        <TextBox Margin="5" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding ModelItem.Operator, Mode=TwoWay}" Width="100" MaxWidth="100"/>
        
      </Grid>
    </sap:ActivityDesigner>
    

    I then used information I found in this post (http://social.msdn.microsoft.com/Forums/en/wfprerelease/thread/f5df75c8-c3e5-406b-99e0-aea1b87b1342) to automatically load my activity as the condition of the FlowDecision.

    WorkflowDesigner wd;
    
        //private void Button_Click(object sender, RoutedEventArgs e)
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
    
          wd = new WorkflowDesigner();
          (new DesignerMetadata()).Register();
    
          RegisterCustomMetaData();
          wd.Load(new Flowchart());
    
          wd.Context.Services.GetService<ModelService>().ModelChanged += wd_ModelChanged;
    
          Grid.SetRow(wd.View, 1);
          Grid.SetColumn(wd.View, 1);
          DesignerHost.Children.Add(wd.View);
    
          Grid.SetRow(wd.PropertyInspectorView, 1);
          Grid.SetColumn(wd.PropertyInspectorView, 2);
          DesignerHost.Children.Add(wd.PropertyInspectorView);
    
    }
    
    void wd_ModelChanged(object sender, ModelChangedEventArgs e)
        {
          if (e != null && e.ItemsAdded != null)
          {
            foreach (ModelItem item in e.ItemsAdded)
            {
              if (item.ItemType == typeof(FlowDecision))
              {
                item.Properties["Condition"].SetValue(new TrueFalseActivity());
              }
            }
          }
    
        }
    
    private void RegisterCustomMetaData()
        {
          AttributeTableBuilder builder = new AttributeTableBuilder();
          
          builder.AddCustomAttributes(typeof(ViperCustomActivities.ZeroDeviceCodedActivity), new DesignerAttribute(typeof(ZeroDeviceActivityDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.ReadDeviceCodedActivity), new DesignerAttribute(typeof(ReadDeviceActivityDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.WaitCodedActivity), new DesignerAttribute(typeof(DelayActivityDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.SetResultCodedActivity), new DesignerAttribute(typeof(SetResultActivityDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.CalculationActivity), new DesignerAttribute(typeof(CalculationActivityDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.ConstantValueActivity), new DesignerAttribute(typeof(ConstantValueActivityDesigner)));
          builder.AddCustomAttributes(typeof(System.Activities.Statements.FlowDecision), new DesignerAttribute(typeof(CustomFlowDecisionDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.TrueFalseActivity), new DesignerAttribute(typeof(TrueFalseActivityDesigner)));
          MetadataStore.AddAttributeTable(builder.CreateTable());
        }
    

    This produces the look that I am needing (Would Post Picture but dont know if i can.)

    All that is left to do is style my designers to make my activity look correct and to disallow the deletion of the TrueFalseActicity from the condition field of the FlowDecision.

    I hope that this helps others!

    • Marked as answer by Mike Bynum Tuesday, November 23, 2010 8:40 PM
    Tuesday, November 23, 2010 8:39 PM

All replies

  • Hi All,

    I have discovered a very hackish workaround to my situation.

    In theory what I've done is told my designer to use a custom ActivityDesigner in place of the FlowDecision type. This Control simply exposes the designer of the condition activity:

    <sap:ActivityDesigner x:Class="Hach.IIM.Viper.ViperCustomActivities.CustomFlowDecisionDesigner"
      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"
               >
      <Grid Name="DesignerGrid">
        <sap:WorkflowItemPresenter AllowDrop="False" Name="InsideItem" HintText="" Item="{Binding Path=ModelItem.Condition}">
        </sap:WorkflowItemPresenter>
      </Grid>
    </sap:ActivityDesigner>
    

    Then I created a custom activity TrueFalseActivity : CodedActivity<bool>.

    This activity takes my 2 variable names and operator.

    public sealed class TrueFalseActivity : CodeActivity<bool>
      {
        public string Var1 { get; set; }
        public string Var2 { get; set; }
    
        public int Operator { get; set; }
        
        protected override bool Execute(CodeActivityContext context)
        {
          double v1;
          double v2;
    
          if (!ViperWorkflowDataHandler.ContainsKey(Var1))
          {
            if (!double.TryParse(Var1, out v1))
            {
              throw new ViperParameterDoesNotExistsException(Var1);
            }
          }
          else
          {
            v1 = (double)ViperWorkflowDataHandler.GetResult(Var1);
          }
    
    
          if (!ViperWorkflowDataHandler.ContainsKey(Var2))
          {
            if (!double.TryParse(Var2, out v2))
            {
              throw new ViperParameterDoesNotExistsException(Var2);
            }
          }
          else
          {
            v2 = (double)ViperWorkflowDataHandler.GetResult(Var2);
          }
    
          switch(Operator)
          {
            // =
            case 1:
              return v1 == v2;
              
            // <=
            case 2:
              return v1 <= v2;
              
            default:
              throw new ViperParameterDoesNotExistsException("Operator");
    
          }
        }
      }
    

    I created a Designer for this activity that a simple looking form and bound imputes to activity like normal.

    <sap:ActivityDesigner x:Class="Hach.IIM.Viper.ViperCustomActivities.TrueFalseActivityDesigner"
      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:UIStrings="clr-namespace:Hach.IIM.Viper.ViperCustomActivities">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*"/>
          <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
    
        <TextBlock Grid.Column="0" Grid.Row="0" Text="Var1" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="5"/>
        <TextBox Margin="5" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding ModelItem.Var1, Mode=TwoWay}" Width="100" MaxWidth="100"/>
    
        <TextBlock Grid.Column="0" Grid.Row="1" Text="Var2" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="5"/>
        <TextBox Margin="5" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding ModelItem.Var2, Mode=TwoWay}" Width="100" MaxWidth="100"/>
    
        <TextBlock Grid.Column="0" Grid.Row="2" Text="Operator" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="5"/>
        <TextBox Margin="5" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding ModelItem.Operator, Mode=TwoWay}" Width="100" MaxWidth="100"/>
        
      </Grid>
    </sap:ActivityDesigner>
    

    I then used information I found in this post (http://social.msdn.microsoft.com/Forums/en/wfprerelease/thread/f5df75c8-c3e5-406b-99e0-aea1b87b1342) to automatically load my activity as the condition of the FlowDecision.

    WorkflowDesigner wd;
    
        //private void Button_Click(object sender, RoutedEventArgs e)
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
    
          wd = new WorkflowDesigner();
          (new DesignerMetadata()).Register();
    
          RegisterCustomMetaData();
          wd.Load(new Flowchart());
    
          wd.Context.Services.GetService<ModelService>().ModelChanged += wd_ModelChanged;
    
          Grid.SetRow(wd.View, 1);
          Grid.SetColumn(wd.View, 1);
          DesignerHost.Children.Add(wd.View);
    
          Grid.SetRow(wd.PropertyInspectorView, 1);
          Grid.SetColumn(wd.PropertyInspectorView, 2);
          DesignerHost.Children.Add(wd.PropertyInspectorView);
    
    }
    
    void wd_ModelChanged(object sender, ModelChangedEventArgs e)
        {
          if (e != null && e.ItemsAdded != null)
          {
            foreach (ModelItem item in e.ItemsAdded)
            {
              if (item.ItemType == typeof(FlowDecision))
              {
                item.Properties["Condition"].SetValue(new TrueFalseActivity());
              }
            }
          }
    
        }
    
    private void RegisterCustomMetaData()
        {
          AttributeTableBuilder builder = new AttributeTableBuilder();
          
          builder.AddCustomAttributes(typeof(ViperCustomActivities.ZeroDeviceCodedActivity), new DesignerAttribute(typeof(ZeroDeviceActivityDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.ReadDeviceCodedActivity), new DesignerAttribute(typeof(ReadDeviceActivityDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.WaitCodedActivity), new DesignerAttribute(typeof(DelayActivityDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.SetResultCodedActivity), new DesignerAttribute(typeof(SetResultActivityDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.CalculationActivity), new DesignerAttribute(typeof(CalculationActivityDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.ConstantValueActivity), new DesignerAttribute(typeof(ConstantValueActivityDesigner)));
          builder.AddCustomAttributes(typeof(System.Activities.Statements.FlowDecision), new DesignerAttribute(typeof(CustomFlowDecisionDesigner)));
          builder.AddCustomAttributes(typeof(ViperCustomActivities.TrueFalseActivity), new DesignerAttribute(typeof(TrueFalseActivityDesigner)));
          MetadataStore.AddAttributeTable(builder.CreateTable());
        }
    

    This produces the look that I am needing (Would Post Picture but dont know if i can.)

    All that is left to do is style my designers to make my activity look correct and to disallow the deletion of the TrueFalseActicity from the condition field of the FlowDecision.

    I hope that this helps others!

    • Marked as answer by Mike Bynum Tuesday, November 23, 2010 8:40 PM
    Tuesday, November 23, 2010 8:39 PM
  • This was truly helpful!  Thank you for posting it.

    How "supported" by Microsoft is this (i.e., is it likely to break after a .NET update)?  That is, I've just started and haven't read all the documentation yet, so I don't know if anything here is undocumented (and you referred to it as "hackish").  Although the fact that FlowDecision.Condition's type is Activity<bool> indicates that this was anticipated (but I don't know why the built-in designer doesn't support it).

    If I use this technique, will my Workflows save as XAML, Reload, and Execute correctly, etc? 

    I assume I can use other activities, besides the TrueFalseActivity (if they derive from Activity<bool>).  Any advice on having various items in my toolbox which can drag to a Flowchart and create a FlowDecision with different condition activities already populated?


    Tuesday, July 26, 2011 8:20 PM
  • Hi Andy,

    The first hacky part of Mike's solution in terms of wondering the solution is supported is probably his registration of custom metadata to override the default FlowDecision designer association:

    builder.AddCustomAttributes(typeof(System.Activities.Statements.FlowDecision), new DesignerAttribute(typeof(CustomFlowDecisionDesigner)));

    by calling his own RegisterCustomMetadata() function.

    (new DesignerMetadata()).Register();
    RegisterCustomMetaData();

    This technique is very likely continue to work as a way of replacing FlowDesigner in future versions of the .net framework. It's a standard behavior of metadata registration.

    [Mike is relying on observable behavior of a public API - it is more likely to change than the actual public API.]

    Whether it is always bug free is another issue. The other hacky part of the solution is that it breaks an assumption that may be made by Flowchart code, which is that it only contains visual elements which knows about. Such assumptions are arguably regression bugs if they are introduced after release of .net 4.0 if that works ok with this code. From MS point of view, it's easy to say that yes it is a bug if it happens, but it's harder to promise such a regression bug would never happen, since it's not as easily checkable as public API surface.

    Tim


    Wednesday, July 27, 2011 4:04 AM
  • brilliant technique !
    Tuesday, January 1, 2013 12:09 PM