+ {
+ { "customindex.html", StaticFilesContents.CustomIndexHtmlContent },
+ },
+ // The contentRoot is ignored here because in WinForms it would include the absolute physical path to the app's content, which this provider doesn't care about
+ contentRoot: null);
+
+ return new CompositeFileProvider(inMemoryFiles, base.CreateFileProvider(contentRootDir));
+ }
+ }
+}
diff --git a/src/BlazorWebView/samples/BlazorGtkApp/Main.razor b/src/BlazorWebView/samples/BlazorGtkApp/Main.razor
new file mode 100644
index 000000000000..92540ddde04c
--- /dev/null
+++ b/src/BlazorWebView/samples/BlazorGtkApp/Main.razor
@@ -0,0 +1,12 @@
+ο»Ώ
+
+ Home |
+ Other
+
+
+
+
+ Not found
+ Sorry, there's nothing here.
+
+
diff --git a/src/BlazorWebView/samples/BlazorGtkApp/Pages/Index.razor b/src/BlazorWebView/samples/BlazorGtkApp/Pages/Index.razor
new file mode 100644
index 000000000000..84aafcb5dfbd
--- /dev/null
+++ b/src/BlazorWebView/samples/BlazorGtkApp/Pages/Index.razor
@@ -0,0 +1,25 @@
+ο»Ώ@page "/"
+@inject AppState AppState
+@using WebViewAppShared
+
+Hello, world!
+
+The current count is @AppState.Counter
+
+
+
+
+This is a shared component
+
+
+@code {
+ void IncrementCount()
+ {
+ AppState.Counter++;
+ }
+
+ void TriggerException()
+ {
+ throw new InvalidTimeZoneException("This is an exception from an event handler");
+ }
+}
diff --git a/src/BlazorWebView/samples/BlazorGtkApp/Pages/Other.razor b/src/BlazorWebView/samples/BlazorGtkApp/Pages/Other.razor
new file mode 100644
index 000000000000..b763f9c0cb85
--- /dev/null
+++ b/src/BlazorWebView/samples/BlazorGtkApp/Pages/Other.razor
@@ -0,0 +1,26 @@
+ο»Ώ@page "/other"
+@inject NavigationManager NavigationManager
+
+
+ Here is another page. Looks like navigation works.
+
+
+
+
+
+
+
+
+
+
+ The text is: @textValue
+
+
+@code {
+ string textValue = null!;
+
+ void BackToHome()
+ {
+ NavigationManager.NavigateTo("");
+ }
+}
diff --git a/src/BlazorWebView/samples/BlazorGtkApp/Program.cs b/src/BlazorWebView/samples/BlazorGtkApp/Program.cs
new file mode 100644
index 000000000000..133ab40b93bf
--- /dev/null
+++ b/src/BlazorWebView/samples/BlazorGtkApp/Program.cs
@@ -0,0 +1,60 @@
+ο»Ώusing System.IO;
+using BlazorGtkApp;
+using Microsoft.Extensions.DependencyInjection;
+using Gtk;
+using Microsoft.AspNetCore.Components.Web;
+using Microsoft.AspNetCore.Components.WebView.Gtk;
+using WebViewAppShared;
+
+#pragma warning disable CS0162 // Unreachable code detected
+
+AppState _appState = new();
+
+Application.Init();
+
+// Create the parent window
+var window = new Window(WindowType.Toplevel);
+window.DefaultSize = new Gdk.Size(1024, 768);
+
+window.DeleteEvent += (o, e) =>
+{
+ Application.Quit();
+};
+
+// Add the BlazorWebViews
+var services1 = new ServiceCollection();
+services1.AddGtkBlazorWebView();
+#if DEBUG
+services1.AddBlazorWebViewDeveloperTools();
+#endif
+services1.AddSingleton(_appState);
+
+var services2 = new ServiceCollection();
+services2.AddGtkBlazorWebView();
+#if DEBUG
+services2.AddBlazorWebViewDeveloperTools();
+#endif
+
+services2.AddSingleton(_appState);
+
+var nb = new Gtk.Notebook();
+
+var blazorWebView1 = new BlazorWebView();
+blazorWebView1.HostPage = Path.Combine("wwwroot", "index.html");
+blazorWebView1.Services = services1.BuildServiceProvider();
+blazorWebView1.RootComponents.Add("#app");
+blazorWebView1.RootComponents.RegisterForJavaScript("my-dynamic-root-component");
+var tab1 = nb.AppendPage(blazorWebView1, new Label(nameof(blazorWebView1)));
+
+var customFilesBlazorWebView = new CustomFilesBlazorWebView();
+customFilesBlazorWebView.HostPage = Path.Combine("wwwroot", "customindex.html");
+customFilesBlazorWebView.Services = services2.BuildServiceProvider();
+customFilesBlazorWebView.RootComponents.Add("#app");
+
+var tab2 = nb.AppendPage(customFilesBlazorWebView, new Label(nameof(customFilesBlazorWebView)));
+
+
+window.Add(nb);
+window.ShowAll();
+
+Application.Run();
\ No newline at end of file
diff --git a/src/BlazorWebView/samples/BlazorGtkApp/_Imports.razor b/src/BlazorWebView/samples/BlazorGtkApp/_Imports.razor
new file mode 100644
index 000000000000..6ba5da5dbac6
--- /dev/null
+++ b/src/BlazorWebView/samples/BlazorGtkApp/_Imports.razor
@@ -0,0 +1,7 @@
+ο»Ώ@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
diff --git a/src/BlazorWebView/samples/BlazorGtkApp/wwwroot/css/app.css b/src/BlazorWebView/samples/BlazorGtkApp/wwwroot/css/app.css
new file mode 100644
index 000000000000..4f895ce71e4d
--- /dev/null
+++ b/src/BlazorWebView/samples/BlazorGtkApp/wwwroot/css/app.css
@@ -0,0 +1,18 @@
+ο»Ώ#blazor-error-ui {
+ background: lightyellow;
+ bottom: 0;
+ box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
+ display: none;
+ left: 0;
+ padding: 0.6rem 1.25rem 0.7rem 1.25rem;
+ position: fixed;
+ width: 100%;
+ z-index: 1000;
+}
+
+#blazor-error-ui .dismiss {
+ cursor: pointer;
+ position: absolute;
+ right: 0.75rem;
+ top: 0.5rem;
+}
diff --git a/src/BlazorWebView/samples/BlazorGtkApp/wwwroot/index.html b/src/BlazorWebView/samples/BlazorGtkApp/wwwroot/index.html
new file mode 100644
index 000000000000..6259b623e953
--- /dev/null
+++ b/src/BlazorWebView/samples/BlazorGtkApp/wwwroot/index.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+ Blazor Gtk app
+
+
+
+
+
+
+
+
+
+
+ An unhandled error has occurred.
+
Reload
+
π
+
+
+
+
+
+
+
+
+
diff --git a/src/BlazorWebView/src/Gtk.SharedSource/GtkWebViewManager.cs b/src/BlazorWebView/src/Gtk.SharedSource/GtkWebViewManager.cs
new file mode 100644
index 000000000000..43ecf854b4e0
--- /dev/null
+++ b/src/BlazorWebView/src/Gtk.SharedSource/GtkWebViewManager.cs
@@ -0,0 +1,278 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using System.Web;
+using GLib;
+using Gtk;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Web;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
+using WebKit;
+using Process = System.Diagnostics.Process;
+
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+#pragma warning disable CS8601 // Possible null reference assignment.
+#pragma warning disable CS8604 // Possible null reference argument.
+
+namespace GtkSharp.BlazorWebKit;
+
+[SuppressMessage("ApiDesign", "RS0016:Γffentliche Typen und Member der deklarierten API hinzufΓΌgen")]
+public partial class GtkWebViewManager : Microsoft.AspNetCore.Components.WebView.WebViewManager
+{
+
+ protected const string AppHostAddress = "localhost";
+
+ protected static readonly string AppHostScheme = "app";
+
+ ///
+ /// Gets the application's base URI. Defaults to app://localhost/
+ ///
+ protected static string AppOrigin(string appHostScheme, string appHostAddress = AppHostAddress) => $"{appHostScheme}://{appHostAddress}/";
+
+ protected static readonly Uri AppOriginUri = new(AppOrigin(AppHostScheme, AppHostAddress));
+
+ protected Task? WebviewReadyTask;
+
+ protected string MessageQueueId = "webview";
+
+ string _hostPageRelativePath;
+ Uri _appBaseUri;
+
+ UserScript? _script;
+
+ public delegate void WebMessageHandler(IntPtr contentManager, IntPtr jsResult, IntPtr arg);
+
+ public WebView? WebView { get; protected set; }
+
+ protected ILogger? Logger;
+
+ protected GtkWebViewManager(IServiceProvider provider, Dispatcher dispatcher, Uri appBaseUri, IFileProvider fileProvider, JSComponentConfigurationStore jsComponents, string hostPageRelativePath) :
+ base(provider, dispatcher, appBaseUri, fileProvider, jsComponents, hostPageRelativePath)
+ {
+ _appBaseUri = appBaseUri;
+ _hostPageRelativePath = hostPageRelativePath;
+ }
+
+ delegate bool TryGetResponseContentHandler(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out IDictionary headers);
+
+ static readonly Dictionary UriSchemeRequestHandlers = new();
+
+ static bool HandleUriSchemeRequestIsRegistered = false;
+
+ ///
+ /// RegisterUriScheme can only called once per scheme
+ /// so it's needed to have a list of all WebViews registered
+ ///
+ ///
+ ///
+ static void HandleUriSchemeRequest(URISchemeRequest request)
+ {
+ if (!UriSchemeRequestHandlers.TryGetValue(request.WebView.Handle, out var uriSchemeHandler))
+ {
+ throw new Exception($"Invalid scheme \"{request.Scheme}\"");
+ }
+
+ var uri = request.Uri;
+
+ if (request.Path == "/")
+ {
+ uri += uriSchemeHandler._hostPageRelativePath;
+ }
+
+ if (uriSchemeHandler.tryGetResponseContent(uri, false, out int statusCode, out string statusMessage, out Stream content, out IDictionary headers))
+ {
+
+ var (inputStream, length) = InputStreamNewFromStream(content);
+
+ request.Finish(inputStream, length, headers["Content-Type"]);
+
+ inputStream?.Dispose();
+ }
+ else
+ {
+ throw new Exception($"Failed to serve \"{uri}\". {statusCode} - {statusMessage}");
+ }
+ }
+
+ void RegisterUriSchemeRequestHandler()
+ {
+ if (WebView is not { })
+ return;
+
+ if (!UriSchemeRequestHandlers.TryGetValue(WebView.Handle, out var uriSchemeHandler))
+ {
+ UriSchemeRequestHandlers.Add(WebView.Handle, (_hostPageRelativePath, TryGetResponseContent));
+ }
+ }
+
+ protected override void NavigateCore(Uri absoluteUri)
+ {
+ if (WebView is not { })
+ return;
+
+ Logger?.LogInformation($"Navigating to \"{absoluteUri}\"");
+ var loadUri = absoluteUri.ToString();
+
+ WebView.LoadUri(loadUri);
+ }
+
+ public string JsScript(string messageQueueId) =>
+ """
+ window.__receiveMessageCallbacks = [];
+
+ window.__dispatchMessageCallback = function(message) {
+ window.__receiveMessageCallbacks.forEach(function(callback) { callback(message); });
+ };
+
+ window.external = {
+ sendMessage: function(message) {
+ """
+ +
+ $"""
+ window.webkit.messageHandlers.{MessageQueueId}.postMessage(message);
+ """
+ +
+ """
+ },
+ receiveMessage: function(callback) {
+ window.__receiveMessageCallbacks.push(callback);
+ }
+ };
+ """;
+
+ protected virtual void Attach()
+ {
+ if (WebView is not { })
+ throw new ArgumentException();
+
+ if (!HandleUriSchemeRequestIsRegistered)
+ {
+ WebView.Context.RegisterUriScheme(AppHostScheme, HandleUriSchemeRequest);
+ HandleUriSchemeRequestIsRegistered = true;
+ }
+
+ RegisterUriSchemeRequestHandler();
+
+ var jsScript = JsScript(MessageQueueId);
+
+ _script = new UserScript(
+ jsScript,
+ UserContentInjectedFrames.AllFrames,
+ UserScriptInjectionTime.Start,
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ null, null);
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+
+ WebView.Destroyed += (o, args) => Detach();
+
+ WebView.UserContentManager.AddScript(_script);
+
+ WebView.UserContentManager.ScriptMessageReceived += (o, args) =>
+ {
+ var jsValue = args.JsResult.JsValue;
+
+ if (!jsValue.IsString) return;
+
+ var s = jsValue.ToString();
+
+ if (s is not null)
+ {
+ Logger?.LogDebug($"Received message `{s}`");
+
+ try
+ {
+ MessageReceived(_appBaseUri, s);
+ }
+ finally
+ { }
+ }
+ };
+
+ WebView.UserContentManager.RegisterScriptMessageHandler(MessageQueueId);
+ }
+
+ bool _detached = false;
+
+ protected virtual void Detach()
+ {
+ if (WebView is not { })
+ return;
+
+ if (_detached)
+ return;
+
+ WebView.UserContentManager.UnregisterScriptMessageHandler(MessageQueueId);
+ WebView.UserContentManager.RemoveScript(_script);
+ UriSchemeRequestHandlers.Remove(WebView.Handle);
+
+ _detached = true;
+ }
+
+ protected override void SendMessage(string message)
+ {
+ if (WebView is not { })
+ return;
+
+ Logger?.LogDebug($"Dispatching `{message}`");
+
+ var script = $"__dispatchMessageCallback(\"{HttpUtility.JavaScriptStringEncode(message)}\")";
+
+ WebView.RunJavascript(script);
+ }
+
+ protected override async ValueTask DisposeAsyncCore()
+ {
+ Detach();
+ await base.DisposeAsyncCore();
+ }
+
+ protected static string GetHeaderString(IDictionary headers) =>
+ string.Join(Environment.NewLine, headers.Select(kvp => $"{kvp.Key}: {kvp.Value}"));
+
+ protected static string? GetWebView2UserDataFolder()
+ {
+ if (Assembly.GetEntryAssembly() is { } mainAssembly)
+ {
+ // In case the application is running from a non-writable location (e.g., program files if you're not running
+ // elevated), use our own convention of %LocalAppData%\YourApplicationName.WebView.
+ var applicationName = mainAssembly.GetName().Name;
+
+ var result = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ $"{applicationName}.{nameof(WebView)}");
+
+ return result;
+ }
+
+ return null;
+ }
+
+ protected static void LaunchUriInExternalBrowser(Uri uri)
+ {
+ using var launchBrowser = new Process();
+
+ launchBrowser.StartInfo.UseShellExecute = true;
+ launchBrowser.StartInfo.FileName = uri.ToString();
+ launchBrowser.Start();
+ }
+
+
+ public static (InputStream inputStream, int length) InputStreamNewFromStream(Stream content)
+ {
+ using var memoryStream = new MemoryStream();
+ var length = (int)content.Length;
+ content.CopyTo(memoryStream, length);
+ var buffer = memoryStream.GetBuffer();
+ Array.Resize(ref buffer, length);
+ var bytes = new Bytes(buffer);
+ var inputStream = new MemoryInputStream(bytes);
+
+ return (inputStream, length);
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorWebView/src/Gtk/BlazorWebView.cs b/src/BlazorWebView/src/Gtk/BlazorWebView.cs
new file mode 100644
index 000000000000..af2563e0812e
--- /dev/null
+++ b/src/BlazorWebView/src/Gtk/BlazorWebView.cs
@@ -0,0 +1,307 @@
+using System;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Gtk;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.Components.WebView.Gtk
+{
+ ///
+ /// A Gtk Widget for hosting Razor components locally in Windows desktop applications.
+ ///
+ public class BlazorWebView : Bin
+ {
+ private readonly WebKit.WebView _webview;
+ private GtkWebViewManager? _webviewManager;
+ private string? _hostPage;
+ private IServiceProvider? _services;
+
+ ///
+ /// Creates a new instance of .
+ ///
+ public BlazorWebView()
+ {
+ Widgets = CreateWidgetsInstance();
+ ComponentsDispatcher = Dispatcher.CreateDefault();
+
+ RootComponents.CollectionChanged += HandleRootComponentsCollectionChanged;
+
+ _webview = new WebKit.WebView();
+
+ this.Child = _webview;
+ ((BlazorWebViewWidgetCollection)Widgets).AddInternal(_webview);
+ }
+
+ ///
+ /// Returns the inner used by this control.
+ ///
+ ///
+ /// Directly using some functionality of the inner web view can cause unexpected results because its behavior
+ /// is controlled by the that is hosting it.
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public WebKit.WebView WebView => _webview;
+
+ private Dispatcher ComponentsDispatcher { get; }
+
+ WidgetCollection Widgets { get; set; }
+
+ bool Created { get; set; }
+
+ ///
+ protected override void OnShown()
+ {
+ base.OnShown();
+ Created = true;
+
+ StartWebViewCoreIfPossible();
+ }
+
+ ///
+ /// Path to the host page within the application's static files. For example, wwwroot\index.html
.
+ /// This property must be set to a valid value for the Razor components to start.
+ ///
+ [Category("Behavior")]
+ [Description(@"Path to the host page within the application's static files. Example: wwwroot\index.html.")]
+ public string? HostPage
+ {
+ get => _hostPage;
+ set
+ {
+ _hostPage = value;
+ OnHostPagePropertyChanged();
+ }
+ }
+
+ // Learn more about these methods here: https://docs.microsoft.com/en-us/dotnet/desktop/winforms/controls/defining-default-values-with-the-shouldserialize-and-reset-methods?view=netframeworkdesktop-4.8
+ private void ResetHostPage() => HostPage = null;
+
+ private bool ShouldSerializeHostPage() => !string.IsNullOrEmpty(HostPage);
+
+ ///
+ /// A collection of instances that specify the Blazor types
+ /// to be used directly in the specified .
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public RootComponentsCollection RootComponents { get; } = new();
+
+ ///
+ /// Gets or sets an containing services to be used by this control and also by application code.
+ /// This property must be set to a valid value for the Razor components to start.
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ [DisallowNull]
+ public IServiceProvider Services
+ {
+ get => _services!;
+ set
+ {
+ _services = value;
+ OnServicesPropertyChanged();
+ }
+ }
+
+ ///
+ /// Allows customizing how links are opened.
+ /// By default, opens internal links in the webview and external links in an external app.
+ ///
+ [Category("Action")] [Description("Allows customizing how links are opened. By default, opens internal links in the webview and external links in an external app.")]
+ public EventHandler? UrlLoading;
+
+ ///
+ /// Allows customizing the web view before it is created.
+ ///
+ [Category("Action")] [Description("Allows customizing the web view before it is created.")]
+ public EventHandler? BlazorWebViewInitializing;
+
+ ///
+ /// Allows customizing the web view after it is created.
+ ///
+ [Category("Action")] [Description("Allows customizing the web view after it is created.")]
+ public EventHandler? BlazorWebViewInitialized;
+
+ private void OnHostPagePropertyChanged() => StartWebViewCoreIfPossible();
+
+ private void OnServicesPropertyChanged() => StartWebViewCoreIfPossible();
+
+ private bool RequiredStartupPropertiesSet =>
+ Created &&
+ _webview != null &&
+ HostPage != null &&
+ Services != null;
+
+ private void StartWebViewCoreIfPossible()
+ {
+ // We never start the Blazor code in design time because it doesn't make sense to run
+ // a Razor component in the designer.
+ // if (IsAncestorSiteInDesignMode)
+ // {
+ // return;
+ // }
+
+ // If we don't have all the required properties, or if there's already a WebViewManager, do nothing
+ if (!RequiredStartupPropertiesSet || _webviewManager != null)
+ {
+ return;
+ }
+
+ // We assume the host page is always in the root of the content directory, because it's
+ // unclear there's any other use case. We can add more options later if so.
+ string appRootDir;
+ var entryAssemblyLocation = Assembly.GetEntryAssembly()?.Location;
+
+#if !DEBUG
+ if (!string.IsNullOrEmpty(entryAssemblyLocation))
+ {
+ appRootDir = System.IO.Path.GetDirectoryName(entryAssemblyLocation)!;
+ }
+ else
+#endif
+
+ {
+ appRootDir = Environment.CurrentDirectory;
+ }
+
+ var hostPageFullPath = System.IO.Path.GetFullPath(System.IO.Path.Combine(appRootDir, HostPage!)); // HostPage is nonnull because RequiredStartupPropertiesSet is checked above
+ var contentRootDirFullPath = System.IO.Path.GetDirectoryName(hostPageFullPath)!;
+ var contentRootRelativePath = System.IO.Path.GetRelativePath(appRootDir, contentRootDirFullPath);
+ var hostPageRelativePath = System.IO.Path.GetRelativePath(contentRootDirFullPath, hostPageFullPath);
+
+ var fileProvider = CreateFileProvider(contentRootDirFullPath);
+
+ if (_webviewManager != null)
+ {
+ _webviewManager.DisposeAsync()
+ .AsTask()
+ .GetAwaiter()
+ .GetResult();
+ ;
+ }
+
+ _webviewManager = new GtkWebViewManager(
+ _webview,
+ Services,
+ ComponentsDispatcher,
+ fileProvider,
+ RootComponents.JSComponents,
+ contentRootRelativePath,
+ hostPageRelativePath,
+ (args) => UrlLoading?.Invoke(this, args),
+ (args) => BlazorWebViewInitializing?.Invoke(this, args),
+ (args) => BlazorWebViewInitialized?.Invoke(this, args));
+
+ StaticContentHotReloadManager.AttachToWebViewManagerIfEnabled(_webviewManager);
+
+ foreach (var rootComponent in RootComponents)
+ {
+ // Since the page isn't loaded yet, this will always complete synchronously
+ _ = rootComponent.AddToWebViewManagerAsync(_webviewManager);
+ }
+
+ _webviewManager.Navigate("/");
+ }
+
+ private void HandleRootComponentsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs eventArgs)
+ {
+ // If we haven't initialized yet, this is a no-op
+ if (_webviewManager != null)
+ {
+ // Dispatch because this is going to be async, and we want to catch any errors
+ _ = ComponentsDispatcher.InvokeAsync(async () =>
+ {
+ var newItems = (eventArgs.NewItems ?? Array.Empty