File: System\Windows\Forms\Controls\WebBrowser\HtmlShimManager.cs
Web Access
Project: ..\..\..\src\winforms\src\System.Windows.Forms\System.Windows.Forms.csproj (System.Windows.Forms)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Windows.Win32.Web.MsHtml;
 
namespace System.Windows.Forms;
 
/// <summary>
///  HtmlShimManager - this class manages the shims for HtmlWindows, HtmlDocuments, and HtmlElements.
///  essentially we need a long-lasting object to call back on events from the web browser, and the
///  manager is the one in charge of making sure this list stays around as long as needed.
///
///  When a HtmlWindow unloads we prune our list of corresponding document, window, and element shims.
/// </summary>
internal sealed class HtmlShimManager : IDisposable
{
    private Dictionary<HtmlWindow, HtmlWindow.HtmlWindowShim>? _htmlWindowShims;
    private Dictionary<HtmlElement, HtmlElement.HtmlElementShim>? _htmlElementShims;
    private Dictionary<HtmlDocument, HtmlDocument.HtmlDocumentShim>? _htmlDocumentShims;
    private bool _disposed;
 
    internal HtmlShimManager()
    {
    }
 
    /// <summary>
    ///  Adds a <see cref="HtmlDocument.HtmlDocumentShim"/> to list of shims to manage.
    ///  Can create a WindowShim as a side effect so it knows when to self prune from the list.
    /// </summary>
    public void AddDocumentShim(HtmlDocument doc)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        ObjectDisposedException.ThrowIf(doc.NativeHtmlDocument2.IsDisposed, doc);
        HtmlDocument.HtmlDocumentShim? shim = null;
 
        if (_htmlDocumentShims is null)
        {
            _htmlDocumentShims = [];
            shim = new HtmlDocument.HtmlDocumentShim(doc);
            _htmlDocumentShims[doc] = shim;
        }
        else if (!_htmlDocumentShims.ContainsKey(doc))
        {
            shim = new HtmlDocument.HtmlDocumentShim(doc);
            _htmlDocumentShims[doc] = shim;
        }
 
        if (shim is not null)
        {
            OnShimAdded(shim);
        }
    }
 
    /// <summary>
    ///  Adds a <see cref="HtmlWindow.HtmlWindowShim"/> to list of shims to manage.
    /// </summary>
    public void AddWindowShim(HtmlWindow window)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        ObjectDisposedException.ThrowIf(window.NativeHtmlWindow.IsDisposed, window);
        HtmlWindow.HtmlWindowShim? shim = null;
        if (_htmlWindowShims is null)
        {
            _htmlWindowShims = [];
            shim = new HtmlWindow.HtmlWindowShim(window);
            _htmlWindowShims[window] = shim;
        }
        else if (!_htmlWindowShims.ContainsKey(window))
        {
            shim = new HtmlWindow.HtmlWindowShim(window);
            _htmlWindowShims[window] = shim;
        }
 
        // Reuse an existing shim as well: application subscriptions may have created it first.
        (shim ?? _htmlWindowShims[window]).EnsureWindowObservation();
    }
 
    /// <summary> AddElementShim - adds a HtmlDocumentShim to list of shims to manage
    ///  Can create a WindowShim as a side effect so it knows when to self prune from the list.
    /// </summary>
    public void AddElementShim(HtmlElement element)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        ObjectDisposedException.ThrowIf(element.NativeHtmlElement.IsDisposed, element);
        HtmlElement.HtmlElementShim? shim = null;
 
        if (_htmlElementShims is null)
        {
            _htmlElementShims = [];
            shim = new HtmlElement.HtmlElementShim(element);
            _htmlElementShims[element] = shim;
        }
        else if (!_htmlElementShims.ContainsKey(element))
        {
            shim = new HtmlElement.HtmlElementShim(element);
            _htmlElementShims[element] = shim;
        }
 
        if (shim is not null)
        {
            OnShimAdded(shim);
        }
    }
 
    internal HtmlDocument.HtmlDocumentShim? GetDocumentShim(HtmlDocument document)
    {
        // Dictionary equality queries COM identity. A revoked wrapper can share a cached hash with
        // a live replacement after navigation, so reject it before the comparer queries its dead registration.
        if (_htmlDocumentShims is null || document.NativeHtmlDocument2.IsDisposed)
        {
            return null;
        }
 
        if (_htmlDocumentShims.TryGetValue(document, out HtmlDocument.HtmlDocumentShim? value))
        {
            return value;
        }
 
        return null;
    }
 
    internal HtmlElement.HtmlElementShim? GetElementShim(HtmlElement element)
    {
        if (_htmlElementShims is null || element.NativeHtmlElement.IsDisposed)
        {
            return null;
        }
 
        if (_htmlElementShims.TryGetValue(element, out HtmlElement.HtmlElementShim? elementShim))
        {
            return elementShim;
        }
 
        return null;
    }
 
    internal HtmlWindow.HtmlWindowShim? GetWindowShim(HtmlWindow window)
    {
        if (_htmlWindowShims is null || window.NativeHtmlWindow.IsDisposed)
        {
            return null;
        }
 
        if (_htmlWindowShims.TryGetValue(window, out HtmlWindow.HtmlWindowShim? windowShim))
        {
            return windowShim;
        }
 
        return null;
    }
 
    private unsafe void OnShimAdded(HtmlShim addedShim)
    {
        IHTMLWindow2.Interface? associatedWindow = addedShim.AssociatedWindow;
        if (associatedWindow is null)
        {
            return;
        }
 
        using var nativeWindow = ComHelpers.GetComScope<IHTMLWindow2>(associatedWindow);
        if (_htmlWindowShims is not null)
        {
            foreach (var (window, shim) in _htmlWindowShims)
            {
                if (window.NativeHtmlWindow.IsSameNativeObject(nativeWindow.Value))
                {
                    shim.EnsureWindowObservation();
                    return;
                }
            }
        }
 
        // Only create a wrapper when the manager needs a new owner. A discarded duplicate wrapper
        // would leave another GIT registration waiting for finalization on every shim addition.
        AddWindowShim(new HtmlWindow(this, ComHelpers.GetComPointer<IHTMLWindow2>(associatedWindow)));
    }
 
    /// <summary>
    ///  HtmlWindowShim calls back on us when it has unloaded the page. At this point we need to
    ///  walk through our lists and make sure we've cleaned up
    /// </summary>
    internal void OnWindowUnloaded(HtmlWindow unloadedWindow)
    {
        if (_disposed)
        {
            return;
        }
 
        List<HtmlShim> unloadedShims = [];
        if (_htmlDocumentShims is not null)
        {
            foreach (HtmlDocument.HtmlDocumentShim shim in _htmlDocumentShims.Values.ToArray())
            {
                if (IsAssociatedWindow(shim, unloadedWindow))
                {
                    _htmlDocumentShims.Remove(shim.Document);
                    unloadedShims.Add(shim);
                }
            }
        }
 
        if (_htmlElementShims is not null)
        {
            foreach (HtmlElement.HtmlElementShim shim in _htmlElementShims.Values.ToArray())
            {
                if (IsAssociatedWindow(shim, unloadedWindow))
                {
                    _htmlElementShims.Remove(shim.Element);
                    unloadedShims.Add(shim);
                }
            }
        }
 
        if (_htmlWindowShims is not null
            && _htmlWindowShims.Remove(unloadedWindow, out HtmlWindow.HtmlWindowShim? windowShim))
        {
            unloadedShims.Add(windowShim);
        }
 
        // Remove all affected owners before native releases can reenter the manager. Attempt every
        // disposal even if one detach fails, without disturbing another frame's live subscriptions.
        HtmlShim.DisposeAll(unloadedShims);
    }
 
    private static unsafe bool IsAssociatedWindow(HtmlShim shim, HtmlWindow window)
    {
        IHTMLWindow2.Interface? associatedWindow = shim.AssociatedWindow;
        if (associatedWindow is null)
        {
            return false;
        }
 
        // RCWs and AgileComPointer wrappers are not comparable; COM identity must be queried
        // in the same apartment or the unloaded page's shims will never be removed.
        using var nativeWindow = ComHelpers.GetComScope<IHTMLWindow2>(associatedWindow);
        return window.NativeHtmlWindow.IsSameNativeObject(nativeWindow.Value);
    }
 
    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }
 
        _disposed = true;
        List<HtmlShim> shims = [];
        if (_htmlElementShims is not null)
        {
            shims.AddRange(_htmlElementShims.Values);
        }
 
        if (_htmlDocumentShims is not null)
        {
            shims.AddRange(_htmlDocumentShims.Values);
        }
 
        if (_htmlWindowShims is not null)
        {
            shims.AddRange(_htmlWindowShims.Values);
        }
 
        // A retained browser/wrapper must not retain disposed element shims through the manager.
        // Clear before releasing COM objects, which may call back into this manager.
        _htmlElementShims = null;
        _htmlDocumentShims = null;
        _htmlWindowShims = null;
        HtmlShim.DisposeAll(shims);
    }
}