MicrosoftEdge / WebView2Feedback

Feedback and discussions about Microsoft Edge WebView2
https://aka.ms/webview2
452 stars 55 forks source link

Can't net6 pass C# Object to JS? #3503

Closed hbl917070 closed 1 year ago

hbl917070 commented 1 year ago

Here is a simple test Get a Stream Object from C# using js, save it as a js variable Then send this js variable (C# Object) back to C#

netToJs.html

<html>
<body>
    <script>
        async function init() {
            var WV_Stream = window.chrome.webview.hostObjects.WV_Stream;
            var stream = await WV_Stream.GetStream("D:\\a.jpg");
            await WV_Stream.SaveStream(stream, "D:\\b.jpg");
        }
        init();
    </script>
</body>
</html>

Form1.cs

using Microsoft.Web.WebView2.WinForms;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace WV_netToJs {
    public partial class Form1 : Form {
        public WebView2 wv2;
        public Form1() {
            InitializeComponent();
            Init();
        }
        public async void Init() {
            wv2 = new WebView2();
            this.Controls.Add(wv2);
            wv2.Dock = DockStyle.Fill;
            await wv2.EnsureCoreWebView2Async();
            wv2.CoreWebView2.AddHostObjectToScript("WV_Stream", new WV_Stream());
            string path = @"C:\Users\u1\Desktop\netToJs.html";
            wv2.Source = new Uri(path);
        }
    }

    [ClassInterface(ClassInterfaceType.AutoDual)]
    [ComVisible(true)]
    public class WV_Stream {
        public Stream GetStream(string path) {
            Stream fs = File.OpenRead(path);
            return fs;
        }
        public void SaveStream(Stream fs,string path) {
            using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) {
                fs.CopyTo(fileStream);
            }
        }
    }
}

This code works fine in net4.7 or net4.8,

But in net6 or net7, await WV_Stream.GetStream("D:\\a.jpg") returns null

I'm not sure if this is a limitation due to security concerns or simply a bug.

image

OS: Windows 10 19045.2965 WebView2 version: 1.0.1774.30 User agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.50

DebugCodeBody commented 1 year ago

Can only return primitive types,int,string,number。

hbl917070 commented 1 year ago

Can only return primitive types,int,string,number。

In net4.7 or net4.8, there are no such restrictions

image

DebugCodeBody commented 1 year ago

Can only return primitive types,int,string,number。

In net4.7 or net4.8, there are no such restrictions

image

I'm sorry I didn't read it carefully. I tried to return object before, but all failed. You have given me confidence here. Use your code for normal use in my environment, here is my environment

OS: Windows 10 18363 WebView2 version: 1.0.1370.28 User agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.78

DebugCodeBody commented 1 year ago

Can I call Stream.Write(byte[] buffer, int offset, int count) in javaScript?

hbl917070 commented 1 year ago

Can I call Stream.Write(byte[] buffer, int offset, int count) in javaScript?

I don't think it will work to use stream.Write directly in js. Although js can obtain a C# Object and use its properties and methods, js does not have the same Method Overloading feature as C#. If there is such a need, it is still recommended to create a function inside C# to handle this matter and then expose it to js.

Also, byte[] seems to have the problem of transformation failure, you can try to use string instead.

js

var fileStream = await WV_Stream.NewFileStream("D:\\output.jpg");
var bytes = await WV_Stream.GetBytes("D:\\input.jpg");
await WV_Stream.WriteStream(fileStream, JSON.stringify(bytes), 0, bytes.length);
fileStream.Dispose();

Form1.cs

using Microsoft.Web.WebView2.WinForms;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace WV_netToJs {
    public partial class Form1 : Form {

        public WebView2 wv2;

        public Form1() {
            InitializeComponent();
            Init();
        }

        public async void Init() {
            wv2 = new WebView2();
            this.Controls.Add(wv2);
            wv2.Dock = DockStyle.Fill;
            await wv2.EnsureCoreWebView2Async();
            wv2.CoreWebView2.AddHostObjectToScript("WV_Stream", new WV_Stream());
            string path = @"C:\Users\u1\Desktop\netToJs.html";
            wv2.Source = new Uri(path);
        }

    }

    [ClassInterface(ClassInterfaceType.AutoDual)]
    [ComVisible(true)]
    public class WV_Stream {

        public FileStream NewFileStream(string path) {
            FileStream fs = File.Create(path) ;
            return fs;
        }

        public byte[] GetBytes(string path) {
            byte[] result;
            using (FileStream fs = File.OpenRead(path)) {
                result = new byte[fs.Length];
                fs.Read(result, 0, (int)fs.Length);
            }
            return result;
        }

        public void WriteStream(Stream fs, string buffer, int offset, int count) {
            fs.Write(StringToBytes(buffer), offset, count);            
        }

        private byte[] StringToBytes(string data) {
            string[] splitData = data.Trim(new char[] { '[', ']' }).Split(',');
            byte[] result = new byte[splitData.Length];
            for (int i = 0; i < splitData.Length; i++) {
                result[i] = byte.Parse(splitData[i]);
            }
            return result;
        }

    }
}

Of course, the prerequisite for doing this is to be able to pass C# objects to js. In net7, we can only pass primitive types to js. This gives me a headache.

yunate commented 1 year ago

Hi @hbl917070, thanks for your feedback! I'm looking into this issue, this really a issue and I'm tring to find the root cause. If there is any discovery, I will update the progress here.

yunate commented 1 year ago

Hi @hbl917070 , I have identified the problem:

In .NET Framework, the [ComVisible(true)] attribute is used to indicate that a type or assembly is visible to COM interop, allowing COM clients to access and use the types. This attribute is commonly used when developing components that need to be consumed by COM applications.

However, in .NET 5 and later versions (including .NET 6), the default behavior for ComVisible is changed. By default, types and members are no longer visible to COM interop unless explicitly marked with [ComVisible(true)]. This change was introduced to align with the more secure and modern development practices and to reduce the surface area for potential security vulnerabilities.

As a workaround, you can wrap the System.IO.Stream:

#pragma warning disable CS0618
    [ClassInterface(ClassInterfaceType.AutoDual)]
#pragma warning restore CS0618
    [ComVisible(true)]
    public class MyStream
    {
        public Stream stream { get; set; }
        public MyStream(Stream stream)
        {
            this.stream = stream;
        }
    }

And use the MyStream instead of Stream:

        public MyStream GetStream(string path)
        {
            System.IO.Stream fs = File.OpenRead(path);
            return new MyStream(fs);
        }
        public void SaveStream(MyStream fs, string path)
        {
            using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write))
            {
                fs.stream.CopyTo(fileStream);
            }
        }
DebugCodeBody commented 1 year ago
  [ClassInterface(ClassInterfaceType.AutoDual)]
    [ComVisible(true)]
    public class WV_Stream {
        public Stream GetStream(string path) {
            Stream fs = File.OpenRead(path);
            return fs;
        }
        public void SaveStream(Stream fs,string path) {
            using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) {
                fs.CopyTo(fileStream);
            }
        }
    }
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class testType
{
    public void test(Object param)
    {
        Console.WriteLine(param);
    }
}

How do you get an object that javascript passes in? Call C# in the console, passing object

VBNP$Z))VTG3CD3`5EV 29I

C# will get a System.__ComObject type parameter, this type I looked up a lot of information, can not get the key and value inside. Can you help me

LD M%9@(KO2T665V1D4X)A7

DebugCodeBody commented 1 year ago

Can I call Stream.Write(byte[] buffer, int offset, int count) in javaScript?

I don't think it will work to use stream.Write directly in js. Although js can obtain a C# Object and use its properties and methods, js does not have the same Method Overloading feature as C#. If there is such a need, it is still recommended to create a function inside C# to handle this matter and then expose it to js.

Also, byte[] seems to have the problem of transformation failure, you can try to use string instead.

js

var fileStream = await WV_Stream.NewFileStream("D:\\output.jpg");
var bytes = await WV_Stream.GetBytes("D:\\input.jpg");
await WV_Stream.WriteStream(fileStream, JSON.stringify(bytes), 0, bytes.length);
fileStream.Dispose();

Form1.cs

using Microsoft.Web.WebView2.WinForms;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace WV_netToJs {
    public partial class Form1 : Form {

        public WebView2 wv2;

        public Form1() {
            InitializeComponent();
            Init();
        }

        public async void Init() {
            wv2 = new WebView2();
            this.Controls.Add(wv2);
            wv2.Dock = DockStyle.Fill;
            await wv2.EnsureCoreWebView2Async();
            wv2.CoreWebView2.AddHostObjectToScript("WV_Stream", new WV_Stream());
            string path = @"C:\Users\u1\Desktop\netToJs.html";
            wv2.Source = new Uri(path);
        }

    }

    [ClassInterface(ClassInterfaceType.AutoDual)]
    [ComVisible(true)]
    public class WV_Stream {

        public FileStream NewFileStream(string path) {
            FileStream fs = File.Create(path) ;
            return fs;
        }

        public byte[] GetBytes(string path) {
            byte[] result;
            using (FileStream fs = File.OpenRead(path)) {
                result = new byte[fs.Length];
                fs.Read(result, 0, (int)fs.Length);
            }
            return result;
        }

        public void WriteStream(Stream fs, string buffer, int offset, int count) {
            fs.Write(StringToBytes(buffer), offset, count);            
        }

        private byte[] StringToBytes(string data) {
            string[] splitData = data.Trim(new char[] { '[', ']' }).Split(',');
            byte[] result = new byte[splitData.Length];
            for (int i = 0; i < splitData.Length; i++) {
                result[i] = byte.Parse(splitData[i]);
            }
            return result;
        }

    }
}

Of course, the prerequisite for doing this is to be able to pass C# objects to js. In net7, we can only pass primitive types to js. This gives me a headache.

In my tests, I passed Uint8Array and ArrayBuffer data. __ComObject in C#, which is the closest type to byte data

The only good news is that you can pass Array[int]. In C# it can keep an array state

hbl917070 commented 1 year ago

Hi @yunate ,

The solution of enclosing the stream in another class can work smoothly.

Since this solution works, is it possible to use the Inheritance solution? The following code works in net4.8, but gets an error in net7 Error (0x13D) while retrieving error. (0x80131509) at <anonymous>:1:28550

image

#pragma warning disable CS0618
    [ClassInterface(ClassInterfaceType.AutoDual)]
#pragma warning restore CS0618
    [ComVisible(true)]
    public class WV_Form {
        public MyForm NewForm() {
            var w = new MyForm();
            w.Show();
            return w;
        }
    }

MyForm

#pragma warning disable CS0618
    [ClassInterface(ClassInterfaceType.AutoDual)]
#pragma warning restore CS0618
    [ComVisible(true)]
    public class MyForm : Form {
    }
yunate commented 1 year ago

@hbl917070, If the parent class is not visible, and the child class is marked as ComVisible(true), the child class will not be visible. In COM interop, the visibility of a class is determined by the visibility of its members and their accessibility from COM. If the parent class is not visible, its members will not be accessible from COM, including any child classes derived from it. So, this code may work well in net4.8, but will not in net7.

yunate commented 1 year ago
  [ClassInterface(ClassInterfaceType.AutoDual)]
    [ComVisible(true)]
    public class WV_Stream {
        public Stream GetStream(string path) {
            Stream fs = File.OpenRead(path);
            return fs;
        }
        public void SaveStream(Stream fs,string path) {
            using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) {
                fs.CopyTo(fileStream);
            }
        }
    }
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class testType
{
    public void test(Object param)
    {
        Console.WriteLine(param);
    }
}

How do you get an object that javascript passes in? Call C# in the console, passing object

VBNP$Z))VTG3CD3`5EV 29I

C# will get a System.__ComObject type parameter, this type I looked up a lot of information, can not get the key and value inside. Can you help me

LD M%9@(KO2T665V1D4X)A7

Hi @DebugCodeBody, {a:1} will be seem as an object, on the C # side, we did not perform any special processing on this, as it is treated as a Dispatch object (which can be understood as an interface) , and if you want to handle this, you can convert it into a string and then pass it to C#.

DebugCodeBody commented 1 year ago
  [ClassInterface(ClassInterfaceType.AutoDual)]
    [ComVisible(true)]
    public class WV_Stream {
        public Stream GetStream(string path) {
            Stream fs = File.OpenRead(path);
            return fs;
        }
        public void SaveStream(Stream fs,string path) {
            using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) {
                fs.CopyTo(fileStream);
            }
        }
    }
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class testType
{
    public void test(Object param)
    {
        Console.WriteLine(param);
    }
}

How do you get an object that javascript passes in? Call C# in the console, passing object VBNP$Z))VTG3CD3`5EV 29I C# will get a System.__ComObject type parameter, this type I looked up a lot of information, can not get the key and value inside. Can you help me LD M%9@(KO2T665V1D4X)A7

Hi @DebugCodeBody, {a:1} will be seem as an object, on the C # side, we did not perform any special processing on this, as it is treated as a Dispatch object (which can be understood as an interface) , and if you want to handle this, you can convert it into a string and then pass it to C#.

I already know what to do, thank you for your reply