如何为C#应用构建类浏览器的带智能提示脚本控制台?适配DI
Great question! Building a scriptable console with IntelliSense that hooks into your C# app's runtime state and DI container is totally feasible—here's a practical, WPF-focused breakdown to make it happen:
The backbone of your console will be Microsoft Roslyn, .NET's official compiler-as-a-service framework. It lets you dynamically compile and execute C# code at runtime, supports modern C# syntax (including async/await), and integrates seamlessly with your app's existing assemblies. This is way more robust than older alternatives like CodeDOM.
To let scripts interact with your app's services, you need to expose your DI container (or specific services) to the script context. Here's a clean way to do it:
- Create a
ScriptGlobalsclass that holds references to your DI provider or individual services. This acts as the "bridge" between your app and the script. - Pass this
ScriptGlobalsinstance when executing scripts, so the script can call methods that leverage your app's injected dependencies.
Example ScriptGlobals and engine setup:
using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; public class ScriptGlobals { private readonly IServiceProvider _serviceProvider; public ScriptGlobals(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } // Expose app-specific actions to scripts public void OpenMenu() { var menuService = _serviceProvider.GetRequiredService<IMenuService>(); menuService.OpenMainMenu(); WriteToConsole("Main menu opened successfully"); } // Async method example public async Task<string> FetchUserAsync(int userId) { var userService = _serviceProvider.GetRequiredService<IUserService>(); var user = await userService.GetUserByIdAsync(userId); return user?.DisplayName ?? "User not found"; } // Helper to write to the console output public void WriteToConsole(string message) { // Hook this up to your WPF output UI later Console.WriteLine($"[Script Output]: {message}"); } } public class AppScriptEngine { private readonly ScriptGlobals _globals; private readonly ScriptOptions _scriptOptions; // Inject your app's IServiceProvider into the engine public AppScriptEngine(IServiceProvider serviceProvider) { _globals = new ScriptGlobals(serviceProvider); // Configure script references/imports to match your app's dependencies _scriptOptions = ScriptOptions.Default .AddReferences(typeof(IMenuService).Assembly, typeof(IUserService).Assembly) .AddImports("System", "System.Threading.Tasks", "YourAppNamespace.Services"); } // Execute scripts asynchronously public async Task<object> RunScriptAsync(string scriptCode) { try { var script = CSharpScript.Create(scriptCode, _scriptOptions, typeof(ScriptGlobals)); var executionResult = await script.RunAsync(_globals); return executionResult.ReturnValue ?? "Script executed successfully"; } catch (Exception ex) { return $"Error: {ex.Message}\n{ex.StackTrace}"; } } }
For a Chrome/Firefox-like experience, you'll need real-time code completion. For WPF, AvalonEdit (a mature open-source text editor control) is your best bet—it supports syntax highlighting, line numbers, and extensible IntelliSense. Combine it with Roslyn's semantic analysis:
- Use Roslyn's
AdhocWorkspaceto analyze the current script text as the user types. - Fetch completion suggestions via Roslyn's
CompletionServiceand display them in AvalonEdit'sCompletionWindow.
Simplified IntelliSense setup for AvalonEdit:
using ICSharpCode.AvalonEdit; using ICSharpCode.AvalonEdit.CodeCompletion; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.CSharp; private async void ScriptEditor_TextChanged(object sender, EventArgs e) { var editor = (TextEditor)sender; var caretPosition = editor.CaretOffset; var scriptText = editor.Document.Text; // Create a temporary Roslyn workspace to analyze the script var workspace = new AdhocWorkspace(); var project = workspace.AddProject("ScriptProject", LanguageNames.CSharp) .AddMetadataReferences(_scriptEngine._scriptOptions.References); var docId = workspace.AddDocument(project.Id, "TempScript.cs", scriptText); var roslynDoc = workspace.GetDocument(docId); // Get completion suggestions from Roslyn var completionService = CompletionService.GetService(roslynDoc); var completionList = await completionService.GetCompletionsAsync(roslynDoc, caretPosition); // Show completion window if there are suggestions if (completionList?.Items.Any() == true) { var completionWindow = new CompletionWindow(editor.TextArea); var completionData = completionWindow.CompletionList.CompletionData; foreach (var item in completionList.Items) { completionData.Add(new CompletionData(item.DisplayText, item.DisplayText, null)); } completionWindow.Show(); } }
Build a simple, intuitive UI with these components:
- An AvalonEdit control for script input (with syntax highlighting set to C#).
- A read-only
TextBoxorListBoxfor output (to display script results/errors). - A "Run" button (or support Ctrl+Enter) to trigger script execution.
Example XAML:
<Window x:Class="YourApp.ScriptConsole" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" Title="App Script Console" Height="600" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!-- Output Panel --> <TextBox Grid.Row="0" Name="OutputBox" IsReadOnly="True" VerticalScrollBarVisibility="Auto" Margin="5"/> <!-- Script Input Editor --> <avalonEdit:TextEditor Grid.Row="1" Name="ScriptEditor" SyntaxHighlighting="C#" ShowLineNumbers="True" Margin="5" Height="200"/> <!-- Run Button --> <Button Grid.Row="2" Content="Run Script (Ctrl+Enter)" Click="RunScript_Click" Margin="5" HorizontalAlignment="Right"/> </Grid> </Window>
And the code-behind to tie it all together:
public partial class ScriptConsole : Window { private readonly AppScriptEngine _scriptEngine; public ScriptConsole(AppScriptEngine scriptEngine) { InitializeComponent(); _scriptEngine = scriptEngine; // Enable Ctrl+Enter to run scripts ScriptEditor.PreviewKeyDown += (s, e) => { if (e.Key == Key.Enter && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { RunScript_Click(null, null); e.Handled = true; } }; } private async void RunScript_Click(object sender, RoutedEventArgs e) { var scriptCode = ScriptEditor.Text; if (string.IsNullOrWhiteSpace(scriptCode)) return; OutputBox.AppendText($"[Running]: {scriptCode}\n"); var result = await _scriptEngine.RunScriptAsync(scriptCode); OutputBox.AppendText($"[Result]: {result}\n\n"); // Scroll to bottom of output OutputBox.ScrollToEnd(); } }
- Security: Running arbitrary C# code is risky. Restrict access to trusted users only, and avoid exposing sensitive services via
ScriptGlobals. You could also useAssemblyLoadContextto isolate script execution. - Performance: Roslyn's script compilation has overhead—cache frequently used scripts if possible.
- Error Handling: Add robust error parsing to display user-friendly messages in the output panel.
Putting it all together, you'll have a fully functional console that lets users interact with your app's runtime state, use DI services, and get real-time code suggestions—just like browser dev tools.
内容的提问来源于stack exchange,提问作者MoreMag

