惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threat Research - Cisco Blogs
V
V2EX
爱范儿
爱范儿
Martin Fowler
Martin Fowler
T
The Blog of Author Tim Ferriss
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
C
Check Point Blog
罗磊的独立博客
A
About on SuperTechFans
MyScale Blog
MyScale Blog
S
Security @ Cisco Blogs
博客园 - 聂微东
Simon Willison's Weblog
Simon Willison's Weblog
Cyberwarzone
Cyberwarzone
云风的 BLOG
云风的 BLOG
U
Unit 42
Latest news
Latest news
Apple Machine Learning Research
Apple Machine Learning Research
Security Latest
Security Latest
Y
Y Combinator Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Hacker News
The Hacker News
C
Cyber Attacks, Cyber Crime and Cyber Security
Cisco Talos Blog
Cisco Talos Blog
AWS News Blog
AWS News Blog
NISL@THU
NISL@THU
美团技术团队
S
Securelist
P
Privacy International News Feed
T
Tenable Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理
S
SegmentFault 最新的问题
Hugging Face - Blog
Hugging Face - Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
K
Kaspersky official blog
Google Online Security Blog
Google Online Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com
D
Darknet – Hacking Tools, Hacker News & Cyber Security
B
Blog RSS Feed
W
WeLiveSecurity
A
Arctic Wolf
Hacker News - Newest:
Hacker News - Newest: "LLM"
Google DeepMind News
Google DeepMind News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Recent Commits to openclaw:main
Recent Commits to openclaw:main
N
Netflix TechBlog - Medium
有赞技术团队
有赞技术团队
V2EX - 技术
V2EX - 技术

博客园 - dalgleish

C# Avalonia 23- OpenGL- 例子通用类 C# Avalonia 22- SoundAndVideo- SpeechVoiceTest C# Avalonia 22- SoundAndVideo- SpeechRecognition C# Avalonia 22- SoundAndVideo- SpeechSynthesis C# Avalonia 22- SoundAndVideo- CodePlayback C# Avalonia 22- SoundAndVideo- SoundPlayerTest C# Avalonia 21- MenusAndToolbars- RibbonTest C# Avalonia 21- MenusAndToolbars- ProportionalStatusBar C# Avalonia 21- MenusAndToolbars- BasicToolbar C# Avalonia 21- MenusAndToolbars- ToolBar/Ribbon开源项目介绍和修复 C# Avalonia 21- MenusAndToolbars- SidebarMenu C# Avalonia 21- MenusAndToolbars- MixedMenus C# Avalonia 20 - WindowsMenu- 魔改Hyperlink - 使用例子 C# Avalonia 20 - WindowsMenu- 魔改Hyperlink C# Avalonia 20 - WindowsMenu- WebViewTest C# Avalonia 20 - WindowsMenu- ModernWindowTest C# Avalonia 20 - WindowsMenu- ModernWindow C# Avalonia 20 - WindowsMenu- TransparentWithShapes C# Avalonia 20 - WindowsMenu- TransparentBackground C# Avalonia 20 - WindowsMenu- OpenFileTest C# Avalonia 20 - WindowsMenu- SavePostion C# Avalonia 20 - WindowsMenu- CenterScreen C# Avalonia 19- DataBinding- DataGridGrouping C# Avalonia 19- DataBinding- DirectoryTreeView C# Avalonia 19- DataBinding- BoundTreeView C# Avalonia 19- DataBinding- CustomListViewTest
C# Avalonia 20 - WindowsMenu- WebView
dalgleish · 2026-03-24 · via 博客园 - dalgleish

Avalonia的WebView会收费,所以自己写一个,下一个例子给出使用例子。结构示意图。

image

WebView.cs代码

using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Threading;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Xilium.CefGlue;
using Xilium.CefGlue.Avalonia;
using Xilium.CefGlue.Common.Events;

namespace Shares.Avalonia.CustomControls.WebView;

public sealed partial class WebView : UserControl, IDisposable
{
    public static readonly StyledProperty<Uri> SourceProperty =
        AvaloniaProperty.Register<WebView, Uri>(
            nameof(Source),
            defaultValue: BlankUri!,
            defaultBindingMode: BindingMode.TwoWay);

    public static readonly DirectProperty<WebView, bool> CanGoBackProperty =
        AvaloniaProperty.RegisterDirect<WebView, bool>(nameof(CanGoBack), o => o.cangoback);

    public static readonly DirectProperty<WebView, bool> CanGoForwardProperty =
        AvaloniaProperty.RegisterDirect<WebView, bool>(nameof(CanGoForward), o => o.cangoforward);

    private static readonly Uri BlankUri = new("about:blank");

    private readonly AvaloniaCefBrowser view;
    private readonly WebViewCookieManager cookieManager;
    private readonly WebViewScripting scripting;
    private readonly Dictionary<string, object> jsObjectsByName = new(StringComparer.Ordinal);
    private readonly Dictionary<string, WebViewJsObjectInfo> jsInfosByName = new(StringComparer.Ordinal);

    private CefBrowser? cefBrowser;
    private bool isDisposed;
    private bool isDisposing;
    private bool isUpdatingSourceFromBrowser;
    private bool isApplyingSource;
    private int forwardedNavigationDepth;
    private bool cangoback;
    private bool cangoforward;

    public WebView()
    {
        view = new AvaloniaCefBrowser
        {
            RequestHandler = new RequestHandlerImpl(this),
            LifeSpanHandler = new LifeSpanHandlerImpl(this),
        };

        var cookiePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Cookies");
        if (!System.IO.Directory.Exists(cookiePath))
        {
            System.IO.Directory.CreateDirectory(cookiePath);
        }

        var contextSettings = new CefRequestContextSettings
        {
            CachePath = cookiePath,
            PersistSessionCookies = true // 持久化 Session Cookie
        };

        // 创建独立的上下文,不再使用 CefRequestContext.GetGlobalContext()
        var requestContext = CefRequestContext.CreateContext(contextSettings, null);
        var manager = requestContext.GetCookieManager(null);

        cookieManager = new WebViewCookieManager(manager);
        scripting = new WebViewScripting(view);

        Content = view;

        view.LoadStart += OnLoadStart;
        view.LoadEnd += OnLoadEnd;
        DetachedFromVisualTree += OnDetachedFromVisualTree;

        ApplySourceCore(Source);
        ResetNavState();
    }
    public Uri Source
    {
        get => GetValue(SourceProperty);
        set => SetValue(SourceProperty, value);
    }

    public bool CanGoBack
    {
        get => cangoback;
        private set => SetAndRaise(CanGoBackProperty, ref cangoback, value);
    }

    public bool CanGoForward
    {
        get => cangoforward;
        private set => SetAndRaise(CanGoForwardProperty, ref cangoforward, value);
    }

    public WebViewCookieManager CookieManager => cookieManager;
    public WebViewScripting Scripting => scripting;
    public IReadOnlyCollection<WebViewJsObjectInfo> JavascriptObjectInfos => [.. jsInfosByName.Values];

    public event EventHandler<WebViewRequestingEventArgs>? Requesting;
    public event EventHandler<WebViewRequestedEventArgs>? Requested;
    public event EventHandler<WebViewNavigationCompletedEventArgs>? NavigationCompleted;

    public void Navigate(string url)
    {
        if (string.IsNullOrWhiteSpace(url))
            return;

        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
            return;

        Navigate(uri);
    }

    public void Navigate(Uri uri)
    {
        ArgumentNullException.ThrowIfNull(uri);

        ExecuteOnUiThread(() =>
        {
            if (!CanUseControl())
                return;

            if (forwardedNavigationDepth == 0 && TryDispatchProgrammaticNavigation(uri))
                return;

            ApplySourceCore(uri);
        });
    }

    public bool GoBack() => ExecuteOnUiThread(GoBackCore);

    public bool GoForward() => ExecuteOnUiThread(GoForwardCore);

    public bool Refresh() => ExecuteOnUiThread(RefreshCore);

    public bool Stop() => ExecuteOnUiThread(StopCore);

    public void ExecuteScript(string script)
    {
        if (string.IsNullOrWhiteSpace(script))
            return;

        ExecuteOnUiThread(() =>
        {
            if (!CanUseControl())
                return;

            try
            {
                scripting.Execute(script);
            }
            catch
            {
            }
        });
    }

    public Task<T> EvaluateScriptAsync<T>(string script)
    {
        if (string.IsNullOrWhiteSpace(script))
            return Task.FromResult(default(T)!);

        if (!CanUseControl())
            return Task.FromResult(default(T)!);

        if (Dispatcher.UIThread.CheckAccess())
        {
            try
            {
                if (!CanUseControl())
                    return Task.FromResult(default(T)!);

                return scripting.Evaluate<T>(script);
            }
            catch
            {
                return Task.FromResult(default(T)!);
            }
        }

        var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);

        try
        {
            Dispatcher.UIThread.Post(async () =>
            {
                if (!CanUseControl())
                {
                    tcs.TrySetResult(default!);
                    return;
                }

                try
                {
                    var result = await scripting.Evaluate<T>(script).ConfigureAwait(false);
                    tcs.TrySetResult(result);
                }
                catch
                {
                    tcs.TrySetResult(default!);
                }
            });
        }
        catch
        {
            tcs.TrySetResult(default!);
        }

        return tcs.Task;
    }

    public void AddScriptObject(string name, object instance)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Name cannot be null or whitespace.", nameof(name));

        ArgumentNullException.ThrowIfNull(instance);

        ExecuteOnUiThread(() =>
        {
            if (!CanUseControl())
                return;

            if (jsObjectsByName.TryGetValue(name, out var existing))
            {
                if (ReferenceEquals(existing, instance))
                    return;

                throw new InvalidOperationException($"Duplicate JS name '{name}'.");
            }

            var info = CreateJsObjectInfo(name, instance);

            view.RegisterJavascriptObject(instance, name);

            jsObjectsByName[name] = instance;
            jsInfosByName[name] = info;
        });
    }

    public bool RemoveScriptObject(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
            return false;

        return ExecuteOnUiThread(() =>
        {
            if (!CanUseControl())
                return false;

            return RemoveScriptObjectCore(name);
        });
    }

    public void ClearScriptObjects()
    {
        ExecuteOnUiThread(() =>
        {
            if (!CanUseControl())
                return;

            ClearScriptObjectsCore();
        });
    }

    public void Dispose()
    {
        if (isDisposed || isDisposing)
            return;

        if (!Dispatcher.UIThread.CheckAccess())
        {
            Dispatcher.UIThread.Post(Dispose);
            return;
        }

        isDisposing = true;

        try
        {
            DetachedFromVisualTree -= OnDetachedFromVisualTree;
        }
        catch
        {
        }

        try
        {
            view.LoadStart -= OnLoadStart;
            view.LoadEnd -= OnLoadEnd;
        }
        catch
        {
        }

        try
        {
            ClearScriptObjectsCore();
        }
        catch
        {
        }

        try
        {
            view.Dispose();
        }
        catch
        {
        }

        cefBrowser = null;
        Content = null;
        isDisposed = true;
        isDisposing = false;
        ResetNavState();
    }

    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        base.OnPropertyChanged(change);

        if (change.Property != SourceProperty)
            return;

        if (!CanUseControl())
            return;

        if (isUpdatingSourceFromBrowser || isApplyingSource)
            return;

        var uri = change.GetNewValue<Uri>();

        if (uri is null)
            return;

        if (forwardedNavigationDepth == 0 && TryDispatchProgrammaticNavigation(uri))
            return;

        ApplySourceCore(uri);
    }

    private bool CanUseControl() => !isDisposed && !isDisposing;

    private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
    {
        Dispose();
    }

    private bool TryDispatchProgrammaticNavigation(Uri uri)
    {
        var args = CreateRequestedArgs(WebViewRequestKind.Navigation, uri, "GET", true);

        DispatchRequested(args);

        if (args.Target is not null)
        {
            NavigateForwarded(args.Target, args.Uri);
            return true;
        }

        return args.Cancel;
    }

    private void ApplySourceCore(Uri uri)
    {
        if (!CanUseControl())
            return;

        if (uri is null)
            return;

        isApplyingSource = true;

        try
        {
            view.Address = uri.ToString();
        }
        catch
        {
        }
        finally
        {
            isApplyingSource = false;
        }
    }

    private bool GoBackCore()
    {
        if (!TryGetUsableBrowser(out var browser))
            return false;

        try
        {
            if (!browser.CanGoBack)
            {
                UpdateNavState();
                return false;
            }

            browser.GoBack();
            UpdateNavState();
            return true;
        }
        catch
        {
            ResetNavState();
            return false;
        }
    }

    private bool GoForwardCore()
    {
        if (!TryGetUsableBrowser(out var browser))
            return false;

        try
        {
            if (!browser.CanGoForward)
            {
                UpdateNavState();
                return false;
            }

            browser.GoForward();
            UpdateNavState();
            return true;
        }
        catch
        {
            ResetNavState();
            return false;
        }
    }

    private bool RefreshCore()
    {
        if (!TryGetUsableBrowser(out var browser))
            return false;

        try
        {
            browser.Reload();
            return true;
        }
        catch
        {
            return false;
        }
    }

    private bool StopCore()
    {
        if (!TryGetUsableBrowser(out var browser))
            return false;

        try
        {
            browser.StopLoad();
            return true;
        }
        catch
        {
            return false;
        }
    }

    private bool TryGetUsableBrowser(out CefBrowser browser)
    {
        browser = null!;

        if (!CanUseControl())
            return false;

        if (cefBrowser is null)
            return false;

        browser = cefBrowser;
        return true;
    }

    private void UpdateNavState()
    {
        if (!TryGetUsableBrowser(out var browser))
        {
            ResetNavState();
            return;
        }

        try
        {
            CanGoBack = browser.CanGoBack;
            CanGoForward = browser.CanGoForward;
        }
        catch
        {
            ResetNavState();
        }
    }

    private void ResetNavState()
    {
        CanGoBack = false;
        CanGoForward = false;
    }

    private bool RemoveScriptObjectCore(string name)
    {
        if (!jsObjectsByName.ContainsKey(name))
            return false;

        TryUnregisterJavascriptObject(name);

        jsObjectsByName.Remove(name);
        jsInfosByName.Remove(name);
        return true;
    }

    private void ClearScriptObjectsCore()
    {
        if (jsObjectsByName.Count == 0)
            return;

        foreach (var name in jsObjectsByName.Keys.ToArray())
            TryUnregisterJavascriptObject(name);

        jsObjectsByName.Clear();
        jsInfosByName.Clear();
    }

    private void TryUnregisterJavascriptObject(string name)
    {
        try
        {
            view.UnregisterJavascriptObject(name);
        }
        catch
        {
        }
    }

    private void OnLoadStart(object? sender, LoadStartEventArgs e)
    {
        if (!ShouldHandleLoadEvent(e.Frame))
            return;

        cefBrowser = e.Frame.Browser;
        var uri = SafeUri(e.Frame.Url);

        Dispatcher.UIThread.Post(() =>
        {
            if (!CanUseControl())
                return;

            isUpdatingSourceFromBrowser = true;

            try
            {
                SetCurrentValue(SourceProperty, uri);
                UpdateNavState();
            }
            finally
            {
                isUpdatingSourceFromBrowser = false;
            }
        });
    }

    private void OnLoadEnd(object? sender, LoadEndEventArgs e)
    {
        if (!ShouldHandleLoadEvent(e.Frame))
            return;

        cefBrowser = e.Frame.Browser;
        var uri = SafeUri(e.Frame.Url);
        var statusCode = e.HttpStatusCode;
        var isSuccess = statusCode == 0 || statusCode is >= 200 and <= 399;
        var error = isSuccess ? null : $"HTTP {statusCode}";

        Dispatcher.UIThread.Post(() =>
        {
            if (!CanUseControl())
                return;

            UpdateNavState();
            NavigationCompleted?.Invoke(this, new WebViewNavigationCompletedEventArgs(uri, isSuccess, error, cookieManager, scripting));
        });
    }

    private static bool ShouldHandleLoadEvent(CefFrame frame)
    {
        if (!frame.IsMain)
            return false;

        if (frame.Browser.IsPopup)
            return false;

        return true;
    }

    private void DispatchRequested(WebViewRequestedEventArgs args)
    {
        if (!CanUseControl())
        {
            args.Cancel = true;
            return;
        }

        Requested?.Invoke(this, args);
    }

    private void RaiseRequesting(WebViewRequestingEventArgs args)
    {
        if (!CanUseControl())
        {
            args.Cancel = true;
            return;
        }

        Requesting?.Invoke(this, args);
    }

    private WebViewRequestedEventArgs CreateRequestedArgs(WebViewRequestKind kind, Uri uri, string method, bool isMainFrame, WebViewRequestHeaders? headers = null)
        => new(
            kind: kind,
            uri: uri,
            method: method,
            isMainFrame: isMainFrame,
            headers: headers ?? new WebViewRequestHeaders(),
            cookieManager: cookieManager,
            scripting: scripting);

    private WebViewRequestingEventArgs CreateRequestingArgs(WebViewRequestKind kind, Uri uri, string method, bool isMainFrame, WebViewRequestHeaders headers)
        => new(
            kind: kind,
            uri: uri,
            method: method,
            isMainFrame: isMainFrame,
            headers: headers,
            cookieManager: cookieManager,
            scripting: scripting);

    private static void TryApplyHeaders(CefRequest request, WebViewRequestHeaders headers)
    {
        try
        {
            request.SetHeaderMap(headers.Raw);
        }
        catch
        {
        }
    }

    private T ExecuteOnUiThread<T>(Func<T> func)
    {
        if (Dispatcher.UIThread.CheckAccess())
            return func();

        return Dispatcher.UIThread.InvokeAsync(func).GetAwaiter().GetResult();
    }

    private void ExecuteOnUiThread(Action action)
    {
        if (Dispatcher.UIThread.CheckAccess())
        {
            action();
            return;
        }

        Dispatcher.UIThread.InvokeAsync(action).GetAwaiter().GetResult();
    }

    private void InvokeOnUiThreadAndWait(Action action)
    {
        if (Dispatcher.UIThread.CheckAccess())
        {
            action();
            return;
        }

        Dispatcher.UIThread.InvokeAsync(action).GetAwaiter().GetResult();
    }

    private static WebViewJsObjectInfo CreateJsObjectInfo(string name, object instance)
    {
        var type = instance.GetType();

        var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public)
            .Where(m => !m.IsSpecialName)
            .Where(m => m.DeclaringType != typeof(object))
            .Select(m => m.Name)
            .Distinct(StringComparer.Ordinal)
            .OrderBy(x => x, StringComparer.Ordinal)
            .ToArray();

        return new WebViewJsObjectInfo
        {
            Name = name,
            Type = type,
            Methods = methods
        };
    }

    private static Uri SafeUri(string? url)
        => Uri.TryCreate(url, UriKind.Absolute, out var uri) ? uri : BlankUri;

    private static void NavigateLater(WebView target, Uri uri)
    {
        Dispatcher.UIThread.Post(() =>
        {
            if (!target.CanUseControl())
                return;

            target.Navigate(uri);
        });
    }

    private static void NavigateForwarded(WebView target, Uri uri)
    {
        Dispatcher.UIThread.Post(() =>
        {
            if (!target.CanUseControl())
                return;

            target.forwardedNavigationDepth++;

            try
            {
                target.Navigate(uri);
            }
            finally
            {
                target.forwardedNavigationDepth--;
            }
        });
    }
}

WebViewCookieManager.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xilium.CefGlue;

namespace Shares.Avalonia.CustomControls.WebView;

public sealed class WebViewCookieManager
{
    private readonly CefCookieManager manager;

    internal WebViewCookieManager(CefCookieManager manager) => this.manager = manager;

    public async Task<CefCookie[]> GetCookiesAsync(Uri uri, bool includeHttpOnly = true)
    {
        var collector = new CookieCollector();
        var ok = manager.VisitUrlCookies(uri.ToString(), includeHttpOnly, collector);

        if (!ok)
            return [];

        return await collector.Task.ConfigureAwait(false);
    }

    public Task<bool> SetCookieAsync(Uri uri, CefCookie cookie)
    {
        var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        var ok = manager.SetCookie(uri.ToString(), cookie, new SetCookieCallback(v => tcs.TrySetResult(v)));

        if (!ok)
            tcs.TrySetResult(false);

        return tcs.Task;
    }

    public Task<int> DeleteCookiesAsync(Uri uri, string? cookieName = null)
    {
        var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
        var ok = manager.DeleteCookies(uri.ToString(), cookieName ?? string.Empty, new DeleteCookiesCallback(v => tcs.TrySetResult(v)));

        if (!ok)
            tcs.TrySetResult(0);

        return tcs.Task;
    }

    private sealed class CookieCollector : CefCookieVisitor
    {
        private readonly TaskCompletionSource<CefCookie[]> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
        private readonly List<CefCookie> cookies = [];
        private bool completed;

        public Task<CefCookie[]> Task => tcs.Task;

        protected override bool Visit(CefCookie cookie, int count, int total, out bool deleteCookie)
        {
            deleteCookie = false;

            if (completed)
                return false;

            cookies.Add(cookie);

            if (count >= total - 1)
                Complete();

            return true;
        }

        protected override void Dispose(bool disposing)
        {
            Complete();
            base.Dispose(disposing);
        }

        private void Complete()
        {
            if (completed)
                return;

            completed = true;
            tcs.TrySetResult([.. cookies]);
        }
    }

    private sealed class SetCookieCallback(Action<bool> action) : CefSetCookieCallback
    {
        protected override void OnComplete(bool success) => action(success);
    }

    private sealed class DeleteCookiesCallback(Action<int> action) : CefDeleteCookiesCallback
    {
        protected override void OnComplete(int numDeleted) => action(numDeleted);
    }
}

WebViewHandlers.cs

using Avalonia.Threading;
using Xilium.CefGlue;
using Xilium.CefGlue.Common.Handlers;

namespace Shares.Avalonia.CustomControls.WebView;

public sealed partial class WebView
{
    private sealed class LifeSpanHandlerImpl(WebView owner) : LifeSpanHandler
    {
        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)
        {
            var uri = SafeUri(targetUrl);

            var args = owner.CreateRequestedArgs(
                kind: WebViewRequestKind.Popup,
                uri: uri,
                method: "GET",
                isMainFrame: true);

            owner.InvokeOnUiThreadAndWait(() => owner.DispatchRequested(args));

            if (args.Target is not null)
            {
                NavigateForwarded(args.Target, args.Uri);
                return true;
            }

            if (!args.Cancel)
                NavigateLater(owner, args.Uri);

            return true;
        }
    }

    private sealed class RequestHandlerImpl(WebView owner) : RequestHandler
    {
        protected override CefResourceRequestHandler GetResourceRequestHandler(
            CefBrowser browser,
            CefFrame frame,
            CefRequest request,
            bool isNavigation,
            bool isDownload,
            string requestInitiator,
            ref bool disableDefaultHandling)
        {
            return new ResourceRequestHandlerImpl(owner);
        }

        protected override bool OnBeforeBrowse(CefBrowser browser, CefFrame frame, CefRequest request, bool userGesture, bool isRedirect)
        {
            if (!frame.IsMain)
                return false;

            var uri = SafeUri(request.Url);
            var method = request.Method ?? "GET";
            var headers = new WebViewRequestHeaders(request.GetHeaderMap());

            var args = owner.CreateRequestedArgs(
                kind: WebViewRequestKind.Navigation,
                uri: uri,
                method: method,
                isMainFrame: true,
                headers: headers);

            owner.InvokeOnUiThreadAndWait(() => owner.DispatchRequested(args));

            if (args.Target is not null)
            {
                NavigateForwarded(args.Target, args.Uri);
                return true;
            }

            if (args.Cancel)
                return true;

            TryApplyHeaders(request, headers);
            return false;
        }
    }

    private sealed class ResourceRequestHandlerImpl(WebView owner) : CefResourceRequestHandler
    {
        protected override CefCookieAccessFilter GetCookieAccessFilter(CefBrowser browser, CefFrame frame, CefRequest request)
            => null!;

        protected override CefReturnValue OnBeforeResourceLoad(CefBrowser browser, CefFrame frame, CefRequest request, CefCallback callback)
        {
            var uri = SafeUri(request.Url);
            var method = request.Method ?? "GET";
            var kind = frame.IsMain ? WebViewRequestKind.Navigation : WebViewRequestKind.Resource;
            var headers = new WebViewRequestHeaders(request.GetHeaderMap());

            var args = owner.CreateRequestingArgs(
                kind: kind,
                uri: uri,
                method: method,
                isMainFrame: frame.IsMain,
                headers: headers);

            if (Dispatcher.UIThread.CheckAccess())
            {
                owner.RaiseRequesting(args);

                if (args.Cancel)
                    return CefReturnValue.Cancel;

                TryApplyHeaders(request, headers);
                return CefReturnValue.Continue;
            }

            Dispatcher.UIThread.Post(() =>
            {
                try
                {
                    owner.RaiseRequesting(args);

                    if (!args.Cancel)
                        TryApplyHeaders(request, headers);
                }
                catch
                {
                }
                finally
                {
                    try
                    {
                        if (args.Cancel)
                            callback?.Cancel();
                        else
                            callback?.Continue();
                    }
                    catch
                    {
                    }
                }
            });

            return CefReturnValue.ContinueAsync;
        }
    }
}

WebViewJsObjectInfo.cs

using System;

namespace Shares.Avalonia.CustomControls.WebView;

public sealed class WebViewJsObjectInfo
{
    public required string Name { get; init; }
    public required Type Type { get; init; }
    public required string[] Methods { get; init; }
}

WebViewNavigationCompletedEventArgs.cs

using System;

namespace Shares.Avalonia.CustomControls.WebView;

public sealed class WebViewNavigationCompletedEventArgs(
    Uri uri,
    bool isSuccess,
    string? error,
    WebViewCookieManager cookieManager,
    WebViewScripting scripting) : EventArgs
{
    public Uri Uri { get; } = uri;
    public bool IsSuccess { get; } = isSuccess;
    public string? Error { get; } = error;
    public WebViewCookieManager CookieManager { get; } = cookieManager;
    public WebViewScripting Scripting { get; } = scripting;
}

WebViewRequestedEventArgs.cs

using System;

namespace Shares.Avalonia.CustomControls.WebView;

public sealed class WebViewRequestedEventArgs : WebViewRequestingEventArgs
{
    internal WebViewRequestedEventArgs(
        WebViewRequestKind kind,
        Uri uri,
        string method,
        bool isMainFrame,
        WebViewRequestHeaders headers,
        WebViewCookieManager cookieManager,
        WebViewScripting scripting)
        : base(kind, uri, method, isMainFrame, headers, cookieManager, scripting)
    {
    }

    internal WebView? Target { get; private set; }

    public void OpenIn(WebView target) => Target = target ?? throw new ArgumentNullException(nameof(target));
}

WebViewRequestHeaders.cs

using System;
using System.Collections.Specialized;

namespace Shares.Avalonia.CustomControls.WebView;

public sealed class WebViewRequestHeaders
{
    private readonly NameValueCollection headers;

    public WebViewRequestHeaders()
        : this(new NameValueCollection(StringComparer.OrdinalIgnoreCase))
    {
    }

    internal WebViewRequestHeaders(NameValueCollection headers) => this.headers = headers;

    public NameValueCollection Raw => headers;

    public string? GetSingle(string name) => headers[name];

    public void SetSingle(string name, string value) => headers[name] = value;

    public void Remove(string name) => headers.Remove(name);
}

WebViewRequestingEventArgs.cs

using System;

namespace Shares.Avalonia.CustomControls.WebView;

public class WebViewRequestingEventArgs : EventArgs
{
    internal WebViewRequestingEventArgs(
        WebViewRequestKind kind,
        Uri uri,
        string method,
        bool isMainFrame,
        WebViewRequestHeaders headers,
        WebViewCookieManager cookieManager,
        WebViewScripting scripting)
    {
        Kind = kind;
        Uri = uri;
        Method = method;
        IsMainFrame = isMainFrame;
        Headers = headers;
        CookieManager = cookieManager;
        Scripting = scripting;
    }

    public WebViewRequestKind Kind { get; }
    public Uri Uri { get; }
    public string Method { get; }
    public bool IsMainFrame { get; }
    public WebViewRequestHeaders Headers { get; }
    public WebViewCookieManager CookieManager { get; }
    public WebViewScripting Scripting { get; }

    public bool Cancel { get; set; }
}

WebViewRequestKind.cs

namespace Shares.Avalonia.CustomControls.WebView;

public enum WebViewRequestKind
{
    Navigation,
    Resource,
    Popup
}

WebViewScripting.cs

using System.Threading.Tasks;
using Xilium.CefGlue.Avalonia;

namespace Shares.Avalonia.CustomControls.WebView;

public sealed class WebViewScripting
{
    private readonly AvaloniaCefBrowser browser;

    internal WebViewScripting(AvaloniaCefBrowser browser) => this.browser = browser;

    public void Execute(string script) => browser.ExecuteJavaScript(script);

    public Task<T> Evaluate<T>(string script) => browser.EvaluateJavaScript<T>(script);
}