简体   繁体   中英

.NET reference design for GUI app with built-in interactive terminal console (e.g. SublimeText, Visual Studio)

I'm trying to build a GUI app that has an interactive console, much like the one found in SublimeText. I hope it is a valid question because it seems to be "a practical, answerable problem that is unique to software development".

In short, I see huge benefits having an interactive console inside a GUI app for

  • debugging, probing internal variables at runtime
  • logging
  • quick configuration changes

However, I have not come across any existing open-source applications that uses such a design. I'm hoping someone has done it before and can share his/her design approach.

While I do have a semi-working solution using reflection and invoke in .NET, it is limited to only function calls and I'm not able to probe into nested internal variables (eg object.property.property).

To make the question more specific, these are the problems I'm facing:

  1. Not easily extensible (Need to wire every new GUI command to a console command, vice-versa), any design tips? Routed commands (I could not find a useful example either)?
  2. How to execute dynamic code that can access all existing object instances in the entire .NET app?

Thank you.

So here comes the code which worked for me:

namespace ReflectionsTest
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        //Events excluded

        private void ExecuteCommand(string command)
        {

                string cmd = "";
                cmd += @"using System;
                         using System.Collections.Generic;
                         using System.ComponentModel;
                         using System.Drawing;
                         using System.Text;
                         using System.Windows.Forms;
                         using System.Linq;
                         using Microsoft.CSharp;
                         using System.Reflection;
                         using ReflectionsTest;";
                         // i included a using statement for every namespace i want to adress direct
                cmd += @"namespace ReflectionConsole 
                        { 
                            public class RuntimeExecution 
                            { 
                                public static void Main(MainForm parent, TextBox output, FieldInfo[] privateFields) 
                                {
                                    try {";
                       //the code in a trycatch because i can send every error to a specific output defined as output parameter
                cmd += command;

                cmd += "}catch (Exception ex) { if(output != null){" +
                        "output.Text += ex.Message + \"\\n\\r\";"
                        +"}else{MessageBox.Show(ex.Message);}}}}}";
                try {
                    ExecuteCSharp(cmd);
                }
                catch (Exception ex) {
                    textBox2.Text += ex.Message + "\n\r";
                }
            }

        private void ExecuteCSharp(string code)
        {
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters();
            List<AssemblyName> assemblys = (Assembly.GetExecutingAssembly().GetReferencedAssemblies()).ToList<AssemblyName>();
            foreach (var item in assemblys) {
                parameters.ReferencedAssemblies.Add(item.Name + ".dll");
            }
            string t = Assembly.GetExecutingAssembly().GetName().Name;
            parameters.ReferencedAssemblies.Add(t + ".exe");
            //Here you have to reference every assembly the console wants access
            parameters.GenerateInMemory = true;
            parameters.GenerateExecutable = false;
            CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
            if (results.Errors.HasErrors) {
                StringBuilder sb = new StringBuilder();
                foreach (CompilerError error in results.Errors) {
                    sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
                    }
                throw new InvalidOperationException(sb.ToString());
            }
            else {
                Assembly assembly = results.CompiledAssembly;
                Type program = assembly.GetType("ReflectionConsole.RuntimeExecution");
                MethodInfo main = program.GetMethod("Main");
                FieldInfo[] fields = this.GetType().GetFields(
                         BindingFlags.NonPublic |
                         BindingFlags.Instance);
                //if everything is correct start the method with some arguments:
                // containing class, output, private fields of the containing class for easier access
                main.Invoke(null, new object[]{this, textBox2, fields});
            }
        }
    }
}

Some Explanations:

You have pass the highest class of your program which contains everything else, because it is easier to access members than parent objects.

public objects you can access like parent.obect1.Text = "textXYZ";

private objects you can access by name. These objects are listed in privateFields. for the subclasses you have two options: change the first and third parameter when calling main.Invoke([...]) or recollect the private fields.

as Suggestion you could include a .dll in the command which already gives you methods to achieve this much faster. For example GetValueFromFieldByName(object class, string name, Type resultType)

I hope that is what you've hoped for ^^

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM