Элемент управления Xamarin Forms WebView — это абстрактное представление платформенных определенных элементов управления Android WebView, iOS UIWebView и UWP WebBrowser.
Хотя WebView кажется довольно необычным элементом управления для мобильного приложения, он продолжает служить различным целям, таким как автоматизация ввода учетных данных в фоновом режиме, просмотр внутренних web-страниц, работа API на основе CMS, поддерживающих HTML, и рендеринг в мобильных приложениях. Данный вид управления нравится не всем, но он проникает во многие проекты по причине ограничения API в авторизации или того, что это способ простого рендеринга изменяющегося контента.
Далее будут рассмотрены возможности, которые облегчат работу разработчика.
Привязываемые методы
Если нужно использовать функции Refresh или GoBack, необходимо расширить управление WebView, чтобы сделать эти функции доступными в ViewModel.
Выполнение Javascript
В управлении Xamarin Forms WebView есть способ встраивать javascript в страницу, но он не позволяет возвращать значение. В этом посте будет рассмотрено как получить значение Javascript вызовом.
Отладка WebView
Можно отладить WebView через Chrome на компьютере в эмуляторе или реальном устройстве. Такой способ отлично помогает при отладке определенных проблем.
Обмен cookie-файлами
Обычно WebView обменивается cookie-файлами с помощью HTTPClient, за исключением Android, которому нужна дополнительная помощь. Этот пост подскажет, как обмениваться cookie-файлами и как удалять их из Cookie Container.
Конфигурация движка рендеринга
Каждая платформа использует свой движок web-рендеринга. Более того, движки рендеринга разных версий одной платформы тоже отличаются. Это касается и движка рендеринга Javascript. Далее будут рассмотрены некоторые отличия.
Привязываемые методы
В WebView есть только определенное количество функций и свойств, которые доступны с помощью прямого доступа к управлению. Для того, чтобы поддерживать чистый код и вызывать функции из ViewModel, необходимо создать новое индивидуальное управление , которое обеспечит новые привязываемые свойства.
Расширенное управление
Сначала необходимо создать новое индивидуальное управление, которое наследуется от WebView. Для этого нужно взять наиболее используемые функции: Refresh, GoBack и функцию, которая возвращает результат для CanGoBack.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public namespace Mobile.Control { public class WebViewer : WebView { public static BindableProperty RefreshCommandProperty = BindableProperty.Create(nameof(RefreshCommand), typeof(Action), typeof(WebViewer), null, BindingMode.OneWayToSource); public Action RefreshCommand { get { return (Action)GetValue(RefreshCommandProperty); } set { SetValue(RefreshCommandProperty, value); } } public static BindableProperty GoBackCommandProperty = BindableProperty.Create(nameof(GoBackCommand), typeof(Action), typeof(WebViewer), null, BindingMode.OneWayToSource); public Action GoBackCommand { get { return (Action)GetValue(GoBackCommandProperty); } set { SetValue(GoBackCommandProperty, value); } } public static BindableProperty CanGoBackFunctionProperty = BindableProperty.Create(nameof(CanGoBackFunction), typeof(Func<bool>), typeof(WebViewer), null, BindingMode.OneWayToSource); public Func<bool> CanGoBackFunction { get { return (Func<bool>)GetValue(CanGoBackFunctionProperty); } set { SetValue(CanGoBackFunctionProperty, value); } } } } |
Android рендерер
Необходимо создать индивидуальный рендерер в Android проекте.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | [assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))] namespace Mobile.Droid { public class WebViewRender : WebViewRenderer { protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e) { base.OnElementChanged(e); if (Control != null && e.NewElement != null) { InitializeCommands((WebViewer)e.NewElement); } } private void InitializeCommands(WebViewer element) { element.RefreshCommand = () => { Control?.Reload(); }; element.GoBackCommand = () => { var ctrl = Control; if (ctrl == null) return; if (ctrl.CanGoBack()) ctrl.GoBack(); }; element.CanGoBackFunction = () => { var ctrl = Control; if (ctrl == null) return false; return ctrl.CanGoBack(); }; } } } |
iOS рендерер
Необходимо создать индивидуальный рендерер в IOS проекте.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | [assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))] namespace Mobile.iOS { public class WebViewRender : WebViewRenderer { protected override void OnElementChanged(VisualElementChangedEventArgs e) { base.OnElementChanged(e); if (NativeView != null && e.NewElement != null) InitializeCommands((WebViewer)e.NewElement); } private void InitializeCommands(WebViewer element) { element.RefreshCommand = () => { ((UIWebView)NativeView).Reload(); }; element.GoBackCommand = () => { var control = ((UIWebView)NativeView); if (control.CanGoBack) { element.IsBackNavigating = true; control.GoBack(); } }; element.CanGoBackFunction = () => { return ((UIWebView)NativeView).CanGoBack; }; } } } |
UWP
Необходимо создать индивидуальный рендерер в UWP проекте.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | [assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))] namespace Mobile.UWP { public class WebViewRender : WebViewRenderer { protected override void OnElementChanged(ElementChangedEventArgs<WebView> e) { base.OnElementChanged(e); if (Control != null && e.NewElement != null) InitializeCommands((WebViewer)e.NewElement); } private void InitializeCommands(WebViewer element) { element.RefreshCommand = () => { Control.Refresh(); }; element.GoBack = () => { if (Control.CanGoBack) Control.GoBack(); }; element.CanGoBackFunction = () => { return Control.CanGoBack; }; } } } |
Вызов из ViewModel
Сначала нужно создать определенный набор свойств в ViewModel, которые будут привязаны к новым командам.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | private Action _refresh; public Action Refresh { get { return _refresh; } set { Set(value); } } private Action _goBack; public Action GoBack { get { return _goBack; } set { _goBack = value; } } private Func<bool> _canGoBack; public Func<bool> CanGoBack { get { return _canGoBack; } set { _canGoBack = value; } } |
Далее необходимо добавить следующие атрибуты, чтобы обеспечит работу нового управления.
1 | xmlns:control="clr-namespace:Mobile.Control" |
Теперь следует добавить в управление и привязать каждую функцию к свойству в ViewModel. Здесь нужна OneWayToSource привязка в качестве управления функцией, и не нужно выходить из ViewModel, чтобы переписать ее.
1 2 3 4 5 | <control:WebViewer Source="{Binding WebViewSource}" CanGoBackFunction="{Binding CanGoBack, Mode=OneWayToSource}" GoBackCommand="{Binding GoBackCommand, Mode=OneWayToSource}" RefreshCommand="{Binding RefreshCommand, Mode=OneWayToSource}" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" /> |
Чтобы вызвать что-либо из этого, можно выполнить любое действие или вызвать любую функцию из ViewModel
1 2 3 4 5 | // Из любого места в ViewModel GoBack(); var result = CanGoBack(); Refresh(); |
Выполнение JavaScript
В существующем управлении WebView есть функция выполнения Javascript на загруженной странице, однако она не имеет возможности возвращать значение. В этой статье будет рассмотрено как добавить такую возможность.
Расширенное управление
Чтобы расширить базовые возможности WebView, нужно добавить функцию, названную EvaluateJavascript. Эта функция принимает строку с Javascript, который нужно исполнить, и возвращает строку с результатом.
1 2 3 4 5 6 7 8 9 10 11 | public class WebViewer : WebView { public static BindableProperty EvaluateJavascriptProperty = BindableProperty.Create(nameof(EvaluateJavascript), typeof(Func<string, Task<string>>), typeof(WebViewer), null, BindingMode.OneWayToSource); public Func<string, Task<string>> EvaluateJavascript { get { return (Func<string, Task<string>>)GetValue(EvaluateJavascriptProperty); } set { SetValue(EvaluateJavascriptProperty, value); } } } |
Рендереры
Далее необходимо добавить индивидуальные рендереры для каждой платформы, чтобы заработала новая функция.
Android
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | [assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))] namespace Mobile.Droid { public class WebViewRender : WebViewRenderer { protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e) { base.OnElementChanged(e); var webView = e.NewElement as WebViewer; if (webView != null) webView.EvaluateJavascript = async (js) => { var reset = new ManualResetEvent(false); var response = string.Empty; Device.BeginInvokeOnMainThread(() => { Control?.EvaluateJavascript(js, new JavascriptCallback((r) => { response = r; reset.Set(); })); }); await Task.Run(() => { reset.WaitOne(); }); return response; }; } } internal class JavascriptCallback : Java.Lang.Object, IValueCallback { public JavascriptCallback(Action<string> callback) { _callback = callback; } private Action<string> _callback; public void OnReceiveValue(Java.Lang.Object value) { _callback?.Invoke(Convert.ToString(value)); } } } |
iOS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | [assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))] namespace Mobile.iOS { public class WebViewRender : WebViewRenderer { protected override void OnElementChanged(VisualElementChangedEventArgs e) { base.OnElementChanged(e); var webView = e.NewElement as WebViewer; if (webView != null) webView.EvaluateJavascript = (js) => { return Task.FromResult(this.EvaluateJavascript(js)); }; } } } |
UWP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | [assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))] namespace Mobile.UWP.CustomRenderers { public class WebViewRender : WebViewRenderer { protected async override void OnElementChanged(ElementChangedEventArgs<WebView> e) { base.OnElementChanged(e); var webView = e.NewElement as WebViewer; if (webView != null) webView.EvaluateJavascript = async (js) => { return await Control.InvokeScriptAsync("eval", new[] { js }); }; } } } |
Вызов из View Model
Сначала необходимо создать свойство в View Model как показано ниже.
1 2 3 4 5 6 | private Func<string, Task<string>> _evaluateJavascript; public Func<string, Task<string>> EvaluateJavascript { get { return _evaluateJavascript; } set { _evaluateJavascript = value; } } |
Далее нужно привязать это свойство к View Model.
1 2 | <control:WebViewer Source="{Binding WebViewSource}" EvaluateJavascript="{Binding EvaluateJavascript}, Mode=OneWayToSource}" /> |
Теперь можно вызвать ранее созданную функцию из View Model и получит значение.
1 | var result = await EvaluateJavascript("document.getElementById('test');"); |
Предупреждения
Разработчик должен знать о большом подвохе, с которым может столкнуться, запуская Javascript.
1 | document.getElementById('myElement').value; |
В версиях Android 4.1 и предыдущих разработчик не опасаясь может использовать этот код Javascript и получить результат.
Но в версиях 4.2 и последующих движок Javascript изменился и поэтому, когда разработчик запускает этот код, он возвращает изначально правильное значение, однако также задает новый объект документа в DOM. Теперь, если вызвать эту функцию снова, то скрипт не сможет найти указанный элемент, потому что он больше не существует. Чтобы обойти это препятствие, нужно результату скрипта назначить переменную. Нет необходимости возвращать переменную, а следует просто ее назначить, и полученное значение не отразится на DOM существующей страницы.
1 | var x = document.getElementById('myElement').value; |
Отладка WebView
Менее известная функция отладки для Android — отладка WebView внутри приложения Android в режиме реального времени. Это можно сделать через браузер Chrome. Эта функция позволяет изменять HTML и исполнять Javascript в режиме реального времени с помощью окна WebView платформы Android.
WebView
Для начала можно начать с простого окна WebView, которое показывает домашнюю страницу Google.
1 | <WebView Source="https://google.com" /> |
Эмулятор
Когда запустится эмулятор, появится домашняя страница Google. Во время подключения Chrome к WebView, необходимо, чтобы приложение было запущено, и был открыт эмулятор.
Chrome
Далее нужно открыть Chrome на компьютере (не эмулятор) и нажать F12. Затем нажать кнопку с изображением 3 вертикальных точек, затем выбрать More tools (больше инструментов) > Remote Devices (удаленные устройства). После этого внизу колонки появится окно,показывающее Devices (устройства) и Settings (настройки).
Выбор устройств
Сначала необходимо нажать кнопку Devices (устройства), затем выбрать нужное устройство. После этого появится WebView URL и кнопка проверки. Нажимаем кнопку Inspect (проверка).
Проверка
После нажатия этой кнопки появится окно с HTML и управлением WebView, которое понадобится во время отладки и проверки web-страницы в Chrome.
Здесь можно делать практически все, как и в случае с обычной web-страницей, включая:
- Вставка или удаление html-элементов в режиме реального времени
- Исполнение Javascript
- Наведение на пункты меню для визуального отображения в устройстве/эмуляторе
- Просмотр визуальных индикаторов каждого отдельного элемента
Итог
Это отличный инструмент для решения значительных проблем, связанных с отладкой в WebView. Он сэкономит существенное количество времени в случае, когда разработчик изменяет различные элементы web-обозревателя до нужного результата, а затем применяет эти изменения к серверу вместо того, чтобы постоянно перезагружать приложение при применении изменений на стороне сервера, что увидеть эти изменения.
Обмен cookie-файлами
Несмотря на давность, cookie-файлы уместны так же в настоящем времени, как и когда они только появились. WebView может обработать cookie-файлы как обычный браузер. Когда осуществляются web-запросы через нативный http-клиент, этот сервис тоже читает и хранит cookie-файлы во время загрузки web-страниц, которые их содержат. К счастью, в случае UWP и iOS происходит автоматический обмен cookie-содержимым между WebView и нативным http-клиентом, но не в случае с Android. Один из самых распространенных сценариев обмена cookie-файлами в WebView является загрузка страницы ввода учетных данных. После того, как учетные данные введены, http-клиент посылает запрос POST или GET на web-страницу с этими данными в cookie-файлах.
iOS
В iOS, если используется NSUrlSession, UIWebView/WKWebView автоматически обменяется cookie-файлами с NSUrlSession.
Чтобы получит доступ к cookie-контейнеру, нужно послать такой запрос: NSHttpCookieStorage.SharedStorage.Cookies.
Удаление cookie-файлов довольно просто: нужно зациклить cookie-файлы, и вызвать удаление для каждого из них.
1 2 3 4 5 | public void ClearCookies() { foreach (var c in NSHttpCookieStorage.SharedStorage.Cookies) NSHttpCookieStorage.SharedStorage.DeleteCookie(c); } |
Android
В нативном платформенном http-клиенте Android, к сожалению, нет возможности такого обмена cookie-файлами. Поэтому нужно использовать нативный http-клиент или System.Net.Http.HttpClient. В Android есть собственный HttpClient, но в этом примере бедет использован Mono.
Если используется System.Net.Http.HttpClient, то необходимо вручную создать собственный Cookie Container, чтобы обеспечит возможность доступа к нему позднее.
1 2 | CookieContainer _cookieContainer = new CookieContainer(); System.Net.Http.HttpClient _client = new System.Net.Http.HttpClient(new HttpClientHandler() { CookieContainer = _cookieContainer }); |
WebView в Android будет использовать Android.WebKit.CookieManager, чтобы обрабатывать cookie-файлы.
1 | CookieManager.Instance; |
Если нужно обмениваться cookie-файлами между ними, придется вручную обрабатывать информацию. Необходимо знать Uri домена для этих cookie-файлов.
1 2 3 4 5 6 7 8 9 10 | private void CopyCookies(HttpResponseMessage result, Uri uri) { foreach (var header in result.Headers) if (header.Key.ToLower() == "set-cookie") foreach (var value in header.Value) _cookieManager.SetCookie($"{uri.Scheme}://{uri.Host}", value); foreach (var cookie in GetAllCookies(_cookieContainer)) _cookieManager.SetCookie(cookie.Domain, cookie.ToString()); } |
1 2 3 4 5 6 7 | public void ReverseCopyCookies(Uri uri) { var cookie = _cookieManager.GetCookie($"{uri.Scheme}://{uri.Host}"); if (!string.IsNullOrEmpty(cookie)) _cookieContainer.SetCookies(new Uri($"{uri.Scheme}://{uri.Host}"), cookie); } |
Если нужно получить доступ ко всем cookie-файлам в Android, то можно применить совершенно нестабильный в будущих версиях подход. Используйте его с осторожностью.
Замечание: В данном случае использована Polly, потому что нет способа блокировки внутренней ссылки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | private IEnumerable<Cookie> GetAllCookies(CookieContainer cookieContainer) { Hashtable domains = (Hashtable)cookieContainer.GetType().GetField("m_domainTable", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(cookieContainer); IList<Cookie> cookies = new List<Cookie>(); var policy = Policy.Handle<InvalidOperationException>() .WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1) }); // Possibility that the container changed while looping. Since we can't lock the internal field, a simple retry is the only solution. return policy.Execute(() => { foreach (DictionaryEntry element in domains) { SortedList list = (SortedList)element.Value.GetType().GetField("m_list", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(element.Value); foreach (var element in list) { var collection = (CookieCollection)((DictionaryEntry)element).Value; foreach (Cookie cookie in collection) cookies.Add(cookie); } } return cookies; }); } |
UWP
В Windows, если используется Windows.Web.Http.HttpClient, доступ к cookie-контейнеру можно получить с помощью добавления Protocol Filter.
1 2 | private readonly HttpBaseProtocolFilter _filter = new HttpBaseProtocolFilter(); private Windows.Web.Http.HttpClient _httpClient = new Windows.Web.Http.HttpClient(_filter); |
Сделав это, можно задействовать_filter.CookieManager и отсюда уже просматривать, добавлять, изменять или удалять cookie-файлы.
К сожалению, есть недостаток в UWP CookieManager — невозможность удалить или сделать доступными все cookie-файлы одной командой. Нужно знать домен cookie-файлов, которые необходимо удалить, получить доступ ко всем cookie-файлам и удалить по одному.
1 2 3 4 5 6 7 8 | public async Task ClearCookies() { var domains = new List<Uri>(); // List domains here, or maybe pass as parameter to this function. foreach (var domain in domains) foreach (var cookie in _filter.CookieManager.GetCookies(domain)) _filter.CookieManager.DeleteCookie(cookie); } |
Итог
iOS и UWP позволяют обмениваться cookie-файлами с помощью нативного http-клиента, а Android требует ручного вмешательства. Если используется Mono Http Client, нужно применять аналогичные Android методы, чтобы обмениваться cookie-файлами между контейнерами, однако такое управление может стать трудоемким и быть подверженным ошибке.
Конфигурация движка рендеринга
WebView недостаточно хорошо настраивается на уровне Xamarin Forms, однако, каждая платформа предоставляет широкий спектр параметров конфигурации для их нативных элементов контроля. У каждой платформы эти параметры свои, и они значительно отличаются друг от друга. Таким образом, легко понять, почему их нельзя настраивать в кросс-платформенном фреймворке.
Важно обратить внимание на то, используется ли WebView механизм визуализации в мобильном приложении. Механизм визуализации не должен быть тем же самым, который используется браузером по умолчанию на каждой платформе. Как правило, здесь используется слегка подправленная и не полная, а урезанная версия механизма визуализации. И это может привести ко многим проблемам рендеринга.
Если требуется внести изменения в настройки механизма визуализации, нужно создать пользовательский рендерер, как показано ниже.
Android
Нативный веб-браузер Android называется WebKit. Каждая версия Android имеет свою версию WebKit, а начиная с 5.0+ WebKit был и вовсе отделен от SDK и теперь обновляется отдельно от операционной системы. Следствием этого становится огромный спектр возможных конфигураций для WebView на Android. В более ранних версиях Android он использовал стандартный механизм визуализации браузера операционной системы под названием WebKit, который не следует путать с элементом контроля Android с таким же названием — WebKit. В версии Android 4.4 начал использоваться элемент на основе хрома, однако, он не был полностью идентичен обычному браузеру Chrome.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | [assembly: ExportRenderer(typeof(WebView), typeof(WebViewRender))] namespace Mobile.Droid.Render { using Android.OS; using System; using Xamarin.Forms.Platform.Android; using Android.Content; using Android.Webkit; public class WebViewRender : WebViewRenderer { protected override void OnElementChanged(ElementChangedEventArgs e) { base.OnElementChanged(e); if (Control != null && e.NewElement != null) SetupControlSettings(); } private void SetupControlSettings() { // Change Settings Here e.g. Control.Settings.JavaScriptEnabled = true; // Handy Hint: PDF JS will show massive fonts unless the minimum font size is defined as 0. I found this doesn't affect anything else I came across. Control.Settings.MinimumFontSize = 0; // Android 4.4 and below doesn't respect the ViewPort in HTML if (Build.VERSION.SdkInt < BuildVersionCodes.Lollipop) Control.Settings.UseWideViewPort = true; } } } |
iOS
До седьмой версии и включительно в iOS используется элемент контроля UIWebView. А начиная с версии 8.0+, появилась возможность выбрать между UIWebView и WKWebKit. WKWebKit является более продвинутым, однако, в Xamarin Forms по-прежнему используется UIWebKit, из-за инвестиций, уже потраченных на его внедрение в Xamarin Forms.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | [assembly: ExportRenderer(typeof(WebView), typeof(WebViewRender))] namespace Mobile.iOS.Render { using UIKit; using Xamarin.Forms.Platform.iOS; public class WebViewRender : WebViewRenderer { protected override void OnElementChanged(VisualElementChangedEventArgs e) { base.OnElementChanged(e); if (NativeView != null && e.NewElement != null) SetupControlSettings(); } private void SetupControlSettings() { var webView = ((UIWebView)NativeView); webView.ScalesPageToFit = true; } } } |
UWP
WebView для UWP известен под названием WebBrowser. В WebBrowser используется пограничный механизм визуализации. И WebBrowser гораздо более ограничен в части элементов контроля по сравнению с другими платформами, но и здесь есть несколько параметров, которые можно изменить.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | [assembly: ExportRenderer(typeof(WebView), typeof(WebViewRender))] namespace Mobile.UWP.Render { using Xamarin.Forms; using System; public class WebViewRender : WebViewRenderer { protected override void OnElementChanged(ElementChangedEventArgs<WebView> e) { base.OnElementChanged(e); if (Control != null && e.NewElement != null) SetupControlSettings(); } private void SetupControlSettings() { Control.Settings.IsJavaScriptEnabled = true; } } } |
Автор: Adam Pedley
Источник: Статья в блоге автора
Оригинал : https://xamarinhelp.com/xamarin-forms-webview-executing-javascript/