I've been stuck on a issue regarding dynamic types combined with jsRuntime invokation.
To have an actual question:
How can I invoke a Javascript function from C# code with a dynamic object as parameter?
If this is not possible, what's the best way to fully convert it so it can be accepted by the InvokeAsync
function of a IJSRuntime
?
Now, to what I've already tried (and obviously failed with).
I'm using a library from github which implements ChartJS in blazor. I've copied the source instead of using the nuget package since something seems to have been broken in the last updates from blazor or some other dependency.
What I'm doing is I'm invoking a Javascript function from my razor component and I'm also passing in my config for said function. The StripNulls
method converts the config (actual type) to a dynamic type without all the properties that were null.
dynamic param = StripNulls(chartConfig);
return jsRuntime.InvokeAsync<bool>("ChartJSInterop.SetupChart", param);
I don't think it's necessary to put the code for the StripNulls
method but maybe I'm missing something important so here's the code.
/// Returns an object that is equivalent to the given parameter but without any null member AND it preserves DotNetInstanceClickHandler/DotNetInstanceHoverHandler members intact
///
/// <para>Preserving DotNetInstanceClick/HoverHandler members is important because they contain DotNetObjectRefs to the instance whose method should be invoked on click/hover</para>
///
/// <para>This whole method is hacky af but necessary. Stripping null members is only needed because the default config for the Line charts on the Blazor side is somehow messed up. If this were not the case no null member stripping were necessary and hence, the recovery of the DotNetObjectRef members would also not be needed. Nevertheless, The Show must go on!</para>
/// </summary>
/// <param name="chartConfig"></param>
/// <returns></returns>
private static ExpandoObject StripNulls(ChartConfigBase chartConfig)
{
// Serializing with the custom serializer settings remove null members
var cleanChartConfigStr = JsonConvert.SerializeObject(chartConfig, JsonSerializerSettings);
// Get back an ExpandoObject dynamic with the clean config - having an ExpandoObject allows us to add/replace members regardless of type
dynamic clearConfigExpando = JsonConvert.DeserializeObject<ExpandoObject>(cleanChartConfigStr, new ExpandoObjectConverter());
// Restore any .net refs that need to be passed intact
var dynamicChartConfig = (dynamic) chartConfig;
if (dynamicChartConfig?.Options?.Legend?.OnClick != null
&& dynamicChartConfig?.Options?.Legend?.OnClick is DotNetInstanceClickHandler)
{
clearConfigExpando.options = clearConfigExpando.options ?? new { };
clearConfigExpando.options.legend = clearConfigExpando.options.legend ?? new { };
clearConfigExpando.options.legend.onClick = dynamicChartConfig.Options.Legend.OnClick;
}
if (dynamicChartConfig?.Options?.Legend?.OnHover != null
&& dynamicChartConfig?.Options?.Legend?.OnHover is DotNetInstanceHoverHandler)
{
clearConfigExpando.options = clearConfigExpando.options ?? new { };
clearConfigExpando.options.legend = clearConfigExpando.options.legend ?? new { };
clearConfigExpando.options.legend.onHover = dynamicChartConfig.Options.Legend.OnHover;
}
return clearConfigExpando;
}
However if I try to invoke the InvokeAsync
method with this dynamic object I get the following error:
System.NotSupportedException: 'The collection type 'System.Dynamic.ExpandoObject' is not supported.'
So after some research I stumbeled upon this answer which suggest converting the dynamic object to a Dictionary.
But sadly the exact same error occured with this code:
dynamic dynParam = StripNulls(chartConfig);
Dictionary<string, object> param = new Dictionary<string, object>(dynParam);
return jsRuntime.InvokeAsync<bool>("ChartJSInterop.SetupChart", param);
I then saw in the debug-inspector that even after I created a Dictionary
there were still ExpandoObject
s inside the Dictionary which probably caused the exception. It fairly surprised me that this convsersion wasn't recursive.
So I created my own recursive function to fully convert a dynamic object to a Dictionary. I implemented it like this and it seemed to work (it's a very big, nested object but all properties I looked at were fine):
private static Dictionary<string, object> ConvertDynamicToDictonary(IDictionary<string, object> value)
{
return value.ToDictionary(
p => p.Key,
p =>
p.Value is IDictionary<string, object>
? ConvertDynamicToDictonary((IDictionary<string, object>)p.Value)
: p.Value
);
}
And called like this (no I did not just accidentally pass in the wrong param):
dynamic dynParam = StripNulls(chartConfig);
Dictionary<string, object> param = ConvertDynamicToDictonary(dynParam);
return jsRuntime.InvokeAsync<bool>("ChartJSInterop.SetupChart", param);
This still throws the same exact exception and now I'm very frustrated and have no idea why it's still telling me about ExpandoObject
when in my mind I don't see how it may not have been fully converted to a Dictionary<string, object>
.
I have no further ideas and hope that some kind internet stanger can help me with this. Maybe there's just something wrong with my recursive solution or a small thing I'm overlooking but I have not managed to find it yet.
Additional information:
Versions:
Everything on the newest preview (.net Core 3, VS 19, C#)
Exception Stack trace:
at System.Text.Json.Serialization.JsonClassInfo.GetElementType(Type propertyType, Type parentType, MemberInfo memberInfo) at System.Text.Json.Serialization.JsonClassInfo.CreateProperty(Type declaredPropertyType, Type runtimePropertyType, PropertyInfo propertyInfo, Type parentClassType, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonClassInfo.AddProperty(Type propertyType, PropertyInfo propertyInfo, Type classType, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonClassInfo..ctor(Type type, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializerOptions.GetOrAddClass(Type classType) at System.Text.Json.Serialization.JsonSerializer.GetRuntimeClassInfo(Object value, JsonClassInfo& jsonClassInfo, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializer.HandleEnumerable(JsonClassInfo elementClassInfo, JsonSerializerOptions options, Utf8JsonWriter writer, WriteStack& state) at System.Text.Json.Serialization.JsonSerializer.Write(Utf8JsonWriter writer, Int32 flushThreshold, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonSerializer.WriteCore(PooledByteBufferWriter output, Object value, Type type, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializer.WriteCoreString(Object value, Type type, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializer.ToString[TValue](TValue value, JsonSerializerOptions options) at Microsoft.JSInterop.JSRuntimeBase.InvokeAsync[T](String identifier, Object[] args) at ChartJs.Blazor.ChartJS.ChartJsInterop.SetupChart(IJSRuntime jsRuntime, ChartConfigBase chartConfig)