2

我一直被困在一个关于动态类型与 jsRuntime 调用相结合的问题上。

有一个实际的问题:
如何使用动态对象作为参数从 C# 代码调用 Javascript 函数?
如果这是不可能的,那么完全转换它以便它可以被 a 的InvokeAsync函数接受的最佳方法是IJSRuntime什么?

现在,我已经尝试过(显然失败了)。

我正在使用来自 github 的库,它在 blazor 中实现 ChartJS。我已经复制了源代码而不是使用 nuget 包,因为在 blazor 或其他一些依赖项的最后更新中似乎有一些东西被破坏了。

我正在做的是从我的 razor 组件调用一个 Javascript 函数,并且我还在为所述函数传递我的配置。该StripNulls方法将配置(实际类型)转换为动态类型,而没有所有为空的属性。

dynamic param = StripNulls(chartConfig);
return jsRuntime.InvokeAsync<bool>("ChartJSInterop.SetupChart", param);

我认为没有必要为该StripNulls方法放置代码,但也许我遗漏了一些重要的东西,所以这里是代码。

/// 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;
}

但是,如果我尝试InvokeAsync使用此动态对象调用该方法,则会收到以下错误:

System.NotSupportedException:'不支持集合类型'System.Dynamic.ExpandoObject'。'

因此,经过一些研究,我偶然发现了这个建议将动态对象转换为字典的答案。
但遗憾的是,这段代码出现了完全相同的错误:

dynamic dynParam = StripNulls(chartConfig);
Dictionary<string, object> param = new Dictionary<string, object>(dynParam);
return jsRuntime.InvokeAsync<bool>("ChartJSInterop.SetupChart", param);

然后我在调试检查器中看到,即使在我创建了 a 之后,字典Dictionary中仍然有ExpandoObjects 可能导致异常。令我惊讶的是,这种转换不是递归的。

所以我创建了自己的递归函数来将动态对象完全转换为字典。我是这样实现的,它似乎可以工作(它是一个非常大的嵌套对象,但我查看的所有属性都很好):

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
    );
}

并像这样调用(不,我不只是不小心传入了错误的参数):

dynamic dynParam = StripNulls(chartConfig);
Dictionary<string, object> param = ConvertDynamicToDictonary(dynParam);
return jsRuntime.InvokeAsync<bool>("ChartJSInterop.SetupChart", param);

仍然会引发完全相同的异常,现在我非常沮丧,不知道为什么它仍然告诉我ExpandoObject在我的脑海中什么时候我看不到它可能没有完全转换为Dictionary<string, object>.
我没有进一步的想法,并希望某种互联网上的陌生人可以帮助我解决这个问题。也许我的递归解决方案有问题,或者我忽略了一件小事,但我还没有设法找到它。

附加信息:

版本:
最新预览版中的所有内容(.net Core 3、VS 19、C#)

异常堆栈跟踪:

在 System.Text.Json.Serialization.JsonClassInfo.GetElementType(类型 propertyType,类型 parentType,MemberInfo memberInfo)在 System.Text.Json.Serialization.JsonClassInfo.CreateProperty(类型声明PropertyType,类型 runtimePropertyType,PropertyInfo propertyInfo,类型 parentClassType,JsonSerializerOptions 选项) 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) Json.Serialization.JsonSerializerOptions.GetOrAddClass(Type classType) 在 System.Text.Json.Serialization.JsonSerializer.GetRuntimeClassInfo(Object value, JsonClassInfo& jsonClassInfo, JsonSerializerOptions options) 在 System.Text.Json。System.Text.Json.Serialization.JsonSerializer.Write(Utf8JsonWriter writer, Int32 flushThreshold, JsonSerializerOptions options, WriteStack& state) at System.Text.Json 的 Serialization.JsonSerializer.HandleEnumerable(JsonClassInfo elementClassInfo, JsonSerializerOptions options, Utf8JsonWriter writer, WriteStack& state) System.Text.Json.Serialization.JsonSerializer.WriteCoreString(Object value, Type type, JsonSerializerOptions options) 在 System.Text.Json.Serialization.JsonSerializer 的 Serialization.JsonSerializer.WriteCore(PooledByteBufferWriter output, Object value, Type type, JsonSerializerOptions options) .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)

4

1 回答 1

2

更新

我已将此功能放在 CodeReview 上(请参阅)并且我有一些改进。首先是一些一般性的东西,但在当前的解决方案中有一个致命的错误。的处理IEnumerable<object>是错误的。只转换ExpandoObjects 很好,但我完全忽略了除ExpandoObject. 这已在新解决方案中得到解决。
您可能想要做的一件事是将其转换为扩展方法以使其更加干净,但就我而言,我不想这样做,因为我希望该函数是私有的。如果您是公开的,您应该真正考虑扩展方法。

/// <summary>
/// This method is specifically used to convert an <see cref="ExpandoObject"/> with a Tree structure to a <see cref="Dictionary{string, object}"/>.
/// </summary>
/// <param name="expando">The <see cref="ExpandoObject"/> to convert</param>
/// <returns>The fully converted <see cref="ExpandoObject"/></returns>
private static Dictionary<string, object> ConvertExpandoObjectToDictionary(ExpandoObject expando) => RecursivelyConvertIDictToDict(expando);

/// <summary>
/// This method takes an <see cref="IDictionary{string, object}"/> and recursively converts it to a <see cref="Dictionary{string, object}"/>. 
/// The idea is that every <see cref="IDictionary{string, object}"/> in the tree will be of type <see cref="Dictionary{string, object}"/> instead of some other implementation like <see cref="ExpandoObject"/>.
/// </summary>
/// <param name="value">The <see cref="IDictionary{string, object}"/> to convert</param>
/// <returns>The fully converted <see cref="Dictionary{string, object}"/></returns>
private static Dictionary<string, object> RecursivelyConvertIDictToDict(IDictionary<string, object> value) =>
    value.ToDictionary(
        keySelector => keySelector.Key,
        elementSelector =>
        {
            // if it's another IDict just go through it recursively
            if (elementSelector.Value is IDictionary<string, object> dict)
            {
                return RecursivelyConvertIDictToDict(dict);
            }

            // if it's an IEnumerable check each element
            if (elementSelector.Value is IEnumerable<object> list)
            {
                // go through all objects in the list
                // if the object is an IDict -> convert it
                // if not keep it as is
                return list
                    .Select(o => o is IDictionary<string, object>
                        ? RecursivelyConvertIDictToDict((IDictionary<string, object>)o)
                        : o
                    );
            }

            // neither an IDict nor an IEnumerable -> it's fine to just return the value it has
            return elementSelector.Value;
        }
    );

原始答案

好几个小时后,我终于找到了答案。问题是(某种预期的)ConvertDynamicToDictionary方法。
我的递归解决方案只检查是否有另一个IDictionary,但最终发生的是ExpandoObject树中某处有一个 s 数组。IEnumerable为s添加此检查后,它起作用了,该方法现在如下所示:

private static Dictionary<string, object> ConvertDynamicToDictonary(IDictionary<string, object> value)
{
    return value.ToDictionary(
        p => p.Key,
        p =>
        {
            // if it's another IDict (might be a ExpandoObject or could also be an actual Dict containing ExpandoObjects) just go trough it recursively
            if (p.Value is IDictionary<string, object> dict)
            {
                return ConvertDynamicToDictonary(dict);
            }

            // if it's an IEnumerable, it might have ExpandoObjects inside, so check for that
            if (p.Value is IEnumerable<object> list)
            {
                if (list.Any(o => o is ExpandoObject))
                { 
                    // if it does contain ExpandoObjects, take all of those and also go trough them recursively
                    return list
                        .Where(o => o is ExpandoObject)
                        .Select(o => ConvertDynamicToDictonary((ExpandoObject)o));
                }
            }

            // neither an IDict nor an IEnumerable -> it's probably fine to just return the value it has
            return p.Value;
        } 
    );
}

我很高兴对此功能提出任何批评,因为我不知道我是否已经涵盖了所有可能性。随时告诉我任何引起您注意的可以改进的地方。它绝对适用于我的情况,所以这将是我对自己问题的回答。

于 2019-06-20T21:28:19.333 回答