Как я могу сделать свое приложение сценарием в C#?
У меня есть настольное приложение, написанное на C#, я хотел бы сделать сценарии на C#/VB. В идеале пользователь должен открыть боковую панель и написать что-то вроде
foreach (var item in myApplication.Items)
item.DoSomething();
С подсветкой синтаксиса и дополнением кода было бы здорово, но я мог бы жить без него. Я не хотел бы требовать, чтобы пользователи установили Visual Studio 2010.
Я думаю о вызове компилятора, загрузке и запуске выходной сборки.
Есть ли способ лучше?
Является ли Microsoft.CSharp ответом?
7 ответов
Вы все равно вызовете компилятор, потому что C# - это скомпилированный язык. Лучший способ сделать это можно проверить в CSharpCodeProvider - класс.
В качестве примера вы можете использовать следующее решение с открытым исходным кодом: https://bitbucket.org/jlyonsmith/coderunner/wiki/Home
У меня была точно такая же проблема, и, немного погуглив и несколько модификаций, я решил ее, используя Microsoft.CSharp.CSharpCodeProvider, который позволяет пользователю редактировать шаблон C#, который я представляю, который предоставляет полную объектную модель моего приложения, и они могут даже передать параметры из / и вернуть результат самому приложению.
Полное решение C# можно скачать с http://qurancode.com/. Но вот основной код, который делает именно это:
using System;
using System.Text;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.Security;
using Model; // this is my application Model with my own classes
public static class ScriptRunner
{
private static string s_scripts_directory = "Scripts";
static ScriptRunner()
{
if (!Directory.Exists(s_scripts_directory))
{
Directory.CreateDirectory(s_scripts_directory);
}
}
/// <summary>
/// Load a C# script fie
/// </summary>
/// <param name="filename">file to load</param>
/// <returns>file content</returns>
public static string LoadScript(string filename)
{
StringBuilder str = new StringBuilder();
string path = s_scripts_directory + "/" + filename;
if (File.Exists(filename))
{
using (StreamReader reader = File.OpenText(path))
{
string line = "";
while ((line = reader.ReadLine()) != null)
{
str.AppendLine(line);
}
}
}
return str.ToString();
}
/// <summary>
/// Compiles the source_code
/// </summary>
/// <param name="source_code">source_code must implements IScript interface</param>
/// <returns>compiled Assembly</returns>
public static CompilerResults CompileCode(string source_code)
{
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters options = new CompilerParameters();
options.GenerateExecutable = false; // generate a Class Library assembly
options.GenerateInMemory = true; // so we don;t have to delete it from disk
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
options.ReferencedAssemblies.Add(assembly.Location);
}
return provider.CompileAssemblyFromSource(options, source_code);
}
/// <summary>
/// Execute the IScriptRunner.Run method in the compiled_assembly
/// </summary>
/// <param name="compiled_assembly">compiled assembly</param>
/// <param name="args">method arguments</param>
/// <returns>object returned</returns>
public static object Run(Assembly compiled_assembly, object[] args, PermissionSet permission_set)
{
if (compiled_assembly != null)
{
// security is not implemented yet !NIY
// using Utilties.PrivateStorage was can save but not diaplay in Notepad
// plus the output is saved in C:\Users\<user>\AppData\Local\IsolatedStorage\...
// no contral over where to save make QuranCode unportable applicaton, which is a no no
//// restrict code security
//permission_set.PermitOnly();
foreach (Type type in compiled_assembly.GetExportedTypes())
{
foreach (Type interface_type in type.GetInterfaces())
{
if (interface_type == typeof(IScriptRunner))
{
ConstructorInfo constructor = type.GetConstructor(System.Type.EmptyTypes);
if ((constructor != null) && (constructor.IsPublic))
{
// construct object using default constructor
IScriptRunner obj = constructor.Invoke(null) as IScriptRunner;
if (obj != null)
{
return obj.Run(args);
}
else
{
throw new Exception("Invalid C# code!");
}
}
else
{
throw new Exception("No default constructor was found!");
}
}
else
{
throw new Exception("IScriptRunner is not implemented!");
}
}
}
// revert security restrictions
//CodeAccessPermission.RevertPermitOnly();
}
return null;
}
/// <summary>
/// Execute a public static method_name(args) in compiled_assembly
/// </summary>
/// <param name="compiled_assembly">compiled assembly</param>
/// <param name="methode_name">method to execute</param>
/// <param name="args">method arguments</param>
/// <returns>method execution result</returns>
public static object ExecuteStaticMethod(Assembly compiled_assembly, string methode_name, object[] args)
{
if (compiled_assembly != null)
{
foreach (Type type in compiled_assembly.GetTypes())
{
foreach (MethodInfo method in type.GetMethods())
{
if (method.Name == methode_name)
{
if ((method != null) && (method.IsPublic) && (method.IsStatic))
{
return method.Invoke(null, args);
}
else
{
throw new Exception("Cannot invoke method :" + methode_name);
}
}
}
}
}
return null;
}
/// <summary>
/// Execute a public method_name(args) in compiled_assembly
/// </summary>
/// <param name="compiled_assembly">compiled assembly</param>
/// <param name="methode_name">method to execute</param>
/// <param name="args">method arguments</param>
/// <returns>method execution result</returns>
public static object ExecuteInstanceMethod(Assembly compiled_assembly, string methode_name, object[] args)
{
if (compiled_assembly != null)
{
foreach (Type type in compiled_assembly.GetTypes())
{
foreach (MethodInfo method in type.GetMethods())
{
if (method.Name == methode_name)
{
if ((method != null) && (method.IsPublic))
{
object obj = Activator.CreateInstance(type, null);
return method.Invoke(obj, args);
}
else
{
throw new Exception("Cannot invoke method :" + methode_name);
}
}
}
}
}
return null;
}
}
Затем я определил интерфейс C#, который будет реализован с помощью пользовательского кода, где он может свободно размещать все, что угодно, в своем конкретном методе Run:
/// <summary>
/// Generic method runner takes any number and type of args and return any type
/// </summary>
public interface IScriptRunner
{
object Run(object[] args);
}
И вот шаблон запуска, который может расширить пользователь:
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Text;
using System.IO;
using Model;
public class MyScript : IScriptRunner
{
private string m_scripts_directory = "Scripts";
/// <summary>
/// Run implements IScriptRunner interface
/// to be invoked by QuranCode application
/// with Client, current Selection.Verses, and extra data
/// </summary>
/// <param name="args">any number and type of arguments</param>
/// <returns>return any type</returns>
public object Run(object[] args)
{
try
{
if (args.Length == 3) // ScriptMethod(Client, List<Verse>, string)
{
Client client = args[0] as Client;
List<Verse> verses = args[1] as List<Verse>;
string extra = args[2].ToString();
if ((client != null) && (verses != null))
{
return MyMethod(client, verses, extra);
}
}
return null;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, Application.ProductName);
return null;
}
}
/// <summary>
/// Write your C# script insde this method.
/// Don't change its name or parameters
/// </summary>
/// <param name="client">Client object holding a reference to the currently selected Book object in TextMode (eg Simplified29)</param>
/// <param name="verses">Verses of the currently selected Chapter/Page/Station/Part/Group/Quarter/Bowing part of the Book</param>
/// <param name="extra">any user parameter in the TextBox next to the EXE button (ex Frequency, LettersToJump, DigitSum target, etc)</param>
/// <returns>true to disply back in QuranCode matching verses. false to keep script window open</returns>
private long MyMethod(Client client, List<Verse> verses, string extra)
{
if (client == null) return false;
if (verses == null) return false;
if (verses.Count == 0) return false;
int target;
if (extra == "")
{
target = 0;
}
else
{
if (!int.TryParse(extra, out target))
{
return false;
}
}
try
{
long total_value = 0L;
foreach (Verse verse in verses)
{
total_value += Client.CalculateValue(verse.Text);
}
return total_value;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, Application.ProductName);
return 0L;
}
}
}
И вот как я это называю из моего MainForm.cs
#region Usage from MainForm
if (!ScriptTextBox.Visible)
{
ScriptTextBox.Text = ScriptRunner.LoadScript(@"Scripts\Template.cs");
ScriptTextBox.Visible = true;
}
else // if visible
{
string source_code = ScriptTextBox.Text;
if (source_code.Length > 0)
{
Assembly compiled_assembly = ScriptRunner.CompileCode(source_code);
if (compiled_assembly != null)
{
object[] args = new object[] { m_client, m_client.Selection.Verses, "19" };
object result = ScriptRunner.Run(compiled_assembly, args);
// process result here
}
}
ScriptTextBox.Visible = false;
}
#endregion
Тем не менее, еще нужно выделить синтаксис и CodeCompletion.
Удачи!
Я бы использовал PowerShell или MEF. Это действительно зависит от того, что вы подразумеваете под scritable и какой тип приложения у вас есть. Самое приятное в PowerShell - это то, что он непосредственно размещается и предназначен для использования интерфейсов.NET в сценариях.
Используйте язык сценариев. Tcl, LUA или даже JavaScript приходят на ум.
Использовать Tcl действительно легко:
using System.Runtime.InteropServices;
using System;
namespace TclWrap {
public class TclAPI {
[DllImport("tcl84.DLL")]
public static extern IntPtr Tcl_CreateInterp();
[DllImport("tcl84.Dll")]
public static extern int Tcl_Eval(IntPtr interp,string skript);
[DllImport("tcl84.Dll")]
public static extern IntPtr Tcl_GetObjResult(IntPtr interp);
[DllImport("tcl84.Dll")]
public static extern string Tcl_GetStringFromObj(IntPtr tclObj,IntPtr length);
}
public class TclInterpreter {
private IntPtr interp;
public TclInterpreter() {
interp = TclAPI.Tcl_CreateInterp();
if (interp == IntPtr.Zero) {
throw new SystemException("can not initialize Tcl interpreter");
}
}
public int evalScript(string script) {
return TclAPI.Tcl_Eval(interp,script);
}
public string Result {
get {
IntPtr obj = TclAPI.Tcl_GetObjResult(interp);
if (obj == IntPtr.Zero) {
return "";
} else {
return TclAPI.Tcl_GetStringFromObj(obj,IntPtr.Zero);
}
}
}
}
}
Тогда используйте это как:
TclInterpreter interp = new TclInterpreter();
string result;
if (interp.evalScript("set a 3; {exp $a + 2}")) {
result = interp.Result;
}
На каком языке написано ваше заявление? Если C++, вы можете рассмотреть Google V8, встраиваемый движок ECMAScript/JavaScript.