I had the exact same problem and with a bit of googling and few modifications I solved it using Microsoft.CSharp.CSharpCodeProvider which allows the user to edit a C# template I present to them that exposes the complete Object Model of my application and they can even pass parameters from / and return result to the application itself.
The full C# solution can be downloaded from http://qurancode.com.
But here is the main code that does just that:
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;
}
}
I then defined a C# Interface to be implemented by the user code where they are free to put anythng they like inside their concrete Run method:
/// <summary>
/// Generic method runner takes any number and type of args and return any type
/// </summary>
public interface IScriptRunner
{
object Run(object[] args);
}
And here is the startup template the user can extends:
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;
}
}
}
And this is how I call it from my 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
Still to do is the Syntax Highlighting and CodeCompletion though.
Good luck!