Как вызвать функцию wpf через JS при посещении https ссылки - PullRequest
2 голосов
/ 25 февраля 2020

Мне нужно выполнить функцию, определенную в проекте wpf, которая вызывается из JS на веб-странице https. Демонстрационный проект всех кодов находится здесь: https://github.com/tomxue/WebViewIssueInWpf

JS часть: Ссылка на веб-страницу https://cmsdev.lenovo.com.cn/musichtml/leHome/weather/index.html?date=&city=&mark=0&speakerId=&reply=

И он содержит нижнюю строку:

<script src="js/index.js" type="text/javascript" charset="utf-8"></script>

И js / index. js содержит следующий код:

setTitle(dataObject.city + weekDay(dataObject.date) +"天气" )

setTitle() определен ниже: использует метод из window.external.notify()

    function setTitle(_str){
        try{
            wtjs.setTitle(_str)
        }catch(e){
            console.log(_str)
            window.external.notify(_str);
        }
    }

Функция window.external.notify() будет вызывать функцию wpf через ScriptNotify().

WPF part: Для WebView внутри проекта wpf

        this.wv.IsScriptNotifyAllowed = true;
        this.wv.ScriptNotify += Wv_ScriptNotify;

И

    private void Wv_ScriptNotify(object sender, Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.WebViewControlScriptNotifyEventArgs e)
    {
        textBlock.Text = e.Value;
    }

Проблемы:

(1) Проблема здесь в том, если веб-страница использует https://, тогда вышеупомянутая функция Wv_ScriptNotify() в wpf не будет запущена. Но если ссылка на веб-страницу использует http://, то вышеупомянутая функция Wv_ScriptNotify() в wpf может быть запущена. Почему и как это решить?

Обновление: 2020-3-2 17:25:55, протестировано только сейчас, https работает. Я не знаю, по каким причинам https не работает ранее

(2) JS на веб-странице используется объект wt js (определенный нами и хорошо работающий с UWP проект с использованием JSBridge). И я хочу использовать подобный метод для UWP, используя мост, чтобы я мог добавить несколько функций / интерфейсов для вызова JS. Недостатком ScriptNotify () является то, что используется только один интерфейс. Чтобы добиться этого, я делаю нижеприведенный код, который сейчас закомментирован.

wv.RegisterName("wtjs", new myBridge());

И другие функции определены так:

    public class myBridge
    {
        public void SetTitle(string title)
        {
            Debug.WriteLine("SetTitle is executing...title = {0}", title);
        }

        public void PlayTTS(string tts)
        {
            Debug.WriteLine("PlayTTS is executing...tts = {0}", tts);
        }
    }

В то время как на стороне JS соответствующие функции будут называться.

wtjs.playTTS(tts)
wtjs.setTitle(_str)

Но на самом деле сторона wpf не работала, в то время как проект UWP, использующий JSBridge, работает с веб-ссылкой (поэтому веб-страница и сценарий JS работоспособны). Как этого добиться?

(3)

Указанные выше две проблемы уже решены с помощью ответа Д. К. Дилипа. Но новая проблема найдена. Пожалуйста, проверьте мой код GitHub, обновите его до последней фиксации. https://github.com/tomxue/WebViewIssueInWpf

Я поместил TextBlock в WebView и ожидаю увидеть текст, плавающий в веб-контенте. Но на самом деле текст покрывается WebView. Почему и как это решить? Спасибо!

1 Ответ

1 голос
/ 01 марта 2020

Для Задачи (1, 2)

HTTPS-ссылка для меня работала нормально, может быть, страница загружается слишком медленно?

Согласно Microsoft ( source ) только ScriptNotify поддерживается в WebView:

Можно ли добавить собственные объекты в мой контент WebViewControl?

Нет. Ни свойство WebBrower (Inte rnet Explorer) ObjectForScripting , ни метод WebView (UWP) AddWebAllowedObject не поддерживаются в WebViewControl. В качестве обходного пути вы можете использовать window.external.notify / ScriptNotify и JavaScript выполнение для связи между слоями, например: https://github.com/rjmurillo/WebView_AddAllowedWebObjectWorkaround

Но выше предложенное Обходное решение, кажется, работает не так, как вы ожидали, поэтому я просто реализую свое собственное решение для эмуляции соглашения JSBridge, которое вы ожидали.

Мое пользовательское решение не проверено в бою, оно может сломаться в некоторых крайних случаях, но оно Кажется, работает хорошо в нескольких простых тестах.

Что поддерживается:

  • Несколько объектов моста
  • JS для C# метод вызов
  • JS до C# получить / установить свойство

C# Использование:

// Add
webView.AddWebAllowedObject("wtjs", new MyBridge(this));
webView.AddWebAllowedObject("myBridge", new MyOtherBridge());

// Remove
webView.RemoveWebAllowedObject("wtjs");

JS Использование:

// Call C# object method (no return value)
wtjs.hello('hello', 'world', 666);
myBridge.saySomething('天猫精灵,叫爸爸!');

// Call C# object method (return value)
wtjs.add(10, 20).then(function (result) { console.log(result); });

// Get C# object property
wtjs.backgroundColor.then(function (color) { console.log(color); });

// Set C# object property
wtjs.niubility = true;

Код

WebViewExtensions.cs

using Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT;
using Microsoft.Toolkit.Wpf.UI.Controls;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Text;

namespace WpfApp3
{
    // Source: https://github.com/dotnet/orleans/issues/1269#issuecomment-171233788
    public static class JsonHelper
    {
        private static readonly Type[] _specialNumericTypes = { typeof(ulong), typeof(uint), typeof(ushort), typeof(sbyte) };

        public static object ConvertWeaklyTypedValue(object value, Type targetType)
        {
            if (targetType == null)
                throw new ArgumentNullException(nameof(targetType));

            if (value == null)
                return null;

            if (targetType.IsInstanceOfType(value))
                return value;

            var paramType = Nullable.GetUnderlyingType(targetType) ?? targetType;

            if (paramType.IsEnum)
            {
                if (value is string)
                    return Enum.Parse(paramType, (string)value);
                else
                    return Enum.ToObject(paramType, value);
            }

            if (paramType == typeof(Guid))
            {
                return Guid.Parse((string)value);
            }

            if (_specialNumericTypes.Contains(paramType))
            {
                if (value is BigInteger)
                    return (ulong)(BigInteger)value;
                else
                    return Convert.ChangeType(value, paramType);
            }

            if (value is long || value is double)
            {
                return Convert.ChangeType(value, paramType);
            }

            return value;
        }
    }

    public enum WebViewInteropType
    {
        Notify = 0,
        InvokeMethod = 1,
        InvokeMethodWithReturn = 2,
        GetProperty = 3,
        SetProperty = 4
    }

    public class WebAllowedObject
    {
        public WebAllowedObject(WebView webview, string name)
        {
            WebView = webview;
            Name = name;
        }

        public WebView WebView { get; private set; }

        public string Name { get; private set; }

        public ConcurrentDictionary<(string, WebViewInteropType), object> FeaturesMap { get; } = new ConcurrentDictionary<(string, WebViewInteropType), object>();

        public EventHandler<WebViewControlNavigationCompletedEventArgs> NavigationCompletedHandler { get; set; }

        public EventHandler<WebViewControlScriptNotifyEventArgs> ScriptNotifyHandler { get; set; }
    }

    public static class WebViewExtensions
    {
        public static bool IsNotification(this WebViewControlScriptNotifyEventArgs e)
        {
            try
            {
                var message = JsonConvert.DeserializeObject<dynamic>(e.Value);

                if (message["___magic___"] != null)
                {
                    return false;
                }
            }
            catch (Exception) { }

            return true;
        }

        public static void AddWebAllowedObject(this WebView webview, string name, object targetObject)
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentNullException(nameof(name));

            if (targetObject == null)
                throw new ArgumentNullException(nameof(targetObject));

            if (webview.Tag == null)
            {
                webview.Tag = new ConcurrentDictionary<string, WebAllowedObject>();
            }
            else if (!(webview.Tag is ConcurrentDictionary<string, WebAllowedObject>))
            {
                throw new InvalidOperationException("WebView.Tag property is already being used for other purpose.");
            }

            var webAllowedObjectsMap = webview.Tag as ConcurrentDictionary<string, WebAllowedObject>;

            var webAllowedObject = new WebAllowedObject(webview, name);

            if (webAllowedObjectsMap.TryAdd(name, webAllowedObject))
            {
                var objectType = targetObject.GetType();
                var methods = objectType.GetMethods();
                var properties = objectType.GetProperties();

                var jsStringBuilder = new StringBuilder();

                jsStringBuilder.Append("(function () {");
                jsStringBuilder.Append("window['");
                jsStringBuilder.Append(name);
                jsStringBuilder.Append("'] = {");

                jsStringBuilder.Append("__callback: {},");
                jsStringBuilder.Append("__newUuid: function () { return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function (c) { return (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16); }); },");

                foreach (var method in methods)
                {
                    if (!method.IsSpecialName)
                    {
                        if (method.ReturnType == typeof(void))
                        {
                            webAllowedObject.FeaturesMap.TryAdd((method.Name, WebViewInteropType.InvokeMethod), method);
                        }
                        else
                        {
                            webAllowedObject.FeaturesMap.TryAdd((method.Name, WebViewInteropType.InvokeMethodWithReturn), method);
                        }

                        var parameters = method.GetParameters();
                        var parametersInString = string.Join(",", parameters.Select(x => x.Position).Select(x => "$$" + x.ToString()));

                        jsStringBuilder.Append(method.Name);
                        jsStringBuilder.Append(": function (");
                        jsStringBuilder.Append(parametersInString);
                        jsStringBuilder.Append(") {");

                        if (method.ReturnType != typeof(void))
                        {
                            jsStringBuilder.Append("var callbackId = window['" + name + "'].__newUuid();");
                        }

                        jsStringBuilder.Append("window.external.notify(JSON.stringify({");
                        jsStringBuilder.Append("source: '");
                        jsStringBuilder.Append(name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("target: '");
                        jsStringBuilder.Append(method.Name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("parameters: [");
                        jsStringBuilder.Append(parametersInString);
                        jsStringBuilder.Append("]");

                        if (method.ReturnType != typeof(void))
                        {
                            jsStringBuilder.Append(",");
                            jsStringBuilder.Append("callbackId: callbackId");
                        }

                        jsStringBuilder.Append("}), ");
                        jsStringBuilder.Append((method.ReturnType == typeof(void)) ? (int)WebViewInteropType.InvokeMethod : (int)WebViewInteropType.InvokeMethodWithReturn);
                        jsStringBuilder.Append(");");


                        if (method.ReturnType != typeof(void))
                        {
                            jsStringBuilder.Append("var promise = new Promise(function (resolve, reject) {");
                            jsStringBuilder.Append("window['" + name + "'].__callback[callbackId] = { resolve, reject };");
                            jsStringBuilder.Append("});");

                            jsStringBuilder.Append("return promise;");
                        }

                        jsStringBuilder.Append("},");
                    }
                }

                jsStringBuilder.Append("};");

                foreach (var property in properties)
                {
                    jsStringBuilder.Append("Object.defineProperty(");
                    jsStringBuilder.Append("window['");
                    jsStringBuilder.Append(name);
                    jsStringBuilder.Append("'], '");
                    jsStringBuilder.Append(property.Name);
                    jsStringBuilder.Append("', {");

                    if (property.CanRead)
                    {
                        webAllowedObject.FeaturesMap.TryAdd((property.Name, WebViewInteropType.GetProperty), property);

                        jsStringBuilder.Append("get: function () {");
                        jsStringBuilder.Append("var callbackId = window['" + name + "'].__newUuid();");
                        jsStringBuilder.Append("window.external.notify(JSON.stringify({");
                        jsStringBuilder.Append("source: '");
                        jsStringBuilder.Append(name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("target: '");
                        jsStringBuilder.Append(property.Name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("callbackId: callbackId,");
                        jsStringBuilder.Append("parameters: []");
                        jsStringBuilder.Append("}), ");
                        jsStringBuilder.Append((int)WebViewInteropType.GetProperty);
                        jsStringBuilder.Append(");");

                        jsStringBuilder.Append("var promise = new Promise(function (resolve, reject) {");
                        jsStringBuilder.Append("window['" + name + "'].__callback[callbackId] = { resolve, reject };");
                        jsStringBuilder.Append("});");

                        jsStringBuilder.Append("return promise;");

                        jsStringBuilder.Append("},");
                    }

                    if (property.CanWrite)
                    {
                        webAllowedObject.FeaturesMap.TryAdd((property.Name, WebViewInteropType.SetProperty), property);

                        jsStringBuilder.Append("set: function ($$v) {");
                        jsStringBuilder.Append("window.external.notify(JSON.stringify({");
                        jsStringBuilder.Append("source: '");
                        jsStringBuilder.Append(name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("target: '");
                        jsStringBuilder.Append(property.Name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("parameters: [$$v]");
                        jsStringBuilder.Append("}), ");
                        jsStringBuilder.Append((int)WebViewInteropType.SetProperty);
                        jsStringBuilder.Append(");");
                        jsStringBuilder.Append("},");
                    }

                    jsStringBuilder.Append("});");
                }

                jsStringBuilder.Append("})();");

                var jsString = jsStringBuilder.ToString();

                webAllowedObject.NavigationCompletedHandler = (sender, e) =>
                {
                    var isExternalObjectCustomized = webview.InvokeScript("eval", new string[] { "window.external.hasOwnProperty('isCustomized').toString();" }).Equals("true");

                    if (!isExternalObjectCustomized)
                    {
                        webview.InvokeScript("eval", new string[] { @"
                            (function () {
                                var originalExternal = window.external;
                                var customExternal = {
                                    notify: function (message, type = 0) {
                                        if (type === 0) {
                                            originalExternal.notify(message);
                                        } else {
                                            originalExternal.notify(JSON.stringify({
                                                ___magic___: true,
                                                type: type,
                                                interop: message
                                            }));
                                        }
                                    },
                                    isCustomized: true
                                };
                                window.external = customExternal;
                            })();" });
                    }

                    webview.InvokeScript("eval", new string[] { jsString });
                };

                webAllowedObject.ScriptNotifyHandler = (sender, e) =>
                {
                    try
                    {
                        var message = JsonConvert.DeserializeObject<dynamic>(e.Value);

                        if (message["___magic___"] != null)
                        {
                            var interopType = (WebViewInteropType)message.type;
                            var interop = JsonConvert.DeserializeObject<dynamic>(message.interop.ToString());
                            var source = (string)interop.source.ToString();
                            var target = (string)interop.target.ToString();
                            var parameters = (object[])interop.parameters.ToObject<object[]>();

                            if (interopType == WebViewInteropType.InvokeMethod)
                            {
                                if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
                                {
                                    if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object methodObject))
                                    {
                                        var method = (MethodInfo)methodObject;

                                        var parameterTypes = method.GetParameters().Select(x => x.ParameterType).ToArray();

                                        var convertedParameters = new object[parameters.Length];

                                        for (var i = 0; i < parameters.Length; i++)
                                        {
                                            convertedParameters[i] = JsonHelper.ConvertWeaklyTypedValue(parameters[i], parameterTypes[i]);
                                        }

                                        method.Invoke(targetObject, convertedParameters);
                                    }
                                }
                            }
                            else if (interopType == WebViewInteropType.InvokeMethodWithReturn)
                            {
                                var callbackId = interop.callbackId.ToString();

                                if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
                                {
                                    if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object methodObject))
                                    {
                                        var method = (MethodInfo)methodObject;

                                        var parameterTypes = method.GetParameters().Select(x => x.ParameterType).ToArray();

                                        var convertedParameters = new object[parameters.Length];

                                        for (var i = 0; i < parameters.Length; i++)
                                        {
                                            convertedParameters[i] = JsonHelper.ConvertWeaklyTypedValue(parameters[i], parameterTypes[i]);
                                        }

                                        var invokeResult = method.Invoke(targetObject, convertedParameters);

                                        webview.InvokeScript("eval", new string[] { string.Format("window['{0}'].__callback['{1}'].resolve({2}); delete window['{0}'].__callback['{1}'];", source, callbackId, JsonConvert.SerializeObject(invokeResult)) });
                                    }
                                }
                            }
                            else if (interopType == WebViewInteropType.GetProperty)
                            {
                                var callbackId = interop.callbackId.ToString();

                                if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
                                {
                                    if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object propertyObject))
                                    {
                                        var property = (PropertyInfo)propertyObject;

                                        var getResult = property.GetValue(targetObject);

                                        webview.InvokeScript("eval", new string[] { string.Format("window['{0}'].__callback['{1}'].resolve({2}); delete window['{0}'].__callback['{1}'];", source, callbackId, JsonConvert.SerializeObject(getResult)) });
                                    }
                                }
                            }
                            else if (interopType == WebViewInteropType.SetProperty)
                            {
                                if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
                                {
                                    if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object propertyObject))
                                    {
                                        var property = (PropertyInfo)propertyObject;

                                        property.SetValue(targetObject, JsonHelper.ConvertWeaklyTypedValue(parameters[0], property.PropertyType));
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        // Do nothing
                    }
                };

                webview.NavigationCompleted += webAllowedObject.NavigationCompletedHandler;
                webview.ScriptNotify += webAllowedObject.ScriptNotifyHandler;
            }
            else
            {
                throw new InvalidOperationException("Object with the identical name is already exist.");
            }
        }

        public static void RemoveWebAllowedObject(this WebView webview, string name)
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentNullException(nameof(name));

            var allowedWebObjectsMap = webview.Tag as ConcurrentDictionary<string, WebAllowedObject>;

            if (allowedWebObjectsMap != null)
            {
                if (allowedWebObjectsMap.TryRemove(name, out WebAllowedObject webAllowedObject))
                {
                    webview.NavigationCompleted -= webAllowedObject.NavigationCompletedHandler;
                    webview.ScriptNotify -= webAllowedObject.ScriptNotifyHandler;

                    webview.InvokeScript("eval", new string[] { "delete window['" + name + "'];" });
                }
            }
        }
    }
}

MainWindow. xaml.cs

using Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT;
using System;
using System.Diagnostics;
using System.Windows;

namespace WpfApp3
{
    public partial class MainWindow : Window
    {
        public class MyBridge
        {
            private readonly MainWindow _window;

            public MyBridge(MainWindow window)
            {
                _window = window;
            }

            public void setTitle(string title)
            {
                Debug.WriteLine(string.Format("SetTitle is executing...title = {0}", title));

                _window.setTitle(title);
            }

            public void playTTS(string tts)
            {
                Debug.WriteLine(string.Format("PlayTTS is executing...tts = {0}", tts));
            }
        }

        public MainWindow()
        {
            this.InitializeComponent();

            this.wv.IsScriptNotifyAllowed = true;
            this.wv.ScriptNotify += Wv_ScriptNotify;
            this.wv.AddWebAllowedObject("wtjs", new MyBridge(this));

            this.Loaded += MainPage_Loaded;
        }

        private void Wv_ScriptNotify(object sender, WebViewControlScriptNotifyEventArgs e)
        {
            if (e.IsNotification())
            {
                Debug.WriteLine(e.Value);
            }
        }

        private void setTitle(string str)
        {
            textBlock.Text = str;
        }

        private void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            this.wv.Source = new Uri("https://cmsdev.lenovo.com.cn/musichtml/leHome/weather/index.html?date=&city=&mark=0&speakerId=&reply=");
        }
    }
}

Результат

Снимок экрана:

MainWindow


Для P roblem (3)

Согласно ( 1 , 2 , 3 ) невозможно наложение элементов пользовательского интерфейса поверх WebView / WebBrowser control.

К счастью, есть альтернативное решение под названием CefSharp , основанное на веб-браузере Chromium и подходящее для вашего случая использования, плюс сработала фоновая анимация ( который не работает в оригинальном WebView контроле).

Однако, нет идеального решения; Представление WPF design неприменимо для CefSharp (с ошибкой Invalid Markup ), но программа просто скомпилируется и запустится. Кроме того, проект может быть построен только с опцией x86 или x64, AnyCPU не будет работать.

MainWindow.xaml

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:cefSharp="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf" 
        x:Class="WpfApp3.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid x:Name="grid">
        <cefSharp:ChromiumWebBrowser x:Name="wv" HorizontalAlignment="Left" Height="405" Margin="50,0,0,0" VerticalAlignment="Top" Width="725" RenderTransformOrigin="-0.45,-0.75" />
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="30,30,0,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top" Height="60" Width="335"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using CefSharp;
using System.Diagnostics;
using System.Windows;

namespace WpfApp3
{
    public partial class MainWindow : Window
    {
        public class MyBridge
        {
            private readonly MainWindow _window;

            public MyBridge(MainWindow window)
            {
                _window = window;
            }

            public void setTitle(string title)
            {
                Debug.WriteLine(string.Format("SetTitle is executing...title = {0}", title));

                _window.setTitle(title);
            }

            public void playTTS(string tts)
            {
                Debug.WriteLine(string.Format("PlayTTS is executing...tts = {0}", tts));
            }
        }

        public MainWindow()
        {
            this.InitializeComponent();

            this.wv.JavascriptObjectRepository.Register("wtjs", new MyBridge(this), true, new BindingOptions() { CamelCaseJavascriptNames = false });
            this.wv.FrameLoadStart += Wv_FrameLoadStart;

            this.Loaded += MainPage_Loaded;
        }

        private void Wv_FrameLoadStart(object sender, FrameLoadStartEventArgs e)
        {
            if (e.Url.StartsWith("https://cmsdev.lenovo.com.cn/musichtml/leHome/weather"))
            {
                e.Browser.MainFrame.ExecuteJavaScriptAsync("CefSharp.BindObjectAsync('wtjs');");
            }
        }

        private void setTitle(string str)
        {
            this.Dispatcher.Invoke(() =>
            {
                textBlock.Text = str;
            });
        }

        private void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            this.wv.Address = "https://cmsdev.lenovo.com.cn/musichtml/leHome/weather/index.html?date=&city=&mark=0&speakerId=&reply=";
        }
    }
}

Снимок экрана:

MainWindow

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...