Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

.NET scripting, a new approach

0.00/5 (No votes)
15 Nov 2004 1  
A new(?) approach to scripting in .NET applications.

Sample Image - scripting.png

Introduction

This project is an example of how to provide scripting support in your application without having to use a scripting host or create a new application domain.

Background

I've come across many articles on how to use C# as a scripting language. All have focused on either shell or runtime scripting. By runtime scripting, I mean scripts that can be modified at runtime with immediate effects in the application. My approach is slightly different. I compile scripts at runtime, but only once. If the user wants to change a script then the application has to be restarted for the changes to have effect. This is a limitation, but I don't feel that it is a serious one. The benefits are the following:

  • No overhead for passing objects over application boundaries
  • No need to specify interfaces between the script and your application
  • Familiar OO-model for scripting

This scripting solution simply allows the user to extend the functionality of any class in your application. All code that is written in the script files will be treated as if it was part of the original code.

Using the code

Presuming that you have a class named ScriptedClass, it's pretty dumb and doesn't utilize all info we are feeding it.

    public class ScriptedClass
    {
        private string name;
        public string Name
        {
            get
            {
                return name;
            }
        }

        private int age;
        public int Age
        {
            get
            {
                return age;
            }
        }

        public ScriptedClass()
        {
        }

        public ScriptedClass(string name, int age)
        {
            this.name=name;
            this.age=age;
        }

        public virtual string Message
        {
            get
            {
                return string.Format("Hi {0}!", name);
            }
        }
    }

To use the ScripterEngine, you first have to initiate it. The Init-method takes three arguments, one path to the directory that contains the script files, one path to the directory to use for temporary files, and a delegate used for progress:

    ScripterEngine.Init(
        string.Format("{0}/script", Application.StartupPath),
        string.Format("{0}/tmp", Application.StartupPath),null);

Then you can create an instance of your class using either any of the constructors.

    //Default constructor

    ScriptedClass clsa= 
      (ScriptedClass)ScripterEngine.CreateObject(typeof(MyClass), 
      Type.EmptyTypes,null);
    
    //Default constructor, shorter form

    ScriptedClass clsb = 
      (ScriptedClass)ScripterEngine.CreateObject(typeof(MyClass));
    
    //Constructor with arguments

    ScriptedClass clsc = 
      (ScriptedClass)ScripterEngine.CreateObject(typeof(MyClass),
      new Type[]{typeof(string),typeof(int)},
      new object[]{"Hugo",23});

If you, or a user, would like to extend the functionality of this class, you have to write a script.

First, we need a configuration file for the script. This file must be placed in the script directory together with the source file.

    <?xml version="1.0" encoding="utf-8" ?> 
    <ScriptConfiguration name="Intelligent messenger" load="true">
        <References>
            <Reference>System.Web</Reference>
        </References>
        <Files>
            <File>messenger.cs</File>
        </Files>
        <Types>
            <Type>ScriptingArchitecture.ScriptedClass, 
                        ScriptingArchitecture</Type>
        </Types>
    </ScriptConfiguration>

As you can see, it's plain XML. The References tag contains names of assemblies that are necessary to compile the script. We don't really need the System.Web assembly, but I put it there to illustrate the syntax. All loaded assemblies are automatically used as references when compiling the script, so there isn't any need to add references to any basic assemblies.

The Files tag contains all the source files that compose the script. There can be one or more files in a script.

The Types tag contains all the types that are extended by the script. Note that the type must be specified using the full name of the type followed by the name of the assembly the type is defined in.

When we get this file, we can create the file for the actual script.

    using ScriptingArchitecture;
    using System.Windows.Forms;
    using System.Drawing;

    namespace Messenger
    {
        public class IntelligentMessage : ScriptingArchitecture.ScriptedClass
        {
            public IntelligentMessage():base()
            {
            }

            public IntelligentMessage(string name, int age):base(name,age)
            {
            }

            public override string Message
            {
                get
                {
                    return string.Format("Hi {0}! You are {1} years old!", Name, Age);
                }
            }
        }
    }

Points of Interest

To make it possible to accommodate ad-hoc scripts form different sources, the scripting engine uses inheritance chains. If a bunch of scripts extends a particular class in your application, they must all co-exist. So, in this scenario where your class is A:

B:A
C:A
D:A
E:A

The code is rewritten to:

B:A
C:B
D:C
E:D

And the class E is supplied when a class of the type A is requested.

What I miss in my scripting implementation is a more sugary syntax for creating scripted objects.

ScriptedClass clsc=(ScriptedClass)ScripterEngine.CreateObject(
    typeof(MyClass), 
    new Type[]{typeof(string),typeof(int)}, 
    new object[]{"Hugo",23});

Isn't exactly beautiful code. I would like something more along the lines with:

ScriptedClass clsc=new ScriptedClass("Hugo",23);

This can be fixed with a static method in ScriptedClass, but I would like a fix to this that is less of a hack.

    public static ScriptedClass GetInstance(string name, int age)
    {
        return (ScriptedClass)ScripterEngine.CreateObject(
            typeof(MyClass), 
            new Type[]{typeof(string),typeof(int)}, 
            new object[]{name,age});
    }

Caveats

There are some limitations to script code that does not exist in normal development:

  • You are limited to one namespace per source file
  • The class that your script extends must be referenced with its full name, i.e.,
    public class Script : MyApplication.ScriptedObject

    not

    public class Script : ScriptedObject
  • You must implement all constructors that are implemented in the base class.

History

  • 2004-11-15

    Uploaded the first version.

  • 2004-11-16

    Source file updated and caveats list added.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here