├── .github └── CODEOWNERS ├── tests ├── Tests.WebView │ ├── Resources │ │ ├── EmbeddedHtml.html │ │ ├── EmbeddedJavascriptFile.js │ │ ├── ResourceJavascriptFile.js │ │ └── dash-folder │ │ │ └── EmbeddedJavascriptFile-With-Dashes.js │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── App.xaml │ ├── App.xaml.cs │ ├── TestObject.cs │ ├── Assertions.cs │ ├── LoadTests.cs │ ├── IsolateCommonTests.cs │ ├── SerializationTests.cs │ ├── Tests.WebView.csproj │ ├── CommonTests.cs │ ├── WebViewTestBase.cs │ ├── RequestInterception.cs │ ├── TestBase.cs │ ├── ResourcesLoading.cs │ ├── JavascriptEvaluation.cs │ └── IsolatedJavascriptEvaluation.cs ├── TestResourceAssembly.V1.0.0.0 │ ├── Resource.txt │ └── TestResourceAssembly.csproj └── TestResourceAssembly.V2.0.0.0 │ ├── Resource.txt │ └── TestResourceAssembly.csproj ├── SampleWebView.Avalonia ├── screenshot.png ├── bundle-osx-x64.sh ├── bundle-osx-arm64.sh ├── app.manifest ├── Program.cs ├── MainWindow.xaml.cs ├── App.xaml ├── App.xaml.cs ├── SampleWebView.Avalonia.csproj ├── MainWindowViewModel.cs └── MainWindow.xaml ├── WebViewControl ├── ProxyAuthentication.cs ├── app.config ├── ChromiumBrowser.cs ├── AssemblyLoader.NETFramework.cs ├── ChromiumBrowser.Wpf.cs ├── SchemeHandlerFactory.cs ├── ResourceType.cs ├── ResourcesManager.Wpf.cs ├── ResourceHandlerExtensions.cs ├── UnhandledAsyncExceptionEventArgs.cs ├── WebViewControl.nuspec ├── HttpResourceRequestHandler.cs ├── Properties │ └── AssemblyInfo.cs ├── WebView.InternalContextMenuHandler.cs ├── RenderProcessTerminatedException.cs ├── WebView.InternalDialogHandler.cs ├── WebView.InternalFocusHandler.cs ├── EditCommands.cs ├── WebView.InternalKeyboardHandler.cs ├── UrlHelper.cs ├── WebViewControl.csproj ├── Request.cs ├── WebView.Extensions.cs ├── WebView.InternalDragHandler.cs ├── WebView.InternalJsDialogHandler.cs ├── WebView.InternalDownloadHandler.cs ├── WebView.JavascriptException.cs ├── WebView.InternalLifeSpanHandler.cs ├── HttpResourceHandler.cs ├── WebView.ResourceHandler.cs ├── JavascriptSerializationHelper.cs ├── WebView.InternalResourceRequestHandler.cs ├── AsyncResourceHandler.cs ├── WebViewLoader.cs ├── WebView.Wpf.cs ├── AssemblyCache.cs ├── GlobalSettings.cs ├── WebView.InternalRequestHandler.cs ├── ResourcesManager.cs ├── ResourceUrl.cs ├── WebView.JavascriptExecutionApi.cs └── WebView.JavascriptExecutor.cs ├── WebViewControl.Avalonia ├── Properties │ └── AssemblyInfo.cs ├── ResourcesManager.Avalonia.cs ├── AssemblyLoader.NETCore.cs ├── ChromiumBrowser.Avalonia.cs ├── WebViewControl.nuspec ├── BaseControl.cs ├── WebView.Avalonia.cs └── WebViewControl.Avalonia.csproj ├── .gitignore ├── Nuget.config ├── Directory.Build.props ├── Directory.Packages.props ├── README.md ├── LICENSE ├── WebView.sln └── .editorconfig /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @OutSystems/ide-team -------------------------------------------------------------------------------- /tests/Tests.WebView/Resources/EmbeddedHtml.html: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /tests/TestResourceAssembly.V1.0.0.0/Resource.txt: -------------------------------------------------------------------------------- 1 | Resource with V1.0.0.0 content -------------------------------------------------------------------------------- /tests/TestResourceAssembly.V2.0.0.0/Resource.txt: -------------------------------------------------------------------------------- 1 | Resource with V2.0.0.0 content -------------------------------------------------------------------------------- /tests/Tests.WebView/Resources/EmbeddedJavascriptFile.js: -------------------------------------------------------------------------------- 1 | embeddedFileLoaded = true; -------------------------------------------------------------------------------- /tests/Tests.WebView/Resources/ResourceJavascriptFile.js: -------------------------------------------------------------------------------- 1 | resourceFileLoaded = true; -------------------------------------------------------------------------------- /tests/Tests.WebView/Resources/dash-folder/EmbeddedJavascriptFile-With-Dashes.js: -------------------------------------------------------------------------------- 1 | embeddedFileLoaded = true; -------------------------------------------------------------------------------- /SampleWebView.Avalonia/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OutSystems/WebView/HEAD/SampleWebView.Avalonia/screenshot.png -------------------------------------------------------------------------------- /tests/Tests.WebView/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | #if !DEBUG 4 | [assembly: Timeout(60000)] 5 | #endif -------------------------------------------------------------------------------- /WebViewControl/ProxyAuthentication.cs: -------------------------------------------------------------------------------- 1 | namespace WebViewControl { 2 | public class ProxyAuthentication { 3 | public string UserName { get; set; } 4 | public string Password { get; set; } 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /WebViewControl/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /WebViewControl/ChromiumBrowser.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | 3 | namespace WebViewControl { 4 | 5 | internal partial class ChromiumBrowser { 6 | 7 | internal CefBrowser GetBrowser() => UnderlyingBrowser; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /WebViewControl.Avalonia/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Tests.ReactView")] 4 | [assembly: InternalsVisibleTo("Tests.WebView")] 5 | [assembly: InternalsVisibleTo("ReactViewControl.Avalonia")] 6 | -------------------------------------------------------------------------------- /WebViewControl/AssemblyLoader.NETFramework.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace WebViewControl { 4 | 5 | internal static class AssemblyLoader { 6 | 7 | internal static Assembly LoadAssembly(string path) => Assembly.LoadFile(path); 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /tests/Tests.WebView/App.xaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /WebViewControl/ChromiumBrowser.Wpf.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue.WPF; 2 | 3 | namespace WebViewControl { 4 | 5 | partial class ChromiumBrowser : WpfCefBrowser { 6 | 7 | public new void CreateBrowser(int width, int height) { 8 | base.CreateBrowser(width, height); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WebViewControl.Avalonia/ResourcesManager.Avalonia.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace WebViewControl { 4 | partial class ResourcesManager { 5 | 6 | private static Stream GetApplicationResource(string assemblyName, string resourceName) { 7 | return null; // TODO 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/Tests.WebView/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Markup.Xaml; 3 | 4 | namespace Tests { 5 | 6 | public class App : Application { 7 | 8 | public App() { } 9 | 10 | public override void Initialize() { 11 | AvaloniaXamlLoader.Load(this); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Tests.WebView/TestObject.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.WebView { 2 | 3 | public enum Kind { 4 | A, 5 | B, 6 | C 7 | } 8 | 9 | public class TestObject { 10 | public string Name; 11 | public int Age; 12 | public TestObject Parent; 13 | public Kind Kind; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SampleWebView.Avalonia/bundle-osx-x64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:Platform=x64 4 | 5 | TARGETAPP=bin/x64/Debug/net8.0/osx-x64/publish/SampleWebView.app/Contents/MacOS 6 | chmod +x "$TARGETAPP/CefGlueBrowserProcess/Xilium.CefGlue.BrowserProcess" 7 | chmod +x "$TARGETAPP/SampleWebView.Avalonia" 8 | -------------------------------------------------------------------------------- /WebViewControl.Avalonia/AssemblyLoader.NETCore.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.Loader; 3 | 4 | namespace WebViewControl { 5 | 6 | internal static class AssemblyLoader { 7 | 8 | internal static Assembly LoadAssembly(string path) => AssemblyLoadContext.Default.LoadFromAssemblyPath(path); 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /SampleWebView.Avalonia/bundle-osx-arm64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-arm64 -p:Platform=ARM64 4 | 5 | TARGETAPP=bin/ARM64/Debug/net8.0/osx-arm64/publish/SampleWebView.app/Contents/MacOS 6 | chmod +x "$TARGETAPP/CefGlueBrowserProcess/Xilium.CefGlue.BrowserProcess" 7 | chmod +x "$TARGETAPP/SampleWebView.Avalonia" 8 | -------------------------------------------------------------------------------- /WebViewControl/SchemeHandlerFactory.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | 3 | namespace WebViewControl { 4 | 5 | internal class SchemeHandlerFactory : CefSchemeHandlerFactory { 6 | 7 | protected override CefResourceHandler Create(CefBrowser browser, CefFrame frame, string schemeName, CefRequest request) { 8 | return null; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SampleWebView.Avalonia/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /WebViewControl.Avalonia/ChromiumBrowser.Avalonia.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue.Avalonia; 2 | 3 | namespace WebViewControl { 4 | 5 | partial class ChromiumBrowser : AvaloniaCefBrowser { 6 | 7 | public new void CreateBrowser(int width, int height) { 8 | if (IsBrowserInitialized) { 9 | return; 10 | } 11 | base.CreateBrowser(width, height); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /WebViewControl/ResourceType.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | 3 | namespace WebViewControl { 4 | 5 | public enum ResourceType { 6 | Stylesheet = CefResourceType.Stylesheet, 7 | Script = CefResourceType.Script, 8 | Image = CefResourceType.Image, 9 | FontResource = CefResourceType.FontResource, 10 | Xhr = CefResourceType.Xhr, 11 | ServiceWorker = CefResourceType.ServiceWorker 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SampleWebView.Avalonia/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.ReactiveUI; 3 | 4 | namespace SampleWebView.Avalonia { 5 | 6 | class Program { 7 | static void Main(string[] args) { 8 | AppBuilder.Configure() 9 | .UsePlatformDetect() 10 | .UseReactiveUI() 11 | .StartWithClassicDesktopLifetime(args); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /WebViewControl/ResourcesManager.Wpf.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Windows; 4 | 5 | namespace WebViewControl { 6 | 7 | partial class ResourcesManager { 8 | 9 | private static Stream GetApplicationResource(string assemblyName, string resourceName) { 10 | return Application.GetResourceStream(new Uri($"/{assemblyName};component/{resourceName}", UriKind.Relative))?.Stream; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /WebViewControl/ResourceHandlerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebViewControl { 4 | 5 | internal static class ResourceHandlerExtensions { 6 | 7 | public static void LoadEmbeddedResource(this ResourceHandler resourceHandler, Uri url) { 8 | var stream = ResourcesManager.TryGetResource(url, true, out string extension); 9 | 10 | if (stream != null) { 11 | resourceHandler.RespondWith(stream, extension); 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/TestResourceAssembly.V1.0.0.0/TestResourceAssembly.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(TargetDotnetVersion) 4 | $(MSBuildProjectDirectory)\bin\ 5 | TestResourceAssembly 6 | 1.0.0.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/TestResourceAssembly.V2.0.0.0/TestResourceAssembly.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(TargetDotnetVersion) 4 | $(MSBuildProjectDirectory)\bin\ 5 | TestResourceAssembly 6 | 2.0.0.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages/ 2 | .csproj.user 3 | *.user 4 | bin 5 | obj 6 | *.suo 7 | *.opendb 8 | *.db 9 | *.nupkg 10 | Release/ 11 | Debug/ 12 | .vs/config/applicationhost.config 13 | .vs/ 14 | *.js 15 | *.tgz 16 | *.css 17 | *.scss.d.ts 18 | manifest.json 19 | WebViewControl/cake_tools 20 | WebViewControl.Avalonia/cake_tools 21 | launchSettings.json 22 | *.cache 23 | .npm-install-stamp 24 | **/TestResults.xml 25 | 26 | .idea 27 | WebViewControl/FodyWeavers.xsd 28 | WebViewControl.Avalonia/FodyWeavers.xsd 29 | **/*.DS_Store 30 | -------------------------------------------------------------------------------- /WebViewControl/UnhandledAsyncExceptionEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebViewControl { 4 | 5 | public class UnhandledAsyncExceptionEventArgs { 6 | 7 | public UnhandledAsyncExceptionEventArgs(Exception e, string frameName) { 8 | Exception = e; 9 | FrameName = frameName; 10 | } 11 | 12 | public Exception Exception { get; private set; } 13 | 14 | public string FrameName { get; private set; } 15 | 16 | public bool Handled { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /WebViewControl/WebViewControl.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $id$ 5 | $version$ 6 | $title$ 7 | $authors$ 8 | OutSystems 9 | false 10 | $description$ 11 | $copyright$ 12 | $packageProjectUrl$ 13 | 14 | -------------------------------------------------------------------------------- /SampleWebView.Avalonia/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | using WebViewControl; 5 | 6 | namespace SampleWebView.Avalonia { 7 | 8 | internal class MainWindow : Window { 9 | 10 | public MainWindow() { 11 | WebView.Settings.LogFile = "ceflog.txt"; 12 | WebView.Settings.BackgroundColor = Color.Bisque; 13 | AvaloniaXamlLoader.Load(this); 14 | 15 | DataContext = new MainWindowViewModel(this.FindControl("webview")); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /WebViewControl.Avalonia/WebViewControl.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $id$ 5 | $version$ 6 | $title$ 7 | $authors$ 8 | OutSystems 9 | false 10 | $description$ 11 | $copyright$ 12 | $packageProjectUrl$ 13 | 14 | -------------------------------------------------------------------------------- /WebViewControl/HttpResourceRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | 3 | namespace WebViewControl { 4 | 5 | internal class HttpResourceRequestHandler : CefResourceRequestHandler { 6 | 7 | protected override CefCookieAccessFilter GetCookieAccessFilter(CefBrowser browser, CefFrame frame, CefRequest request) { 8 | return null; 9 | } 10 | 11 | protected override CefResourceHandler GetResourceHandler(CefBrowser browser, CefFrame frame, CefRequest request) { 12 | return new HttpResourceHandler(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WebViewControl.Avalonia/BaseControl.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.LogicalTree; 3 | 4 | namespace WebViewControl { 5 | public abstract class BaseControl : Control { 6 | protected abstract void InternalDispose(); 7 | 8 | protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { 9 | base.OnDetachedFromLogicalTree(e); 10 | 11 | if (e.Root is Window w && w.PlatformImpl is null) { 12 | // Window was closed. 13 | InternalDispose(); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SampleWebView.Avalonia/App.xaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 0 6 | 0 7 | 15,10,15,8 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /SampleWebView.Avalonia/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace SampleWebView.Avalonia { 6 | public class App : Application { 7 | public override void Initialize() { 8 | AvaloniaXamlLoader.Load(this); 9 | } 10 | 11 | public override void OnFrameworkInitializationCompleted() { 12 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { 13 | desktop.MainWindow = new MainWindow(); 14 | } 15 | base.OnFrameworkInitializationCompleted(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Tests.WebView/Assertions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NUnit.Framework; 4 | 5 | namespace Tests.WebView { 6 | 7 | public static class Assertions { 8 | 9 | public static async Task AssertThrows(AsyncTestDelegate action) where T : Exception { 10 | try { 11 | await action(); 12 | Assert.Fail("Should have thrown exception"); 13 | } catch (Exception exception) { 14 | // ThrowsAsync is not working well 15 | Assert.IsInstanceOf(exception); 16 | return (T)exception; 17 | } 18 | return null; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /WebViewControl/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | // Setting ComVisible to false makes the types in this assembly not visible 5 | // to COM components. If you need to access a type in this assembly from 6 | // COM, set the ComVisible attribute to true on that type. 7 | [assembly: ComVisible(false)] 8 | 9 | // The following GUID is for the ID of the typelib if this project is exposed to COM 10 | [assembly: Guid("a1c2a0c7-df81-4a8f-aeb5-b5375d5d1b47")] 11 | 12 | [assembly: InternalsVisibleTo("Tests.ReactView")] 13 | [assembly: InternalsVisibleTo("Tests.WebView")] 14 | [assembly: InternalsVisibleTo("ReactViewControl")] 15 | -------------------------------------------------------------------------------- /WebViewControl/WebView.InternalContextMenuHandler.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | using Xilium.CefGlue.Common.Handlers; 3 | 4 | namespace WebViewControl { 5 | 6 | partial class WebView { 7 | 8 | private class InternalContextMenuHandler : ContextMenuHandler { 9 | 10 | private WebView OwnerWebView { get; } 11 | 12 | public InternalContextMenuHandler(WebView webView) { 13 | OwnerWebView = webView; 14 | } 15 | 16 | protected override void OnBeforeContextMenu(CefBrowser browser, CefFrame frame, CefContextMenuParams state, CefMenuModel model) { 17 | if (OwnerWebView.DisableBuiltinContextMenus) { 18 | model.Clear(); 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WebViewControl/RenderProcessTerminatedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebViewControl { 4 | 5 | public class RenderProcessCrashedException : Exception { 6 | 7 | internal RenderProcessCrashedException(string message) : base(message) { 8 | } 9 | } 10 | 11 | public class RenderProcessKilledException : Exception { 12 | 13 | public bool IsWebViewDisposing { get; } 14 | 15 | internal RenderProcessKilledException(string message, bool webViewDisposing = false) : base(message) { 16 | IsWebViewDisposing = webViewDisposing; 17 | } 18 | } 19 | 20 | public class RenderProcessOutOfMemoryException : Exception { 21 | 22 | internal RenderProcessOutOfMemoryException(string message) : base(message) { 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /WebViewControl/WebView.InternalDialogHandler.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | using Xilium.CefGlue.Common.Handlers; 3 | 4 | namespace WebViewControl { 5 | 6 | partial class WebView { 7 | private class InternalDialogHandler : DialogHandler { 8 | 9 | private WebView OwnerWebView { get; } 10 | 11 | public InternalDialogHandler(WebView webView) { 12 | OwnerWebView = webView; 13 | } 14 | 15 | protected override bool OnFileDialog(CefBrowser browser, CefFileDialogMode mode, string title, string defaultFilePath, string[] acceptFilters, CefFileDialogCallback callback) { 16 | if (OwnerWebView.DisableFileDialogs) { 17 | return true; 18 | } 19 | return false; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /WebViewControl/WebView.InternalFocusHandler.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | using Xilium.CefGlue.Common.Handlers; 3 | 4 | namespace WebViewControl { 5 | 6 | partial class WebView { 7 | 8 | private class InternalFocusHandler : FocusHandler { 9 | 10 | private WebView OwnerWebView { get; } 11 | 12 | public InternalFocusHandler(WebView webView) { 13 | OwnerWebView = webView; 14 | } 15 | 16 | protected override void OnGotFocus(CefBrowser browser) { 17 | OwnerWebView.OnGotFocus(); 18 | } 19 | 20 | protected override bool OnSetFocus(CefBrowser browser, CefFocusSource source) { 21 | return OwnerWebView.OnSetFocus(source == CefFocusSource.System); 22 | } 23 | 24 | protected override void OnTakeFocus(CefBrowser browser, bool next) { 25 | OwnerWebView.OnLostFocus(); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /WebViewControl/EditCommands.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | 3 | namespace WebViewControl { 4 | 5 | public class EditCommands { 6 | 7 | private ChromiumBrowser ChromiumBrowser { get; } 8 | 9 | internal EditCommands(ChromiumBrowser chromiumBrowser) { 10 | ChromiumBrowser = chromiumBrowser; 11 | } 12 | 13 | private CefFrame GetFocusedFrame() => ChromiumBrowser.GetBrowser()?.GetFocusedFrame() ?? ChromiumBrowser.GetBrowser()?.GetMainFrame(); 14 | 15 | public void Cut() => GetFocusedFrame()?.Cut(); 16 | 17 | public void Copy() => GetFocusedFrame()?.Copy(); 18 | 19 | public void Paste() => GetFocusedFrame()?.Paste(); 20 | 21 | public void SelectAll() => GetFocusedFrame()?.SelectAll(); 22 | 23 | public void Undo() => GetFocusedFrame()?.Undo(); 24 | 25 | public void Redo() => GetFocusedFrame()?.Redo(); 26 | 27 | public void Delete() => GetFocusedFrame()?.Delete(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /WebViewControl/WebView.InternalKeyboardHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xilium.CefGlue; 3 | using Xilium.CefGlue.Common.Handlers; 4 | 5 | namespace WebViewControl { 6 | 7 | partial class WebView { 8 | 9 | private class InternalKeyboardHandler : KeyboardHandler { 10 | 11 | private WebView OwnerWebView { get; } 12 | 13 | public InternalKeyboardHandler(WebView webView) { 14 | OwnerWebView = webView; 15 | } 16 | 17 | protected override bool OnPreKeyEvent(CefBrowser browser, CefKeyEvent keyEvent, IntPtr os_event, out bool isKeyboardShortcut) { 18 | var handler = OwnerWebView.KeyPressed; 19 | if (handler != null && !browser.IsPopup) { 20 | handler(keyEvent, out var handled); 21 | isKeyboardShortcut = false; 22 | return handled; 23 | } 24 | return base.OnPreKeyEvent(browser, keyEvent, os_event, out isKeyboardShortcut); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WebViewControl/UrlHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace WebViewControl { 6 | 7 | internal static class UrlHelper { 8 | 9 | private const string ChromeInternalProtocol = "devtools:"; 10 | 11 | public const string AboutBlankUrl = "about:blank"; 12 | 13 | public static ResourceUrl DefaultLocalUrl = new ResourceUrl(ResourceUrl.LocalScheme, "index.html"); 14 | 15 | public static bool IsChromeInternalUrl(string url) { 16 | return url != null && url.StartsWith(ChromeInternalProtocol, StringComparison.InvariantCultureIgnoreCase); 17 | } 18 | 19 | public static bool IsInternalUrl(string url) { 20 | return IsChromeInternalUrl(url) || url.StartsWith(DefaultLocalUrl.ToString(), StringComparison.InvariantCultureIgnoreCase); 21 | } 22 | 23 | public static void OpenInExternalBrowser(string url) { 24 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { 25 | Process.Start("explorer", "\"" + url + "\""); 26 | } else { 27 | Process.Start("open", url); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | x64;ARM64 6 | 11.0.10 7 | 120.6099.207 8 | 9 | 10 | 11 | 2.0.0.0 12 | 2.0.0.0 13 | 14 | 3.120.11 15 | OutSystems 16 | WebViewControl 17 | Copyright © OutSystems 2023 18 | https://github.com/OutSystems/WebView 19 | $(MSBuildProjectDirectory)\..\nuget 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -ARM64 29 | .ARM64 30 | 31 | 32 | -------------------------------------------------------------------------------- /WebViewControl/WebViewControl.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(TargetDotnetVersion)-windows 5 | WebViewControl WPF 6 | WebViewControl for WPF powered by CefGlue 7 | 8 | 9 | WebViewControl 10 | Copyright © 2023 11 | true 12 | true 13 | WebViewControl-WPF$(PackageSuffix) 14 | Debug;Release;ReleaseAvalonia;ReleaseWPF;ReleaseAvaloniaRemoteDebugSupport 15 | true 16 | 17 | 18 | 19 | true 20 | 21 | 22 | 23 | 24 | 25 | Designer 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/Tests.WebView/LoadTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NUnit.Framework; 3 | using WebViewControl; 4 | 5 | namespace Tests.WebView { 6 | 7 | public class LoadTests : WebViewTestBase { 8 | 9 | protected override Task AfterInitializeView() => Task.CompletedTask; 10 | 11 | [Test(Description = "Custom schemes are loaded")] 12 | public async Task LoadCustomScheme() { 13 | await Run(async () => { 14 | var embeddedResourceUrl = new ResourceUrl(GetType().Assembly, "Resources", "EmbeddedHtml.html"); 15 | 16 | var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 17 | 18 | void OnNavigated(string url, string frameName) { 19 | if (url != UrlHelper.AboutBlankUrl) { 20 | TargetView.Navigated -= OnNavigated; 21 | taskCompletionSource.SetResult(true); 22 | } 23 | } 24 | TargetView.Navigated += OnNavigated; 25 | TargetView.LoadResource(embeddedResourceUrl); 26 | await taskCompletionSource.Task; 27 | 28 | var content = await TargetView.EvaluateScript("return document.documentElement.innerText"); 29 | Assert.AreEqual("test", content); 30 | }); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /WebViewControl/Request.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Specialized; 2 | using Xilium.CefGlue; 3 | 4 | namespace WebViewControl { 5 | 6 | public class Request { 7 | 8 | private CefRequest CefRequest { get; } 9 | private string UrlOverride { get; } 10 | 11 | internal Request(CefRequest request, string urlOverride) { 12 | CefRequest = request; 13 | UrlOverride = urlOverride; 14 | } 15 | 16 | public string Method { 17 | get { return CefRequest.Method; } 18 | } 19 | 20 | public string Url { 21 | get { return UrlOverride ?? CefRequest.Url; } 22 | } 23 | 24 | public virtual void Cancel() { 25 | Canceled = true; 26 | } 27 | 28 | public bool Canceled { get; private set; } 29 | 30 | internal bool IsMainFrame => CefRequest.ResourceType == CefResourceType.MainFrame; 31 | 32 | public NameValueCollection GetHeaderMap() => 33 | CefRequest.GetHeaderMap(); 34 | 35 | public void SetHeaderMap(NameValueCollection headers) => 36 | CefRequest.SetHeaderMap(headers); 37 | 38 | public string GetHeaderByName(string name) => 39 | CefRequest.GetHeaderByName(name); 40 | 41 | public void SetHeaderByName(string name, string value, bool overwrite) => 42 | CefRequest.SetHeaderByName(name, value, overwrite); 43 | } 44 | } -------------------------------------------------------------------------------- /WebViewControl/WebView.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Xilium.CefGlue; 3 | 4 | namespace WebViewControl { 5 | 6 | internal static class WebViewExtensions { 7 | 8 | public static string[] GetFrameNames(this WebView webview) { 9 | return webview.GetCefBrowser()?.GetFrameNames().Where(n => !webview.IsMainFrame(n)).ToArray() ?? new string[0]; 10 | } 11 | 12 | internal static bool HasFrame(this WebView webview, string name) { 13 | return webview.GetFrame(name) != null; 14 | } 15 | 16 | internal static CefFrame GetFrame(this WebView webview, string frameName) { 17 | return webview.GetCefBrowser()?.GetFrame(frameName ?? ""); 18 | } 19 | 20 | internal static bool IsMainFrame(this WebView webview, string frameName) { 21 | return string.IsNullOrEmpty(frameName); 22 | } 23 | 24 | internal static void SendKeyEvent(this WebView webview, CefKeyEvent keyEvent) { 25 | webview.GetCefBrowser()?.GetHost()?.SendKeyEvent(keyEvent); 26 | } 27 | 28 | internal static void SetAccessibilityState(this WebView webview, CefState state) { 29 | webview.GetCefBrowser()?.GetHost()?.SetAccessibilityState(state); 30 | } 31 | 32 | private static CefBrowser GetCefBrowser(this WebView webview) { 33 | return webview.UnderlyingBrowser.GetBrowser(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /WebViewControl/WebView.InternalDragHandler.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | using Xilium.CefGlue.Common.Handlers; 3 | 4 | namespace WebViewControl { 5 | partial class WebView { 6 | private class InternalDragHandler : DragHandler { 7 | 8 | private WebView OwnerWebView { get; } 9 | 10 | public InternalDragHandler(WebView owner) { 11 | OwnerWebView = owner; 12 | } 13 | 14 | protected override bool OnDragEnter(CefBrowser browser, CefDragData dragData, CefDragOperationsMask mask) { 15 | var filesDragging = OwnerWebView.FilesDragging; 16 | if (filesDragging != null) { 17 | var fileNames = dragData.GetFileNames(); 18 | if (fileNames != null) { 19 | filesDragging(fileNames); 20 | } 21 | } 22 | 23 | var textDragging = OwnerWebView.TextDragging; 24 | if (textDragging != null) { 25 | var textContent = dragData.FragmentText; 26 | if (!string.IsNullOrEmpty(textContent)) { 27 | textDragging(textContent); 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | 34 | protected override void OnDraggableRegionsChanged(CefBrowser browser, CefFrame frame, CefDraggableRegion[] regions) { 35 | 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /WebViewControl/WebView.InternalJsDialogHandler.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | using Xilium.CefGlue.Common.Handlers; 3 | 4 | namespace WebViewControl { 5 | 6 | partial class WebView { 7 | 8 | private class InternalJsDialogHandler : JSDialogHandler { 9 | 10 | private WebView OwnerWebView { get; } 11 | 12 | public InternalJsDialogHandler(WebView webView) { 13 | OwnerWebView = webView; 14 | } 15 | 16 | protected override bool OnJSDialog(CefBrowser browser, string originUrl, CefJSDialogType dialogType, string message_text, string default_prompt_text, CefJSDialogCallback callback, out bool suppress_message) { 17 | suppress_message = false; 18 | 19 | var javacriptDialogShown = OwnerWebView.JavacriptDialogShown; 20 | if (javacriptDialogShown == null) { 21 | return false; 22 | } 23 | 24 | void Close() { 25 | callback.Continue(true, ""); 26 | callback.Dispose(); 27 | } 28 | 29 | javacriptDialogShown.Invoke(message_text, Close); 30 | return true; 31 | } 32 | 33 | protected override bool OnBeforeUnloadDialog(CefBrowser browser, string messageText, bool isReload, CefJSDialogCallback callback) { 34 | return false; // use default 35 | } 36 | 37 | protected override void OnResetDialogState(CefBrowser browser) { } 38 | 39 | protected override void OnDialogClosed(CefBrowser browser) { } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Tests.WebView/IsolateCommonTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using Avalonia.Controls; 5 | using NUnit.Framework; 6 | using WebViewControl; 7 | 8 | namespace Tests.WebView { 9 | 10 | public class IsolateCommonTests : WebViewTestBase { 11 | 12 | protected override Task SetUp() { 13 | return Task.CompletedTask; 14 | } 15 | 16 | protected override Task TearDown() { 17 | return Task.CompletedTask; 18 | } 19 | 20 | /*[Test(Description = "Tests that the webview is disposed when host window is not shown")] 21 | public async Task WebViewIsNotDisposedWhenUnloaded() { 22 | await Run(async () => { 23 | var taskCompletionSource = new TaskCompletionSource(); 24 | var view = new WebViewControl.WebView(); 25 | view.Disposed += delegate { 26 | taskCompletionSource.SetResult(true); 27 | }; 28 | 29 | var window = new Window { 30 | Title = CurrentTestName, 31 | Content = view 32 | }; 33 | 34 | try { 35 | window.Show(); 36 | 37 | window.Content = null; 38 | Assert.IsFalse(taskCompletionSource.Task.IsCompleted); 39 | 40 | window.Content = view; 41 | } finally { 42 | window.Close(); 43 | } 44 | var disposed = await taskCompletionSource.Task; 45 | Assert.IsTrue(disposed); 46 | }); 47 | }*/ 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Tests.WebView/SerializationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NUnit.Framework; 3 | using WebViewControl; 4 | 5 | namespace Tests.WebView { 6 | 7 | public class SerializationTests { 8 | 9 | private enum SerializationEnum { 10 | Value0, 11 | Value1 12 | } 13 | 14 | [Test(Description = "Tests that javascript data serialization works as expected")] 15 | public void JavascriptDataSerialization() { 16 | Assert.AreEqual("true", JavascriptSerializer.Serialize(true)); 17 | Assert.AreEqual("false", JavascriptSerializer.Serialize(false)); 18 | Assert.AreEqual("undefined", JavascriptSerializer.Serialize(JavascriptSerializer.Undefined)); 19 | 20 | Assert.AreEqual("1", JavascriptSerializer.Serialize(1)); 21 | Assert.AreEqual("-1", JavascriptSerializer.Serialize(-1)); 22 | Assert.AreEqual("1.1", JavascriptSerializer.Serialize(1.1)); 23 | Assert.AreEqual("-1.1", JavascriptSerializer.Serialize(-1.1)); 24 | 25 | Assert.AreEqual("\"hello \\\"world\\\"\"", JavascriptSerializer.Serialize("hello \"world\"")); 26 | 27 | Assert.AreEqual("1", JavascriptSerializer.Serialize(SerializationEnum.Value1)); 28 | 29 | Assert.AreEqual("[1,2,3]", JavascriptSerializer.Serialize(new[] { 1, 2, 3 })); 30 | 31 | Assert.AreEqual("{\"prop-a\":\"value-a\",\"prop-b\":\"value-b\",\"prop-c\":1.1,\"prop-d\":undefined}", 32 | JavascriptSerializer.Serialize(new Dictionary() { { "prop-b", "value-b" }, { "prop-a", "value-a" }, { "prop-c", 1.1 }, { "prop-d", JavascriptSerializer.Undefined } })); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /WebViewControl/WebView.InternalDownloadHandler.cs: -------------------------------------------------------------------------------- 1 | using Xilium.CefGlue; 2 | using Xilium.CefGlue.Common.Handlers; 3 | 4 | namespace WebViewControl { 5 | 6 | partial class WebView { 7 | 8 | private class InternalDownloadHandler : DownloadHandler { 9 | 10 | private WebView OwnerWebView { get; } 11 | 12 | public InternalDownloadHandler(WebView owner) { 13 | OwnerWebView = owner; 14 | } 15 | 16 | protected override void OnBeforeDownload(CefBrowser browser, CefDownloadItem downloadItem, string suggestedName, CefBeforeDownloadCallback callback) { 17 | callback.Continue(downloadItem.SuggestedFileName, showDialog: true); 18 | } 19 | 20 | protected override void OnDownloadUpdated(CefBrowser browser, CefDownloadItem downloadItem, CefDownloadItemCallback callback) { 21 | if (downloadItem.IsComplete) { 22 | if (OwnerWebView.DownloadCompleted != null) { 23 | OwnerWebView.AsyncExecuteInUI(() => OwnerWebView.DownloadCompleted?.Invoke(downloadItem.FullPath)); 24 | } 25 | } else if (downloadItem.IsCanceled) { 26 | if (OwnerWebView.DownloadCancelled != null) { 27 | OwnerWebView.AsyncExecuteInUI(() => OwnerWebView.DownloadCancelled?.Invoke(downloadItem.FullPath)); 28 | } 29 | } else { 30 | if (OwnerWebView.DownloadProgressChanged != null) { 31 | OwnerWebView.AsyncExecuteInUI(() => OwnerWebView.DownloadProgressChanged?.Invoke(downloadItem.FullPath, downloadItem.ReceivedBytes, downloadItem.TotalBytes)); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Tests.WebView/Tests.WebView.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(TargetDotnetVersion) 5 | $(MSBuildProjectDirectory)\bin\ 6 | true 7 | $(Platform) 8 | false 9 | Debug;Release;ReleaseAvalonia;ReleaseWPF;ReleaseAvaloniaRemoteDebugSupport 10 | win-x64;win-arm64 11 | Major 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | MSBuild:Compile 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 13 | 14 | $(PrivateAssets);compile 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 | -------------------------------------------------------------------------------- /WebViewControl/WebView.JavascriptException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Xilium.CefGlue.Common.Events; 5 | 6 | namespace WebViewControl { 7 | 8 | partial class WebView { 9 | 10 | public class JavascriptException : Exception { 11 | 12 | private JavascriptStackFrame[] JsStack { get; } 13 | private string InnerStack { get; } 14 | 15 | internal JavascriptException(string message, IEnumerable stack = null, string innerStackTrace = null) 16 | : base(message, null) { 17 | JsStack = stack?.ToArray() ?? new JavascriptStackFrame[0]; 18 | InnerStack = innerStackTrace; 19 | } 20 | 21 | internal JavascriptException(string name, string message, IEnumerable stack = null, string baseStackTrace = null) 22 | : this((string.IsNullOrEmpty(name) ? "" : name + ": ") + message, stack, baseStackTrace) { 23 | } 24 | 25 | public override string StackTrace { 26 | get { return string.Join(Environment.NewLine, JsStack.Select(FormatStackFrame).Concat(new[] { BaseStackTrace })); } 27 | } 28 | 29 | private string BaseStackTrace => (InnerStack != null ? InnerStack + Environment.NewLine : "") + base.StackTrace; 30 | 31 | private static string FormatStackFrame(JavascriptStackFrame frame) { 32 | var functionName = string.IsNullOrEmpty(frame.FunctionName) ? "" : frame.FunctionName; 33 | var location = string.IsNullOrEmpty(frame.ScriptNameOrSourceUrl) ? "" : ($" in {frame.ScriptNameOrSourceUrl}:line {frame.LineNumber} {frame.Column}"); 34 | return $" at {functionName}{location}"; 35 | } 36 | 37 | public override string ToString() { 38 | return GetType().FullName + ": " + Message + Environment.NewLine + StackTrace; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /WebViewControl/WebView.InternalLifeSpanHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Xilium.CefGlue; 4 | using Xilium.CefGlue.Common.Handlers; 5 | 6 | namespace WebViewControl { 7 | 8 | partial class WebView { 9 | 10 | private class InternalLifeSpanHandler : LifeSpanHandler { 11 | 12 | private WebView OwnerWebView { get; } 13 | 14 | public InternalLifeSpanHandler(WebView webView) { 15 | OwnerWebView = webView; 16 | } 17 | 18 | protected override bool OnBeforePopup(CefBrowser browser, CefFrame frame, string targetUrl, string targetFrameName, CefWindowOpenDisposition targetDisposition, bool userGesture, CefPopupFeatures popupFeatures, CefWindowInfo windowInfo, ref CefClient client, CefBrowserSettings settings, ref CefDictionaryValue extraInfo, ref bool noJavascriptAccess) { 19 | if (UrlHelper.IsChromeInternalUrl(targetUrl)) { 20 | return false; 21 | } 22 | 23 | if (Uri.IsWellFormedUriString(targetUrl, UriKind.RelativeOrAbsolute)) { 24 | var uri = new Uri(targetUrl); 25 | if (!uri.IsAbsoluteUri) { 26 | // turning relative urls into full path to avoid that someone runs custom command lines 27 | targetUrl = new Uri(new Uri(frame.Url), uri).AbsoluteUri; 28 | } 29 | } else { 30 | return false; // if the url is not well formed let's use the browser to handle the things 31 | } 32 | 33 | try { 34 | var popupOpening = OwnerWebView.PopupOpening; 35 | if (popupOpening != null) { 36 | popupOpening(targetUrl); 37 | } else { 38 | UrlHelper.OpenInExternalBrowser(targetUrl); 39 | } 40 | } catch { 41 | // if we can't handle the command line let's continue the normal request with the popup 42 | // with this, will not blow in the users face 43 | return false; 44 | } 45 | 46 | return true; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SampleWebView.Avalonia/SampleWebView.Avalonia.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | $(TargetDotnetVersion) 6 | LatestMajor 7 | Debug;Release;ReleaseAvalonia;ReleaseWPF;ReleaseAvaloniaRemoteDebugSupport 8 | osx-x64;win-x64;osx-arm64;win-arm64 9 | false 10 | app.manifest 11 | 12 | 13 | 14 | SampleWebView.Avalonia 15 | SampleWebView 16 | com.example 17 | 1.0.0 18 | AAPL 19 | 4242 20 | DemoAvalonia 21 | SampleWebView.Avalonia 22 | AppName.icns 23 | NSApplication 24 | true 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | MSBuild:Compile 36 | 37 | 38 | MSBuild:Compile 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/Tests.WebView/CommonTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Avalonia.Controls; 3 | using NUnit.Framework; 4 | using WebViewControl; 5 | 6 | namespace Tests.WebView { 7 | 8 | public class CommonTests : WebViewTestBase { 9 | 10 | [Test(Description = "Before navigate hook is called")] 11 | public async Task BeforeNavigateHookCalled() { 12 | await Run(async () => { 13 | var taskCompletionSource = new TaskCompletionSource(); 14 | TargetView.BeforeNavigate += (request) => { 15 | taskCompletionSource.SetResult(true); 16 | request.Cancel(); 17 | }; 18 | TargetView.Address = "https://www.google.com"; 19 | var beforeNavigateCalled = await taskCompletionSource.Task; 20 | Assert.IsTrue(beforeNavigateCalled, "BeforeNavigate hook was not called!"); 21 | }); 22 | } 23 | 24 | [Test(Description = "Javascript evaluation on navigated event does not block")] 25 | public async Task JavascriptEvaluationOnNavigatedDoesNotBlock() { 26 | await Run(async () => { 27 | var taskCompletionSource = new TaskCompletionSource(); 28 | TargetView.Navigated += delegate { 29 | TargetView.EvaluateScript("1+1"); 30 | taskCompletionSource.SetResult(true); 31 | }; 32 | await Load(""); 33 | var navigatedCalled = await taskCompletionSource.Task; 34 | Assert.IsTrue(navigatedCalled, "JS evaluation on navigated event is blocked!"); 35 | }); 36 | } 37 | 38 | [Test(Description = "Tests that the webview is disposed when host window is not shown")] 39 | public async Task WebViewIsDisposedWhenHostWindowIsNotShown() { 40 | await Run(async () => { 41 | var taskCompletionSource = new TaskCompletionSource(); 42 | var view = new WebViewControl.WebView(); 43 | view.Disposed += delegate { 44 | taskCompletionSource.SetResult(true); 45 | }; 46 | 47 | var window = new Window { Title = CurrentTestName }; 48 | 49 | try { 50 | window.Content = view; 51 | window.Close(); 52 | 53 | var disposed = await taskCompletionSource.Task; 54 | Assert.IsTrue(disposed); 55 | } finally { 56 | window.Close(); 57 | } 58 | }); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /WebViewControl/HttpResourceHandler.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Xilium.CefGlue; 5 | using Xilium.CefGlue.Common.Handlers; 6 | 7 | namespace WebViewControl { 8 | 9 | internal class HttpResourceHandler : DefaultResourceHandler { 10 | 11 | private const string AccessControlAllowOriginHeaderKey = "Access-Control-Allow-Origin"; 12 | 13 | internal static readonly CefResourceType[] AcceptedResources = new CefResourceType[] { 14 | // These resources types need an "Access-Control-Allow-Origin" header response entry 15 | // to comply with CORS security restrictions. 16 | CefResourceType.SubFrame, 17 | CefResourceType.FontResource, 18 | CefResourceType.Stylesheet 19 | }; 20 | 21 | protected override RequestHandlingFashion ProcessRequestAsync(CefRequest request, CefCallback callback) { 22 | Task.Run(async () => { 23 | try { 24 | var httpRequest = WebRequest.CreateHttp(request.Url); 25 | var headers = request.GetHeaderMap(); 26 | foreach (var key in headers.AllKeys) { 27 | httpRequest.Headers.Add(key, headers[key]); 28 | } 29 | 30 | var response = (HttpWebResponse) await httpRequest.GetResponseAsync(); 31 | Response = response.GetResponseStream(); 32 | Headers = response.Headers; 33 | 34 | MimeType = response.ContentType; 35 | Status = (int) response.StatusCode; 36 | StatusText = response.StatusDescription; 37 | 38 | // we have to smash any existing value here 39 | Headers.Remove(AccessControlAllowOriginHeaderKey); 40 | Headers.Add(AccessControlAllowOriginHeaderKey, "*"); 41 | 42 | } catch { 43 | // we should catch exceptions.. network errors cannot crash the app 44 | } finally { 45 | callback.Continue(); 46 | } 47 | 48 | }); 49 | return RequestHandlingFashion.ContinueAsync; 50 | } 51 | 52 | protected override bool Read(Stream outResponse, int bytesToRead, out int bytesRead, CefResourceReadCallback callback) { 53 | var buffer = new byte[bytesToRead]; 54 | bytesRead = Response?.Read(buffer, 0, buffer.Length) ?? 0; 55 | 56 | if (bytesRead == 0) { 57 | return false; 58 | } 59 | 60 | outResponse.Write(buffer, 0, bytesRead); 61 | return bytesRead > 0; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /WebViewControl/WebView.ResourceHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Xilium.CefGlue; 5 | 6 | namespace WebViewControl { 7 | 8 | public sealed class ResourceHandler : Request { 9 | 10 | private bool isAsync; 11 | 12 | private readonly object syncRoot = new object(); 13 | 14 | internal ResourceHandler(CefRequest request, string urlOverride) 15 | : base(request, urlOverride) { 16 | } 17 | 18 | internal AsyncResourceHandler Handler { get; private set; } 19 | 20 | public bool Handled { get; private set; } 21 | 22 | public Stream Response => Handler?.Response; 23 | 24 | private AsyncResourceHandler GetOrCreateCefResourceHandler() { 25 | if (Handler != null) { 26 | return Handler; 27 | } 28 | 29 | lock (syncRoot) { 30 | if (Handler != null) { 31 | return Handler; 32 | } 33 | 34 | var handler = new AsyncResourceHandler(); 35 | handler.Headers.Add("cache-control", "public, max-age=315360000"); 36 | Handler = handler; 37 | return handler; 38 | } 39 | } 40 | 41 | public void BeginAsyncResponse(Action handleResponse) { 42 | isAsync = true; 43 | var handler = GetOrCreateCefResourceHandler(); 44 | Task.Run(() => { 45 | handleResponse(); 46 | handler.Continue(); 47 | }); 48 | } 49 | 50 | private void Continue() { 51 | var handler = Handler; 52 | Handled = handler != null && (handler.Response != null || !string.IsNullOrEmpty(handler.RedirectUrl)); 53 | if (isAsync || handler == null) { 54 | return; 55 | } 56 | handler.Continue(); 57 | } 58 | 59 | public void RespondWith(string filename) { 60 | var fileStream = File.OpenRead(filename); 61 | GetOrCreateCefResourceHandler().SetResponse(fileStream, ResourcesManager.GetMimeType(filename), autoDisposeStream: true); 62 | Continue(); 63 | } 64 | 65 | public void RespondWithText(string text) { 66 | GetOrCreateCefResourceHandler().SetResponse(text); 67 | Continue(); 68 | } 69 | 70 | public void RespondWith(Stream stream, string extension = null) { 71 | GetOrCreateCefResourceHandler().SetResponse(stream, ResourcesManager.GetExtensionMimeType(extension)); 72 | Continue(); 73 | } 74 | 75 | public void Redirect(string url) { 76 | GetOrCreateCefResourceHandler().RedirectTo(url); 77 | Continue(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /WebViewControl/JavascriptSerializationHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Web; 7 | using JavascriptObject = System.Collections.Generic.IEnumerable>; 8 | using SerializationHandler = System.Func; 9 | 10 | namespace WebViewControl { 11 | 12 | public static class JavascriptSerializer { 13 | 14 | public static object Undefined { get; } = new object(); 15 | 16 | public static string Serialize(object o, SerializationHandler handleComplexType = null) { 17 | if (o == null) return "null"; 18 | if (o == Undefined) return "undefined"; 19 | if (o is string str) return Serialize(str); 20 | if (o is IEnumerable col) return Serialize(col); 21 | if (o is JavascriptObject jso) return Serialize(jso); 22 | 23 | var type = o.GetType(); 24 | 25 | if (type.IsPrimitive) return Convert.ToString(o, CultureInfo.InvariantCulture).ToLowerInvariant(); // ints, bools, ... but not structs 26 | if (type.IsEnum) return ((int) o).ToString(); 27 | if (handleComplexType != null) return handleComplexType(o); 28 | return SerializeComplexType(o); 29 | } 30 | 31 | public static string Serialize(JavascriptObject o, SerializationHandler handleComplexType = null) { 32 | // order members to create a stable serialization 33 | return "{" + string.Join(",", o.OrderBy(kvp => kvp.Key).Select(kvp => Serialize(kvp.Key) + ":" + Serialize(kvp.Value, handleComplexType))) + "}"; 34 | } 35 | 36 | public static string Serialize(string str) { 37 | return str == null ? "null" : HttpUtility.JavaScriptStringEncode(str, true); 38 | } 39 | 40 | public static string Serialize(bool boolean) { 41 | return boolean.ToString().ToLowerInvariant(); 42 | } 43 | 44 | public static string Serialize(IEnumerable arr, SerializationHandler handleComplexType = null) { 45 | return "[" + string.Join(",", arr.Cast().Select(i => Serialize(i, handleComplexType))) + "]"; 46 | } 47 | 48 | private static string SerializeComplexType(object obj) { 49 | var fields = obj.GetType().GetProperties().Where(p => p.PropertyType.IsPrimitive || p.PropertyType == typeof(string)); 50 | return Serialize(fields.Select(f => new KeyValuePair(f.Name, f.GetValue(obj, null)))); 51 | } 52 | 53 | internal static string GetJavascriptName(string str) { 54 | if (string.IsNullOrEmpty(str)) { 55 | return string.Empty; 56 | } 57 | 58 | return char.ToLowerInvariant(str[0]) + str.Substring(1); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebView 2 | [![WebViewControl-Avalonia](https://img.shields.io/nuget/v/WebViewControl-Avalonia.svg?style=flat&label=WebView-Avalonia)](https://www.nuget.org/packages/WebViewControl-Avalonia/) 3 | [![WebViewControl-WPF](https://img.shields.io/nuget/v/WebViewControl-WPF.svg?style=flat&label=WebView-WPF)](https://www.nuget.org/packages/WebViewControl-WPF/) 4 | 5 | Avalonia/WPF control that wraps CefGlue webview control 6 | 7 | ![Screenshot](./SampleWebView.Avalonia/screenshot.png) 8 | 9 | WebView lets you embed Chromium in .NET apps. It is a .NET wrapper control around [CefGlue](https://github.com/OutSystems/CefGlue) and provides a better and simple API. Likewise CefGlue it can be used from C# or any other CLR language and provides both Avalonia and WPF web browser control implementations. 10 | 11 | Here's a table for supported architectures, frameworks and operating systems: 12 | 13 | | OS | x64 | ARM64 | WPF | Avalonia | 14 | |---------|-----|-------|-----|----------| 15 | | Windows | ✔️ | ✔️ | ✔️ | ✔️ | 16 | | macOS | ✔️ | ✔️ | ❌ | ✔️ | 17 | | Linux | ✔️ | 🔘 | ❌ | ✔️ | 18 | 19 | ✔️ Supported | ❌ Not supported | 🔘 Works with issues. 20 | 21 | See [LINUX.md](https://github.com/OutSystems/CefGlue/blob/main/LINUX.md) for more information about issues and tested distribution list. 22 | Currently only x64 and ARM64 architectures are supported. 23 | 24 | It also provides the following additional features: 25 | - Strongly-typed javascript evaluation: results of javascript evaluation returns the appropriate type 26 | - Scripts are aggregated and executed in bulk for improved performance 27 | - Ability to evaluate javascript synchronously 28 | - Javascript error handling with call stack information 29 | - Events to intercept and respond to resources loading 30 | - Events to track file download progress 31 | - Ability to load embedded resources using custom protocols 32 | - Ability to disable history navigation 33 | - Error handling 34 | - Proxy configuration support 35 | - Option to run in [offscreen rendering mode](https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage#markdown-header-off-screen-rendering) (not recommended as it has several issues) 36 | 37 | ## Releases 38 | Stable binaries are released on NuGet, and contain everything you need to embed Chromium in your .NET/CLR application. 39 | 40 | ## Documentation 41 | See the [Sample](SampleWebView.Avalonia) project for example web browsers built with WebView. It demos some of the available features. 42 | 43 | ## Other 44 | - [Avalonia FuncUI Support](https://github.com/WhiteBlackGoose/MoreFuncUI#morefuncuiwebview). 45 | 46 | ## Versioning 47 | The versioning system works as follows: 48 | 49 | `..` 50 | 51 | Whenever you fix a bug, please increase the patch version. \ 52 | Whenever you bring a new feature, please increase the major version. \ 53 | Use the minor version for the current cef version. 54 | 55 | ## TODO 56 | - Improve documentation 57 | -------------------------------------------------------------------------------- /WebViewControl/WebView.InternalResourceRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xilium.CefGlue; 3 | 4 | namespace WebViewControl { 5 | 6 | partial class WebView { 7 | 8 | private class InternalResourceRequestHandler : CefResourceRequestHandler { 9 | 10 | public InternalResourceRequestHandler(WebView ownerWebView) { 11 | OwnerWebView = ownerWebView; 12 | } 13 | 14 | private WebView OwnerWebView { get; } 15 | 16 | protected override CefCookieAccessFilter GetCookieAccessFilter(CefBrowser browser, CefFrame frame, CefRequest request) { 17 | return null; 18 | } 19 | 20 | protected override CefResourceHandler GetResourceHandler(CefBrowser browser, CefFrame frame, CefRequest request) { 21 | if (request.Url == OwnerWebView.DefaultLocalUrl) { 22 | return AsyncResourceHandler.FromText(OwnerWebView.htmlToLoad ?? ""); 23 | } 24 | 25 | if (UrlHelper.IsChromeInternalUrl(request.Url)) { 26 | return null; 27 | } 28 | 29 | var resourceHandler = new ResourceHandler(request, OwnerWebView.GetRequestUrl(request.Url, (ResourceType)request.ResourceType)); 30 | 31 | void TriggerBeforeResourceLoadEvent() { 32 | var beforeResourceLoad = OwnerWebView.BeforeResourceLoad; 33 | if (beforeResourceLoad != null) { 34 | OwnerWebView.ExecuteWithAsyncErrorHandling(() => beforeResourceLoad(resourceHandler)); 35 | } 36 | } 37 | 38 | if (Uri.TryCreate(resourceHandler.Url, UriKind.Absolute, out var url) && url.Scheme == ResourceUrl.EmbeddedScheme) { 39 | resourceHandler.BeginAsyncResponse(() => { 40 | var urlWithoutQuery = new UriBuilder(url); 41 | if (!string.IsNullOrEmpty(url.Query)) { 42 | urlWithoutQuery.Query = string.Empty; 43 | } 44 | 45 | OwnerWebView.ExecuteWithAsyncErrorHandling(() => resourceHandler.LoadEmbeddedResource(urlWithoutQuery.Uri)); 46 | 47 | TriggerBeforeResourceLoadEvent(); 48 | 49 | if (resourceHandler.Handled || OwnerWebView.IgnoreMissingResources) { 50 | return; 51 | } 52 | 53 | var resourceLoadFailed = OwnerWebView.ResourceLoadFailed; 54 | if (resourceLoadFailed != null) { 55 | resourceLoadFailed(url.ToString()); 56 | } else { 57 | OwnerWebView.ForwardUnhandledAsyncException(new InvalidOperationException("Resource not found: " + url)); 58 | } 59 | }); 60 | } else { 61 | TriggerBeforeResourceLoadEvent(); 62 | } 63 | 64 | return resourceHandler.Handler; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /WebViewControl/AsyncResourceHandler.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | using Xilium.CefGlue; 4 | using Xilium.CefGlue.Common.Handlers; 5 | 6 | namespace WebViewControl { 7 | 8 | internal class AsyncResourceHandler : DefaultResourceHandler { 9 | 10 | private CefCallback responseCallback; 11 | private bool autoDisposeStream; 12 | private object SyncRoot { get; } = new object(); 13 | 14 | protected override RequestHandlingFashion ProcessRequestAsync(CefRequest request, CefCallback callback) { 15 | lock (SyncRoot) { 16 | if (Response == null && string.IsNullOrEmpty(RedirectUrl)) { 17 | responseCallback = callback; 18 | return RequestHandlingFashion.ContinueAsync; 19 | } 20 | return RequestHandlingFashion.Continue; 21 | } 22 | } 23 | 24 | public void SetResponse(string response, string mimeType = null) { 25 | var responseStream = GetMemoryStream(response, Encoding.UTF8, includePreamble: true); 26 | SetResponse(responseStream, mimeType, autoDisposeStream); 27 | } 28 | 29 | public void SetResponse(Stream response, string mimeType = null, bool autoDisposeStream = false) { 30 | lock (SyncRoot) { 31 | Response = response; 32 | MimeType = mimeType; 33 | this.autoDisposeStream = autoDisposeStream; 34 | } 35 | } 36 | 37 | public void RedirectTo(string targetUrl) { 38 | lock (SyncRoot) { 39 | RedirectUrl = targetUrl; 40 | } 41 | } 42 | 43 | public void Continue() { 44 | lock (SyncRoot) { 45 | if (responseCallback != null) { 46 | using (responseCallback) { 47 | responseCallback.Continue(); 48 | } 49 | } 50 | } 51 | } 52 | 53 | public static DefaultResourceHandler FromText(string text) { 54 | var handler = new AsyncResourceHandler(); 55 | handler.SetResponse(text); 56 | return handler; 57 | } 58 | 59 | private static MemoryStream GetMemoryStream(string text, Encoding encoding, bool includePreamble = true) { 60 | if (includePreamble) { 61 | var preamble = encoding.GetPreamble(); 62 | var bytes = encoding.GetBytes(text); 63 | 64 | var memoryStream = new MemoryStream(preamble.Length + bytes.Length); 65 | 66 | memoryStream.Write(preamble, 0, preamble.Length); 67 | memoryStream.Write(bytes, 0, bytes.Length); 68 | 69 | memoryStream.Position = 0; 70 | 71 | return memoryStream; 72 | } 73 | 74 | return new MemoryStream(encoding.GetBytes(text)); 75 | } 76 | 77 | protected override void Dispose(bool disposing) { 78 | base.Dispose(disposing); 79 | if (autoDisposeStream) { 80 | var response = Response; 81 | if (response != null) { 82 | lock (SyncRoot) { 83 | response.Dispose(); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /WebViewControl/WebViewLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Runtime.CompilerServices; 7 | using Xilium.CefGlue; 8 | using Xilium.CefGlue.Common; 9 | using Xilium.CefGlue.Common.Shared; 10 | 11 | namespace WebViewControl { 12 | 13 | internal static class WebViewLoader { 14 | 15 | private static string[] CustomSchemes { get; } = new[] { 16 | ResourceUrl.LocalScheme, 17 | ResourceUrl.EmbeddedScheme, 18 | ResourceUrl.CustomScheme, 19 | Uri.UriSchemeHttp, 20 | Uri.UriSchemeHttps 21 | }; 22 | 23 | private static GlobalSettings globalSettings; 24 | 25 | [MethodImpl(MethodImplOptions.NoInlining)] 26 | public static void Initialize(GlobalSettings settings) { 27 | if (CefRuntimeLoader.IsLoaded) { 28 | return; 29 | } 30 | 31 | globalSettings = settings; 32 | 33 | var cefSettings = new CefSettings { 34 | LogSeverity = string.IsNullOrWhiteSpace(settings.LogFile) ? CefLogSeverity.Disable : (settings.EnableErrorLogOnly ? CefLogSeverity.Error : CefLogSeverity.Verbose), 35 | LogFile = settings.LogFile, 36 | UncaughtExceptionStackSize = 100, // enable stack capture 37 | CachePath = settings.CachePath, // enable cache for external resources to speedup loading 38 | WindowlessRenderingEnabled = settings.OsrEnabled, 39 | RemoteDebuggingPort = settings.GetRemoteDebuggingPort(), 40 | UserAgent = settings.UserAgent, 41 | BackgroundColor = new CefColor((uint)settings.BackgroundColor.ToArgb()) 42 | }; 43 | 44 | var customSchemes = CustomSchemes.Select(s => new CustomScheme() { 45 | SchemeName = s, 46 | SchemeHandlerFactory = new SchemeHandlerFactory() 47 | }).ToArray(); 48 | 49 | settings.AddCommandLineSwitch("enable-experimental-web-platform-features", null); 50 | 51 | if (settings.EnableVideoAutoplay) { 52 | settings.AddCommandLineSwitch("autoplay-policy", "no-user-gesture-required"); 53 | } 54 | 55 | CefRuntimeLoader.Initialize(settings: cefSettings, flags: settings.CommandLineSwitches.ToArray(), customSchemes: customSchemes); 56 | 57 | AppDomain.CurrentDomain.ProcessExit += delegate { Cleanup(); }; 58 | } 59 | 60 | /// 61 | /// Release all resources and shutdown web view 62 | /// 63 | [DebuggerNonUserCode] 64 | public static void Cleanup() { 65 | CefRuntime.Shutdown(); // must shutdown cef to free cache files (so that cleanup is able to delete files) 66 | 67 | if (globalSettings.PersistCache) { 68 | return; 69 | } 70 | 71 | try { 72 | var dirInfo = new DirectoryInfo(globalSettings.CachePath); 73 | if (dirInfo.Exists) { 74 | dirInfo.Delete(true); 75 | } 76 | } catch (UnauthorizedAccessException) { 77 | // ignore 78 | } catch (IOException) { 79 | // ignore 80 | } 81 | } 82 | 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /SampleWebView.Avalonia/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Reactive; 3 | using ReactiveUI; 4 | using WebViewControl; 5 | 6 | namespace SampleWebView.Avalonia { 7 | class MainWindowViewModel : ReactiveObject { 8 | 9 | private string address; 10 | private string currentAddress; 11 | 12 | public MainWindowViewModel(WebView webview) { 13 | Address = CurrentAddress = "http://www.google.com/"; 14 | 15 | NavigateCommand = ReactiveCommand.Create(() => { 16 | CurrentAddress = Address; 17 | }); 18 | 19 | ShowDevToolsCommand = ReactiveCommand.Create(() => { 20 | webview.ShowDeveloperTools(); 21 | }); 22 | 23 | CutCommand = ReactiveCommand.Create(() => { 24 | webview.EditCommands.Cut(); 25 | }); 26 | 27 | CopyCommand = ReactiveCommand.Create(() => { 28 | webview.EditCommands.Copy(); 29 | }); 30 | 31 | PasteCommand = ReactiveCommand.Create(() => { 32 | webview.EditCommands.Paste(); 33 | }); 34 | 35 | UndoCommand = ReactiveCommand.Create(() => { 36 | webview.EditCommands.Undo(); 37 | }); 38 | 39 | RedoCommand = ReactiveCommand.Create(() => { 40 | webview.EditCommands.Redo(); 41 | }); 42 | 43 | SelectAllCommand = ReactiveCommand.Create(() => { 44 | webview.EditCommands.SelectAll(); 45 | }); 46 | 47 | DeleteCommand = ReactiveCommand.Create(() => { 48 | webview.EditCommands.Delete(); 49 | }); 50 | 51 | BackCommand = ReactiveCommand.Create(() => { 52 | webview.GoBack(); 53 | }); 54 | 55 | ForwardCommand = ReactiveCommand.Create(() => { 56 | webview.GoForward(); 57 | }); 58 | 59 | PropertyChanged += OnPropertyChanged; 60 | } 61 | 62 | private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { 63 | if (e.PropertyName == nameof(CurrentAddress)) { 64 | Address = CurrentAddress; 65 | } 66 | } 67 | 68 | public string Address { 69 | get => address; 70 | set => this.RaiseAndSetIfChanged(ref address, value); 71 | } 72 | 73 | public string CurrentAddress { 74 | get => currentAddress; 75 | set => this.RaiseAndSetIfChanged(ref currentAddress, value); 76 | } 77 | 78 | public ReactiveCommand NavigateCommand { get; } 79 | 80 | public ReactiveCommand ShowDevToolsCommand { get; } 81 | 82 | public ReactiveCommand CutCommand { get; } 83 | 84 | public ReactiveCommand CopyCommand { get; } 85 | 86 | public ReactiveCommand PasteCommand { get; } 87 | 88 | public ReactiveCommand UndoCommand { get; } 89 | 90 | public ReactiveCommand RedoCommand { get; } 91 | 92 | public ReactiveCommand SelectAllCommand { get; } 93 | 94 | public ReactiveCommand DeleteCommand { get; } 95 | 96 | public ReactiveCommand BackCommand { get; } 97 | 98 | public ReactiveCommand ForwardCommand { get; } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /SampleWebView.Avalonia/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 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 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /tests/Tests.WebView/WebViewTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using NUnit.Framework; 6 | using WebViewControl; 7 | 8 | namespace Tests.WebView { 9 | 10 | public class WebViewTestBase : TestBase { 11 | private readonly List registeredJavascriptObjects = new(); 12 | 13 | protected override void InitializeView() { 14 | if (TargetView != null) { 15 | TargetView.UnhandledAsyncException += OnUnhandledAsyncException; 16 | } 17 | base.InitializeView(); 18 | } 19 | 20 | protected override async Task AfterInitializeView() { 21 | await base.AfterInitializeView(); 22 | 23 | var taskCompletionSource = new TaskCompletionSource(); 24 | TargetView.WebViewInitialized += () => { 25 | taskCompletionSource.SetResult(true); 26 | }; 27 | 28 | await taskCompletionSource.Task; 29 | await Load("Test page"); 30 | } 31 | 32 | protected override Task TearDown() { 33 | registeredJavascriptObjects.ForEach(objName => TargetView.UnregisterJavascriptObject(objName)); 34 | registeredJavascriptObjects.Clear(); 35 | 36 | return base.TearDown(); 37 | } 38 | 39 | protected Task Load(string html) { 40 | var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 41 | 42 | void OnNavigated(string url, string frameName) { 43 | if (url != UrlHelper.AboutBlankUrl) { 44 | TargetView.Navigated -= OnNavigated; 45 | taskCompletionSource.SetResult(true); 46 | } 47 | } 48 | TargetView.Navigated += OnNavigated; 49 | TargetView.LoadHtml(html); 50 | return taskCompletionSource.Task; 51 | } 52 | 53 | protected async Task WithUnhandledExceptionHandling(AsyncTestDelegate action, Func onException) { 54 | void OnUnhandledException(UnhandledAsyncExceptionEventArgs e) { 55 | e.Handled = onException(e.Exception); 56 | } 57 | 58 | var failOnAsyncExceptions = FailOnAsyncExceptions; 59 | FailOnAsyncExceptions = false; 60 | TargetView.UnhandledAsyncException += OnUnhandledException; 61 | 62 | try { 63 | await action(); 64 | } finally { 65 | TargetView.UnhandledAsyncException -= OnUnhandledException; 66 | FailOnAsyncExceptions = failOnAsyncExceptions; 67 | } 68 | } 69 | 70 | protected override bool ShowDebugConsole() { 71 | if (TargetView.IsDisposing) { 72 | return false; 73 | } 74 | TargetView.ShowDeveloperTools(); 75 | return true; 76 | } 77 | 78 | protected void RegisterJavascriptObject(string name, object objectToBind, Func, object> interceptCall = null) { 79 | registeredJavascriptObjects.Add(name); 80 | TargetView.RegisterJavascriptObject(name, objectToBind, interceptCall); 81 | } 82 | 83 | protected Task RunScript(string script) { 84 | var boundChecks = registeredJavascriptObjects.Select(name => $"cefglue.checkObjectBound('{name}')"); 85 | var html = $""; 86 | 87 | return Load(html); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /WebViewControl.Avalonia/WebView.Avalonia.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ExceptionServices; 3 | using Avalonia; 4 | using Avalonia.Controls; 5 | using Avalonia.Data; 6 | using Avalonia.Input; 7 | using Avalonia.Threading; 8 | 9 | namespace WebViewControl { 10 | 11 | partial class WebView : BaseControl { 12 | 13 | private bool IsInDesignMode => false; 14 | 15 | public static readonly StyledProperty AddressProperty = 16 | AvaloniaProperty.Register(nameof(Address), defaultBindingMode: BindingMode.TwoWay); 17 | 18 | public string Address { 19 | get => GetValue(AddressProperty); 20 | set => SetValue(AddressProperty, value); 21 | } 22 | 23 | protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { 24 | base.OnPropertyChanged(change); 25 | 26 | if (change.Property == AddressProperty) { 27 | InternalAddress = Address; 28 | } 29 | } 30 | 31 | partial void ExtraInitialize() { 32 | VisualChildren.Add(chromium); 33 | chromium[!FocusableProperty] = this[!FocusableProperty]; 34 | chromium.AddressChanged += (o, address) => ExecuteInUI(() => Address = address); 35 | } 36 | 37 | protected override void OnKeyDown(KeyEventArgs e) { 38 | if (AllowDeveloperTools && e.Key == Key.F12) { 39 | ToggleDeveloperTools(); 40 | e.Handled = true; 41 | } 42 | } 43 | 44 | protected override void OnGotFocus(GotFocusEventArgs e) { 45 | if (!e.Handled) { 46 | e.Handled = true; 47 | base.OnGotFocus(e); 48 | 49 | // use async call to avoid reentrancy, otherwise the webview will fight to get the focus 50 | Dispatcher.UIThread.Post(() => { 51 | if (IsFocused) { 52 | chromium.Focus(); 53 | } 54 | }, DispatcherPriority.Background); 55 | } 56 | } 57 | 58 | protected override void InternalDispose() => Dispose(); 59 | 60 | private void ForwardException(ExceptionDispatchInfo exceptionInfo) { 61 | // TODO 62 | } 63 | 64 | private T ExecuteInUI(Func action) { 65 | if (Dispatcher.UIThread.CheckAccess()) { 66 | return action(); 67 | } 68 | return Dispatcher.UIThread.InvokeAsync(action).Result; 69 | } 70 | 71 | private void AsyncExecuteInUI(Action action) { 72 | if (isDisposing) { 73 | return; 74 | } 75 | // use async call to avoid dead-locks, otherwise if the source action tries to to evaluate js it would block 76 | Dispatcher.UIThread.InvokeAsync( 77 | () => { 78 | if (!isDisposing) { 79 | ExecuteWithAsyncErrorHandling(action); 80 | } 81 | }, 82 | DispatcherPriority.Normal); 83 | } 84 | 85 | internal void InitializeBrowser(int initialWidth, int initialHeight) { 86 | chromium.CreateBrowser(initialWidth, initialHeight); 87 | } 88 | 89 | /// 90 | /// Called when the webview is requesting focus. Return false to allow the 91 | /// focus to be set or true to cancel setting the focus. 92 | /// True if is a system focus event, or false if is a navigation 93 | /// 94 | protected virtual bool OnSetFocus(bool isSystemEvent) { 95 | // VisualRoot can be null when webview is not yet added to the Visual tree 96 | var focusedElement = TopLevel.GetTopLevel(this)?.FocusManager.GetFocusedElement(); 97 | return !(focusedElement == chromium || focusedElement == this); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /WebViewControl/WebView.Wpf.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Runtime.ExceptionServices; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using System.Windows.Input; 7 | using System.Windows.Threading; 8 | 9 | namespace WebViewControl { 10 | 11 | partial class WebView : UserControl { 12 | 13 | internal IInputElement FocusableElement => chromium; 14 | 15 | private bool IsInDesignMode => DesignerProperties.GetIsInDesignMode(this); 16 | 17 | public static readonly DependencyProperty AddressProperty = DependencyProperty.Register(nameof(Address), typeof(string), typeof(WebView)); 18 | 19 | public string Address { 20 | get { return (string)GetValue(AddressProperty); } 21 | set { SetValue(AddressProperty, value); } 22 | } 23 | 24 | protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { 25 | base.OnPropertyChanged(e); 26 | 27 | // IWindowService is a WPF internal property set when component is loaded into a new window, even if the window isn't shown 28 | switch (e.Property.Name) { 29 | case "IWindowService": 30 | if (e.OldValue is Window oldWindow) { 31 | oldWindow.Closed -= OnHostWindowClosed; 32 | } 33 | 34 | if (e.NewValue is Window newWindow) { 35 | newWindow.Closed += OnHostWindowClosed; 36 | } 37 | break; 38 | 39 | case nameof(Address): 40 | InternalAddress = (string)e.NewValue; 41 | break; 42 | } 43 | } 44 | 45 | partial void ExtraInitialize() { 46 | Content = chromium; 47 | 48 | chromium.AddressChanged += (o, address) => AsyncExecuteInUI(() => Address = address); 49 | 50 | FocusManager.SetIsFocusScope(this, true); 51 | FocusManager.SetFocusedElement(this, FocusableElement); 52 | } 53 | 54 | protected override void OnPreviewKeyDown(KeyEventArgs e) { 55 | if (AllowDeveloperTools && e.Key == Key.F12) { 56 | ToggleDeveloperTools(); 57 | e.Handled = true; 58 | } 59 | } 60 | 61 | private void OnHostWindowClosed(object sender, EventArgs e) { 62 | ((Window)sender).Closed -= OnHostWindowClosed; 63 | Dispose(); 64 | } 65 | 66 | private void ForwardException(ExceptionDispatchInfo exceptionInfo) { 67 | // don't use invoke async, as it won't forward the exception to the dispatcher unhandled exception event 68 | Dispatcher.BeginInvoke((Action)(() => { 69 | if (!isDisposing) { 70 | exceptionInfo?.Throw(); 71 | } 72 | })); 73 | } 74 | 75 | private T ExecuteInUI(Func action) { 76 | return Dispatcher.Invoke(action); 77 | } 78 | 79 | private void AsyncExecuteInUI(Action action) { 80 | if (isDisposing) { 81 | return; 82 | } 83 | // use async call to avoid dead-locks, otherwise if the source action tries to to evaluate js it would block 84 | Dispatcher.InvokeAsync( 85 | () => { 86 | if (!isDisposing) { 87 | ExecuteWithAsyncErrorHandling(action); 88 | } 89 | }, 90 | DispatcherPriority.Normal, 91 | AsyncCancellationTokenSource.Token); 92 | } 93 | 94 | internal void InitializeBrowser(int initialWidth, int initialHeight) { 95 | chromium.CreateBrowser(initialWidth, initialHeight); 96 | } 97 | 98 | /// 99 | /// Called when the webview is requesting focus. Return false to allow the 100 | /// focus to be set or true to cancel setting the focus. 101 | /// True if is a system focus event, or false if is a navigation 102 | /// 103 | protected virtual bool OnSetFocus(bool isSystemEvent) => false; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/Tests.WebView/RequestInterception.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | 6 | namespace Tests.WebView { 7 | 8 | public class RequestInterception : WebViewTestBase { 9 | 10 | private const string ResourceJs = "resource.js"; 11 | 12 | private static string HtmlWithResource { get; } = $"Test Page"; 13 | 14 | private static Stream ToStream(string str) { 15 | var stream = new MemoryStream(); 16 | var writer = new StreamWriter(stream); 17 | writer.Write(str); 18 | writer.Flush(); 19 | stream.Position = 0; 20 | return stream; 21 | } 22 | 23 | [Test(Description = "Resource requested is intercepted")] 24 | public async Task ResourceRequestIsIntercepted() { 25 | await Run(async () => { 26 | var taskCompletionSource = new TaskCompletionSource(); 27 | TargetView.BeforeResourceLoad += (resourceHandler) => { 28 | taskCompletionSource.SetResult(resourceHandler.Url); 29 | }; 30 | await Load(HtmlWithResource); 31 | var resourceRequested = await taskCompletionSource.Task; 32 | 33 | Assert.AreEqual("/" + ResourceJs, new Uri(resourceRequested).AbsolutePath); 34 | }); 35 | } 36 | 37 | [Test(Description = "Resource response with a stream is loaded properly")] 38 | public async Task InterceptedResourceRequestIsLoaded() { 39 | await Run(async () => { 40 | var taskCompletionSource = new TaskCompletionSource(); 41 | TargetView.BeforeResourceLoad += (resourceHandler) => { 42 | resourceHandler.RespondWith(ToStream("scriptLoaded = true"), "js"); // declare x 43 | taskCompletionSource.SetResult(true); 44 | }; 45 | await Load(HtmlWithResource); 46 | await taskCompletionSource.Task; 47 | 48 | var loaded = await TargetView.EvaluateScript("return scriptLoaded"); // check that the value of x is what was declared before in the resource 49 | Assert.IsTrue(loaded); 50 | }); 51 | } 52 | 53 | [Test(Description = "Resource request canceled is not loaded")] 54 | public async Task ResourceRequestIsCanceled() { 55 | await Run(async () => { 56 | var taskCompletionSource = new TaskCompletionSource(); 57 | TargetView.BeforeResourceLoad += (resourceHandler) => { 58 | resourceHandler.Cancel(); 59 | taskCompletionSource.SetResult(true); 60 | }; 61 | await Load(HtmlWithResource); 62 | await taskCompletionSource.Task; 63 | 64 | var failed = await TargetView.EvaluateScript("return scriptFailed"); // check that the value of x is what was declared before in the resource 65 | Assert.IsTrue(failed); 66 | }); 67 | } 68 | 69 | [Test(Description = "Resource request is redirected")] 70 | public async Task RequestRedirect() { 71 | await Run(async () => { 72 | var taskCompletionSource = new TaskCompletionSource(); 73 | const string RedirectUrl = "anotherResource.js"; 74 | 75 | TargetView.BeforeResourceLoad += (resourceHandler) => { 76 | var url = new Uri(resourceHandler.Url); 77 | switch (url.AbsolutePath.TrimStart('/')) { 78 | case ResourceJs: 79 | resourceHandler.Redirect(url.GetLeftPart(UriPartial.Authority) + "/" + RedirectUrl); 80 | break; 81 | case RedirectUrl: 82 | taskCompletionSource.SetResult(true); 83 | break; 84 | } 85 | }; 86 | await Load(HtmlWithResource); 87 | var redirected = await taskCompletionSource.Task; 88 | 89 | Assert.IsTrue(redirected); 90 | }); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /WebViewControl/AssemblyCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace WebViewControl { 8 | 9 | internal class AssemblyCache { 10 | 11 | private object SyncRoot { get; } = new object(); 12 | 13 | // We now allow to load multiple versions of the same assembly, which means that resource urls can 14 | // (optionally) specify the version. We don't force the version to be specified to maintain backwards 15 | // compatibility, and thus for each assembly name we two entries in the dictionary: with and without a version. 16 | // Note that no guarantee is provided about which version is resolved if there are multiple loaded assemblies 17 | // with the same name and no specific version is provided. 18 | // This, consumer apps are encouraged to include the version in the resource url 19 | private IDictionary<(string AssemblyName, Version AssemblyVersion), Assembly> assemblies; 20 | 21 | private bool newAssembliesLoaded = true; 22 | 23 | internal Assembly ResolveResourceAssembly(Uri resourceUrl, bool failOnMissingAssembly) { 24 | if (assemblies == null) { 25 | lock (SyncRoot) { 26 | if (assemblies == null) { 27 | assemblies = new Dictionary<(string, Version), Assembly>(); 28 | AppDomain.CurrentDomain.AssemblyLoad += delegate { newAssembliesLoaded = true; }; 29 | } 30 | } 31 | } 32 | 33 | var (assemblyName, assemblyVersion) = ResourceUrl.GetEmbeddedResourceAssemblyNameAndVersion(resourceUrl); 34 | var assembly = GetAssemblyByNameAndVersion(assemblyName, assemblyVersion); 35 | 36 | if (assembly == null) { 37 | if (newAssembliesLoaded) { 38 | lock (SyncRoot) { 39 | if (newAssembliesLoaded) { 40 | // add loaded assemblies to cache 41 | newAssembliesLoaded = false; 42 | foreach (var domainAssembly in AppDomain.CurrentDomain.GetAssemblies()) { 43 | AddOrReplace(domainAssembly); 44 | } 45 | } 46 | } 47 | } 48 | 49 | assembly = GetAssemblyByNameAndVersion(assemblyName, assemblyVersion); 50 | if (assembly == null) { 51 | try { 52 | // try loading the assembly from a file named AssemblyName.dll (or AssemblyName-AssemblyVersion.dll if 53 | // a version was provided) 54 | var fileName = $"{assemblyName}{(assemblyVersion == null ? "" : $"-{assemblyVersion}")}.dll"; 55 | var assemblyPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); 56 | assembly = AssemblyLoader.LoadAssembly(assemblyPath); 57 | } catch (IOException) { 58 | // ignore 59 | } 60 | 61 | if (assembly != null) { 62 | lock (SyncRoot) { 63 | AddOrReplace(assembly); 64 | } 65 | } 66 | } 67 | } 68 | 69 | if (failOnMissingAssembly && assembly == null) { 70 | throw new InvalidOperationException("Could not find assembly for: " + resourceUrl); 71 | } 72 | return assembly; 73 | } 74 | 75 | private void AddOrReplace(Assembly assembly) { 76 | var identity = assembly.GetName(); 77 | var assemblyName = identity.Name; 78 | if (assemblyName == null) { 79 | return; 80 | } 81 | 82 | // add two entries, with and without the version. 83 | // for the null-version entry, keep the assembly with the highest version 84 | var version = identity.Version; 85 | if (!assemblies.TryGetValue((assemblyName, null), out var nullVersionAssembly) || 86 | (nullVersionAssembly.GetName().Version is { } previousVersion && previousVersion < version)) { 87 | assemblies[(assemblyName, null)] = assembly; 88 | } 89 | assemblies[(assemblyName, version)] = assembly; 90 | } 91 | 92 | private Assembly GetAssemblyByNameAndVersion(string assemblyName, Version assemblyVersion) { 93 | lock (SyncRoot) { 94 | assemblies.TryGetValue((assemblyName, assemblyVersion), out var assembly); 95 | return assembly; 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /WebViewControl/GlobalSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.IO; 5 | using Xilium.CefGlue.Common; 6 | 7 | namespace WebViewControl { 8 | 9 | public class GlobalSettings { 10 | 11 | private Color backgroundColor = Color.White; 12 | private bool persistCache; 13 | private bool enableErrorLogOnly; 14 | private bool enableVideoAutoplay = false; 15 | private bool osrEnabled = false; 16 | private string userAgent; 17 | private string logFile; 18 | private string cachePath = Path.Combine(Path.GetTempPath(), "WebView" + Guid.NewGuid().ToString().Replace("-", null) + DateTime.UtcNow.Ticks); 19 | private readonly List> commandLineSwitches = new(); 20 | 21 | /// 22 | /// Use this method to pass flags to the browser. List of available flags: https://peter.sh/experiments/chromium-command-line-switches/ 23 | /// 24 | public void AddCommandLineSwitch(string key, string value) { 25 | EnsureNotLoaded(nameof(AddCommandLineSwitch)); 26 | commandLineSwitches.Add(new KeyValuePair(key, value)); 27 | } 28 | 29 | public IEnumerable> CommandLineSwitches => commandLineSwitches; 30 | 31 | /// 32 | /// Gets or sets the background color of the WebView control. 33 | /// Default is . 34 | /// Note: Transparent colors (alpha < 255) are not supported. 35 | /// 36 | public Color BackgroundColor { 37 | get => backgroundColor; 38 | set { 39 | EnsureNotLoaded(nameof(BackgroundColor)); 40 | backgroundColor = value; 41 | } 42 | } 43 | 44 | public string CachePath { 45 | get => cachePath; 46 | set { 47 | EnsureNotLoaded(nameof(CachePath)); 48 | cachePath = value; 49 | } 50 | } 51 | 52 | public bool PersistCache { 53 | get => persistCache; 54 | set { 55 | EnsureNotLoaded(nameof(PersistCache)); 56 | persistCache = value; 57 | } 58 | } 59 | 60 | public bool EnableErrorLogOnly { 61 | get => enableErrorLogOnly; 62 | set { 63 | EnsureNotLoaded(nameof(EnableErrorLogOnly)); 64 | enableErrorLogOnly = value; 65 | } 66 | } 67 | 68 | public string UserAgent { 69 | get => userAgent; 70 | set { 71 | EnsureNotLoaded(nameof(UserAgent)); 72 | userAgent = value; 73 | } 74 | } 75 | 76 | public string LogFile { 77 | get => logFile; 78 | set { 79 | EnsureNotLoaded(nameof(LogFile)); 80 | logFile = value; 81 | } 82 | } 83 | 84 | /// 85 | /// Set to true to enable off-screen rendering support. 86 | /// Do not enable this setting if the application does not use off-screen rendering 87 | /// as it may reduce rendering performance and cause some issues. 88 | /// 89 | public bool OsrEnabled { 90 | get => osrEnabled; 91 | set { 92 | EnsureNotLoaded(nameof(OsrEnabled)); 93 | osrEnabled = value; 94 | } 95 | } 96 | 97 | /// 98 | /// Set to true to enable video autoplay without requiring user interaction. 99 | /// This allows muted videos with the autoplay attribute to play automatically. 100 | /// Default is false for security and user experience considerations. 101 | /// 102 | public bool EnableVideoAutoplay { 103 | get => enableVideoAutoplay; 104 | set { 105 | EnsureNotLoaded(nameof(EnableVideoAutoplay)); 106 | enableVideoAutoplay = value; 107 | } 108 | } 109 | 110 | private void EnsureNotLoaded(string propertyName) { 111 | if (CefRuntimeLoader.IsLoaded) { 112 | throw new InvalidOperationException($"Cannot set {propertyName} after WebView engine has been loaded"); 113 | } 114 | } 115 | 116 | internal int GetRemoteDebuggingPort() { 117 | #if REMOTE_DEBUG_SUPPORT 118 | var port = Environment.GetEnvironmentVariable("WEBVIEW_REMOTE_DEBUGGING_PORT"); 119 | int.TryParse(port != null ? port : "", out var result); 120 | return result; 121 | #else 122 | return 0; 123 | #endif 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/Tests.WebView/TestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Avalonia; 6 | using Avalonia.Controls; 7 | using Avalonia.Threading; 8 | using NUnit.Framework; 9 | 10 | namespace Tests { 11 | 12 | [TestFixture] 13 | public abstract class TestBase where T : class, IDisposable, new() { 14 | 15 | private static object initLock = new object(); 16 | private static bool initialized = false; 17 | 18 | private Window window; 19 | private T view; 20 | 21 | protected static string CurrentTestName => TestContext.CurrentContext.Test.Name; 22 | 23 | protected Task Run(Func func) => Dispatcher.UIThread.InvokeAsync(func, DispatcherPriority.Background); 24 | 25 | protected Task Run(Action action) => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Background).GetTask(); 26 | 27 | [OneTimeSetUp] 28 | protected Task OneTimeSetUp() { 29 | if (initialized) { 30 | return Task.CompletedTask; 31 | } 32 | 33 | lock (initLock) { 34 | if (initialized) { 35 | return Task.CompletedTask; 36 | } 37 | 38 | var taskCompletionSource = new TaskCompletionSource(); 39 | var uiThread = new Thread(() => { 40 | AppBuilder.Configure().UsePlatformDetect().SetupWithoutStarting(); 41 | 42 | Dispatcher.UIThread.Post(() => { 43 | initialized = true; 44 | taskCompletionSource.SetResult(true); 45 | }); 46 | Dispatcher.UIThread.MainLoop(CancellationToken.None); 47 | }); 48 | uiThread.IsBackground = true; 49 | uiThread.Start(); 50 | return taskCompletionSource.Task; 51 | } 52 | } 53 | 54 | [OneTimeTearDown] 55 | protected async Task OneTimeTearDown() { 56 | await Run(() => { 57 | if (view != null) { 58 | view.Dispose(); 59 | } 60 | window.Close(); 61 | }); 62 | } 63 | 64 | [SetUp] 65 | protected virtual async Task SetUp() { 66 | var currentTestName = CurrentTestName; // we cannot access TestContext properly in asynchronous mode 67 | 68 | await Run(async () => { 69 | window = new Window { 70 | Title = "Running: " + currentTestName 71 | }; 72 | 73 | if (view == null) { 74 | view = CreateView(); 75 | window.Content = view; 76 | 77 | InitializeView(); 78 | 79 | if (view != null) { 80 | await AfterInitializeView(); 81 | } 82 | } 83 | }); 84 | } 85 | 86 | protected Window Window => window; 87 | 88 | protected virtual T CreateView() { 89 | return new T(); 90 | } 91 | 92 | protected virtual void InitializeView() => window.Show(); 93 | 94 | protected virtual Task AfterInitializeView() { 95 | return Task.CompletedTask; 96 | } 97 | 98 | [TearDown] 99 | protected virtual async Task TearDown() { 100 | if (Debugger.IsAttached && TestContext.CurrentContext.Result.FailCount > 0) { 101 | if (ShowDebugConsole()) { 102 | await new TaskCompletionSource().Task; 103 | } 104 | } else { 105 | await Run(() => { 106 | if (view != null) { 107 | view.Dispose(); 108 | view = null; 109 | } 110 | window.Content = null; 111 | window.Close(); 112 | }); 113 | } 114 | } 115 | 116 | protected abstract bool ShowDebugConsole(); 117 | 118 | protected T TargetView { 119 | get { return view; } 120 | } 121 | 122 | protected bool FailOnAsyncExceptions { get; set; } = !Debugger.IsAttached; 123 | 124 | protected void OnUnhandledAsyncException(WebViewControl.UnhandledAsyncExceptionEventArgs e) { 125 | if (FailOnAsyncExceptions) { 126 | Console.WriteLine("An async exception ocurred: " + e.Exception.ToString()); 127 | Assert.Fail("An async exception ocurred: " + e.Exception.ToString()); 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /WebViewControl/WebView.InternalRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Xilium.CefGlue; 4 | using Xilium.CefGlue.Common.Handlers; 5 | 6 | namespace WebViewControl { 7 | 8 | partial class WebView { 9 | 10 | private class InternalRequestHandler : RequestHandler { 11 | 12 | private static readonly Lazy HttpResourceRequestHandler = new Lazy(() => new HttpResourceRequestHandler()); 13 | 14 | private WebView OwnerWebView { get; } 15 | 16 | private InternalResourceRequestHandler ResourceRequestHandler { get; } 17 | 18 | public InternalRequestHandler(WebView webView) { 19 | OwnerWebView = webView; 20 | ResourceRequestHandler = new InternalResourceRequestHandler(OwnerWebView); 21 | } 22 | 23 | protected override bool GetAuthCredentials(CefBrowser browser, string originUrl, bool isProxy, string host, int port, string realm, string scheme, CefAuthCallback callback) { 24 | using (callback) { 25 | if (OwnerWebView.ProxyAuthentication != null) { 26 | callback.Continue(OwnerWebView.ProxyAuthentication.UserName, OwnerWebView.ProxyAuthentication.Password); 27 | } 28 | return true; 29 | } 30 | } 31 | 32 | protected override bool OnBeforeBrowse(CefBrowser browser, CefFrame frame, CefRequest request, bool userGesture, bool isRedirect) { 33 | if (UrlHelper.IsInternalUrl(request.Url)) { 34 | return false; 35 | } 36 | 37 | if (OwnerWebView.IsHistoryDisabled && request.TransitionType.HasFlag(CefTransitionType.ForwardBackFlag)) { 38 | return true; 39 | } 40 | 41 | var cancel = false; 42 | var beforeNavigate = OwnerWebView.BeforeNavigate; 43 | if (beforeNavigate != null) { 44 | var wrappedRequest = new Request(request, OwnerWebView.GetRequestUrl(request.Url, (ResourceType) request.ResourceType)); 45 | OwnerWebView.ExecuteWithAsyncErrorHandling(() => beforeNavigate(wrappedRequest)); 46 | cancel = wrappedRequest.Canceled; 47 | } 48 | 49 | return cancel; 50 | } 51 | 52 | protected override bool OnCertificateError(CefBrowser browser, CefErrorCode certError, string requestUrl, CefSslInfo sslInfo, CefCallback callback) { 53 | using (callback) { 54 | if (OwnerWebView.IgnoreCertificateErrors) { 55 | callback.Continue(); 56 | return true; 57 | } 58 | return false; 59 | } 60 | } 61 | 62 | protected override void OnRenderProcessTerminated(CefBrowser browser, CefTerminationStatus status) { 63 | OwnerWebView.HandleRenderProcessCrashed(); 64 | 65 | const string ExceptionPrefix = "WebView render process "; 66 | 67 | Exception exception; 68 | 69 | switch (status) { 70 | case CefTerminationStatus.ProcessCrashed: 71 | exception = new RenderProcessCrashedException(ExceptionPrefix + "crashed"); 72 | break; 73 | case CefTerminationStatus.WasKilled: 74 | exception = new RenderProcessKilledException(ExceptionPrefix + "was killed", OwnerWebView.IsDisposing); 75 | break; 76 | case CefTerminationStatus.OutOfMemory: 77 | exception = new RenderProcessOutOfMemoryException(ExceptionPrefix + "ran out of memory"); 78 | break; 79 | default: 80 | exception = new RenderProcessCrashedException(ExceptionPrefix + "terminated with an unknown reason"); 81 | break; 82 | } 83 | 84 | OwnerWebView.ForwardUnhandledAsyncException(exception); 85 | } 86 | 87 | protected override CefResourceRequestHandler GetResourceRequestHandler(CefBrowser browser, CefFrame frame, CefRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) { 88 | if (OwnerWebView.IsSecurityDisabled && HttpResourceHandler.AcceptedResources.Contains(request.ResourceType) && request.Url != null) { 89 | var url = new Uri(request.Url); 90 | if (url.Scheme == Uri.UriSchemeHttp || url.Scheme == Uri.UriSchemeHttps) { 91 | return HttpResourceRequestHandler.Value; 92 | } 93 | } 94 | return ResourceRequestHandler; 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /tests/Tests.WebView/ResourcesLoading.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using NUnit.Framework; 6 | using WebViewControl; 7 | 8 | namespace Tests.WebView { 9 | 10 | public class ResourcesLoading : WebViewTestBase { 11 | 12 | [Test(Description = "Html load encoding is well handled")] 13 | public async Task HtmlIsWellEncoded() { 14 | await Run(async () => { 15 | const string BodyContent = "some text and a double byte char '●'"; 16 | await Load($"{BodyContent}"); 17 | 18 | var body = await TargetView.EvaluateScript("return document.body.innerText"); 19 | Assert.AreEqual(BodyContent, body); 20 | }); 21 | } 22 | 23 | [Test(Description = "Embedded files are correctly loaded")] 24 | public async Task EmbeddedFilesLoad() { 25 | await Run(async () => { 26 | var embeddedResourceUrl = new ResourceUrl(GetType().Assembly, "Resources", "EmbeddedJavascriptFile.js"); 27 | await Load($""); 28 | 29 | var embeddedFileLoaded = await TargetView.EvaluateScript("return embeddedFileLoaded"); 30 | Assert.IsTrue(embeddedFileLoaded); 31 | }); 32 | } 33 | 34 | [Test(Description = "Embedded files with dashes in the filename are correctly loaded")] 35 | public async Task EmbeddedFilesWithDashesInFilenameLoad() { 36 | await Run(async () => { 37 | var embeddedResourceUrl = new ResourceUrl(GetType().Assembly, "Resources", "dash-folder", "EmbeddedJavascriptFile-With-Dashes.js"); 38 | await Load($""); 39 | 40 | var embeddedFileLoaded = await TargetView.EvaluateScript("return embeddedFileLoaded"); 41 | Assert.IsTrue(embeddedFileLoaded); 42 | }); 43 | } 44 | 45 | [Test(Description = "Avalonia resource files are loaded")] 46 | public async Task ResourceFile() { 47 | await Run(async () => { 48 | var embeddedResourceUrl = new ResourceUrl(GetType().Assembly, "Resources", "ResourceJavascriptFile.js"); 49 | await Load($""); 50 | 51 | var resourceFileLoaded = await TargetView.EvaluateScript("return resourceFileLoaded"); 52 | Assert.IsTrue(resourceFileLoaded); 53 | 54 | Stream missingResource = null; 55 | Assert.DoesNotThrow(() => missingResource = ResourcesManager.TryGetResourceWithFullPath(GetType().Assembly, new[] { "Resources", "Missing.txt" })); 56 | Assert.IsNull(missingResource); 57 | }); 58 | } 59 | 60 | [Test(Description = "Resources from dynamically loaded assemblies can be loaded and the correct version is fetched")] 61 | public void DynamicallyLoadedAssemblyFile() { 62 | var resourcesAssemblyName = "TestResourceAssembly"; 63 | 64 | ResourceUrl GetResourceUrl(Version version) { 65 | var executingAssembly = Assembly.GetExecutingAssembly(); 66 | var executingDdirectory = Path.GetDirectoryName(executingAssembly.Location); 67 | var dllDirectory = executingDdirectory.Replace(executingAssembly.GetName().Name, $"{resourcesAssemblyName}.V{version}"); 68 | var dllPath = Path.Combine(dllDirectory, $"{resourcesAssemblyName}.dll"); 69 | var assembly = Assembly.Load(File.ReadAllBytes(dllPath)); 70 | return new ResourceUrl(assembly, "Resource.txt"); 71 | } 72 | 73 | string GetExpectedContent(Version version) => $"Resource with V{version} content"; 74 | 75 | string GetResourceContent(Uri uri) { 76 | Stream resourceStream = null; 77 | Assert.DoesNotThrow(() => resourceStream = ResourcesManager.TryGetResource(uri)); 78 | Assert.IsNotNull(resourceStream); 79 | 80 | using var reader = new StreamReader(resourceStream); 81 | return reader.ReadToEnd(); 82 | } 83 | 84 | var version1 = new Version(1, 0, 0, 0); 85 | var version2 = new Version(2, 0, 0, 0); 86 | var versionsToTry = new[] { version1, version2 }; 87 | foreach (var version in versionsToTry) { 88 | var uri = new Uri(GetResourceUrl(version).ToString()); 89 | var content = GetResourceContent(uri); 90 | Assert.That(content, Is.EqualTo(GetExpectedContent(version))); 91 | } 92 | 93 | // check that we are also able to retrieve the resource without specifying a version, 94 | // but in that case the resource may be from either version 95 | var unversionedUri = new Uri($"embedded://webview/{resourcesAssemblyName}/Resource.txt"); 96 | var unversionedContent = GetResourceContent(unversionedUri); 97 | Assert.That(unversionedContent, Is.AnyOf(GetExpectedContent(version1), GetExpectedContent(version2))); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /WebViewControl.Avalonia/WebViewControl.Avalonia.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(TargetDotnetVersion) 5 | WebViewControl 6 | WebViewControl Avalonia 7 | WebViewControl for Avalonia powered by CefGlue 8 | 9 | 10 | true 11 | true 12 | WebViewControl-Avalonia$(PackageSuffix) 13 | Debug;Release;ReleaseAvalonia;ReleaseWPF;ReleaseAvaloniaRemoteDebugSupport 14 | 15 | 16 | 17 | REMOTE_DEBUG_SUPPORT 18 | WebViewControl-Avalonia-RemoteDebugSupport$(PackageSuffix) 19 | true 20 | 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /WebViewControl/ResourcesManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using Xilium.CefGlue; 7 | 8 | namespace WebViewControl { 9 | 10 | public static partial class ResourcesManager { 11 | 12 | private static readonly AssemblyCache cache = new AssemblyCache(); 13 | 14 | private static Stream InternalTryGetResource(string assemblyName, string defaultNamespace, IEnumerable resourcePath, bool failOnMissingResource) { 15 | var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == assemblyName); 16 | if (assembly == null) { 17 | if (failOnMissingResource) { 18 | throw new InvalidOperationException("Assembly not found: " + assemblyName); 19 | } 20 | return null; 21 | } 22 | return InternalTryGetResource(assembly, defaultNamespace, resourcePath, failOnMissingResource); 23 | } 24 | 25 | private static string ComputeEmbeddedResourceName(string defaultNamespace, IEnumerable resourcePath) { 26 | var resourceParts = (new[] { defaultNamespace }).Concat(resourcePath).ToArray(); 27 | for (int i = 0; i < resourceParts.Length - 1; i++) { 28 | resourceParts[i] = resourceParts[i].Replace('-', '_').Replace('@', '_'); 29 | } 30 | return string.Join(".", resourceParts); 31 | } 32 | 33 | private static Stream InternalTryGetResource(Assembly assembly, string defaultNamespace, IEnumerable resourcePath, bool failOnMissingResource) { 34 | var resourceName = ComputeEmbeddedResourceName(defaultNamespace, resourcePath); 35 | var stream = assembly.GetManifestResourceStream(resourceName); 36 | if (stream == null) { 37 | var assemblyName = assembly.GetName().Name; 38 | var alternativeResourceName = string.Join(ResourceUrl.PathSeparator, resourcePath); 39 | try { 40 | stream = GetApplicationResource(assemblyName, alternativeResourceName); 41 | } catch (IOException) { 42 | // ignore 43 | } 44 | } 45 | if (failOnMissingResource && stream == null) { 46 | throw new InvalidOperationException("Resource not found: " + resourceName); 47 | } 48 | return stream; 49 | } 50 | 51 | public static Stream GetResourceWithFullPath(Assembly assembly, IEnumerable resourcePath) { 52 | return InternalTryGetResource(assembly, resourcePath.First(), resourcePath.Skip(1), true); 53 | } 54 | 55 | public static Stream GetResource(Assembly assembly, IEnumerable resourcePath) { 56 | return InternalTryGetResource(assembly, assembly.GetName().Name, resourcePath, true); 57 | } 58 | 59 | public static Stream GetResource(string assemblyName, IEnumerable resourcePath) { 60 | return InternalTryGetResource(assemblyName, assemblyName, resourcePath, true); 61 | } 62 | 63 | public static Stream TryGetResource(Assembly assembly, IEnumerable resourcePath) { 64 | return InternalTryGetResource(assembly, assembly.GetName().Name, resourcePath, false); 65 | } 66 | 67 | public static Stream TryGetResource(string assemblyName, IEnumerable resourcePath) { 68 | return InternalTryGetResource(assemblyName, assemblyName, resourcePath, false); 69 | } 70 | 71 | public static Stream TryGetResourceWithFullPath(Assembly assembly, IEnumerable resourcePath) { 72 | return InternalTryGetResource(assembly, resourcePath.First(), resourcePath.Skip(1), false); 73 | } 74 | 75 | public static Stream TryGetResourceWithFullPath(string assemblyName, IEnumerable resourcePath) { 76 | return InternalTryGetResource(assemblyName, resourcePath.First(), resourcePath.Skip(1), false); 77 | } 78 | 79 | internal static Stream TryGetResource(Uri url, bool failOnMissingAssembly, out string extension) { 80 | var resourceAssembly = cache.ResolveResourceAssembly(url, failOnMissingAssembly); 81 | if (resourceAssembly == null) { 82 | extension = string.Empty; 83 | return null; 84 | } 85 | var resourcePath = ResourceUrl.GetEmbeddedResourcePath(url); 86 | 87 | extension = Path.GetExtension(resourcePath.Last()).ToLower(); 88 | var resourceStream = TryGetResourceWithFullPath(resourceAssembly, resourcePath); 89 | 90 | return resourceStream; 91 | } 92 | 93 | public static Stream TryGetResource(Uri url) { 94 | return TryGetResource(url, false, out _); 95 | } 96 | 97 | public static string GetMimeType(string resourceName) { 98 | var extension = Path.GetExtension(resourceName); 99 | return GetExtensionMimeType(extension); 100 | } 101 | 102 | public static string GetExtensionMimeType(string extension) { 103 | extension = string.IsNullOrEmpty(extension) ? "html" : extension.TrimStart('.'); 104 | return CefRuntime.GetMimeType(extension); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /WebViewControl/ResourceUrl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | namespace WebViewControl { 6 | 7 | public partial class ResourceUrl { 8 | 9 | public const string LocalScheme = "local"; 10 | public const string CustomScheme = "custom"; 11 | 12 | internal const string EmbeddedScheme = "embedded"; 13 | internal const string PathSeparator = "/"; 14 | 15 | private const string AssemblyPathSeparator = ";"; 16 | private const char AssemblyVersionSeparator = '-'; 17 | private const string AssemblyPrefix = "assembly:"; 18 | private const string DefaultDomain = "webview{0}"; 19 | 20 | private string Url { get; } 21 | 22 | public ResourceUrl(params string[] path) { 23 | Url = string.Join("/", path); 24 | } 25 | 26 | public ResourceUrl(Assembly assembly, params string[] path) : this(path) { 27 | var identity = assembly.GetName(); 28 | var assemblyName = identity.Name; 29 | var assemblyVersion = identity.Version is { } version ? $"{AssemblyVersionSeparator}{version}" : ""; 30 | 31 | if (Url.StartsWith(PathSeparator)) { 32 | // only prefix with assembly if necessary, to avoid having the same resource loaded from multiple locations 33 | Url = AssemblyPrefix + assemblyName + assemblyVersion + AssemblyPathSeparator + Url.Substring(1); 34 | } else { 35 | Url = assemblyName + assemblyVersion + PathSeparator + Url; 36 | } 37 | Url = BuildUrl(EmbeddedScheme, Url); 38 | } 39 | 40 | internal ResourceUrl(string scheme, string path) { 41 | Url = BuildUrl(scheme, path); 42 | } 43 | 44 | private static string BuildUrl(string scheme, string path) { 45 | return scheme + Uri.SchemeDelimiter + CombinePath(DefaultDomain, path); 46 | } 47 | 48 | private static string CombinePath(string path1, string path2) { 49 | return path1 + (path1.EndsWith(PathSeparator) ? "" : PathSeparator) + (path2.StartsWith(PathSeparator) ? path2.Substring(1) : path2); 50 | } 51 | 52 | public override string ToString() { 53 | return string.Format(Url, ""); 54 | } 55 | 56 | private static bool ContainsAssemblyLocation(Uri url) { 57 | return url.Scheme == EmbeddedScheme && url.AbsolutePath.StartsWith(PathSeparator + AssemblyPrefix); 58 | } 59 | 60 | /// 61 | /// Supported syntax: 62 | /// embedded://webview/assembly:AssemblyName;Path/To/Resource 63 | /// embedded://webview/assembly:AssemblyName-AssemblyVersion;Path/To/Resource 64 | /// embedded://webview/AssemblyName/Path/To/Resource (AssemblyName is also assumed as default namespace) 65 | /// embedded://webview/AssemblyName-AssemblyVersion/Path/To/Resource 66 | /// 67 | internal static string[] GetEmbeddedResourcePath(Uri resourceUrl) { 68 | if (ContainsAssemblyLocation(resourceUrl)) { 69 | var indexOfPath = resourceUrl.AbsolutePath.IndexOf(AssemblyPathSeparator); 70 | return resourceUrl.AbsolutePath.Substring(indexOfPath + 1).Split(new [] { PathSeparator }, StringSplitOptions.None); 71 | } 72 | var uriParts = resourceUrl.Segments.Select(p => p.Replace(PathSeparator, "")).ToArray(); 73 | var (assemblyName, _) = GetAssemblyNameAndVersion(uriParts[1]); 74 | return uriParts.Skip(2).Prepend(assemblyName).ToArray(); 75 | } 76 | 77 | /// 78 | /// Supported syntax: 79 | /// embedded://webview/assembly:AssemblyName;Path/To/Resource 80 | /// embedded://webview/assembly:AssemblyName-AssemblyVersion;Path/To/Resource 81 | /// embedded://webview/AssemblyName/Path/To/Resource (AssemblyName is also assumed as default namespace) 82 | /// embedded://webview/AssemblyName-AssemblyVersion/Path/To/Resource 83 | /// 84 | public static (string, Version) GetEmbeddedResourceAssemblyNameAndVersion(Uri resourceUrl) { 85 | if (ContainsAssemblyLocation(resourceUrl)) { 86 | var resourcePath = resourceUrl.AbsolutePath.Substring((PathSeparator + AssemblyPrefix).Length); 87 | var indexOfPath = Math.Max(0, resourcePath.IndexOf(AssemblyPathSeparator)); 88 | return GetAssemblyNameAndVersion(resourcePath.Substring(0, indexOfPath)); 89 | } 90 | if (resourceUrl.Segments.Length > 1) { 91 | var assemblySegment = resourceUrl.Segments[1]; 92 | // default assembly name to the first path 93 | return GetAssemblyNameAndVersion(assemblySegment.EndsWith(PathSeparator) ? assemblySegment.Substring(0, assemblySegment.Length - PathSeparator.Length) : assemblySegment); 94 | } 95 | return (string.Empty, null); 96 | } 97 | 98 | private static (string, Version) GetAssemblyNameAndVersion(string assemblyNameAndVersion) { 99 | var parts = assemblyNameAndVersion.Split(AssemblyVersionSeparator); 100 | return parts.Length == 2 ? 101 | (parts[0], new Version(parts[1])) : 102 | (parts[0], null); 103 | } 104 | 105 | internal string WithDomain(string domain) { 106 | return string.Format(Url, string.IsNullOrEmpty(domain) ? "" : ("_" + domain)); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /WebViewControl/WebView.JavascriptExecutionApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace WebViewControl { 5 | 6 | partial class WebView { 7 | 8 | /// 9 | /// Registers an object with the specified name in the window context of the browser 10 | /// 11 | /// Name of the object that will be available on the JS window context. 12 | /// .Net object instance that will be bound to the JS proxy object. 13 | /// Optional method that allows intercepting every call. 14 | /// True if the object was registered or false if the object was already registered before 15 | public bool RegisterJavascriptObject(string name, object objectToBind, Func, object> interceptCall = null) { 16 | if (interceptCall == null) { 17 | interceptCall = target => target(); 18 | } 19 | return InnerRegisterJavascriptObject(name, objectToBind, interceptCall); 20 | } 21 | 22 | /// 23 | /// Registers an object with the specified name in the window context of the browser. 24 | /// Use this overload with async interceptor methods. 25 | /// 26 | /// Name of the object that will be available on the JS window context. 27 | /// .Net object instance that will be bound to the JS proxy object. 28 | /// Async method that allows intercepting every call. 29 | /// True if the object was registered or false if the object was already registered before 30 | public bool RegisterJavascriptObject(string name, object objectToBind, Func, Task> interceptCall) { 31 | return InnerRegisterJavascriptObject(name, objectToBind, interceptCall); 32 | } 33 | 34 | private bool InnerRegisterJavascriptObject(string name, object objectToBind, Func, T> interceptCall) where T : class { 35 | if (chromium.IsJavascriptObjectRegistered(name)) { 36 | return false; 37 | } 38 | 39 | T CallTargetMethod(Func target) { 40 | if (isDisposing) { 41 | return default; 42 | } 43 | 44 | var isAsync = false; 45 | try { 46 | JavascriptPendingCalls.AddCount(); 47 | 48 | if (isDisposing) { 49 | // check again, to avoid concurrency problems with dispose 50 | return default; 51 | } 52 | 53 | var result = interceptCall(target); 54 | if (result is Task task) { 55 | task.ContinueWith(t => JavascriptPendingCalls.Signal()); 56 | isAsync = true; 57 | } 58 | 59 | return result; 60 | } finally { 61 | if (!isAsync) { 62 | JavascriptPendingCalls.Signal(); 63 | } 64 | } 65 | } 66 | 67 | chromium.RegisterJavascriptObject(objectToBind, name, CallTargetMethod); 68 | 69 | return true; 70 | } 71 | 72 | /// 73 | /// Unregisters an object with the specified name in the window context of the browser 74 | /// 75 | /// 76 | public void UnregisterJavascriptObject(string name) { 77 | chromium.UnregisterJavascriptObject(name); 78 | } 79 | 80 | public Task EvaluateScript(string script, string frameName = MainFrameName, TimeSpan? timeout = null) { 81 | var jsExecutor = GetJavascriptExecutor(frameName); 82 | if (jsExecutor != null) { 83 | return jsExecutor.EvaluateScript(script, timeout: timeout); 84 | } 85 | return Task.FromResult(default(T)); 86 | } 87 | 88 | public void ExecuteScript(string script, string frameName = MainFrameName) { 89 | GetJavascriptExecutor(frameName)?.ExecuteScript(script); 90 | } 91 | 92 | public void ExecuteScriptFunction(string functionName, params string[] args) { 93 | ExecuteScriptFunctionInFrame(functionName, MainFrameName, args); 94 | } 95 | 96 | public void ExecuteScriptFunctionInFrame(string functionName, string frameName, params string[] args) { 97 | GetJavascriptExecutor(frameName)?.ExecuteScriptFunction(functionName, false, args); 98 | } 99 | 100 | public Task EvaluateScriptFunction(string functionName, params string[] args) { 101 | return EvaluateScriptFunctionInFrame(functionName, MainFrameName, args); 102 | } 103 | 104 | public Task EvaluateScriptFunctionInFrame(string functionName, string frameName, params string[] args) { 105 | var jsExecutor = GetJavascriptExecutor(frameName); 106 | if (jsExecutor != null) { 107 | return jsExecutor.EvaluateScriptFunction(functionName, false, args); 108 | } 109 | return Task.FromResult(default(T)); 110 | } 111 | 112 | protected void ExecuteScriptFunctionWithSerializedParams(string functionName, params object[] args) { 113 | ExecuteScriptFunctionWithSerializedParamsInFrame(functionName, MainFrameName, args); 114 | } 115 | 116 | protected void ExecuteScriptFunctionWithSerializedParamsInFrame(string functionName, string frameName, params object[] args) { 117 | GetJavascriptExecutor(frameName)?.ExecuteScriptFunction(functionName, true, args); 118 | } 119 | 120 | protected Task EvaluateScriptFunctionWithSerializedParams(string functionName, params object[] args) { 121 | return EvaluateScriptFunctionWithSerializedParamsInFrame(functionName, MainFrameName, args); 122 | } 123 | 124 | protected Task EvaluateScriptFunctionWithSerializedParamsInFrame(string functionName, string frameName, params object[] args) { 125 | var jsExecutor = GetJavascriptExecutor(frameName); 126 | if (jsExecutor != null) { 127 | return jsExecutor.EvaluateScriptFunction(functionName, true, args); 128 | } 129 | return Task.FromResult(default(T)); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/Tests.WebView/JavascriptEvaluation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | using JavascriptException = WebViewControl.WebView.JavascriptException; 6 | 7 | namespace Tests.WebView { 8 | 9 | public class JavascriptEvaluation : WebViewTestBase { 10 | 11 | [Test(Description = "A simple script evaluates correctly")] 12 | public async Task EvaluateSimpleScript() { 13 | await Run(async () => { 14 | var result = await TargetView.EvaluateScript("return 2+1"); 15 | Assert.AreEqual(3, result); 16 | }); 17 | } 18 | 19 | [Test(Description = "A simple script evaluates correctly")] 20 | public async Task EvaluateSimpleScriptFunction() { 21 | await Run(async () => { 22 | TargetView.ExecuteScript("window.simpleFunction = function(a,b) {return a+b};"); 23 | var result = await TargetView.EvaluateScriptFunction("simpleFunction", "2", "1"); 24 | Assert.AreEqual(3, result); 25 | }); 26 | } 27 | 28 | [Test(Description = "The order of the executed scripts is respected")] 29 | public async Task ExecutionOrderIsRespected() { 30 | await Run(async() => { 31 | try { 32 | TargetView.ExecuteScript("x = ''"); 33 | var expectedResult = ""; 34 | // queue 10000 scripts 35 | for (var i = 0; i < 10000; i++) { 36 | TargetView.ExecuteScript($"x += '{i},'"); 37 | expectedResult += i + ","; 38 | } 39 | var result = await TargetView.EvaluateScript("return x"); 40 | Assert.AreEqual(expectedResult, result); 41 | 42 | TargetView.ExecuteScript("x = '-'"); 43 | result = await TargetView.EvaluateScript("return x"); 44 | Assert.AreEqual("-", result); 45 | 46 | } finally { 47 | await TargetView.EvaluateScript("delete x"); 48 | } 49 | }); 50 | } 51 | 52 | [Test(Description = "Evaluation of complex objects returns the expected results")] 53 | public async Task ComplexObjectsEvaluation() { 54 | await Run(async () => { 55 | var result = await TargetView.EvaluateScript("return ({ Name: 'Snows', Age: 32, Parent: { Name: 'Snows Parent', Age: 60 }, Kind: 2 })"); 56 | Assert.IsNotNull(result); 57 | Assert.AreEqual("Snows", result.Name); 58 | Assert.AreEqual(32, result.Age); 59 | Assert.IsNotNull(result.Parent); 60 | Assert.AreEqual("Snows Parent", result.Parent.Name); 61 | Assert.AreEqual(60, result.Parent.Age); 62 | Assert.AreEqual(Kind.C, result.Kind); 63 | }); 64 | } 65 | 66 | [Test(Description = "Evaluation of scripts with errors returns stack and message details")] 67 | public async Task EvaluationErrorsContainsMessageAndJavascriptStack() { 68 | await Run(async () => { 69 | var exception = await Assertions.AssertThrows(async () => await TargetView.EvaluateScript("(function foo() { (function bar() { throw new Error('ups'); })() })()")); 70 | 71 | Assert.AreEqual("Error: ups", exception.Message); 72 | var stack = exception.StackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); 73 | Assert.Greater(stack.Length, 2); 74 | StringAssert.StartsWith(" at bar in about", stack.ElementAt(0)); 75 | StringAssert.StartsWith(" at foo in about", stack.ElementAt(1)); 76 | }); 77 | } 78 | 79 | [Test(Description = "Evaluation of scripts includes evaluated function but not args")] 80 | public async Task EvaluationErrorsContainsEvaluatedJavascript() { 81 | await Run(async () => { 82 | var exception = await Assertions.AssertThrows(async () => await TargetView.EvaluateScriptFunction("Math.min", "123", "(function() { throw new Error() })()")); 83 | 84 | var stack = exception.StackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); 85 | Assert.Greater(stack.Length, 1); 86 | StringAssert.StartsWith(" at Math.min in eval", stack.ElementAt(0)); 87 | StringAssert.DoesNotContain("123", stack.ElementAt(0)); 88 | }); 89 | } 90 | 91 | [Test(Description = "Evaluation of scripts with comments, json objects, and var declarations")] 92 | public async Task ScriptsWithComplexSyntaxAreEvaluated() { 93 | await Run(async () => { 94 | var result = await TargetView.EvaluateScript("return 2+1 // some comments"); 95 | Assert.AreEqual(3, result); 96 | 97 | result = await TargetView.EvaluateScript("var x = 1; return 5"); 98 | Assert.AreEqual(5, result); 99 | 100 | var resultObj = await TargetView.EvaluateScript("return ({ Name: 'Snows', Age: 32})"); 101 | Assert.IsNotNull(resultObj); 102 | }); 103 | } 104 | 105 | [Test(Description = "Evaluation of scripts timesout after timeout elapsed")] 106 | public async Task EvaluationTimeoutIsThrown() { 107 | await Run(async () => { 108 | var exception = await Assertions.AssertThrows( 109 | async() => await TargetView.EvaluateScript("var start = new Date().getTime(); while((new Date().getTime() - start) < 150);", timeout: TimeSpan.FromMilliseconds(50))); 110 | StringAssert.Contains("Timeout", exception.Message); 111 | }); 112 | } 113 | 114 | [Test(Description = "Evaluation of null returns empty array when result is array type")] 115 | public async Task EvaluationReturnsEmptyArraysWhenNull() { 116 | await Run(async () => { 117 | var result = await TargetView.EvaluateScript("null"); 118 | Assert.IsNotNull(result); 119 | Assert.AreEqual(0, result.Length); 120 | }); 121 | } 122 | 123 | [Test(Description = "Unhandled Exception event is called when an async error occurs")] 124 | public async Task UnhandledExceptionEventIsCalled() { 125 | const string ExceptionMessage = "nooo"; 126 | 127 | var taskCompletionSource = new TaskCompletionSource(); 128 | await Run(async () => { 129 | await WithUnhandledExceptionHandling(async () => { 130 | TargetView.ExecuteScript($"throw new Error('{ExceptionMessage}')"); 131 | 132 | var result = await TargetView.EvaluateScript("return 1+1"); // force exception to occur 133 | Assert.AreEqual(2, result, "Result should not be affected"); 134 | 135 | await taskCompletionSource.Task; 136 | var exception = taskCompletionSource.Task.Result; 137 | 138 | StringAssert.Contains(ExceptionMessage, exception.Message); 139 | }, 140 | e => { 141 | taskCompletionSource.SetResult(e); 142 | return true; 143 | }); 144 | }); 145 | } 146 | 147 | [Test(Description = "Javascript async errors throw unhandled exception")] 148 | public async Task JavascriptAsyncErrorsThrowUnhandledException() { 149 | const string ExceptionMessage = "nooo"; 150 | 151 | var taskCompletionSource = new TaskCompletionSource(); 152 | 153 | await Run(async () => { 154 | await WithUnhandledExceptionHandling(async () => { 155 | TargetView.ExecuteScript($"function foo() {{ throw new Error('{ExceptionMessage}'); }}; setTimeout(function() {{ foo(); }}, 1); "); 156 | 157 | await taskCompletionSource.Task; 158 | var exception = taskCompletionSource.Task.Result; 159 | 160 | StringAssert.Contains(ExceptionMessage, exception.Message); 161 | 162 | var stack = exception.StackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); 163 | Assert.AreEqual(2, stack.Length); 164 | StringAssert.StartsWith(" at foo in about:blank:line 1", stack.ElementAt(0), "Found " + stack.ElementAt(0)); 165 | StringAssert.StartsWith(" at in about:blank:line 1", stack.ElementAt(1), "Found " + stack.ElementAt(1)); 166 | }, 167 | e => { 168 | taskCompletionSource.SetResult(e); 169 | return true; 170 | }); 171 | }); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /tests/Tests.WebView/IsolatedJavascriptEvaluation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Avalonia.Threading; 5 | using NUnit.Framework; 6 | using static WebViewControl.WebView; 7 | 8 | namespace Tests.WebView { 9 | 10 | public class IsolatedJavascriptEvaluation : WebViewTestBase { 11 | 12 | protected override void InitializeView() => base.InitializeView(); 13 | 14 | protected override Task AfterInitializeView() { 15 | return Task.CompletedTask; 16 | } 17 | 18 | [Test(Description = "Evaluation timeouts when javascript engine is not initialized")] 19 | public async Task JavascriptEngineInitializationTimeout() { 20 | await Run(async () => { 21 | await Load(""); 22 | 23 | var exception = await Assertions.AssertThrows(async () => await TargetView.EvaluateScript("1", timeout: TimeSpan.FromMilliseconds(0))); 24 | Assert.IsNotNull(exception); 25 | StringAssert.Contains("Timeout", exception.Message); 26 | }); 27 | } 28 | 29 | [Test(Description = "Evaluation works after engine is initialized")] 30 | public async Task EvaluateAfterInitialization() { 31 | await Run(async () => { 32 | await Load("1"); 33 | await Load("2"); 34 | var result = await TargetView.EvaluateScript("return 1", timeout: TimeSpan.FromSeconds(30)); 35 | Assert.AreEqual(result, 1); 36 | }); 37 | } 38 | 39 | [Test(Description = "Method interception function is called")] 40 | public async Task RegisteredJsObjectMethodInterception() { 41 | await Run(async () => { 42 | const string DotNetObject = "DotNetObject"; 43 | var interceptorCalled = false; 44 | var taskCompletionSource = new TaskCompletionSource(); 45 | Func functionToCall = () => { 46 | taskCompletionSource.SetResult(true); 47 | return 10; 48 | }; 49 | object Interceptor(Func originalFunc) { 50 | interceptorCalled = true; 51 | return originalFunc(); 52 | } 53 | RegisterJavascriptObject(DotNetObject, functionToCall, Interceptor); 54 | 55 | var script = $"{DotNetObject}.invoke();"; 56 | await RunScript(script); 57 | 58 | var functionCalled = await taskCompletionSource.Task; 59 | 60 | Assert.IsTrue(functionCalled); 61 | Assert.IsTrue(interceptorCalled); 62 | }); 63 | } 64 | 65 | [Test(Description = ".Net Method params serialization works with nulls")] 66 | public async Task RegisteredJsObjectMethodNullParamsSerialization() { 67 | await Run(async () => { 68 | const string DotNetObject = "DotNetObject"; 69 | var taskCompletionSource = new TaskCompletionSource>(); 70 | 71 | Action functionToCall = (string arg1, string[] arg2) => { 72 | taskCompletionSource.SetResult(new Tuple(arg1, arg2)); 73 | }; 74 | RegisterJavascriptObject(DotNetObject, functionToCall); 75 | 76 | var script = $"{DotNetObject}.invoke(null, ['hello', null, 'world']);"; 77 | await RunScript(script); 78 | 79 | var obtainedArgs = await taskCompletionSource.Task; 80 | Assert.AreEqual(null, obtainedArgs.Item1); 81 | Assert.That(new[] { "hello", null, "world" }, Is.EquivalentTo(obtainedArgs.Item2)); 82 | }); 83 | } 84 | 85 | [Test(Description = ".Net Method returned objects serialization")] 86 | public async Task RegisteredJsObjectReturnObjectSerialization() { 87 | await Run(async () => { 88 | const string DotNetObject = "DotNetObject"; 89 | const string DotNetSetResult = "DotNetSetResult"; 90 | 91 | var testObject = new TestObject() { 92 | Age = 33, 93 | Kind = Kind.B, 94 | Name = "John", 95 | Parent = new TestObject() { 96 | Name = "John Parent", 97 | Age = 66, 98 | Kind = Kind.C 99 | } 100 | }; 101 | var taskCompletionSource = new TaskCompletionSource(); 102 | 103 | Func functionToCall = () => testObject; 104 | Action setResult = (r) => { 105 | taskCompletionSource.SetResult(r); 106 | }; 107 | 108 | RegisterJavascriptObject(DotNetObject, functionToCall); 109 | RegisterJavascriptObject(DotNetSetResult, setResult); 110 | 111 | var script = $"var result = await {DotNetObject}.invoke(); {DotNetSetResult}.invoke(result);"; 112 | await RunScript(script); 113 | 114 | var result = await taskCompletionSource.Task; 115 | Assert.IsNotNull(result); 116 | Assert.AreEqual(testObject.Name, result.Name); 117 | Assert.AreEqual(testObject.Age, result.Age); 118 | Assert.AreEqual(testObject.Kind, result.Kind); 119 | Assert.IsNotNull(result.Parent); 120 | Assert.AreEqual(testObject.Parent.Name, result.Parent.Name); 121 | Assert.AreEqual(testObject.Parent.Age, result.Parent.Age); 122 | Assert.AreEqual(testObject.Parent.Kind, result.Parent.Kind); 123 | }); 124 | } 125 | 126 | [Test(Description = "Dispose is scheduled when there are js pending calls")] 127 | public async Task WebViewDisposeDoesNotBlockWhenHasPendingJSCalls() { 128 | await Run(async () => { 129 | const string DotNetObject = "DotNetObject"; 130 | 131 | var taskCompletionSourceFunction = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 132 | var taskCompletionSourceDispose = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 133 | 134 | Func functionToCall = () => { 135 | TargetView.Dispose(); 136 | var disposeRan = taskCompletionSourceDispose.Task.IsCompleted; 137 | taskCompletionSourceFunction.SetResult(disposeRan); 138 | return 1; 139 | }; 140 | 141 | TargetView.RegisterJavascriptObject(DotNetObject, functionToCall); 142 | await Load($""); 143 | 144 | TargetView.Disposed += () => taskCompletionSourceDispose.SetResult(true); 145 | 146 | var result = await TargetView.EvaluateScriptFunction("test"); 147 | Assert.AreEqual(0, result, "Script evaluation should be cancelled and default value returned"); 148 | 149 | var disposed = await taskCompletionSourceFunction.Task; 150 | Assert.IsFalse(disposed, "Dispose should have been scheduled"); 151 | 152 | disposed = await taskCompletionSourceDispose.Task; 153 | Assert.IsTrue(disposed); 154 | }); 155 | } 156 | 157 | [Test(Description = "Javascript evaluation returns default values after webview is disposed")] 158 | public async Task JsEvaluationReturnsDefaultValuesAfterWebViewDispose() { 159 | await Run(async () => { 160 | var taskCompletionSource = new TaskCompletionSource(); 161 | await Load(""); 162 | 163 | TargetView.Disposed += () => { 164 | taskCompletionSource.SetResult(true); 165 | }; 166 | TargetView.Dispose(); 167 | 168 | var disposeCalled = await taskCompletionSource.Task; 169 | 170 | var result = await TargetView.EvaluateScriptFunction("test"); 171 | 172 | Assert.IsTrue(disposeCalled); 173 | Assert.AreEqual(result, 0); 174 | }); 175 | } 176 | 177 | [Test(Description = "Evaluation runs successfully on an iframe")] 178 | public async Task JavascriptEvaluationOnIframe() { 179 | await Run(async () => { 180 | await Load( 181 | "" + 182 | "" + 183 | "" + 186 | "" + 187 | "" + 188 | "" 189 | ); 190 | 191 | var x = await TargetView.EvaluateScript("return x", ""); 192 | var y = await TargetView.EvaluateScript("return test.y", ""); 193 | Assert.AreEqual(1, x); 194 | Assert.AreEqual(2, y); 195 | }); 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 OutSystems. All rights reserved. 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | -------------------------------------------------------------------------------- /WebView.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34525.116 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebViewControl", "WebViewControl\WebViewControl.csproj", "{A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebViewControl.Avalonia", "WebViewControl.Avalonia\WebViewControl.Avalonia.csproj", "{1B789CDF-1B73-450B-BA1D-EC265D498EFA}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.WebView", "tests\Tests.WebView\Tests.WebView.csproj", "{BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}" 11 | ProjectSection(ProjectDependencies) = postProject 12 | {50037503-26BC-4853-BBF5-7F217E6FA421} = {50037503-26BC-4853-BBF5-7F217E6FA421} 13 | {A4E4B970-35F0-4641-B5B9-5538931727F2} = {A4E4B970-35F0-4641-B5B9-5538931727F2} 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleWebView.Avalonia", "SampleWebView.Avalonia\SampleWebView.Avalonia.csproj", "{68B0F20F-77AE-4819-83CB-92C9C78E8E05}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FD047A58-F2D5-4AE1-B918-685240AE2D57}" 19 | ProjectSection(SolutionItems) = preProject 20 | .editorconfig = .editorconfig 21 | Directory.Packages.props = Directory.Packages.props 22 | Directory.Build.props = Directory.Build.props 23 | EndProjectSection 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "V1", "V1", "{54FF8FC2-5D9C-45AB-A7E6-6A69BD323FEC}" 26 | EndProject 27 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestResourceAssembly", "tests\TestResourceAssembly.V1.0.0.0\TestResourceAssembly.csproj", "{A4E4B970-35F0-4641-B5B9-5538931727F2}" 28 | EndProject 29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "V2", "V2", "{BFFEA0F6-3BF7-4A1F-8EC6-73B21BA6766E}" 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestResourceAssembly", "tests\TestResourceAssembly.V2.0.0.0\TestResourceAssembly.csproj", "{50037503-26BC-4853-BBF5-7F217E6FA421}" 32 | EndProject 33 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B779EFF9-07A0-4BEF-8533-CF3F68830AB4}" 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|x64 = Debug|x64 38 | Debug|ARM64 = Debug|ARM64 39 | Release|x64 = Release|x64 40 | Release|ARM64 = Release|ARM64 41 | ReleaseAvalonia|x64 = ReleaseAvalonia|x64 42 | ReleaseAvalonia|ARM64 = ReleaseAvalonia|ARM64 43 | ReleaseAvaloniaRemoteDebugSupport|x64 = ReleaseAvaloniaRemoteDebugSupport|x64 44 | ReleaseAvaloniaRemoteDebugSupport|ARM64 = ReleaseAvaloniaRemoteDebugSupport|ARM64 45 | ReleaseWPF|x64 = ReleaseWPF|x64 46 | ReleaseWPF|ARM64 = ReleaseWPF|ARM64 47 | EndGlobalSection 48 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 49 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.Debug|x64.ActiveCfg = Debug|x64 50 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.Debug|x64.Build.0 = Debug|x64 51 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.Release|x64.ActiveCfg = Release|x64 52 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.Release|x64.Build.0 = Release|x64 53 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.ReleaseAvalonia|x64.ActiveCfg = ReleaseAvalonia|x64 54 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.ReleaseAvaloniaRemoteDebugSupport|x64.ActiveCfg = ReleaseAvaloniaRemoteDebugSupport|x64 55 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.ReleaseWPF|x64.ActiveCfg = ReleaseWPF|x64 56 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.ReleaseWPF|x64.Build.0 = ReleaseWPF|x64 57 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.ReleaseAvalonia|ARM64.ActiveCfg = ReleaseAvalonia|ARM64 58 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.ReleaseAvaloniaRemoteDebugSupport|ARM64.ActiveCfg = ReleaseAvaloniaRemoteDebugSupport|ARM64 59 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.ReleaseWPF|ARM64.ActiveCfg = ReleaseWPF|ARM64 60 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.ReleaseWPF|ARM64.Build.0 = ReleaseWPF|ARM64 61 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.Debug|ARM64.ActiveCfg = Debug|ARM64 62 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.Debug|ARM64.Build.0 = Debug|ARM64 63 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.Release|ARM64.ActiveCfg = Release|ARM64 64 | {A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}.Release|ARM64.Build.0 = Release|ARM64 65 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.Debug|x64.ActiveCfg = Debug|x64 66 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.Debug|x64.Build.0 = Debug|x64 67 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.Release|x64.ActiveCfg = Release|x64 68 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.Release|x64.Build.0 = Release|x64 69 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.ReleaseAvalonia|x64.ActiveCfg = ReleaseAvalonia|x64 70 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.ReleaseAvalonia|x64.Build.0 = ReleaseAvalonia|x64 71 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.ReleaseAvaloniaRemoteDebugSupport|x64.ActiveCfg = ReleaseAvaloniaRemoteDebugSupport|x64 72 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.ReleaseAvaloniaRemoteDebugSupport|x64.Build.0 = ReleaseAvaloniaRemoteDebugSupport|x64 73 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.ReleaseWPF|x64.ActiveCfg = ReleaseWPF|x64 74 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.Debug|ARM64.ActiveCfg = Debug|ARM64 75 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.Debug|ARM64.Build.0 = Debug|ARM64 76 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.Release|ARM64.ActiveCfg = Release|ARM64 77 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.Release|ARM64.Build.0 = Release|ARM64 78 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.ReleaseAvalonia|ARM64.ActiveCfg = ReleaseAvalonia|ARM64 79 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.ReleaseAvalonia|ARM64.Build.0 = ReleaseAvalonia|ARM64 80 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.ReleaseAvaloniaRemoteDebugSupport|ARM64.ActiveCfg = ReleaseAvaloniaRemoteDebugSupport|ARM64 81 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.ReleaseAvaloniaRemoteDebugSupport|ARM64.Build.0 = ReleaseAvaloniaRemoteDebugSupport|ARM64 82 | {1B789CDF-1B73-450B-BA1D-EC265D498EFA}.ReleaseWPF|ARM64.ActiveCfg = ReleaseWPF|ARM64 83 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.Debug|x64.ActiveCfg = Debug|x64 84 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.Debug|x64.Build.0 = Debug|x64 85 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.ReleaseAvalonia|x64.ActiveCfg = ReleaseAvalonia|x64 86 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.ReleaseAvalonia|x64.Build.0 = ReleaseAvalonia|x64 87 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.ReleaseAvaloniaRemoteDebugSupport|x64.ActiveCfg = ReleaseAvaloniaRemoteDebugSupport|x64 88 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.ReleaseAvaloniaRemoteDebugSupport|x64.Build.0 = ReleaseAvaloniaRemoteDebugSupport|x64 89 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.ReleaseWPF|x64.ActiveCfg = ReleaseWPF|x64 90 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.ReleaseAvalonia|ARM64.ActiveCfg = ReleaseAvalonia|ARM64 91 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.ReleaseAvalonia|ARM64.Build.0 = ReleaseAvalonia|ARM64 92 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.ReleaseAvaloniaRemoteDebugSupport|ARM64.ActiveCfg = ReleaseAvaloniaRemoteDebugSupport|ARM64 93 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.ReleaseAvaloniaRemoteDebugSupport|ARM64.Build.0 = ReleaseAvaloniaRemoteDebugSupport|ARM64 94 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.ReleaseWPF|ARM64.ActiveCfg = ReleaseWPF|ARM64 95 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.Debug|ARM64.ActiveCfg = Debug|ARM64 96 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.Debug|ARM64.Build.0 = Debug|ARM64 97 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.Release|ARM64.ActiveCfg = Release|ARM64 98 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.Release|ARM64.Build.0 = Release|ARM64 99 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.Release|x64.ActiveCfg = Release|x64 100 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}.Release|x64.Build.0 = Release|x64 101 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.Debug|x64.ActiveCfg = Debug|x64 102 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.Debug|x64.Build.0 = Debug|x64 103 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.Release|x64.ActiveCfg = Release|x64 104 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.Release|x64.Build.0 = Release|x64 105 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseAvalonia|x64.ActiveCfg = ReleaseAvalonia|x64 106 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseAvalonia|x64.Build.0 = ReleaseAvalonia|x64 107 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseAvaloniaRemoteDebugSupport|x64.ActiveCfg = ReleaseAvaloniaRemoteDebugSupport|x64 108 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseAvaloniaRemoteDebugSupport|x64.Build.0 = ReleaseAvaloniaRemoteDebugSupport|x64 109 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseWPF|x64.ActiveCfg = ReleaseWPF|x64 110 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.Debug|ARM64.ActiveCfg = Debug|ARM64 111 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.Debug|ARM64.Build.0 = Debug|ARM64 112 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.Release|ARM64.ActiveCfg = Release|ARM64 113 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.Release|ARM64.Build.0 = Release|ARM64 114 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseAvalonia|ARM64.ActiveCfg = ReleaseAvalonia|ARM64 115 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseAvalonia|ARM64.Build.0 = ReleaseAvalonia|ARM64 116 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseAvaloniaRemoteDebugSupport|ARM64.ActiveCfg = ReleaseAvaloniaRemoteDebugSupport|ARM64 117 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseAvaloniaRemoteDebugSupport|ARM64.Build.0 = ReleaseAvaloniaRemoteDebugSupport|ARM64 118 | {68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseWPF|ARM64.ActiveCfg = ReleaseWPF|ARM64 119 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.Debug|ARM64.ActiveCfg = Debug|ARM64 120 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.Debug|ARM64.Build.0 = Debug|ARM64 121 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.Debug|x64.ActiveCfg = Debug|x64 122 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.Debug|x64.Build.0 = Debug|x64 123 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.Release|ARM64.ActiveCfg = Release|ARM64 124 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.Release|ARM64.Build.0 = Release|ARM64 125 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.Release|x64.ActiveCfg = Release|x64 126 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.Release|x64.Build.0 = Release|x64 127 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvalonia|ARM64.ActiveCfg = Release|ARM64 128 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvalonia|ARM64.Build.0 = Release|ARM64 129 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvalonia|x64.ActiveCfg = Release|x64 130 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvalonia|x64.Build.0 = Release|x64 131 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvaloniaRemoteDebugSupport|ARM64.ActiveCfg = Release|ARM64 132 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvaloniaRemoteDebugSupport|ARM64.Build.0 = Release|ARM64 133 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvaloniaRemoteDebugSupport|x64.ActiveCfg = Release|x64 134 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvaloniaRemoteDebugSupport|x64.Build.0 = Release|x64 135 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseWPF|ARM64.ActiveCfg = Release|ARM64 136 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseWPF|ARM64.Build.0 = Release|ARM64 137 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseWPF|x64.ActiveCfg = Release|x64 138 | {A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseWPF|x64.Build.0 = Release|x64 139 | {50037503-26BC-4853-BBF5-7F217E6FA421}.Debug|ARM64.ActiveCfg = Debug|ARM64 140 | {50037503-26BC-4853-BBF5-7F217E6FA421}.Debug|ARM64.Build.0 = Debug|ARM64 141 | {50037503-26BC-4853-BBF5-7F217E6FA421}.Debug|x64.ActiveCfg = Debug|x64 142 | {50037503-26BC-4853-BBF5-7F217E6FA421}.Debug|x64.Build.0 = Debug|x64 143 | {50037503-26BC-4853-BBF5-7F217E6FA421}.Release|ARM64.ActiveCfg = Release|ARM64 144 | {50037503-26BC-4853-BBF5-7F217E6FA421}.Release|ARM64.Build.0 = Release|ARM64 145 | {50037503-26BC-4853-BBF5-7F217E6FA421}.Release|x64.ActiveCfg = Release|x64 146 | {50037503-26BC-4853-BBF5-7F217E6FA421}.Release|x64.Build.0 = Release|x64 147 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvalonia|ARM64.ActiveCfg = Release|ARM64 148 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvalonia|ARM64.Build.0 = Release|ARM64 149 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvalonia|x64.ActiveCfg = Release|x64 150 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvalonia|x64.Build.0 = Release|x64 151 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvaloniaRemoteDebugSupport|ARM64.ActiveCfg = Release|ARM64 152 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvaloniaRemoteDebugSupport|ARM64.Build.0 = Release|ARM64 153 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvaloniaRemoteDebugSupport|x64.ActiveCfg = Release|x64 154 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvaloniaRemoteDebugSupport|x64.Build.0 = Release|x64 155 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseWPF|ARM64.ActiveCfg = Release|ARM64 156 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseWPF|ARM64.Build.0 = Release|ARM64 157 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseWPF|x64.ActiveCfg = Release|x64 158 | {50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseWPF|x64.Build.0 = Release|x64 159 | EndGlobalSection 160 | GlobalSection(SolutionProperties) = preSolution 161 | HideSolutionNode = FALSE 162 | EndGlobalSection 163 | GlobalSection(NestedProjects) = preSolution 164 | {BE65D793-AFB3-4F5D-A85F-7B7396A5FE59} = {B779EFF9-07A0-4BEF-8533-CF3F68830AB4} 165 | {54FF8FC2-5D9C-45AB-A7E6-6A69BD323FEC} = {B779EFF9-07A0-4BEF-8533-CF3F68830AB4} 166 | {A4E4B970-35F0-4641-B5B9-5538931727F2} = {54FF8FC2-5D9C-45AB-A7E6-6A69BD323FEC} 167 | {BFFEA0F6-3BF7-4A1F-8EC6-73B21BA6766E} = {B779EFF9-07A0-4BEF-8533-CF3F68830AB4} 168 | {50037503-26BC-4853-BBF5-7F217E6FA421} = {BFFEA0F6-3BF7-4A1F-8EC6-73B21BA6766E} 169 | EndGlobalSection 170 | GlobalSection(ExtensibilityGlobals) = postSolution 171 | SolutionGuid = {FD9150A9-9C08-4609-B953-D4B900FD27C0} 172 | EndGlobalSection 173 | EndGlobal 174 | -------------------------------------------------------------------------------- /WebViewControl/WebView.JavascriptExecutor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Runtime.Serialization; 7 | using System.Runtime.Serialization.Json; 8 | using System.Text; 9 | using System.Text.RegularExpressions; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Xilium.CefGlue; 13 | using Xilium.CefGlue.Common.Events; 14 | 15 | namespace WebViewControl { 16 | 17 | partial class WebView { 18 | 19 | [DataContract] 20 | internal class JsError { 21 | [DataMember(Name = "stack")] 22 | public string Stack; 23 | [DataMember(Name = "name")] 24 | public string Name; 25 | [DataMember(Name = "message")] 26 | public string Message; 27 | } 28 | 29 | internal class ScriptTask { 30 | 31 | public ScriptTask(string script, string functionName, Action evaluate = null) { 32 | Script = script; 33 | Evaluate = evaluate; 34 | FunctionName = functionName; 35 | } 36 | 37 | public string Script { get; } 38 | 39 | /// 40 | /// We store the function name apart from the script and use it later in the exception details 41 | /// this prevents any params to be shown in the message because they can contain sensitive information 42 | /// 43 | public string FunctionName { get; } 44 | 45 | public Action Evaluate { get; } 46 | } 47 | 48 | internal class JavascriptExecutor : IDisposable { 49 | 50 | private const string InternalException = "|WebViewInternalException"; 51 | 52 | private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); 53 | private static readonly TimeSpan InitializationTimeout = TimeSpan.FromSeconds(15); 54 | 55 | private static Regex StackFrameRegex { get; } = new(@"at\s*(?.*?)\s\(?(?[^\s]+):(?\d+):(?\d+)", RegexOptions.Compiled); 56 | 57 | private BlockingCollection PendingScripts { get; } = new(); 58 | private CancellationTokenSource FlushTaskCancellationToken { get; } = new(); 59 | 60 | private WebView OwnerWebView { get; } 61 | 62 | #if DEBUG 63 | private string Id { get; } = Guid.NewGuid().ToString(); 64 | #endif 65 | 66 | private CefFrame frame; 67 | private Task flushTask; 68 | 69 | public JavascriptExecutor(WebView owner, CefFrame frame = null) { 70 | OwnerWebView = owner; 71 | this.frame = frame; 72 | } 73 | 74 | public bool IsValid => frame == null || frame.IsValid; // consider valid when not bound (yet) or frame is valid 75 | 76 | private bool IsFlushTaskInitializing => flushTask == null || flushTask.Status < TaskStatus.Running; 77 | 78 | public void StartFlush(CefFrame frame) { 79 | #if DEBUG 80 | System.Diagnostics.Debug.WriteLine($"{nameof(StartFlush)} ('{Id}')"); 81 | #endif 82 | lock (FlushTaskCancellationToken) { 83 | if (flushTask != null || !frame.IsValid || FlushTaskCancellationToken.IsCancellationRequested) { 84 | return; 85 | } 86 | this.frame = frame; 87 | flushTask = Task.Factory.StartNew(FlushScripts, FlushTaskCancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); 88 | } 89 | } 90 | 91 | private void StopFlush() { 92 | #if DEBUG 93 | System.Diagnostics.Debug.WriteLine($"{nameof(StopFlush)} ('{Id}')"); 94 | #endif 95 | try { 96 | lock (FlushTaskCancellationToken) { 97 | if (FlushTaskCancellationToken.IsCancellationRequested) { 98 | return; 99 | } 100 | 101 | FlushTaskCancellationToken.Cancel(); 102 | PendingScripts.CompleteAdding(); 103 | } 104 | } catch (ObjectDisposedException) { } 105 | } 106 | 107 | private ScriptTask QueueScript(string script, string functionName = null, Action evaluate = null) { 108 | lock (FlushTaskCancellationToken) { 109 | if (FlushTaskCancellationToken.IsCancellationRequested) { 110 | return null; 111 | } 112 | 113 | var scriptTask = new ScriptTask(script, functionName, evaluate); 114 | PendingScripts.Add(scriptTask); 115 | return scriptTask; 116 | } 117 | } 118 | 119 | private void FlushScripts() { 120 | #if DEBUG 121 | System.Diagnostics.Debug.WriteLine($"{nameof(FlushScripts)} running ('{Id}')"); 122 | #endif 123 | OwnerWebView.ExecuteWithAsyncErrorHandling(InnerFlushScripts); 124 | } 125 | 126 | private void InnerFlushScripts() { 127 | try { 128 | var scriptsToExecute = new List(); 129 | foreach (var scriptTask in PendingScripts.GetConsumingEnumerable()) { 130 | if (scriptTask.Evaluate == null) { 131 | scriptsToExecute.Add(scriptTask); 132 | } 133 | if ((PendingScripts.Count == 0 || scriptTask.Evaluate != null) && scriptsToExecute.Count > 0) { 134 | BulkExecuteScripts(scriptsToExecute); 135 | scriptsToExecute.Clear(); 136 | } 137 | scriptTask.Evaluate?.Invoke(scriptTask.Script); 138 | } 139 | } catch (OperationCanceledException) { 140 | // stop 141 | } finally { 142 | PendingScripts.Dispose(); 143 | FlushTaskCancellationToken.Dispose(); 144 | } 145 | } 146 | 147 | private void BulkExecuteScripts(IEnumerable scriptsToExecute) { 148 | var script = string.Join(";" + Environment.NewLine, scriptsToExecute.Select(s => s.Script)); 149 | if (frame.IsValid) { 150 | var frameName = frame.Name; 151 | try { 152 | var timeout = OwnerWebView.DefaultScriptsExecutionTimeout ?? DefaultTimeout; 153 | var task = OwnerWebView.chromium.EvaluateJavaScript(WrapScriptWithErrorHandling(script), timeout: timeout); 154 | task.Wait(FlushTaskCancellationToken.Token); 155 | } catch (OperationCanceledException) { 156 | // ignore 157 | } catch (Exception e) { 158 | var evaluatedScriptFunctions = scriptsToExecute.Select(s => s.FunctionName); 159 | OwnerWebView.ForwardUnhandledAsyncException(ParseException(e, evaluatedScriptFunctions), frameName); 160 | } 161 | } 162 | } 163 | 164 | public async Task EvaluateScript(string script, string functionName = null, TimeSpan? timeout = null) { 165 | const string TimeoutExceptionName = "Timeout"; 166 | #if DEBUG 167 | System.Diagnostics.Debug.WriteLine($"{nameof(EvaluateScript)} '{script}' on ('{Id}')"); 168 | #endif 169 | var evaluationTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 170 | 171 | void Evaluate(string scriptToEvaluate) { 172 | #if DEBUG 173 | System.Diagnostics.Debug.WriteLine($"Evaluating '{script}' on ('{Id}')"); 174 | #endif 175 | try { 176 | var innerEvaluationTask = OwnerWebView.chromium.EvaluateJavaScript(WrapScriptWithErrorHandling(scriptToEvaluate), timeout: timeout); 177 | innerEvaluationTask.Wait(FlushTaskCancellationToken.Token); 178 | evaluationTask.SetResult(GetResult(innerEvaluationTask.Result)); 179 | } catch (Exception e) { 180 | if (FlushTaskCancellationToken.IsCancellationRequested) { 181 | evaluationTask.SetResult(GetResult(default(T))); 182 | } else if (e.InnerException is TaskCanceledException) { 183 | evaluationTask.SetException(new JavascriptException(TimeoutExceptionName, "Script evaluation timed out")); 184 | } else { 185 | evaluationTask.SetException(ParseException(e, new[] { functionName })); 186 | } 187 | } 188 | } 189 | 190 | var scriptTask = QueueScript(script, functionName, Evaluate); 191 | if (scriptTask == null) { 192 | return default; 193 | } 194 | 195 | if (timeout.HasValue) { 196 | var tasks = new [] { 197 | evaluationTask.Task, 198 | Task.Delay(timeout.Value) 199 | }; 200 | 201 | // wait with timeout if flush is not running yet to avoid hanging forever 202 | var task = await Task.WhenAny(tasks).ConfigureAwait(false); 203 | 204 | if (task != evaluationTask.Task) { 205 | if (IsFlushTaskInitializing) { 206 | throw new JavascriptException(TimeoutExceptionName, $"Javascript engine is not initialized after {InitializationTimeout.Seconds}s"); 207 | } 208 | // flush is already running, timeout will fire from the evaluation 209 | } 210 | } 211 | 212 | return await evaluationTask.Task; 213 | } 214 | 215 | public Task EvaluateScriptFunction(string functionName, bool serializeParams, params object[] args) { 216 | return EvaluateScript(MakeScript(functionName, serializeParams, args, emitReturn: true), functionName); 217 | } 218 | 219 | public void ExecuteScriptFunction(string functionName, bool serializeParams, params object[] args) { 220 | QueueScript(MakeScript(functionName, serializeParams, args, emitReturn: false), functionName); 221 | } 222 | 223 | public void ExecuteScript(string script) { 224 | QueueScript(script); 225 | } 226 | 227 | private T GetResult(object result) { 228 | var targetType = typeof(T); 229 | if (result == null) { 230 | if (targetType.IsArray) { 231 | // return empty arrays when value is null and return type is array 232 | return (T)(object)Array.CreateInstance(targetType.GetElementType(), 0); 233 | } 234 | return default(T); // return default T (its safer, because we allow returning null and converting into a default struct value) 235 | } 236 | if (IsBasicType(targetType)) { 237 | return (T)result; 238 | } 239 | return (T)result; 240 | } 241 | 242 | public void Dispose() { 243 | StopFlush(); 244 | } 245 | 246 | private static bool IsBasicType(Type type) { 247 | return type.IsPrimitive || type.IsEnum || type == typeof(string); 248 | } 249 | 250 | private static string MakeScript(string functionName, bool serializeParams, object[] args, bool emitReturn) { 251 | string SerializeParam(object value) { 252 | if (serializeParams || value == null) { 253 | return JavascriptSerializer.Serialize(value); 254 | } 255 | // TODO complex types 256 | return value.ToString(); 257 | } 258 | var argsSerialized = args.Select(SerializeParam); 259 | return $"{(emitReturn ? "return " : string.Empty)}{functionName}({string.Join(",", argsSerialized)})"; 260 | } 261 | 262 | private static string WrapScriptWithErrorHandling(string script) { 263 | return "try {" + script + Environment.NewLine + "} catch (e) { throw JSON.stringify({ stack: e.stack, message: e.message, name: e.name }) + '" + InternalException + "' }"; 264 | } 265 | 266 | private static T DeserializeJSON(string json) { 267 | var serializer = new DataContractJsonSerializer(typeof(JsError)); 268 | using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) { 269 | return (T)serializer.ReadObject(stream); 270 | } 271 | } 272 | 273 | private static Exception MakeTimeoutException(string functionName, TimeSpan timeout) { 274 | return new JavascriptException("Timeout", $"More than {timeout.TotalMilliseconds}ms elapsed evaluating: '{functionName}'"); 275 | } 276 | 277 | private static Exception ParseException(Exception exception, IEnumerable evaluatedScriptFunctions) { 278 | var jsErrorJSON = ((exception is AggregateException aggregateException) ? aggregateException.InnerExceptions.FirstOrDefault(e => IsInternalException(e.Message))?.Message : exception.Message) ?? ""; 279 | 280 | // try parse js exception 281 | jsErrorJSON = jsErrorJSON.Substring(Math.Max(0, jsErrorJSON.IndexOf("{"))); 282 | jsErrorJSON = jsErrorJSON.Substring(0, jsErrorJSON.LastIndexOf("}") + 1); 283 | 284 | var evaluatedStackFrames = evaluatedScriptFunctions.Where(f => !string.IsNullOrEmpty(f)) 285 | .Select(f => new JavascriptStackFrame(f, "eval", 0, 0)); 286 | 287 | if (!string.IsNullOrEmpty(jsErrorJSON)) { 288 | JsError jsError = null; 289 | try { 290 | jsError = DeserializeJSON(jsErrorJSON); 291 | } catch { 292 | // ignore will throw error at the end 293 | } 294 | if (jsError != null) { 295 | jsError.Name = jsError.Name ?? ""; 296 | jsError.Message = jsError.Message ?? ""; 297 | jsError.Stack = jsError.Stack ?? ""; 298 | var jsStack = jsError.Stack.Substring(Math.Min(jsError.Stack.Length, (jsError.Name + ": " + jsError.Message).Length)) 299 | .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); 300 | 301 | var parsedStack = new List(); 302 | 303 | parsedStack.AddRange(evaluatedStackFrames); 304 | 305 | foreach (var stackFrame in jsStack) { 306 | var frameParts = StackFrameRegex.Match(stackFrame); 307 | if (frameParts.Success) { 308 | parsedStack.Add(new JavascriptStackFrame(frameParts.Groups["method"].Value, frameParts.Groups["location"].Value, int.Parse(frameParts.Groups["column"].Value), int.Parse(frameParts.Groups["line"].Value))); 309 | } 310 | } 311 | 312 | return new JavascriptException(jsError.Name, jsError.Message, parsedStack); 313 | } 314 | } 315 | 316 | return new JavascriptException(exception.Message, evaluatedStackFrames, exception.StackTrace); 317 | } 318 | 319 | internal static bool IsInternalException(string exceptionMessage) { 320 | return exceptionMessage.EndsWith(InternalException); 321 | } 322 | } 323 | } 324 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | # C# files 4 | [*.cs] 5 | #### Core EditorConfig Options #### 6 | # Indentation and spacing 7 | indent_size = 4 8 | indent_style = space 9 | tab_width = 4 10 | # New line preferences 11 | end_of_line = crlf 12 | insert_final_newline = false 13 | #### .NET Coding Conventions #### 14 | # Organize usings 15 | dotnet_separate_import_directive_groups = false 16 | dotnet_sort_system_directives_first = true 17 | # this. and Me. preferences 18 | dotnet_style_qualification_for_event = false:silent 19 | dotnet_style_qualification_for_field = false:silent 20 | dotnet_style_qualification_for_method = false:silent 21 | dotnet_style_qualification_for_property = false:silent 22 | # Language keywords vs BCL types preferences 23 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 24 | dotnet_style_predefined_type_for_member_access = true:silent 25 | # Parentheses preferences 26 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 27 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 28 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 29 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 30 | # Modifier preferences 31 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 32 | # Expression-level preferences 33 | csharp_style_deconstructed_variable_declaration = true:suggestion 34 | csharp_style_inlined_variable_declaration = true:suggestion 35 | csharp_style_throw_expression = true:suggestion 36 | dotnet_style_coalesce_expression = true:suggestion 37 | dotnet_style_collection_initializer = true:suggestion 38 | dotnet_style_explicit_tuple_names = true:suggestion 39 | dotnet_style_null_propagation = true:suggestion 40 | dotnet_style_object_initializer = true:suggestion 41 | dotnet_style_prefer_auto_properties = true:silent 42 | dotnet_style_prefer_compound_assignment = true:suggestion 43 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 44 | dotnet_style_prefer_conditional_expression_over_return = true:silent 45 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 46 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 47 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 48 | # Field preferences 49 | dotnet_style_readonly_field = true:suggestion 50 | # Parameter preferences 51 | dotnet_code_quality_unused_parameters = all:suggestion 52 | #### C# Coding Conventions #### 53 | # var preferences 54 | csharp_style_var_elsewhere = false:silent 55 | csharp_style_var_for_built_in_types = false:silent 56 | csharp_style_var_when_type_is_apparent = false:silent 57 | # Expression-bodied members 58 | csharp_style_expression_bodied_accessors = true:silent 59 | csharp_style_expression_bodied_constructors = false:silent 60 | csharp_style_expression_bodied_indexers = true:silent 61 | csharp_style_expression_bodied_lambdas = true:silent 62 | csharp_style_expression_bodied_local_functions = false:silent 63 | csharp_style_expression_bodied_methods = false:silent 64 | csharp_style_expression_bodied_operators = false:silent 65 | csharp_style_expression_bodied_properties = true:silent 66 | # Pattern matching preferences 67 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 68 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 69 | # Null-checking preferences 70 | csharp_style_conditional_delegate_call = true:suggestion 71 | # Modifier preferences 72 | csharp_prefer_static_local_function = true:suggestion 73 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async 74 | # Code-block preferences 75 | csharp_prefer_braces = true:suggestion 76 | csharp_prefer_simple_using_statement = true:suggestion 77 | # Expression-level preferences 78 | csharp_prefer_simple_default_expression = true:suggestion 79 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 80 | csharp_style_prefer_index_operator = true:suggestion 81 | csharp_style_prefer_range_operator = true:suggestion 82 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 83 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 84 | # 'using' directive preferences 85 | csharp_using_directive_placement = outside_namespace:silent 86 | #### C# Formatting Rules #### 87 | # New line preferences 88 | csharp_new_line_before_catch = false 89 | csharp_new_line_before_else = false 90 | csharp_new_line_before_finally = false 91 | csharp_new_line_before_members_in_anonymous_types = true 92 | csharp_new_line_before_members_in_object_initializers = true 93 | csharp_new_line_before_open_brace = none 94 | csharp_new_line_between_query_expression_clauses = true 95 | # Indentation preferences 96 | csharp_indent_block_contents = true 97 | csharp_indent_braces = false 98 | csharp_indent_case_contents = true 99 | csharp_indent_case_contents_when_block = true 100 | csharp_indent_labels = no_change 101 | csharp_indent_switch_labels = true 102 | # Space preferences 103 | csharp_space_after_cast = false 104 | csharp_space_after_colon_in_inheritance_clause = true 105 | csharp_space_after_comma = true 106 | csharp_space_after_dot = false 107 | csharp_space_after_keywords_in_control_flow_statements = true 108 | csharp_space_after_semicolon_in_for_statement = true 109 | csharp_space_around_binary_operators = before_and_after 110 | csharp_space_around_declaration_statements = false 111 | csharp_space_before_colon_in_inheritance_clause = true 112 | csharp_space_before_comma = false 113 | csharp_space_before_dot = false 114 | csharp_space_before_open_square_brackets = false 115 | csharp_space_before_semicolon_in_for_statement = false 116 | csharp_space_between_empty_square_brackets = false 117 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 118 | csharp_space_between_method_call_name_and_opening_parenthesis = false 119 | csharp_space_between_method_call_parameter_list_parentheses = false 120 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 121 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 122 | csharp_space_between_method_declaration_parameter_list_parentheses = false 123 | csharp_space_between_parentheses = false 124 | csharp_space_between_square_brackets = false 125 | # Wrapping preferences 126 | csharp_preserve_single_line_blocks = true 127 | csharp_preserve_single_line_statements = true 128 | 129 | ########################################## 130 | # Styles 131 | ########################################## 132 | 133 | # camel_case_style - Define the camelCase style 134 | dotnet_naming_style.camel_case_style.capitalization = camel_case 135 | # pascal_case_style - Define the PascalCase style 136 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 137 | # first_upper_style - The first character must start with an upper-case character 138 | dotnet_naming_style.first_upper_style.capitalization = first_word_upper 139 | # prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I' 140 | dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case 141 | dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I 142 | # prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T' 143 | dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case 144 | dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T 145 | 146 | 147 | ########################################## 148 | # .NET Design Guideline Field Naming Rules 149 | # Naming rules for fields follow the .NET Framework design guidelines 150 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/index 151 | ########################################## 152 | 153 | # All public/protected/protected_internal constant fields must be PascalCase 154 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field 155 | dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal 156 | dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const 157 | dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field 158 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group 159 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style 160 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = suggestion 161 | 162 | # All public/protected/protected_internal static readonly fields must be PascalCase 163 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field 164 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal 165 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly 166 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field 167 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group 168 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style 169 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = suggestion 170 | 171 | # No other public/protected/protected_internal fields are allowed 172 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field 173 | dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal 174 | dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field 175 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group 176 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error 177 | 178 | ########################################## 179 | # StyleCop Field Naming Rules 180 | # Naming rules for fields follow the StyleCop analyzers 181 | # This does not override any rules using disallowed_style above 182 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers 183 | ########################################## 184 | 185 | # All constant fields must be PascalCase 186 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md 187 | dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private 188 | dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const 189 | dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field 190 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group 191 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style 192 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = suggestion 193 | 194 | # All static readonly fields must be PascalCase 195 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md 196 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private 197 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly 198 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field 199 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group 200 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style 201 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = suggestion 202 | 203 | # No non-private instance fields are allowed 204 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md 205 | dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected 206 | dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field 207 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group 208 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error 209 | 210 | # Private fields must be camelCase 211 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md 212 | dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private 213 | dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field 214 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group 215 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style 216 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = suggestion 217 | 218 | # Local variables must be camelCase 219 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md 220 | dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local 221 | dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local 222 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group 223 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style 224 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent 225 | 226 | # This rule should never fire. However, it's included for at least two purposes: 227 | # First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. 228 | # Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). 229 | dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * 230 | dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field 231 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group 232 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style 233 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error 234 | 235 | 236 | ########################################## 237 | # Other Naming Rules 238 | ########################################## 239 | 240 | # All of the following must be PascalCase: 241 | # - Namespaces 242 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces 243 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md 244 | # - Classes and Enumerations 245 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 246 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md 247 | # - Delegates 248 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types 249 | # - Constructors, Properties, Events, Methods 250 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members 251 | dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property 252 | dotnet_naming_rule.element_rule.symbols = element_group 253 | dotnet_naming_rule.element_rule.style = pascal_case_style 254 | dotnet_naming_rule.element_rule.severity = suggestion 255 | 256 | # Interfaces use PascalCase and are prefixed with uppercase 'I' 257 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 258 | dotnet_naming_symbols.interface_group.applicable_kinds = interface 259 | dotnet_naming_rule.interface_rule.symbols = interface_group 260 | dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style 261 | dotnet_naming_rule.interface_rule.severity = suggestion 262 | 263 | # Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' 264 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 265 | dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter 266 | dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group 267 | dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style 268 | dotnet_naming_rule.type_parameter_rule.severity = suggestion 269 | 270 | # Function parameters use camelCase 271 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters 272 | dotnet_naming_symbols.parameters_group.applicable_kinds = parameter 273 | dotnet_naming_rule.parameters_rule.symbols = parameters_group 274 | dotnet_naming_rule.parameters_rule.style = camel_case_style 275 | dotnet_naming_rule.parameters_rule.severity = suggestion 276 | 277 | # suppress warnings about missing documentation for public elements 278 | # (hopefully one day we can turn these off) 279 | # missing on public element 280 | dotnet_diagnostic.CS1591.severity = none 281 | # missing parameter doc 282 | dotnet_diagnostic.CS1573.severity = none 283 | 284 | --------------------------------------------------------------------------------