【问题标题】:Invoke javascript method from c# with dynamic parameter使用动态参数从 c# 调用 javascript 方法
【发布时间】:2019-11-03 16:40:33
【问题描述】:

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

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

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

我正在使用library from github which implements ChartJS in blazor。我已经复制了源代码而不是使用 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'。'

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

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

然后我在调试检查器中看到,即使在我创建了 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&lt;string, object&gt;
我没有进一步的想法,并希望某种互联网上的陌生人可以帮助我解决这个问题。也许我的递归解决方案有问题,或者我忽略了一件小事,但我还没有设法找到它。

其他信息:

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

异常堆栈跟踪:

在 System.Text.Json.Serialization.JsonClassInfo.GetElementType(Type propertyType, Type parentType, MemberInfo memberInfo) 在 System.Text.Json.Serialization.JsonClassInfo.CreateProperty(类型声明的PropertyType,类型 runtimePropertyType,PropertyInfo propertyInfo,类型 parentClassType,JsonSerializerOptions 选项) 在 System.Text.Json.Serialization.JsonClassInfo.AddProperty(类型 propertyType,PropertyInfo propertyInfo,类型 classType,JsonSerializerOptions 选项) 在 System.Text.Json.Serialization.JsonClassInfo..ctor(类型类型,JsonSerializerOptions 选项) 在 System.Text.Json.Serialization.JsonSerializerOptions.GetOrAddClass(类型 classType) 在 System.Text.Json.Serialization.JsonSerializer.GetRuntimeClassInfo(对象值,JsonClassInfo& jsonClassInfo,JsonSerializerOptions 选项) 在 System.Text.Json.Serialization.JsonSerializer.HandleEnumerable(JsonClassInfo elementClassInfo,JsonSerializerOptions 选项,Utf8JsonWriter writer,WriteStack& 状态) 在 System.Text.Json.Serialization.JsonSerializer.Write(Utf8JsonWriter writer,Int32 flushThreshold,JsonSerializerOptions 选项,WriteStack& 状态) 在 System.Text.Json.Serialization.JsonSerializer.WriteCore(PooledByteBufferWriter 输出,对象值,类型类型,JsonSerializerOptions 选项) 在 System.Text.Json.Serialization.JsonSerializer.WriteCoreString(对象值,类型类型,JsonSerializerOptions 选项) 在 System.Text.Json.Serialization.JsonSerializer.ToString[TValue](TValue 值,JsonSerializerOptions 选项) 在 Microsoft.JSInterop.JSRuntimeBase.InvokeAsync[T](字符串标识符,对象 [] args) 在 ChartJs.Blazor.ChartJS.ChartJsInterop.SetupChart(IJSRuntime jsRuntime, ChartConfigBase chartConfig)

【问题讨论】:

  • 如果可能的话,我会重写 StripNulls 以完全不使用任何 ExpandoObjects,并尽可能摆脱所有 dynamic。如果链中的任何东西都是dynamic,那么实际上所有东西都是(“动态传染”),因为在编译时无法确定地知道dynamic。你有没有在运行时设置断点并完全展开param,递归地仔细检查它的类型和其中的每个对象,以及其中每个对象的每个属性等等?
  • 我尝试完全删除StripNulls 方法,但该方法存在并且出于某种目的而骇人听闻,没有它就无法工作。如果您知道如何在不使用动态的情况下从对象中删除属性,那当然很好,但我不知道如何。
  • 在我看来,您正在传递一个 ExpandoObject,并且错误消息告诉您不要传递。我的建议是尽量不要通过。
  • 也不,我没有递归检查每个属性,我相信这就是问题所在。我目前正在重写ConvertDynamicToDictonary 方法,并且我知道问题可能是什么。如果我成功了,我会更新问题,以便您查看。
  • 哦,有道理。如您所见,我并没有过多地使用动态。是的,我的意思是实现当然不是派生 :)

标签: c# asp.net dictionary recursion dynamic


【解决方案1】:

更新

我已将此功能放在 CodeReview (see) 上,并且我有一些改进。首先是一些一般性的东西,但是在当前的解决方案中有一个致命的错误IEnumerable&lt;object&gt; 的处理是错误的。只转换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,但最终发生的是在树的某处有一个ExpandoObjects 数组。在为IEnumerables 添加此检查后它起作用了,该方法现在看起来像这样:

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

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

【讨论】:

  • 很高兴能帮上忙 :) 对我来说也很难。
  • 嗨,Joelius,我认为这对这个主题有帮助 blazorise.com/docs/extensions/chart
  • @JawadSabir 这只是另一个可以做图表的库。我已经有一个我可以调整的等等。也许我会使用它,但我会怀疑它,因为我目前使用的那个似乎比你展示的那个允许更多的自定义。
  • @Juelius,是的,我同意,图书馆不像预期的那样通用。但是,在链接中,他们说 Blazor 中 Json 序列化的变化会影响默认 ChartJs.Blazor
  • @JawadSabir 我已经更新了答案。使用新解决方案非常重要,因为旧解决方案的IEnumerable 部分存在一个致命错误,可能导致您丢失条目。
猜你喜欢
  • 2013-09-07
  • 1970-01-01
  • 1970-01-01
  • 2018-06-19
  • 2012-10-15
  • 2012-10-10
  • 2023-03-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多