This tutorial will demonstrate how create the simple "text editor" application (Text Processor), which uses user entered C# code for text manipulations. The "type sharing" pattern in this tutorial is implemented with passing well-known type between the script host and the actual script. This tutorial also contains implementation of Local and Remote dynamic assembly loading.
Only the code, which logically belongs to the script hosting implementation will be discussed here. To accomplish this tutorial you will need to download TextProcessor.zip file. Then extract the content of the zip file in the convenient location.
The host application for this tutorial comes in two flavors: C# script (textprocessor.cs) and Visual Studio 2003 (VS7.1) project (content of extracted textprocessor folder). All code snippets in this tutorial are taken from textprocessor.cs script, however they are applicable to the VS7.1 project to the same degree.
This tutorial demonstrates how to:
Firstly let's see the Text
Processor in action:
The script will create and show the following dialog.
The top panel (script panel) contains a C# code (script) template for implementation of the text manipulation routine. The default C# script inserts a TAB character at the start of the text in the bottom panel (text panel).
If you press "Process" button it will modify the text this way:
Now change the script code to do replacing all space characters with underscore as following:
using System; public class Script { static public string Process(string text) { return text.Replace(" ", "_"); } } |
If you now press "Undo" and then "Process" buttons it will modify text this way:
Calling instance and static methods
The method ExecuteProcessTextLocaly() in textprocessor.cs contains actual implementation of loading and executing the script. Calling CSScript.LoadCode is the most important part of the method.
public interface ITextProcessor { string Process(string str); } ...... void ExecuteProcessTextLocaly() { try { if (defClasslessButton.Checked) { //script contains only one static method so no need to deal with interfaces, //typecasting... var Process = CSScript.LoadMethod(textBoxScript.Text) .GetStaticMethod(); textBoxText.Text = (string)Process(textBoxText.Text); } else { //We cannot typecase to ITextProcessor as the Script class does not implement it. //But we can use InterfaceAlignment (DuckTyping) ITextProcessor script = CSScript.LoadCode(textBoxScript.Text) .CreateObject("Script") .AlignToInterface<ITextProcessor>(); textBoxText.Text = script.Process(textBoxText.Text); } } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } |
LoadCode takes the string of C# script code, compiles it into assembly, loads the assembly to the current AppDomain and returns the loaded assembly. Now we can instantiate any public type or call any public method implemented in this assembly. You can use either Reflection, AsmHelper, Dynamic Method or Interface Alignment (DuckTyping) from the CSScriptLibrary for this purpose.
AsmHelper is just a simple utility class, which simplifies assembly browsing. Basically it is a Reflection helper. This is how AsmHelper can be used in to call static method:
Assembly scriptAsm = CSScript.LoadCode(textBoxScript.Text); AsmHelper helper = new AsmHelper(scriptAsm); textBoxText.Text = (string)helper.Invoke("Script.Process", textBoxText.Text); |
However in our example we use for calling static methods Dynamic Method emitted by CS-Script engine (GetStaticMethod) and Interface Alignment for calling instance methods (AlignToInterface).
Note: Dynamic Methods and Interface Alignents demonstrate superior performance comparing to any Reflection based execution.Creating objects and typecasting to interface
Now let's instantiate a type implemented in a script. The only type in our script is the class Script. It can be instantiated either with Assembly.CreateInstance() or AsmHelper.CreateObject() methods. Both methods return an instance of Object type, which has to be type casted to the actual type you want to use. The only problem with this is that the type Script is not known by the host application. In fact it cannot be known because the code for class Script did not exist at the time the host application was written/compiled. This restriction is not applicable for well-known types (e.g. String). That is why we can inherit our Script class from some interface (defined in any GAC or the host assembly) in order to make it possible for the host application to understand it. The advantages of using well-known types are discussed in the "Passing well-known type..." section.
Let's implement in our script some class that can be used for capitalizing operations (the type must implement ITextCapitalizer interface).
public interface ITextCapitalizer { string ToUpper(string text); string ToLower(string text); } |
Script:
using System; public class Script : MarshalByRefObject, ITextCapitalizer { public string ToUpper(string text) { return text.ToUpper(); } public string ToLower(string text) { return text.ToLower(); } } |
This is how Script type is instantiated in the event handler for the
"ToUpper/ToLower" button.
Assembly scriptAsm = CSScript.LoadCode(textBoxScript.Text, null, true); AsmHelper helper = new AsmHelper(scriptAsm); var capitalizer = (ITextCapitalizer)helper.CreateObject("Script"); textBoxText.Text = capitalizer.ToLower(textBoxText.Text); |
Loading/Unloading the script assembly
Another interesting point to discuss is the way how the compiled script is loaded. In the ExecuteProcessLocaly() method discussed above the script was compiled and loaded into current AppDomain. The code snippet below is the implementation of the toUpperBtn_Click(). This method is called when button the "ToUpper" is pressed and the "Unload when done" check box is checked.
void toUpperBtn_Click() { try { if (remoteCheckBox.Checked) { string asmFile = CSScript.CompileCode(textBoxScript.Text, null, true); using (AsmHelper script = new AsmHelper(asmFile, null, true)) { var capitalizer = (ITextCapitalizer)script.CreateObject("Script"); textBoxText.Text = capitalizer.ToUpper(textBoxText.Text); } } else { ..... } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } |
In this code AsmHelper loads the compiled script assembly (asmFile) to a
new temporary AppDomain (remote loading). After execution of
the "Script.Process" method the instance of AsmHelper disposed. When AsmHelper
is being disposed it also unloads the whole temporary AppDomain with
all its assemblies.
How AsmHelper will load assembly depends on which constructor was used to instantiate this type (see CSScriptLibrary reference):
public AsmHelper(Assembly asm) - loading to the current AppDomain.
public AsmHelper(string asmFile, string domainName, bool deletOnExit) - loading to the temporary AppDomain
This is done in order to overcome the limitation of the CLR with respect to loaded assemblies:
Once the assembly loaded it
cannot be unloaded any more.
Use the remote loading carefully as it introduces important constraint on the types implemented in your script:
The type, which is to cross AppDomain boundaries must be either serializable or inherited from MarshalByRefObject.
Thus, if you repeat the test for creating ITextCapitalizer
object with the "Unload when done" check box checked and Script class not being derived from MarshalByRefObject you will have the following exception.
In this tutorial almost all possible execution scenarios were
implemented just to demonstrate all available implementation options.
However it is unlikely you would need to implement all of them in real
development. The next tutorial (Image
Processor) is an example of more practical/simple approach
where the host application is implemented according "Script hosting
guideline".
CS-Script tutorials | "Type sharing" pattern | Dynamic assembly loading