-
Notifications
You must be signed in to change notification settings - Fork 79
Create a custom javascript object
First of all, here's the complete source code for this example: https://github.com/downloads/fholm/IronJS/IronJS-NativeObjectDemo.zip
This tutorial will guide you through the process of creating a custom native JavaScript object in IronJS, this is not the same as exposing a C# or F# class to IronJS. The object created here is a true JS object and will act as such inside user code.
We're going to create a simple image object that allows us to clear the image to white, draw an elipse and save it to disk.
I assume you know how to setup an empty C# console project in visual studio and add the IronJS dll you need, the binary packages can be downloaded from https://github.com/fholm/IronJS/downloads. You also need to reference System.Drawing if you're going to follow along step-by-step.
The first thing we need to do is to just create an IronJS hosting context in the Main
function:
using System;
namespace IronJSDemo
{
class Program
{
static void Main(string[] args)
{
var ctx = new IronJS.Hosting.CSharp.Context();
}
}
}
After that we need to create two new classes, the first one will extend IronJS.CommonObject
- call it ImageObject:
using System.Drawing;
using IronJS;
using CO = IronJS.CommonObject;
using Env = IronJS.Environment;
using FO = IronJS.FunctionObject;
namespace IronJSDemo
{
public class ImageObject : CommonObject
{
Image image;
Graphics graphics;
}
}
Then create a static class called ImageConstructor
:
using System;
using IronJS;
using IronJS.Hosting;
using IronJS.Native;
using CO = IronJS.CommonObject;
using FO = IronJS.FunctionObject;
namespace IronJSDemo
{
public static class ImageConstructor
{
}
}
The first thing we're going to add is the constructor for our ImageObject
:
namespace IronJSDemo
{
public class ImageObject : CommonObject
{
Image image;
Graphics graphics;
public ImageObject(double width, double height, Env env, CommonObject prototype)
: base(env, env.Maps.Base, prototype)
{
Put("width", (double)width, DescriptorAttrs.Immutable);
Put("height", (double)height, DescriptorAttrs.Immutable);
image = new Bitmap((int)width, (int)height);
graphics = Graphics.FromImage(image);
graphics.Clear(Color.Black);
}
}
}
Now it might be time to explain the parameters, the first two are pretty obvious - they're the width and height of our image. IronJS currently only deals with doubles so they have to be typed as such. The third parameter is the IronJS.Environment object which holds all the data required by the currently running context, the fourth parameter is the prototype object for our ImageObject.
env
and prototype
will just be passed to the base constructor, but we also pass env.Maps.Base
- this is what keeps the hidden classes (an optimization tricks IronJS has borrowed from self/V8) running. Always pass the Base map in.
Now inside the constructor body, we want to expose the height and width to user code, so we set the two properties with Put which will allow JavaScript code to access .height and .width. We also pass in DescriptorAttrs.Immutable
since it doesn't make sense to delete or change the width or height of an image.
After this we just create our bitmap image and it's associated graphics object and store it in our ImageObject instance.
However, we also want to overload the class name of our object, this isn't necessary - but we're doing it anyway to show how. This is the class name that Object.prototype.toString
reads to print a string like [object Object]
.
namespace IronJSDemo
{
public class ImageObject : CommonObject
{
/// ... snipped for brevity
public override string ClassName
{
get
{
return "Image";
}
}
}
}
Now for something a bit more tricky, we're going to create our clearWhite method that allows us to clear our bitmap to white, here's the code:
namespace IronJSDemo
{
public class ImageObject : CommonObject
{
/// ... snipped for brevity
internal static void ClearWhite(FO func, CO that)
{
var self = that.CastTo<ImageObject>();
self.graphics.Clear(Color.White);
}
}
}
There's two parameters to ClearWhite
that needs explaining. func
is the javascript function object representing the ClearWhite
method itself inside the IronJS runtime (basically ClearWhite
gets passed into itself when it's called). that
is the object we're calling ClearWhite
on and is the same as the C# this
parameter.
Since IronJS inside user code doesn't care if the object it's accessing is the base CommonObject or a subclass the parameters will always be typed as CommonObject
(or CO for short) we need to cast our CO instance to the type we need. The CommonObject instance supplies a function for this with the signature: T CastTo<T>(void)
, this method will throw the appropiate javascript TypeError that is catchable by user code if the type isn't correct. This is what we do on the first line of the method body.
Then we just access .graphics
and call .Clear(Color.White) on it
. Next is our DrawElipse
function.
namespace IronJSDemo
{
public class ImageObject : CommonObject
{
/// ... snipped for brevity
internal static void DrawElipse(FO func, CO that, double x, double y, double width, double height)
{
var self = that.CastTo<ImageObject>();
var p = new Pen(Color.Black, 5);
var rect = new Rectangle((int)x, (int)y, (int)width, (int)height);
self.graphics.DrawEllipse(p, rect);
}
}
}
This does pretty much the same thing as ClearWhite
except it takes four parameters (the coordinats in which to draw the elipse) - these must be typed as double if we need them to be numbers since IronJS, in the way the ECMA3 specification dictates, only deal with doubles.
Other then that we cast the CO instance to ImageObject once more and then use standard System.Drawing
calls to draw an elipse. Time for SaveFile
.
namespace IronJSDemo
{
public class ImageObject : CommonObject
{
/// ... snipped for brevity
internal static bool SaveFile(FO func, CO that, string name)
{
if (name == null)
return func.Env.RaiseTypeError<bool>("Name was null");
var self = that.CastTo<ImageObject>();
self.image.Save(name);
return true;
}
}
}
The only new thing going on here that we havn't seen before is the return func.Env.RaiseTypeError<bool>("Name was null");
call, this is a shorthand into the IronJS environment for raising user catchable exceptions.
Now, setting up the ImageConstructor is a bit trickier, this could be done without it's own class but I like to encapsulate the logic in a static class plus two methods so it's easy to re-use. First lets setup the method that will act as our JavaScript constructor and is what allows user code to do var img = new Image(200, 200);
, this is not much different from the functions we defined in ImageObject
:
namespace IronJSDemo
{
public static class ImageConstructor
{
/// Snipped for brevity
static CO Construct(FO ctor, CO _, double width, double height)
{
var prototype = ctor.GetT<CO>("prototype");
return new ImageObject(width, height, ctor.Env, prototype);
}
}
}
The only thing going on here that is different from the previous functions is that we call T GetT<T>(string)
to get the .prototype of our constructor function. We havn't created this yet, but will do in a bit. After that we just call our ImageObject constructor with the parameters passed, the environment we get of the ctor object and our prototype. Notice that the return type of Construct is CommonObject
(or CO for short), this is very important.
Time to create the function that does the setup and attaches the constructor to our running context, allowing our user code to access it.
namespace IronJSDemo
{
public static class ImageConstructor
{
/// Snipped for brevity
public static void AttachToContext(CSharp.Context context)
{
var prototype = context.Environment.NewObject();
var constructor = Utils.CreateConstructor<Func<FO, CO, double, double, CO>>(context.Environment, 2,Construct);
var clearWhite = Utils.CreateFunction<Action<FO, CO>>(context.Environment, 0, ImageObject.ClearWhite);
var saveFile = Utils.CreateFunction<Func<FO, CO, string, bool>>(context.Environment, 1, ImageObject.SaveFile);
var drawElipse = Utils.CreateFunction<Action<FO, CO, double, double, double, double>>(context.Environment, 4, ImageObject.DrawElipse);
prototype.Prototype = context.Environment.Prototypes.Object;
prototype.Put("clearWhite", clearWhite, DescriptorAttrs.Immutable);
prototype.Put("saveFile", saveFile, DescriptorAttrs.Immutable);
prototype.Put("drawElipse", drawElipse, DescriptorAttrs.Immutable);
constructor.Put("prototype", prototype, DescriptorAttrs.Immutable);
context.SetGlobal("Image", constructor);
}
}
}
the type parameter has to be a delegate and needs to match the signature of the last parameter we pass in. The first line of the AttachToContext
method creates a new ordinary javascript object (an instance of CommonObject
from the .NETs point of view). This will be our .prototype property on our constructor and also the object set as the prototype of our ImageObjects.
Next we call the utility functions HostFunctionObject CreateConstructor<T>
and HostFunctionObject CreateFunction<T>
, The first parameter is the context environment, the second one is the .length
propertys value and the last one is the .NET method that should be called when this function is invoked from JavaScript code.
We then need to setup our prototype object. First we attach the Object.prototype
object to be the prototype of the Image.prototype
object - this is done by assigning the .Prototype
property. All default native object prototypes can be accessed through context.Environment.Prototypes
.
Now it's time to add the functions to our Image.prototype
so our image objects can access them, we call the Put
function and also pass in DescriptorAttrs.Immutable
so they can't be removed or changed.
Only two steps left, assign the prototype object to the constructor object so it can be accessed through Image.prototype
and we can find it later inside our Construct
function.
And, finally, expose the constructor as the global Image
to our context by calling SetGlobal
. We're now ready for attaching the object to our context and start using it from JavaScript.
Update our Main
function so it looks like this:
using System;
namespace IronJSDemo
{
class Program
{
static void Main(string[] args)
{
var ctx = new IronJS.Hosting.CSharp.Context();
ImageConstructor.AttachToContext(ctx);
ctx.CreatePrintFunction();
ctx.Execute(@"
var img = new Image(100, 100);
print(img); // [object Image]
img.clearWhite();
img.drawElipse(5, 5, 20, 20);
img.drawElipse(50, 50, 70, 70);
img.saveFile('test.bmp');
");
var img = ctx.GetGlobalAs<ImageObject>("img");
}
}
}
What the above code does should be clear after this tutorial :) Enjoy!