Вызвать метод javascript из c # с динамическим параметром - PullRequest
1 голос
/ 20 июня 2019

Я застрял в проблеме с динамическими типами в сочетании с вызовом jsRuntime.

Чтобы иметь актуальный вопрос:
Как я могу вызвать функцию Javascript из кода C # с динамическим объектом в качестве параметра?
Если это невозможно, каков наилучший способ полностью преобразовать его, чтобы его можно было принять с помощью функции InvokeAsync IJSRuntime?

Теперь, к тому, что я уже попробовал (и, очевидно, не удалось).

Я использую библиотеку от github, которая реализует ChartJS в Blazor . Я скопировал исходный код вместо использования пакета nuget, поскольку в последних обновлениях от Blazor или какой-либо другой зависимости что-то, похоже, было повреждено.

Что я делаю, так это то, что я вызываю функцию 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);

Затем я увидел в инспекторе отладки, что даже после создания Dictionary в Словаре все еще оставалось ExpandoObject, что, вероятно, вызвало исключение. Меня довольно удивило, что это убеждение не было рекурсивным.

Итак, я создал собственную рекурсивную функцию для полного преобразования динамического объекта в словарь. Я реализовал это так, и это, похоже, работало (это очень большой вложенный объект, но все свойства, на которые я смотрел, были в порядке):

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 #)

Трассировка стека исключений:

at 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 (тип 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, WriteStack и состояние) в System.Text.Json.Serialization.JsonSerializer.Write (модуль записи Utf8JsonWriter, Int32 flushThreshold, параметры JsonSerializerOptions, WriteStack & state)в System.Text.Json.Serialization.JsonSerializer.WriteCore (выход PooledByteBufferWriter, значение объекта, тип Type, параметры JsonSerializerOptions) в System.Text.Json.Serialization.JsonSerializer.WriteCoreString (значение объекта, тип Type, параметры JsonSerializerOptions)Text.Json.Serialization.JsonSerializer.IJSRuntime jsRuntime, ChartConfigBase chartConfig)

1 Ответ

2 голосов
/ 21 июня 2019

Обновление

Я поместил эту функцию в CodeReview ( см. ), и у меня есть некоторые улучшения. Прежде всего, некоторые общие вещи, но затем в текущем решении есть одна фатальная ошибка . Обработка для IEnumerable<object> неверна. Преобразование только ExpandoObject с в порядке, но Я полностью опускаю все, кроме 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. После добавления этой проверки для 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;
        } 
    );
}

Я рад любой критике этой функции, так как я не знаю, охватил ли я все возможности. Не стесняйтесь говорить мне все, что бросается в глаза, что можно улучшить. Это определенно работает в моем случае, так что это будет мой ответ на мой собственный вопрос.

...