access indexer of a hosted object

Mar 31, 2013 at 5:40 PM
Edited Mar 31, 2013 at 5:41 PM
hello, great work on your project!

during my tests i was wondering how to access the indexer of a class. i was expecting one of the evaluates in the attached code to work, but unfortunately i get no result.
        public class TestClass
        {
            public int this[String name]
            {
                get
                {
                    Console.WriteLine(name);
                    return 1;
                }
            }
        }

        static void Main(string[] args)
        {
            using (JScriptEngine engine = new JScriptEngine())
            {
                engine.AddHostObject("abc", new TestClass());
                engine.Evaluate("abc['test']");
                engine.Evaluate("abc.test");
            }
            Console.ReadLine();
        }
Coordinator
Mar 31, 2013 at 10:40 PM
Edited Aug 3, 2013 at 3:27 AM
Greetings mleandrok!

Indexers are an interesting case.

Beneath the cover of C# syntax, indexers are actually indexed properties named Item. What's an indexed property? It's a property with parameters - a CLR facility that isn't used in C# outside of indexers but is common in languages such as Visual Basic.

ClearScript supports indexed properties, but it doesn't do anything special for C# indexers. So the short answer is that the following should do what you need:
// C#
engine.Evaluate("abc.Item('test')");
However, what if your indexer were writable? Most JavaScript engines reject this:
// JavaScript
abc.Item('test') = value;  // error: invalid left-hand side in assignment
Instead, ClearScript supports the following for generalized access to indexed properties:
// JavaScript
var value1 = abc.Item.get('test');  // equivalent: var value1 = abc.Item('test');
abc.Item.set('test', value2);
Please let us know if you have more questions or if you encounter further issues. Thank you!
Coordinator
Mar 31, 2013 at 11:56 PM
Edited Aug 3, 2013 at 3:27 AM
A couple more notes related to indexers:

First, in your code above, you're using JScript. Unlike V8, JScript does support this syntax:
// JavaScript
abc.Item('test') = value;  // JScript allows this
However, this is a non-standard JScript extension and we prefer the generalized syntax above.

Also, it's worth mentioning that ClearScript provides a convenient shortcut for objects that implement System.Collections.IList. This covers standard .NET lists and one-dimensional arrays:
// C#
engine.AddHostObject("arr", new[] { "foo", "bar", "baz" });
var value = (string)engine.Evaluate("arr[1]");  // value is "bar"
Cheers!
Dec 12, 2014 at 9:16 AM
Edited Dec 12, 2014 at 9:27 AM
I've run into this problem, too. I have tried to implement IList<> in the hopes that the object could then be indexed as an array in V8 script land:

e.g.

.NET
    public class NodeList : IList<Node>
    {
       ....
        public Node this[int index]
        {
            get
            {
                return item(index);
            }
            set
            {
            }
        }
       ...
    }
Script:
   document.getElementsByTagName('head')[0]
document.getElementsByTagName('head') returns a NodeList as expected but attempting to get [0] returns undefined (although .Item.get(0) works)

Is it possible to get an object to act as an array? Is IList the interface I should use? Should I make it a DynamicObject instead?

UPDATE: Neeeevermind. I realized I should have used the non-templated IList. It's late :P
Dec 30, 2014 at 9:23 PM
I am trying to get my NamedNodeMap type working which may be indexed either by a number or a string. If I implement IList then I can evaluate expressions like map[0] -> Attr(href="") but map['href'] = undefined. If I implement/inherit either IPropertyBag or DynamicObject then map['href'] acts as expected but map[0] = undefined.

Am I missing something obvious? I tried using DynamicObject's overrides for getting/setting members by name and index.
Coordinator
Dec 31, 2014 at 12:55 AM
Edited Dec 31, 2014 at 12:58 AM
Hi krisoye,

How about something like this:
public class Map : DynamicObject, IEnumerable {
    private readonly IDictionary<string, object> dict = new Dictionary<string, object>();
    private IList list;
    public void Add(string key, object value) {
        dict.Add(key, value);
        list = null;
    }
    public override IEnumerable<string> GetDynamicMemberNames() {
        foreach (var name in dict.Keys) {
            yield return name;
        }
        foreach (var index in Enumerable.Range(0, dict.Count())) {
            yield return index.ToString(CultureInfo.InvariantCulture);
        }
    }
    public override bool TryGetMember(GetMemberBinder binder, out object result) {
        var name = binder.Name;
        var found = dict.TryGetValue(name, out result);
        if (!found) {
            int index;
            if (int.TryParse(name, out index) && (index >= 0) && (index < dict.Count())) {
                if (list == null) {
                    list = dict.ToList();
                }
                result = list[index];
                found = true;
            }
        }
        return found;
    }
    public IEnumerator GetEnumerator() {
        return dict.GetEnumerator();
    }
}
Here's a usage sample:
engine.Script.map = new Map { { "foo", 123 }, { "bar", 456 }, { "baz", 789 } };
engine.AddHostType("Console", typeof(Console));
engine.Execute(@"
    Console.WriteLine(map[0].Key);  // foo
    Console.WriteLine(map[1].Key);  // bar
    Console.WriteLine(map[2].Key);  // baz
    Console.WriteLine(map['foo']);  // 123
    Console.WriteLine(map['bar']);  // 456
    Console.WriteLine(map['baz']);  // 789
");
The trick is to expose the indices as property names.

Good luck!
Dec 31, 2014 at 2:28 AM
Oh facepalm I swore I tried something like that. Thank you so much! Works great! Go-go Micorsoft ClearScript! I have now successfully loaded jQuery into my fledgling browser! Woot!
Sep 12, 2015 at 6:48 AM
Hi,

I have the following problem with a default property indexer (from DataRow dotnet Framework class).

In C# :
scriptEngine.AddHostObject("DotNet", new HostTypeCollection("mscorlib", "System.Core", "System.Data"));
In my Javascript :
var dt = new DotNet.System.Data.DataTable("testDataTable");
dt.Columns.Add("Col1",host.typeOf(DotNet.System.String));
var dr = dt.NewRow();
dr('Col1') = 'Test1'; // Works only with JScriptEngine, not with  V8ScriptEngine :-(
dr.set('Col1','Test1'); // Does not work :-(
dt.Rows.Add(dr);
1°/ Like you said in previous post, JScriptEngine & V8ScriptEngine are different, so dr('Col1') = 'Test1'; run only on JScriptEngine. For now, I was thinking using V8ScriptEngine, because error handling seems to be better.
2°/ The 2nd code you said in previous code dr.set('Col1','Test1'); does not run. Perharps because it's a default property indexer (this[]) ?

Do you have a solution or a workaround for this problem please ?

Thanks for advance.

Sybaris
Coordinator
Sep 12, 2015 at 5:16 PM
Hi Sybaris,

The actual name of the indexer property in this case (and most cases) is "Item":
dr.Item.set('Col1', 'Test1'); // should work :-)
Cheers!
Sep 13, 2015 at 7:53 AM
Once again you are right :-)
Thanks you very much