Pactical example of adding C# scripting support to an application

0
45

Why we need scripting support in our applications?

There are two main reasons for adding for scripting support to an application; Customization and integration.

Customization: Customizing the behaviour of the application. Custom validation rules, adding custom steps to a workflow, creating custom reports…

Integration: Integrating our applications with other applications. Importing files generated by other applications, exporting the application’s data to different formats, translating the message between the applications…

Which language is better for scripting?

Actually there is no common answer to this question. My personal idea is choosing a platform and a language that has easy to use and you are comfortable will be better.

There are many widely used scripting languages like VBA, Python, Boo and Pascal. But you do not need to use them because they are widely used.

Shouldn’t I worry about the users that will write the scripts?

Most of the time “no.” Because 99.9% of the cases, your users or customers will not be the person who will write the scripts. They will just contact with you, and someone from your team will write the scripts for them and customer will pay for it. So, selecting a language that your team already use and comfortable with and has tools for scripting support will be a better choice.

Steps of implementing the scripting support

To add scripting support, you only need to complete three simple steps.

1- Write the script

The script is no more than some text. So, you can write it with a simple text editor like Notepad or Notepad++. You can also create a simple class in Microsoft Visual Studio then import its code to your application.

Having a code editor for writing scripts in your application may be a good feature but not required. If it is hard for you to find a good script editor that you can easily embed to your application, just provide a way to import script files or text content  to your application.

Following is a simple script that gets a collection of Person objects and writes thier information to a single comma separated file.

using System; using System.IO; using System.Collections.Generic; using System.Windows.Forms; using ScriptingSampleApp.Core; namespace ScriptingSampleApp.Scripts { class ScriptClass { public void ExportPersonList(IList personList) { string _filePath = askFileName(); if (String.IsNullOrEmpty(_filePath)) return; using (StreamWriter _writer = new StreamWriter(_filePath)) { for (int i = 0; i < personList.Count; i++) { _writer.WriteLine(personList[i].Name + "," + personList[i].Surname + "," + personList[i].Age); } } } private string askFileName() { } } }

As you can see, your script should be structured as a complete C# code file. It should contain necessary using statements, namespace, class and method declerations.

Consider putting all the shared classes that will be accessed from the scripts to a separate DLL file and reference it in your scripts. For example, sample script uses the Person class under the namespace ScriptingSampleApp.Core from the dll file ScriptingSampleApp.Core.dll. The next step shows how to reference these additional dll files.

2- Compile the script

The next step is compiling the script. Compiling the script has following sub steps;

  1. Create a C# code compiler
  2. Prepare compiler parameters
  3. Compile the script either from a string or providing the script files’ paths
  4. Check if the compilation succeeded or generated any error.
  5. Get the reference to the compiled assembly

Following sample code block demonstrates these steps.

 CodeDomProvider _codeDomProvider = CodeDomProvider.CreateProvider("CSharp"); CompilerParameters _compilerParameters = new CompilerParameters(); _compilerParameters.ReferencedAssemblies.Add("system.dll"); _compilerParameters.ReferencedAssemblies.Add("system.windows.forms.dll"); _compilerParameters.ReferencedAssemblies.Add("ScriptingSampleApp.Core.dll"); _compilerParameters.GenerateExecutable = false; _compilerParameters.GenerateInMemory = true; CompilerResults _compilerResults = _codeDomProvider.CompileAssemblyFromSource(_compilerParameters, m_txtScript.Text); if (_compilerResults.Errors.HasErrors) { string _errorMessage = "Compilation failed.\r\n"; foreach (string _error in _compilerResults.Output) { _errorMessage += _error + "\r\n"; } MessageBox.Show(_errorMessage); return; } Assembly _compiledAssembly = _compilerResults.CompiledAssembly;

At this step, do not forget to add all the system and additional assemblies that your script uses, to the ReferencedAssemblies collection of the CompilerParameters you prepared. This sample script references the system.dll and system.windows.forms.dll which come with .Net Framework and it also references additional ScriptingSampleApp.Core.dll which contains the Person class.

3- Invoke the methods in the compiled assembly

Now the script is compiled to an assembly you can create instances of the classes defined in the assembly and invoke its methods. This step requires using reflection.

Technics for using the compiled script assembly, are the same as using dynamically loaded plugins. You must decide the way your code finds and invokes the operations in the scripts.

There are too many ways of deciding which classes to create and which methods to invoke in the compiled assembly.

For deciding which class(es) to instantiate, you may;

  • Predefine or set the rules for the class names. For example, class name must be “ScriptClass” or class names must start with “ExportScriptClass_”. It is up to you.
  • Add custom attributes to the classes that can contains the invokable methods. You may define a custom attribute like [ExportScriptClassAttribute] and add this attribute to every class in the script that contains the invokable methods.
  • Define base interfaces or classes and derive your classes in the scripts from them. For example, define an interface like IExportScript and implement this interface in your script class.

Deciding which method or methods to call within the script you may use the same technics as deciding for classes to instantiate.

Following sample code requires that the script contains a single class named ScriptClass under the namespace ScriptingSampleApp.Scripts and has a method named ExportPersonList. It creates an instance of that class, gets the required methods MethodInvoke and invokes the method with required parameters.

Assembly _compiledAssembly = _compilerResults.CompiledAssembly; var _customScriptClassInstance = _compiledAssembly.CreateInstance("ScriptingSampleApp.Scripts.ScriptClass"); var _typeInfo = _customScriptClassInstance.GetType(); var _methodInfo = _typeInfo.GetMethod("ExportPersonList"); var oReturnValue = _methodInfo.Invoke(_customScriptClassInstance, new object[] { m_PersonList });

Using the sample code

Sample code contains two porjects; one WinForms application and a class library project.

ScriptingSampleApp: Contains a single form that has a data grid filled with sample data, a large multi-line TextBox for scripts and three button for importing, saving and executing the scripts. Sample scripts are under the “Scripts” folder in this project.

ScriptingSampleApp.Core: This project is for demonstrating the shared class libraries that contains shared classes. In this case it only contains one class Person which the script needs to use.

To try the sample code, first click the “Load Script From File” button and select one of the sample script files. Then, click the “Export using this script” button and enter the file name that will be generated.

More

You probably do not compile the scripts every time your application starts. So you will need to save the compiled assemblies to disk or to database and then reload the previously compiled assembly when your application starts. Keeping extra information about the script like its name, function, type… may also be needed.

LEAVE A REPLY