| File: System\Windows\Forms\Controls\WebBrowser\HtmlShim.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 System.ComponentModel; using System.Runtime.ExceptionServices; using Windows.Win32.Web.MsHtml; namespace System.Windows.Forms; /// <summary> /// Owns the native event connections and managed delegates for an HTML wrapper. /// </summary> /// <remarks> /// <para> /// Connection-point events and individually attached dispatch proxies have independent lifetimes. /// Removing the last standard handler must not detach proxies registered through AttachEventHandler. /// </para> /// </remarks> internal abstract class HtmlShim : IDisposable { private EventHandlerList? _events; private int _eventCount; private List<(EventHandler Handler, HtmlToClrEventProxy Proxy)>? _attachedEventList; internal bool IsDisposed { get; private set; } protected HtmlShim() { } private EventHandlerList Events => _events ??= new EventHandlerList(); public void AttachEventHandler(string eventName, EventHandler eventHandler) { ObjectDisposedException.ThrowIf(IsDisposed, this); ArgumentNullException.ThrowIfNull(eventHandler); HtmlToClrEventProxy proxy = new(eventName, eventHandler); if (!AttachEventProxy(proxy)) { // MSHTML rejects some names (including null) with S_OK/false. Preserve the existing // no-op behavior, but do not retain a proxy for a registration that never happened. Debug.WriteLine("The native HTML object did not attach the event."); return; } // A native call can reenter disposal. Do not leave a new connection outside the disposed owner. if (IsDisposed) { DetachEventProxy(proxy); throw new ObjectDisposedException(GetType().Name); } // Each attach creates a distinct native dispatch identity, even for the same name and delegate. // Retaining only one proxy loses the information needed to detach the earlier registrations. (_attachedEventList ??= []).Add((eventHandler, proxy)); } protected abstract bool AttachEventProxy(HtmlToClrEventProxy proxy); protected abstract void DetachEventProxy(HtmlToClrEventProxy proxy); public void AddHandler(object key, Delegate? value) { ObjectDisposedException.ThrowIf(IsDisposed, this); if (value is null) { return; } _eventCount += value.GetInvocationList().Length; Events.AddHandler(key, value); OnEventHandlerAdded(); } public void DetachEventHandler(string eventName, EventHandler eventHandler) { if (_attachedEventList is not { } registrations) { return; } for (int index = registrations.Count - 1; index >= 0; index--) { var registration = registrations[index]; if (registration.Handler != eventHandler || registration.Proxy.EventName != eventName) { continue; } // Remove before the COM call so a reentrant detach cannot select the same proxy. registrations.RemoveAt(index); try { DetachEventProxy(registration.Proxy); } catch (Exception exception) when (!exception.IsCriticalException()) { if (!IsDisposed) { // A failed detach must remain owned and retryable, not become an untracked native sink. (_attachedEventList ??= []).Insert(Math.Min(index, _attachedEventList.Count), registration); } throw; } return; } } public abstract IHTMLWindow2.Interface? AssociatedWindow { get; } /// create connectionpoint cookie public abstract void ConnectToEvents(); public abstract void DisconnectFromEvents(); /// return the sender for events, usually the HtmlWindow, HtmlElement, HtmlDocument protected abstract object GetEventSender(); public void Dispose() { if (IsDisposed) { return; } IsDisposed = true; Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { var registrations = _attachedEventList; _attachedEventList = null; _events?.Dispose(); _events = null; _eventCount = 0; List<Exception>? exceptions = null; try { DisconnectFromEvents(); } catch (Exception exception) when (!exception.IsCriticalException()) { (exceptions ??= []).Add(exception); } if (registrations is not null) { foreach (var registration in registrations) { try { DetachEventProxy(registration.Proxy); } catch (Exception exception) when (!exception.IsCriticalException()) { (exceptions ??= []).Add(exception); } } } // Attempt every independent release before reporting failures; one failed native detach // must not leave unrelated event sources and their subscribers connected. ThrowCleanupExceptions(exceptions); } } public void FireEvent(object key, EventArgs e) { Delegate? delegateToInvoke = _events?[key]; if (delegateToInvoke is not null) { try { delegateToInvoke.DynamicInvoke(GetEventSender(), e); } catch (Exception ex) { // Note: this check is for the debugger, so we can catch exceptions in the debugger instead of // throwing a thread exception. if (NativeWindow.WndProcShouldBeDebuggable) { throw; } else { Application.OnThreadException(ex); } } } } protected virtual void OnEventHandlerAdded() { ConnectToEvents(); } protected virtual void OnEventHandlerRemoved() { if (_eventCount <= 0) { DisconnectFromEvents(); _eventCount = 0; } } public void RemoveHandler(object key, Delegate? value) { if (_events is null || value is null) { return; } int before = _events[key]?.GetInvocationList().Length ?? 0; _events.RemoveHandler(key, value); int removed = before - (_events[key]?.GetInvocationList().Length ?? 0); if (removed > 0) { // Unmatched -= is a no-op. Counting it (or counting a multicast delegate as one) // disconnects the native source while valid handlers are still stored in EventHandlerList. _eventCount -= removed; OnEventHandlerRemoved(); } } internal static void DisposeAll(IEnumerable<HtmlShim> shims) { List<Exception>? exceptions = null; foreach (HtmlShim shim in shims) { try { shim.Dispose(); } catch (Exception exception) when (!exception.IsCriticalException()) { (exceptions ??= []).Add(exception); } } ThrowCleanupExceptions(exceptions); } private static void ThrowCleanupExceptions(List<Exception>? exceptions) { if (exceptions is { Count: 1 }) { ExceptionDispatchInfo.Throw(exceptions[0]); } if (exceptions is { Count: > 1 }) { throw new AggregateException(exceptions); } } }