
One of the clear advantages of using Visual Studio for editing workflows over a custom implementation is the readily available auto-completion features of the IDE. A similar style of auto-completion can be added to your re-hosted workflow designer by implementing your own ‘editor service’ and publishing it to the workflow designer control.
In order to provide an expression editor auto-completion service you need to develop a class that implements the System.Activities.Presentation.View.IExpressionEditorService interface and will return editor instances providing the auto-completion. This service then needs to be registered with the workflow designer.
Before we can do that though, we need to define some classes that will provide our auto-completion functionality. First of all, we need to provide the auto-completion terms in a form that can be navigated in a hierarchical structure. The simplest way to achieve this is by implementing a tree structure of nodes.
As with previous posts in this series, the worked example builds upon the basic re-hosted workflow designer application described in this article.
To improve the readability of your code, you may want to group the expression editing auto-completion classes into a separate folder in your code project (e.g. ‘Expressions’.)
using System.Collections.Generic;
/// <summary>
/// Represents a node in an auto-completion expression tree.
/// </summary>
public class ExpressionNode
{
/// <summary>
/// Creates a new instance of the <see cref="ExpressionNode" /> class.
/// </summary>
public ExpressionNode()
{
this.Nodes = new List<ExpressionNode>();
}
/// <summary>
/// Gets or sets a description of the entity expression node represents.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets the type of item being represented by the node
/// (namespace, class, property, method, field, enum, struct, keyword
/// or variable)
/// </summary>
public string ItemType { get; set; }
/// <summary>
/// Gets or sets the name of the expression the node represents.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the parent node in the tree, or null if the current node
/// is the root.
/// </summary>
public ExpressionNode Parent { get; set; }
/// <summary>
/// Gets the collection of sub-nodes in the expression tree.
/// </summary>
public List<ExpressionNode> Nodes { get; private set; }
/// <summary>
/// Gets the full path of the expression node.
/// </summary>
public string Path
{
get
{
return (this.Parent == null)
? this.Name
: this.Parent.Path + "." + this.Name;
}
}
/// <summary>
/// Adds a sub-node below the current node.
/// </summary>
/// <param name="node">The node to add.</param>
public void Add(ExpressionNode node)
{
this.Nodes.Add(node);
}
/// <summary>
/// Performs an alphabetic sort of the nodes sub-tree.
/// </summary>
/// <param name="ascending">
/// True to sort in ascending order, or False for descending order.
/// </param>
public void Sort(bool ascending = true)
{
if (ascending)
{
this.Nodes.Sort((x,y) => x.Name.CompareTo(y.Name));
}
else // descending order
{
this.Nodes.Sort((x,y) => y.Name.CompareTo(x.Name));
}
foreach (ExpressionNode subNode in this.Nodes)
{
subNode.Sort(ascending);
}
}
/// <summary>
/// Searches for a node within the current sub-tree with the specified path.
/// </summary>
/// <param name="path">The full path of the expression node.</param>
public ExpressionNode SearchForNode(string path)
{
return ExpressionNode.SearchForNode(this, path);
}
/// <summary>
/// Searches an expression tree for a node with the specified path.
/// </summary>
/// <param name="target">The root node to search within.</param>
/// <param name="path">The full path to the expression node.</param>
public static ExpressionNode SearchForNode(ExpressionNode target, string path)
{
ExpressionNode match = SearchForNode(target, path, false, false);
return match ?? target; // return the match, or the supplied target.
}
/// <summary>
/// Searches an expression tree for a node with the specified path.
/// </summary>
/// <param name="target">The root node to search within.</param>
/// <param name="path">The full path to the expression node.</param>
/// <param name="isNamespace">
/// True to force the node to be a namespace, or False to intelligently
/// manage type.
/// </param>
/// <param name="createIfMissing">
/// True to create any missing nodes en-route to finding the node being
/// targeted, and False otherwise.
/// </param>
public static ExpressionNode SearchForNode(
ExpressionNode target, string path, bool isNamespace, bool createIfMissing)
{
string[] names = path.Split('.');
string subPath = "";
if (names.Length > 0 && path.Length > names[0].Length)
{
subPath = path.Substring(names[0].Length, path.Length - names[0].Length);
}
if (subPath.StartsWith("."))
{
subPath = subPath.Substring(1);
}
List<ExpressionNode> matches =
(from x in target.Nodes
where x.Name.Equals(names[0], StringComparison.OrdinalIgnoreCase)
select x)
.ToList();
if (matches.Count == 0)
{
if (!createIfMissing) return null;
ExpressionNode subNode = new ExpressionNode
{
Name = names[0],
ItemType = isNamespace || names.Length > 1 ? "namespace" : "class",
Parent = target,
Description = isNamespace || names.Length > 1
? string.Format("Namespace {0}", names[0])
: string.Format("Class {0}", names[0])
};
target.Nodes.Add(subNode);
if (subPath.Trim() != "")
{
return SearchForNode(subNode, subPath, isNamespace, true);
}
return subNode;
}
// A match was found so search within that sub-tree.
if (subPath.Trim() != "")
{
return SearchForNode(matches[0], subPath, isNamespace, createIfMissing);
}
return matches[0];
}
/// <summary>
/// Returns a sub-list of first-tier nodes that meet the partial path filter.
/// </summary>
/// <param name="rootNode">The root node to search within.</param>
/// <param name="filter">The path name to filter on.</param>
public static List<ExpressionNode> SubsetAutoCompletionList(
ExpressionNode rootNode, string filter)
{
string parentPath = "";
string searchTerm = filter;
if (filter.Contains("."))
{
parentPath = filter.Substring(0, filter.LastIndexOf("."));
searchTerm = filter.Substring(parentPath.Length + 1);
}
ExpressionNode targetNode = parentPath != ""
? SearchForNode(rootNode, parentPath)
: rootNode;
List<ExpressionNode> matches = new List<ExpressionNode>();
foreach (ExpressionNode subNode in targetNode.Nodes)
{
if (subNode.Name.StartsWith(
searchTerm, StringComparison.OrdinalIgnoreCase))
{
matches.Add(subNode);
}
}
return matches;
}
}
Code language: C# (cs)
Then we need to define a pop-up window for our auto-completion editor. Create a WPF user control called AutoCompletionPopup and change the root type to ‘Popup’ (you may want to change the base-type in the code-behind at the same time.) Replace the contents of the pop-up with the following XAML markup:
<Grid Name="MainGrid" Width="300" Height="200" >
<ListBox Name="AutoCompletionListBox" IsTextSearchEnabled="True"
ItemsSource="{Binding Path=.}" VirtualizingStackPanel.IsVirtualizing="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<EventSetter Event="MouseDoubleClick"
Handler="OnListBoxItemDoubleClick" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Width="Auto" Text="{Binding Path=Name}"
ToolTip="{Binding Path=Description}" Margin="2" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
Code language: HTML, XML (xml)
Switch to the code-behind file for the pop-up window and add the following code:
/// <summary>
/// Occurs when an item in the auto-completion list is double clicked.
/// </summary>
public event MouseButtonEventHandler ListBoxItemDoubleClick;
/// <summary>
/// Gets or sets the selected item in the auto-completion list.
/// </summary>
public ExpressionNode SelectedItem
{
get { return (ExpressionNode)this.AutoCompletionListBox.SelectedItem; }
set { this.AutoCompletionListBox.SelectedItem = value; }
}
/// <summary>
/// Gets or sets the index of the selected item in the auto-completion list.
/// </summary>
public int SelectedIndex
{
get { return this.AutoCompletionListBox.SelectedIndex; }
set
{
// Protect against the index under- or over-ranging.
if (value < -1 || value > this.AutoCompletionListBox.Items.Count - 1) return;
this.AutoCompletionListBox.SelectedIndex = value;
this.AutoCompletionListBox.ScrollIntoView(
this.AutoCompletionListBox.SelectedItem);
}
}
/// <summary>
/// Gets the number of items in the auto-completion list.
/// </summary>
public int Count
{
get { return this.AutoCompletionListBox.Items.Count; }
}
/// <summary>
/// Occurs when an item in the auto-completion list is double-clicked.
/// </summary>
/// <param name="sender">The object from which the event initiated.</param>
/// <param name="e">The object that contains the event data.</param>
protected void OnListBoxItemDoubleClick(object sender, MouseButtonEventArgs e)
{
if (this.ListBoxItemDoubleClick != null) {
{
this.ListBoxItemDoubleClick.Invoke(sender, e);
}
}
Code language: C# (cs)
Next, we need to create our editor. For this we will create a class that inherits from IExpressionEditorInstance and add the functionality to manage auto-completion in an editor window. Initially implement the interface and then add the following code to the class.
/// <summary>
/// The text box auto-completion editing is being done in.
/// </summary>
private readonly TextBox _editor;
/// <summary>
/// Creates a new instance of the <see cref="EditorInstance" /> class.
/// </summary>
public EditorInstance()
{
this.Id = Guid.NewGuid();
this._editor = new TextBox();
this._editor.KeyDown += this._editor_KeyDown;
this._editor.LostFocus += this._editor_LostFocus;
this._editor.PreviewKeyDown += this._editor_KeyPress;
this._editor.TextChanged += this._editor_TextChanged;
}
/// <summary>
/// Gets or sets the root node of the expression tree that auto-completion
/// editing is based on.
/// </summary>
public ExpressionNode AutoCompletionList { get; set; }
/// <summary>
/// Gets or sets the language-specific keywords (operators, types, etc.)
/// </summary>
public List<string> LanguageKeywords { get; set; }
/// <summary>
/// Gets the unique ID of the editor instance.
/// </summary>
public Guid Id { get; private set; }
private void Editor_TextChanged(object sender, TextChangedEventArgs e)
{
throw new NotImplementedException();
}
private void Editor_KeyPress(object sender, KeyEventArgs e)
{
throw new NotImplementedException();
}
private void Editor_LostFocus(object sender, RoutedEventArgs e)
{
throw new NotImplementedException();
}
private void Editor_KeyDown(object sender, KeyEventArgs e)
{
throw new NotImplementedException();
}
Code language: C# (cs)
Update the implemented interface properties and methods of the class so that the following return ‘true’:
- CanCompleteWord
- CanCopy
- CanCut
- CanPaste
- CompleteWord
- Copy
- Cut
- HasAggregateFocus
- Paste
- Redo
- Undo
Update the interface properties and methods of the class so that the following return ‘false’:
- CanDecreaseFilterLevel
- CanGlobalIntellisense
- CanIncreaseFilterLevel
- CanParameterInfo
- CanQuickInfo
- DecreaseFilterLevel
- GlobalIntellisense
- IncreaseFilterLevel
- ParameterInfo
- QuickInfo
Leave the contents of the following methods empty:
- ClearSelection
- Close
The remaining properties and methods of the interface should be implemented like this:
public bool AcceptsReturn
{
get { return this._editor.AcceptsReturn; }
set { this._editor.AcceptsReturn = value; }
}
public bool AcceptsTab
{
get { return this._editor.AcceptsTab; }
set { this._editor.AcceptsTab = value; }
}
public bool CanRedo()
{
return this._editor.CanRedo;
}
public bool CanUndo()
{
return this._editor.CanUndo;
}
public void Focus()
{
this._editor.Focus();
}
public string GetCommittedText()
{
return this._editor.Text;
}
public ScrollBarVisibility HorizontalScrollBarVisibility
{
get { return this._editor.HorizontalScrollBarVisibility; }
set { this._editor.HorizontalScrollBarVisibility = value; }
}
public Control HostControl
{
get { return this._editor; }
}
public int MaxLines
{
get { return this._editor.MaxLines; }
set { this._editor.MaxLines = value; }
}
public int MinLines
{
get { return this._editor.MinLines; }
set { this._editor.MinLines = value; }
}
public string Text
{
get { return this._editor.Text; }
set { this._editor.Text = value; }
}
public ScrollBarVisibility VerticalScrollBarVisibility
{
get { return this._editor.VerticalScrollBarVisibility; }
set { this._editor.VerticalScrollBarVisibility = value; }
}
Code language: C# (cs)
Now we need to respond to those editing events we created blank handlers for earlier. Update the appropriate methods of the class as follows (and add the helper methods being called too):
/// <summary>
/// The pop-up window that will display the auto-completion options.
/// </summary>
private AutoCompletionPopup _completionPanel;
/// <summary>
/// Returns the current term being auto-completed in the editor.
/// </summary>
private string GetCurrentTerm()
{
string term = this._editor.Text;
if (term.Contains(" "))
{
term = term.Substring(term.LastIndexOf(' ') + 1);
}
return term;
}
/// <summary>
/// Returns a collection of the VB language keywords in scope, based on the
/// current filter.
/// </summary>
/// <param name="filter">The filter to use to sub-set the keywords list.</param>
private List<ExpressionNode> GetKeywordsInScope(string filter)
{
List<ExpressionNode> keywords = new List<ExpressionNode>();
foreach (string keyword in this.LanguageKeywords)
{
if (string.IsNullOrEmpty(filter) ||
keyword.StartsWith(filter, StringComparison.OrdinalIgnoreCase))
{
keywords.Add(new ExpressionNode
{
Description = "Visual Basic Keyword: " + keyword,
Name = keyword,
ItemType = "keyword",
Parent = null
});
}
}
return keywords;
}
/// <summary>
/// Instantiates and displays a new auto-completion window using the supplied
/// auto-completion tree.
/// </summary>
private void ShowPopup(List<ExpressionNode> rootNodes)
{
if (this._completionPanel != null && this._completionPanel.IsOpen)
{
this.HidePopup();
}
this._completionPanel = new AutoCompletionPopup
{
DataContext = rootNodes,
PlacementTarget = this._editor,
Placement = PlacementMode.Bottom
};
this._completionPanel.ListBoxItemDoubleClick += this.ListItem_DoubleClick;
}
/// <summary>
/// Hides and unloads the auto-completion pop-up window.
/// </summary>
private void HidePopup()
{
if (this._completionPanel == null) return;
this._completionPanel.ListBoxItemDoubleClick -= this.ListItem_DoubleClick;
if (this._completionPanel.IsOpen) this._completionPanel.IsOpen = false;
this._completionPanel = null;
}
/// <summary>
/// Displays the auto-completion pop-up list box with the default root nodes
/// and VB language keywords.
/// </summary>
private void ShowRootAutoCompletionList()
{
List<ExpressionNode> keywords =
new List<ExpressionNode>(this.AutoCompletionList.Nodes);
keywords.AddRange(this.GetKeywordsInScope(""));
this.ShowPopup(keywords);
this._completionPanel.IsOpen = true;
}
/// <summary>
/// Filters the auto-completion list based on text that has been entered and then
/// displays the auto-completion pop-up pane with that content.
/// </summary>
private void ShowCustomAutoCompletionList()
{
string term = this._editor.Text;
if (term.Contains(" ")) term = term.Substring(term.LastIndexOf(" ") + 1);
if (term.Trim() == "")
{
this.ShowRootAutoCompletionList();
return;
}
List<ExpressionNode> completionTerms =
ExpressionNode.SubsetAutoCompletionList(this.AutoCompletionList, term);
this.ShowPopup(completionTerms);
this._completionPanel.IsOpen = true;
}
/// <summary>
/// Occurs when a key is pressed down with focus on the editor text-box.
/// </summary>
/// <param name="sender">The object from which the event initiated.</param>
/// <param name="e">The object that contains the event data.</param>
private void Editor_KeyDown(object sender, KeyEventArgs e)
{
if (!this.AcceptsTab && e.Key == Key.Tab &&
Keyboard.Modifiers == ModifierKeys.None)
{
e.Handled = true;
TraversalRequest navigator =
new TraversalRequest(FocusNavigationDirection.Next);
UIElement element = Keyboard.FocusedElement as UIElement;
if (element != null)
{
element.MoveFocus(navigator);
}
}
}
/// <summary>
/// Occurs when a key is pressed with focus on the editor text-box.
/// </summary>
/// <param name="sender">The object from which the event initiated.</param>
/// <param name="e">The object that contains the event data.</param>
private void Editor_KeyPress(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape && this._completionPanel != null)
{
this.HidePopup();
e.Handled = true;
return;
}
if (e.Key == Key.Up || e.Key == Key.Down)
{
if (this._completionPanel == null || !this._completionPanel.IsOpen) return;
if (e.Key == Key.Up) this._completionPanel.SelectedIndex -= 1;
else this._completionPanel.SelectedIndex += 1;
e.Handled = true;
return;
}
if (e.Key == Key.Space && Keyboard.Modifiers == ModifierKeys.Control)
{
if (this._editor.Text == "" || this._editor.Text.EndsWith(" "))
{
// Display the default auto-completion list (root nodes and
// VB language options).
this.ShowRootAutoCompletionList();
}
else // Set a custom (filtered) list.
{
this.ShowCustomAutoCompletionList();
}
e.Handled = true;
}
if (this._completionPanel != null &&
this._completionPanel.IsOpen &&
(e.Key == Key.Enter || e.Key == Key.Tab))
{
string term = this.GetCurrentTerm();
// Check if a word has been explicitly selected first.
string completionWord = "";
if (this._completionPanel != null &&
this._completionPanel.IsOpen &&
this._completionPanel.SelectedIndex > -1)
{
completionWord = this._completionPanel.SelectedItem.Name;
}
List<ExpressionNode> completionTerms = new List<ExpressionNode>();
if (completionWord == "")
{
completionTerms =
ExpressionNode.SubsetAutoCompletionList(this.AutoCompletionList, term);
if (!term.Contains("."))
{
completionTerms.AddRange(this.GetKeywordsInScope(term));
}
if (completionTerms.Count == 1) completionWord = completionTerms[0].Name;
}
if (completionWord != "")
{
if (term.Contains(".")) term = term.Substring(term.LastIndexOf(".") + 1);
// Finish off the term...
this._editor.Text =
this._editor.Text.Substring(0, this._editor.Text.Length - term.Length) +
completionWord;
this._editor.SelectionStart = this._editor.Text.Length;
}
e.Handled = true;
}
}
/// <summary>
/// Occurs when the editor text-box looses focus.
/// </summary>
/// <param name="sender">The object from which the event initiated.</param>
/// <param name="e">The object that contains the event data.</param>
private void Editor_LostFocus(object sender, EventArgs e)
{
ListBoxItem item = Keyboard.FocusedElement as ListBoxItem;
if (item == null)
{
if (this._completionPanel != null && this._completionPanel.IsOpen)
{
this.HidePopup();
}
this.LostAggregateFocus.Invoke(sender, e);
}
}
/// <summary>
/// Occurs when the text in editor text-box has changed.
/// </summary>
/// <param name="sender">The object from which the event initiated.</param>
/// <param name="e">The object that contains the event data.</param>
private void Editor_TextChanged(object sender, EventArgs e)
{
this.Text = this._editor.Text;
if (this._completionPanel == null)
{
if (this._editor.Text == "" || this._editor.Text.EndsWith(" "))
{
// Display the root auto-completion list and the VB language keywords
// as the auto-completion options.
this.ShowRootAutoCompletionList();
}
return;
}
string term = this.GetCurrentTerm();
List<ExpressionNode> rootNodes =
ExpressionNode.SubsetAutoCompletionList(this.AutoCompletionList, term);
if (!term.Contains("."))
{
// Add the VB language keywords as we aren't definitely targeting a
// namespace or type yet...
rootNodes.AddRange(this.GetKeywordsInScope(term));
}
if (rootNodes.Count == 0)
{
this.HidePopup();
return;
}
this._completionPanel.DataContext = rootNodes;
}
/// <summary>
/// Occurs when an item in the auto-completion pop-up is double-clicked.
/// </summary>
/// <param name="sender">The object from which the event initiated.</param>
/// <param name="e">The object that contains the event data.</param>
private void ListItem_DoubleClick(object sender, MouseButtonEventArgs e)
{
ListBoxItem item = sender as ListBoxItem;
if (item == null) return;
ExpressionNode node = item.DataContext as ExpressionNode;
if (node == null) return;
string term = this.GetCurrentTerm();
string output = this._editor.Text;
if (term.Contains("."))
{
output = output.Substring(0, output.Length - term.Length) +
term.Substring(0, term.LastIndexOf(".") + 1) + node.Name;
}
else
{
output = output.Substring(0, output.Length - term.Length) + node.Name;
}
this._editor.Text = output;
this._editor.SelectionStart = this._editor.Text.Length;
this._editor.Focus();
}
Code language: C# (cs)
Finally, create a class called EditorServce that implements IExpressionEditorService and then implement the interface members in the class.
The UpdateContext and CloseExpressionEditors methods can be left empty.
For each of the CreateExpressionEditor methods, make a call to a private method ‘CreateEditor’ that is defined in the code fragment that follows (add all the methods and properties to the class.)
private IExpressionEditorInstance CreateEditor(
AssemblyContextControlItem assemblies,
ImportedNamespaceContextItem importedNamespaces,
List<ModelItem> variables, string text, Type expressionType)
{
EditorInstance instance = new EditorInstance
{
AutoCompletionList = this.AddVariablesToAutoCompletionList(variables),
LanguageKeywords = this.LanguageKeywords,
Text = text
};
return instance;
}
/// <summary>
/// Gets a collection of editing language keywords the editor should support.
/// </summary>
public List<string> LanguageKeywords
{
get
{
return new List<string>(new[] {
"And", "AndAlso", "New", "Or", "OrElse", "Throw" });
}
}
/// <summary>
/// Gets or sets the root node in the expression auto-completion tree.
/// </summary>
public ExpressionNode AutoCompletionData { get; set; }
/// <summary>
/// Returns an updated auto-completion expression tree that uses the baseline
/// data in the <see cref="AutoCompletionData" /> property and appends the
/// specified variable names.
/// </summary>
/// <param name="variables">The collection of variables to include.</param>
private ExpressionNode AddVariablesToAutoCompletionList(List<ModelItem> variables)
{
ExpressionNode data = this.AutoCompletionData;
Type systemType;
ModelProperty property;
foreach (ModelItem item in variables)
{
property = item.Properties["Name"];
if (property == null) continue;
string computedName = property.ComputedValue.ToString();
List<ExpressionNode> results = new List<ExpressionNode>(
from x in data.Nodes
where
x.Name.Equals(computedName) select x);
if (results.Count == 0)
{
data.Nodes.Add(new ExpressionNode
{
Name = computedName,
ItemType = "variable",
Description = "Variable: " + computedName
});
}
}
return data;
}
Code language: C# (cs)
he ‘LanguageKeywords’ property has been kept light-weight here for demonstration purposes but you could conceivably add any Visual Basic keyword you think will be useful in the editor.
The final step is to wire up the editor service in the workflow designer. This is done by instantiating the service and then publishing it to the designer. Open the MainWindow code-behind file for the project and navigate to the NewDesigner() method. Just after the workflow designer is instantiated, add the following:
this.editor.Context.Services.Publish<IExpressionEditorService>(
new EditorService { AutoCompletionData = this._autoCompletionTree });
Code language: C# (cs)
You’ll notice that we have an undefined variable in the above code fragment, let’s add it now and initialise it in the class constructor. There is a fair amount of code to add here, because we need to build a complete list of the loaded types for the editor.
/// <summary>
/// The root node of the expression auto-completion tree.
/// </summary>
private ExpressionNode _autoCompletionTree;
public MainWindow() // Edit existing constructor!
{
InitializeComponent();
// Register the runtime metadata for the designer.
new DesignerMetadata().Register();
// NOTE: assignment of new variable here!
this._autoCompletionTree = this.CreateDefault_autoCompletionTree();
// NOTE: End of assignment!
this.NewDesigner();
this._editor.Load(new ActivityBuilder() { Implementation = new Sequence() });
this.InitialiseToolbox();
}
/// <summary>
/// Builds a default auto-completion navigation tree based on the currently
/// loaded assemblies.
/// </summary>
private ExpressionNode CreateDefault_autoCompletionTree()
{
Assembly target = Assembly.GetExecutingAssembly();
List<Assembly> references =
(from assemblyName in target.GetReferencedAssemblies()
select Assembly.Load(assemblyName))
.ToList();
List<Type> types = new List<Type>(
references.SelectMany(
(assembly) =>
(from childType in assembly.GetTypes()
where (childType.IsPublic &&
childType.IsVisible && childType.Namespace != null)
select childType)
.ToList()
));
ExpressionNode rootNode = new ExpressionNode();
foreach (Type child in types)
{
this.AddTypeToExpressionTree(rootNode, child);
}
rootNode.Sort();
return rootNode;
}
/// <summary>
/// Adds details about a type to the supplied expression tree.
/// </summary>
/// <param name="target">The root node of the expression tree.</param>
/// <param name="child">The type to add.</param>
private void AddTypeToExpressionTree(ExpressionNode target, Type child)
{
ExpressionNode rootNode;
if (child.IsGenericType)
{
rootNode = ExpressionNode.SearchForNode(target, child.Namespace, true, true);
this.AddGenericTypeDetails(rootNode, child);
}
else if (child.IsClass)
{
rootNode = ExpressionNode.SearchForNode(target, child.Namespace, true, true);
this.AddClassDetails(rootNode, child);
}
else if (child.IsEnum)
{
rootNode = ExpressionNode.SearchForNode(target, child.Namespace, true, true);
this.AddEnumeratedTypeDetails(rootNode, child);
}
else if (child.IsValueType)
{
rootNode = ExpressionNode.SearchForNode(target, child.Namespace, true, true);
this.AddValueTypeDetails(rootNode, child);
}
}
private void AddGenericTypeDetails(ExpressionNode rootNode, Type child)
{
ExpressionNode entityNode = new ExpressionNode
{
Description = "Type: " + child.Name,
Name = child.Name,
ItemType = "class",
Parent = rootNode
};
rootNode.Add(entityNode);
this.AddFieldNodes(entityNode, child);
this.AddPropertyNodes(entityNode, child);
this.AddMethodNodes(entityNode, child);
}
private void AddClassDetails(ExpressionNode rootNode, Type child)
{
ExpressionNode entityNode = new ExpressionNode
{
Description = "Class: " + child.Name,
Name = child.Name,
ItemType = "class",
Parent = rootNode
};
rootNode.Add(entityNode);
this.AddFieldNodes(entityNode, child);
this.AddPropertyNodes(entityNode, child);
this.AddMethodNodes(entityNode, child);
}
private void AddEnumeratedTypeDetails(ExpressionNode rootNode, Type child)
{
ExpressionNode enumNode = new ExpressionNode
{
Description = "Enum: " + child.Name,
Name = child.Name,
ItemType = "enum",
Parent = rootNode
};
rootNode.Add(enumNode);
string[] names = Enum.GetNames(child);
Array values = Enum.GetValues(child);
for (int i = 0; i < names.Length; i++)
{
enumNode.Add(new ExpressionNode
{
Description = string.Format(
"Enum Value: {0} = {1} ", names[i], values.GetValue(i)),
Name = names[i],
ItemType = "enum",
Parent = enumNode
});
}
}
private void AddValueTypeDetails(ExpressionNode rootNode, Type child)
{
// TODO: This!
}
private void AddFieldNodes(ExpressionNode target, Type child)
{
foreach (FieldInfo field in child.GetFields(
BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance))
{
ExpressionNode fieldNode = new ExpressionNode
{
Name = field.Name,
ItemType = "field",
Parent = target,
Description = this.GetFieldDescription(field)
};
target.Add(fieldNode);
}
}
private string GetFieldDescription(FieldInfo target)
{
StringBuilder description = new StringBuilder(128);
if (target.IsPublic) description.Append("Public ");
if (target.IsPrivate) description.Append("Private ");
if (target.IsStatic) description.Append("Shared ");
description.Append(target.Name);
description.Append("() ");
description.Append("As " + target.FieldType.Name);
description.Append(this.GetParameters(target.FieldType));
return description.ToString();
}
private string GetParameters(Type target)
{
StringBuilder parameter = new StringBuilder(128);
if (target.IsGenericType)
{
parameter.Append("(Of ");
foreach (Type argument in target.GetGenericArguments())
{
parameter.Append(argument.Name);
parameter.Append(", ");
}
if (parameter.Length > 4)
{
parameter.Remove(parameter.Length - 2, 2);
}
parameter.Append(")");
}
return parameter.ToString();
}
private string GetParameters(MethodInfo target)
{
StringBuilder parameter = new StringBuilder(128);
if (target.IsGenericMethod)
{
parameter.Append("(Of ");
foreach (Type argument in target.GetGenericArguments())
{
parameter.Append(argument.Name);
parameter.Append(", ");
}
if (parameter.Length > 4)
{
parameter.Remove(parameter.Length - 2, 2);
}
parameter.Append(")");
}
return parameter.ToString();
}
private void AddMethodNodes(ExpressionNode target, Type child)
{
// Protect against the properties being identified as methods
// with a 'get_' or 'set_' prefix on their name...
List<string> properties = new List<string>();
foreach (PropertyInfo property in child.GetProperties(
BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance))
{
if (property.CanRead) properties.Add("get_" + property.Name);
if (property.CanWrite) properties.Add("set_" + property.Name);
}
foreach (MethodInfo method in child.GetMethods(
BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance))
{
if (properties.Contains(method.Name)) continue;
ExpressionNode methodNode = new ExpressionNode
{
Name = method.Name,
ItemType = "method",
Parent = target,
Description = this.GetMethodDescription(method)
};
target.Add(methodNode);
}
}
private string GetMethodDescription(MethodInfo target)
{
StringBuilder description = new StringBuilder(128);
if (target.IsPublic) description.Append("Public ");
if (target.IsFamily) description.Append("Protected ");
if (target.IsAssembly) description.Append("Friend ");
if (target.IsPrivate) description.Append("Private ");
if (target.IsAbstract) description.Append("MustOverride ");
if (target.IsVirtual && !target.IsFinal) description.Append("Overridable ");
if (target.IsStatic) description.Append("Shared ");
if (target.ReturnType != typeof(void)) description.Append("Function ");
else description.Append("Sub ");
description.Append(target.Name);
description.Append(GetParameters(target));
description.Append("(");
ParameterInfo[] parameters = target.GetParameters();
foreach (ParameterInfo param in parameters)
{
if (param.IsOptional) description.Append("Optional ");
if (param.IsOut) description.Append("ByRef ");
else description.Append("ByVal ");
description.Append(param.Name + " As " + param.ParameterType.Name);
description.Append(this.GetParameters(param.ParameterType));
if (param.DefaultValue == null) description.Append(" = Nothing");
else description.Append(" = " + param.DefaultValue);
description.Append(", ");
}
//remove trailing comma, if present.
if (parameters.Length > 0) description.Remove(description.Length - 2, 2);
description.Append(") ");
if (target.ReturnType != typeof(void))
{
description.Append("As " + target.ReturnType.Name);
description.Append(this.GetParameters(target.ReturnType));
}
return description.ToString();
}
private void AddPropertyNodes(ExpressionNode target, Type child)
{
foreach (PropertyInfo property in child.GetProperties(
BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance))
{
ExpressionNode propertyNode = new ExpressionNode
{
Name = property.Name,
ItemType = "property",
Parent = target,
Description = this.GetPropertyDescription(property)
};
target.Add(propertyNode);
}
}
private string GetPropertyDescription(PropertyInfo target)
{
StringBuilder description = new StringBuilder(128);
if (!target.CanWrite && target.CanRead) description.Append("ReadOnly ");
else if (target.CanWrite && !target.CanRead) description.Append("WriteOnly ");
description.Append("Property " + target.Name + " As " + target.PropertyType.Name);
description.Append(this.GetParameters(target.PropertyType));
return description.ToString();
}
Code language: C# (cs)
Now simply compile and run the re-hosted workflow designer application and it will have expression auto-completion features enabled!
One last subject to consider is how you are going to refresh the auto-completion tree if you load additional assemblies into the re-hosted designer application. This is fairly straightforward – simply retrieve the editor service from the workflow designer class-level variable and update the AutoCompletionData property with a revised auto-completion tree. For example:
// TODO: Some code that loads the assembly
// (e.g. Open-File prompt and Assembly.LoadFrom.)
if (this._editor != null)
{
EditorService service =
this._editor.Context.Services.GetService<EditorService>;();
if (service != null)
{
foreach (Type entry in myAssembly.GetTypes())
{
if (entry != null && entry.IsPublic)
{
this.AddTypeToExpressionTree(this._autoCompletionTree, entry);
}
}
service.AutoCompletionData = this._autoCompletionTree;
}
}
Code language: C# (cs)