This project has moved. For the latest updates, please go here.

Object.defineProperty on host object

Nov 22, 2016 at 8:53 PM
Hello,

I am trying to use Object.defineProperty's set function to define a setter on a host object, however I remain unsuccessful in doing so. Getters interestingly enough work fine.

I add an instance of the following class to the engine as an host object using engine.AddHostObject("foo", new Foo()):
public class Foo 
{
    private string _bar = "Baz";

    public string Bar
    {
        get
        {
            return _bar;
        }
        set
        {
            _bar = value;
            Console.WriteLine(_bar);
        }
    }
}
I then define the getters and setters in JavaScript:
Object.defineProperty(foo, "bat", {
    set: function (f) {
        console.log("set: " + f);
        foo.Bar = f;
    },
    get: function () {
        return foo.Bar;
    }
});

console.log(foo.bat);
foo.bat = "quux";
console.log(foo.bat); outputs Baz while foo.bat = "quux"; displays an error with the following message:
Error: Object has no suitable property or field named 'bat'
    at Script Document:216:9 -> foo.bat = "quux";
Is there any way of getting this to work?
Coordinator
Nov 23, 2016 at 6:29 PM
Edited Nov 24, 2016 at 11:46 AM
Greetings!

The behavior you're seeing is by design.

Host object properties are accessed via interception, which takes precedence over normal JavaScript property access. Failure to retrieve a nonexistent host property triggers the default handling to provide access to the prototype chain; that's why getters work as expected. Property assignment on the other hand throws an exception if it fails on the host side; this is done to capture as much error information as possible at the point of failure.

This asymmetric behavior is analogous to JavaScript's default property access protocol, where reads use the prototype chain but writes do not. On the other hand, as you've noticed, it also makes most host objects non-extensible on the JavaScript side. An exception is when a host object is itself extensible, such as an instance of System.Dynamic.ExpandoObject.

That's the real issue here; host objects and script objects can have conflicting notions of extensibility. It might be possible to detect that a host object is non-extensible and fall back to JavaScript semantics, but that determination would be nontrivial, and it isn't clear that the resulting behavior would be appropriate in every case. In any case, ClearScript currently doesn't do that.

One possibility might be to wrap the host object in a JavaScript proxy, but currently there's a bug that prevents proxies from targeting host objects directly. Still, you might be able to use the following JavaScript function, or something similar, to produce an object with the desired behavior:
function createProxy(obj) {
    return new Proxy(function () {}, {
        apply: function (target, thisArg, argumentsList) {
            return Reflect.apply(obj, thisArg, argumentsList);
        },
        construct: function(target, argumentsList, newTarget) {
            return Reflect.construct(obj, argumentsList);
        },
        get: function (target, key, receiver) {
            let value = Reflect.get(obj, key);
            return (value === undefined) ? Reflect.get(target, key, receiver) : value;
        },
        set: function (target, key, value, receiver) {
            if (Reflect.has(obj, key))
                return Reflect.set(obj, key, value);
            return Reflect.set(target, key, value, receiver);
        }
    });
}
An alternative might be to create your own JavaScript proxy object that forwards selected methods and properties to the host object.

Good luck!