Passing object containing some properties as JSObject's? #92867
-
I have been using the new interop features in System.Runtime.InteropServices.JavaScript. I would like to be able to encode some of the properties in the event object as JSObject. Right now I just JSON stringify the event, then convert to a dynamic on the C# side. This works pretty well to avoid getting into POCO wrapper hell trying to fully build out POCO's that match the structure of the javascript event object, or destructuring the event object into a bunch of different parameters. JQueryProxy.BindListener = function (jsObject, events, action)
{
let handler = function (e) {
var eEncoded = JSON.stringify(e);// TODO: Replace with JSON.stringify(e,toJSObjectReplacer)
action(eEncoded, e.type);
}.bind(jsObject);
jsObject.on(events, handler);
} public void EventListener(string eventEncoded, string eventType)
{
dynamic eventData = EncodedEventToDynamic(eventEncoded);
Console.WriteLine($"Event listener invoked: {eventType} on {this} with {this.Text} for event details {eventData}");
}
public void InnerOn(string events) {
JQueryProxy.BindListener(this.jsObject, events, EventListener);
}
private dynamic EncodedEventToDynamic(string encodedEvent) {
string jsonString = System.Text.Encoding.UTF8.GetString(encodedEvent);
dynamic eventData = JsonConvert.DeserializeObject<dynamic>(jsonString);
return eventData;
}
...
[JSImport(baseJSNamespace + ".BindListener")]
public static partial void BindListener(JSObject jqObject, string events, [JSMarshalAs<JSType.Function<JSType.Any, JSType.String>>] Action<object, string> handler); Obviously this loses properties like I can change the signature of the action handler to take an object, but it still looks like the JQuery object references get stringified: I was thinking I could do something with the overload of stringify that takes a replacer: Obviously I could code something really specific to the this specific object structure, and for certain properties just pass them as their own JSObject properties, but I was trying to think of a more generic way to handle this. |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments
-
You can marshal event object as [JSExport]
internal static void OnClick(JSObject e)
{
Console.WriteLine($"OnClick '{e.GetPropertyAsJSObject("currentTarget").GetPropertyAsString("innerText")}'");
} In such case javascript object references are maintained and you can pass the object back to javascript to invoke functions on it. |
Beta Was this translation helpful? Give feedback.
-
I think I rubber-ducked this into a solution. I was hoping to continue to expose a
I don't know if each GetProperty* is going across the interop layer or if I can dynamically interogate the type from the C# side. My thought was using a visitor pattern I can make the decisions on the JS side based on the JS type of the property encountered without having anything hardcoded to a specific object structure, doing most of the structure as just generic JSON and certain properties marshaled as JSObject(i.e.
public void IntermediateListener(JSObject[] replacers, string json)
{
//deserialize json to dynamic, then for properties with value of form "SerratedReplacer_0", _1, etc. set to value of replacers[0]
// invoke downstream event listener
} I was trying to determine how to basically manually marshal new JSObject's from the JS side and get a managed pointer or ID, but I'm realizing I can just add them to an array to be passed as an object array through the export/listener and they'll be marshelled for me. I just need my stringify replacer to add the the JS instances to a JS array on the JS side before calling the C# export and passing the array through the callback. |
Beta Was this translation helpful? Give feedback.
-
This is what I ended up with after dealing with the hurdle that Arrays of JSObject's aren't supported in some scenarios: JQueryProxy.BindListener = function (jsObject, events, shouldConvertHtmlElement, action)
{
let handler = function (e) {
// declare a javascript array called replacements, and implement JSON.stringify with a replacer that adds items which are of type HTMLElements to the array
var replacements = [];
var eEncoded = JSON.stringify(e, function (key, value) {
if (this[key] instanceof HTMLElement) {
if (shouldConvertHtmlElement) {
replacements.push(jQuery(value));
}
else {
replacements.push(value);
}
// replace with placeholder and the index of where the object is stored in replacers array
return { serratedPlaceholder: replacements.length - 1 };
}
return value;
});
action(eEncoded, e.type, new ArrayObject(replacements));
}.bind(jsObject);
jsObject.on(events, handler);
return handler; // return reference to the handler so it can be passed later for .off
}
// used for unpacking JSObject references to an array
HelpersProxy.GetArrayObjectItems = function (arrayObject) {
return arrayObject.items;
} [JSImport(baseJSNamespace + ".BindListener")]
public static partial JSObject BindListener(JSObject jqObject, string events, bool shouldConvertHtmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Object>>] Action<string, string, JSObject> handler);
// Used for unpacking an ArrayObject into a JSObject[] array
[JSImport(baseJSNamespace + ".GetArrayObjectItems")]
[return: JSMarshalAs<JSType.Array<JSType.Object>>]
public static partial JSObject[] GetArrayObjectItems(JSObject jqObject); Action passed to BindListener: Action<string, string, JSObject> interopListener =
(eventEncoded, eventType, arrayObject) => {
// unpack the single ArrayObject into it's individual elements, wrapping them as JQueryObjects.
var replacements = HelpersProxy.GetArrayObjectItems(arrayObject).Select(j=>new JQueryObject(j)).ToList();
// Deserialize the eventEncoded JSON string, and restore the native JS objects
dynamic eventData = EncodedEventToDynamic(eventEncoded, replacements);
onEvent[eventName]?.Invoke(this, eventData);
}; Deserializer for the event data that restores JSObject references of our choice(in this case HTMLElements wrapped as JQuery objects, but could be easily adapted to preserve other JS types: // Event data is serialized to JSON when the event is fired from JS and passed to C#
// To preserve some objects as native JS references, they are extracted into a seperate array
// and JSON properties are replaced with `{ serratedPlaceholder: 1 }` where 1 is the index of the object in the array
// Then here when listening to an event we desrialize the JSON into a dynamic and restore the native JS references
private dynamic EncodedEventToDynamic(string encodedEvent, List<JQueryObject> replacements)
{
ExpandoObject eventData = JsonConvert.DeserializeObject<ExpandoObject>(encodedEvent);
ApplyReplacements(eventData, replacements, null, out bool hasPlaceholder);
return eventData;
}
private object ApplyReplacements(ExpandoObject currentExpando, List<JQueryObject> replacements, ExpandoObject parent, out bool hasPlaceholder)
{
// Go through properties of currentExpando,
// find a child expando property containing a value property named serratedPlaceholder, e.g. `target: { serratedPlaceholder : 1 }`
hasPlaceholder = false;// flagged true for parent when currentExpando has a single property named serratedPlaceholder
if (replacements == null || replacements.Count == 0) // nothing to replace
return null;
Dictionary<string, object?> placeholdersFound = new Dictionary<string, object?>();
// recursively search all properties of the ExpandoObject and find expando with property name of serratedPlaceholder
foreach (var property in currentExpando)
{
Console.WriteLine("Property: " + property.Key + " + " + property.Value);
if (property.Key is string && property.Key == "serratedPlaceholder")
{
// Found. The entire currentExpando is the placeholder and needs to be replaced in the property of parent call.
// Flag out param hasPlaceholder and return the appropriate replacement so the parent call can reassign.
hasPlaceholder = true;
int index = Convert.ToInt32(property.Value);
Console.WriteLine("Replacing: " + property.Key + " + " + property.Value + " with " + index);
return replacements[index];
}
else if (property.Value is ExpandoObject currentValue)
{
// If is expando, then recurse into it and scan its properties as well.
var replacement = ApplyReplacements(currentValue, replacements, currentExpando, out bool hasPlaceholderInner);
if(hasPlaceholderInner) // If child property turns out to be a placeholder, then replace it
{
placeholdersFound[property.Key] = replacement;
}
}
}
// we can't modify expando while iterating through it above, so stage replacement in placeholdersFound
foreach (var placeHolder in placeholdersFound)// then apply them to the expando here
{
((IDictionary<String, Object?>)currentExpando)[placeHolder.Key] = placeHolder.Value;
}
return null;
} |
Beta Was this translation helpful? Give feedback.
This is what I ended up with after dealing with the hurdle that Arrays of JSObject's aren't supported in some scenarios: