microsoft / ClearScript

A library for adding scripting to .NET applications. Supports V8 (Windows, Linux, macOS) and JScript/VBScript (Windows).
https://microsoft.github.io/ClearScript/
MIT License
1.77k stars 148 forks source link

VBScriptEngine: Changing object with AddHostObject doesn't work #590

Closed EtienneLaneville closed 2 months ago

EtienneLaneville commented 2 months ago

I am running into a strange issue with VBScriptEngine (using Microsoft.ClearScript.Windows 7.4.5 package from NuGet). I have the following Person class that has a single Name property:

public class Person
{
    private string _name;

    public Person(string name)
    {
        _name = name;
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

I also have a HostWindow class that implements IHostWindow so I can use MsgBox in my script:

class HostWindow : IHostWindow
{
    public IntPtr OwnerHandle
    {
        get { return IntPtr.Zero; }
    }

    public void EnableModeless(bool enable)
    {
        return;
    }
}

With these, I am trying to test what happens when I use AddHostObject multiple times using the same object name:

WindowsScriptEngineFlags flags = new WindowsScriptEngineFlags();
VBScriptEngine host = new VBScriptEngine(flags);
host.AllowReflection = true;
host.HostWindow = new HostWindow();
host.AddHostObject("host", new ExtendedHostFunctions());

Person person = new Person("Original Person");
host.AddHostObject("Person", person);

for (int i = 0; i < 5; i++)
{
    person = new Person("Person " + i.ToString());
    host.AddHostObject("Person", person);

    host.Execute("MsgBox Person.Name");

}

This code throws the following exception:

Microsoft.ClearScript.ScriptEngineException HResult=0x800A01B6 Message=Object doesn't support this property or method: 'Person.Name' Source=ClearScript.Core StackTrace: at Microsoft.ClearScript.ScriptEngine.ThrowScriptError(IScriptEngineException scriptError) at Microsoft.ClearScript.Windows.Core.WindowsScriptEngine.ThrowScriptError(Exception exception) at Microsoft.ClearScript.Windows.Core.WindowsScriptEngine.<>c__DisplayClass58_01.<ScriptInvoke>b__0() at Microsoft.ClearScript.ScriptEngine.ScriptInvokeInternal[T](Func1 func) at Microsoft.ClearScript.ScriptEngine.ScriptInvoke[T](Func1 func) at Microsoft.ClearScript.Windows.Core.WindowsScriptEngine.ScriptInvoke[T](Func1 func) at Microsoft.ClearScript.Windows.Core.WindowsScriptEngine.Execute(UniqueDocumentInfo documentInfo, String code, Boolean evaluate) at Microsoft.ClearScript.Windows.VBScriptEngine.Execute(UniqueDocumentInfo documentInfo, String code, Boolean evaluate) at Microsoft.ClearScript.ScriptEngine.Execute(DocumentInfo documentInfo, String code) at Microsoft.ClearScript.ScriptEngine.Execute(String documentName, Boolean discard, String code) at Microsoft.ClearScript.ScriptEngine.Execute(String documentName, String code) at Microsoft.ClearScript.ScriptEngine.Execute(String code) at WinFormsApp1.Program.Main() in D:.NET Projects\Development\Scripting\MultiAddHostTesting\WinFormsApp1\Program.cs:line 32

I have the same code written in a separate Visual Basic .Net project and it works properly there:

Dim flags As New WindowsScriptEngineFlags
Dim scriptingEngine As VBScriptEngine = New VBScriptEngine(flags)
scriptingEngine.AllowReflection = True
scriptingEngine.HostWindow = New HostWindow()
scriptingEngine.AddHostObject("host", New ExtendedHostFunctions)

Dim person As New Person("Original Person")

scriptingEngine.AddHostObject("Person", person)

For counter As Integer = 1 To 5
    person = New Person("Person " & counter)
    scriptingEngine.AddHostObject("Person", person)
    scriptingEngine.Execute("MsgBox Person.Name")
Next

scriptingEngine.Execute("MsgBox Person.Name")

This code does not throw the same exception. Instead, it displays "Person 1" 6 times. Since I am creating a new instance of the Person class in every iteration and using AddHostObject with the new instance, I would expect it to display "Person 1", "Person 2", "Person 3", "Person 4", "Person 5" and finally "Person 5".

If I remove the MsgBox from inside the For ... Next loop:

For counter As Integer = 1 To 5
    person = New Person("Person " & counter)
    scriptingEngine.AddHostObject("Person", person)
Next

scriptingEngine.Execute("MsgBox Person.Name")

it displays "Person 5". This tells me that AddHostObject worked, at least on the last iteration, but when I access the object using MsgBox in the loop, it seems to fail.

My ultimate goal is to iterate through a collection of objects, adding them one by one to the script engine, and executing user-defined VBScript code. The user code handles each object one at a time and expects the object to be named the same. I am open to alternative approaches but the current one worked well with the old MS ScriptControl and its Reset method.

ClearScriptLib commented 2 months ago

Hi @EtienneLaneville,

This code throws the following exception:

Hmm, we can't reproduce that. Our C# app displays a "Person 0" message box five times.

An exception like the one above often indicates a type or member visibility issue. Are you sure that your Person class and its Name property were both public when you ran your test?

Since I am creating a new instance of the Person class in every iteration and using AddHostObject with the new instance, I would expect it to display "Person 1", "Person 2", "Person 3", "Person 4", "Person 5" and finally "Person 5".

Ah, OK. What you're seeing is actually expected. You've discovered one of ClearScript's dark corners 😱

When you call a Windows Script engine's AddHostObject or AddHostType method, you're creating a so-called host item. One of the ways in which a host item differs from other external objects is that the engine expects it not to change.

Because of that assumption, the engine retrieves a host item's value only once – the first time a script accesses it – and caches it thereafter. That's why the scripts in your test app always see the same Person object.

Host items are a very old Windows Script feature that's meant to be used for host APIs rather than data. ClearScript uses them because it's the only way to access certain functionality – e.g., GlobalMembers.

My ultimate goal is to iterate through a collection of objects, adding them one by one to the script engine, and executing user-defined VBScript code. The user code handles each object one at a time and expects the object to be named the same.

That's no problem; instead of calling AddHostObject, you can do this:

host.Global.SetProperty("Person", person);

Please give that a try. It should behave as you expect.

Good luck!

EtienneLaneville commented 2 months ago

Indeed, regarding the Person.Name exception, the Person class was defined within Program.cs. Moving it out resolved the issue.

Also, host.Global.SetProperty("Person", person) works as expected. Thanks for pointing me in the correct direction.

I adjusted the question title to reflect the second issue, I think it could be more helpful to others.