Skip to content

Commit

Permalink
Add ScriptRunner infrastructure and update UI for script execution
Browse files Browse the repository at this point in the history
Implemented ScriptRunner and supporting classes to execute Roslyn C# scripts. Refactored the `CSharpScriptControl` to utilize this new infrastructure, added a menu for running scripts and auto-run functionality, and introduced a JSON serialization for non-string result types.
  • Loading branch information
frankhaugen committed Aug 16, 2024
1 parent de8d381 commit e0688eb
Show file tree
Hide file tree
Showing 15 changed files with 514 additions and 243 deletions.
97 changes: 97 additions & 0 deletions Frank.Wpf.Controls.CSharpRenderer/CsharpSyntaxTreeViewFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Windows.Controls;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Frank.Wpf.Controls.CSharpRenderer;

public class CsharpSyntaxTreeViewFactory
{
public TreeView Create(SyntaxTree syntaxTree)
{
var treeView = new TreeView();
var root = syntaxTree.GetRoot();

// Find all namespace declarations
var namespaceNodes = root.DescendantNodes().OfType<NamespaceDeclarationSyntax>().ToList();

// Add namespaces first
foreach (var namespaceNode in namespaceNodes)
{
var namespaceItem = new TreeViewItem
{
Header = namespaceNode.Name.ToString()
};

treeView.Items.Add(namespaceItem);

// For each namespace, add the types within it
AddTypesToNamespace(namespaceItem, namespaceNode);
}

// Find all types that are not inside namespaces
var nonNamespaceTypes = root.DescendantNodes().OfType<TypeDeclarationSyntax>()
.Where(t => !t.Ancestors().OfType<NamespaceDeclarationSyntax>().Any()).ToList();

// Add these types at the root level, after namespaces
foreach (var typeNode in nonNamespaceTypes)
{
var typeItem = new TreeViewItem
{
Header = typeNode.Identifier.Text
};

treeView.Items.Add(typeItem);

// Add members (methods, properties, etc.) to the type
AddMembersToType(typeItem, typeNode);
}

return treeView;
}

private void AddTypesToNamespace(TreeViewItem namespaceItem, NamespaceDeclarationSyntax namespaceNode)
{
var types = namespaceNode.DescendantNodes().OfType<TypeDeclarationSyntax>();

foreach (var typeNode in types)
{
var typeItem = new TreeViewItem
{
Header = typeNode.Identifier.Text
};

namespaceItem.Items.Add(typeItem);

// Add members (methods, properties, etc.) to the type
AddMembersToType(typeItem, typeNode);
}
}

private void AddMembersToType(TreeViewItem typeItem, TypeDeclarationSyntax typeNode)
{
// Add methods
var methodNodes = typeNode.DescendantNodes().OfType<MethodDeclarationSyntax>();
foreach (var methodNode in methodNodes)
{
var methodItem = new TreeViewItem
{
Header = $"{methodNode.Identifier.Text}({string.Join(", ", methodNode.ParameterList.Parameters.Select(p => p.Type.ToString()))})"
};

typeItem.Items.Add(methodItem);
}

// Add properties
var propertyNodes = typeNode.DescendantNodes().OfType<PropertyDeclarationSyntax>();
foreach (var propertyNode in propertyNodes)
{
var propertyItem = new TreeViewItem
{
Header = propertyNode.Identifier.Text
};

typeItem.Items.Add(propertyItem);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Frank.Wpf.Core\Frank.Wpf.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion Frank.Wpf.Controls.ExpandoControl/ExpandoControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ public class ExpandoControl : ContentControl
{
static ExpandoControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ExpandoControl), new FrameworkPropertyMetadata(typeof(ExpandoControl)));

}
}
175 changes: 109 additions & 66 deletions Frank.Wpf.Controls.RoslynScript/CSharpScriptControl.cs
Original file line number Diff line number Diff line change
@@ -1,99 +1,142 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
using System.Windows.Controls;
using Frank.Wpf.Controls.SimpleInputs;

namespace Frank.Wpf.Controls.RoslynScript;

public class CSharpScriptControl : ContentControl
public class CSharpScriptControl : UserControl
{
private StackPanel _stackPanel = new();
private CheckBox _autorunCheckBox;
private TextBox _inputTextBox;
private Button _runButton;
private TextBlock _outputTextBlock;

private ScriptRunner _scriptRunner = new();
private readonly TextBoxWithLineNumbers _outputTextBlock;

private readonly ScriptRunner _scriptRunner;
private string _code;

private bool _autorun;
private readonly MenuItem _runMenuItem;

public CSharpScriptControl()
{
_autorunCheckBox = CreateAutorunCheckBox();
var scriptRunnerBuilder = new ScriptRunnerBuilder()
.WithReference(typeof(object).Assembly)
.WithReference(typeof(Enumerable).Assembly)
.WithReference(typeof(MessageBox).Assembly)
.WithReference(typeof(Enumerable).Assembly)
.WithReference(typeof(IQueryable).Assembly);
_scriptRunner = scriptRunnerBuilder.Build();
var menu = new Menu();
_runMenuItem = new MenuItem { Header = "Run" };
_runMenuItem.Click += async (sender, args) => await ExecuteScriptAsync();
menu.Items.Add(_runMenuItem);

var autorunCheckBox = CreateAutorunCheckBox();
var autorunMenuItem = new MenuItem { Header = autorunCheckBox };
menu.Items.Add(autorunMenuItem);

_outputTextBlock = CreateOutputTextBlock();
_runButton = CreateRunButton();
_inputTextBox = CreateInputTextBox();
var inputTextBox = CreateInputTextBox();

var inputGroupBox = new GroupBox
{
Header = "Input",
Content = inputTextBox
};

_stackPanel.Children.Add(_autorunCheckBox);
_stackPanel.Children.Add(_inputTextBox);
_stackPanel.Children.Add(_runButton);
_stackPanel.Children.Add(_outputTextBlock);
var outputGroupBox = new GroupBox
{
Header = "Output",
Content = _outputTextBlock
};

Content = _stackPanel;
var stackPanel = new StackPanel()
{
Orientation = Orientation.Horizontal
};
stackPanel.Children.Add(inputGroupBox);
stackPanel.Children.Add(outputGroupBox);

var outerStackPanel = new StackPanel();
outerStackPanel.Children.Add(menu);
outerStackPanel.Children.Add(stackPanel);

Content = outerStackPanel;
}

private CheckBox CreateAutorunCheckBox()
{
var checkBox = new CheckBox();
checkBox.Content = "Autorun";
checkBox.IsChecked = false;
checkBox.Checked += (sender, args) => _runButton.IsEnabled = false;
checkBox.Unchecked += (sender, args) => _runButton.IsEnabled = true;

var checkBox = new CheckBox
{
Content = "Autorun",
IsChecked = false
};

checkBox.Checked += (sender, args) =>
{
_autorun = true;
_runMenuItem.IsEnabled = false;
};

checkBox.Unchecked += (sender, args) =>
{
_autorun = false;
_runMenuItem.IsEnabled = true;
};

return checkBox;
}

private TextBlock CreateOutputTextBlock()
{
var textBlock = new TextBlock();
textBlock.Text = "Output";
return textBlock;
}

private Button CreateRunButton()

private TextBoxWithLineNumbers CreateOutputTextBlock()
{
var button = new Button();
button.Content = "Run";
button.Click += Button_Click;
return button;
return new TextBoxWithLineNumbers() { Text = "Output" };
}
private TextBox CreateInputTextBox()

private TextBoxWithLineNumbers CreateInputTextBox()
{
var textBox = new TextBox();
textBox.Text = "return \"Hello, \nWorld!\";";
textBox.AcceptsReturn = true;
textBox.AcceptsTab = true;
textBox.TextWrapping = TextWrapping.Wrap;
textBox.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;

textBox.TextChanged += (sender, args) =>
var textBox = new TextBoxWithLineNumbers
{
if (_autorunCheckBox.IsChecked == true)
TextWrapping = TextWrapping.Wrap,
};

textBox.TextChanged += async () =>
{
_code = textBox.Text;
if (_autorun)
{
_code = textBox.Text;
try
{
var result = _scriptRunner.RoslynScriptingAsync<string>(_code).Result;
_outputTextBlock.Text = result;
}
catch (Exception exception)
{
// Do nothing
}
await ExecuteScriptAsync();
}
};

return textBox;
}
private void Button_Click(object sender, RoutedEventArgs e)

private async Task ExecuteScriptAsync()
{
_code = _inputTextBox.Text;
try
{
var result = _scriptRunner.RoslynScriptingAsync<string>(_code).Result;
_outputTextBlock.Text = result;
var result = await _scriptRunner.RunAsync(_code);

var resultType = result?.GetType();
if (resultType == null)
{
_outputTextBlock.Text = "null";
return;
}

if (resultType == typeof(string))
{
_outputTextBlock.Text = result?.ToString() ?? "null";
return;
}

var jsonDocument = JsonSerializer.SerializeToDocument(result);
var jsonElement = jsonDocument.RootElement;
var prettyPrintedJson = JsonSerializer.Serialize(jsonElement, new JsonSerializerOptions { WriteIndented = true, Converters = { new JsonStringEnumConverter()}});

_outputTextBlock.Text = prettyPrintedJson;
}
catch (Exception exception)
catch (Exception ex)
{
_outputTextBlock.Text = exception.Message;
_outputTextBlock.Text = ex.Message;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.10.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.11.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Frank.Wpf.Controls.SimpleInputs\Frank.Wpf.Controls.SimpleInputs.csproj" />
</ItemGroup>

</Project>
13 changes: 13 additions & 0 deletions Frank.Wpf.Controls.RoslynScript/ScriptGlobals.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Frank.Wpf.Controls.RoslynScript;

public class ScriptGlobals
{
private readonly Dictionary<string, object> _globals;

public ScriptGlobals(Dictionary<string, object> globals)
{
_globals = globals;
}

public object? this[string name] => _globals.ContainsKey(name) ? _globals[name] : null;
}
Loading

0 comments on commit e0688eb

Please sign in to comment.