From 90d5f661f7d4229fb760349b913ecc57087bbd1e Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Wed, 9 Feb 2022 22:03:22 +0600 Subject: [PATCH 1/3] Use ComWrappers for AutoComplete sources --- .../src/Interop/Interop.CLSID.cs | 5 +- .../src/Interop/Interop.IID.cs | 6 ++ .../Interop/Shell32/Interop.IAutoComplete2.cs | 37 -------- ...WinFormsComWrappers.AutoCompleteWrapper.cs | 44 ++++++++++ .../WinFormsComWrappers.IEnumStringVtbl.cs | 84 +++++++++++++++++++ .../src/Interop/WinFormsComWrappers.cs | 29 +++++++ .../src/System/Windows/Forms/StringSource.cs | 50 ++++------- 7 files changed, 182 insertions(+), 73 deletions(-) delete mode 100644 src/System.Windows.Forms.Primitives/src/Interop/Shell32/Interop.IAutoComplete2.cs create mode 100644 src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.AutoCompleteWrapper.cs create mode 100644 src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.IEnumStringVtbl.cs diff --git a/src/System.Windows.Forms.Primitives/src/Interop/Interop.CLSID.cs b/src/System.Windows.Forms.Primitives/src/Interop/Interop.CLSID.cs index b02a17be427..e54f65360ee 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/Interop.CLSID.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/Interop.CLSID.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -6,6 +6,9 @@ internal partial class Interop { internal static class CLSID { + // 00BB2763-6A77-11D0-A535-00C04FD7D062 + internal static Guid AutoComplete = new Guid(0x00BB2763, 0x6A77, 0x11D0, 0xA5, 0x35, 0x00, 0xC0, 0x4F, 0xD7, 0xD0, 0x62); + // C0B4E2F3-BA21-4773-8DBA-335EC946EB8B internal static Guid FileSaveDialog = new Guid(0xC0B4E2F3, 0xBA21, 0x4773, 0x8D, 0xBA, 0x33, 0x5E, 0xC9, 0x46, 0xEB, 0x8B); diff --git a/src/System.Windows.Forms.Primitives/src/Interop/Interop.IID.cs b/src/System.Windows.Forms.Primitives/src/Interop/Interop.IID.cs index c104ce58bc8..3d3ce3a36ce 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/Interop.IID.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/Interop.IID.cs @@ -9,6 +9,12 @@ internal static class IID // 618736E0-3C3D-11CF-810C-00AA00389B71 public static Guid IAccessible = new Guid(0x618736E0, 0x3C3D, 0x11CF, 0x81, 0x0C, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71); + // EAC04BC0-3791-11D2-BB95-0060977B464C + public static Guid IAutoComplete2 { get; } = new(0xEAC04BC0, 0x3791, 0x11D2, 0xBB, 0x95, 0x00, 0x60, 0x97, 0x7B, 0x46, 0x4C); + + // 00000101-0000-0000-C000-000000000046 + public static Guid IEnumString { get; } = new(0x00000101, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + // E6FDD21A-163F-4975-9C8C-A69F1BA37034 internal static Guid IFileDialogCustomize { get; } = new(0xE6FDD21A, 0x163F, 0x4975, 0x9C, 0x8C, 0xA6, 0x9F, 0x1B, 0xA3, 0x70, 0x34); diff --git a/src/System.Windows.Forms.Primitives/src/Interop/Shell32/Interop.IAutoComplete2.cs b/src/System.Windows.Forms.Primitives/src/Interop/Shell32/Interop.IAutoComplete2.cs deleted file mode 100644 index 90c48475c28..00000000000 --- a/src/System.Windows.Forms.Primitives/src/Interop/Shell32/Interop.IAutoComplete2.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; - -internal static partial class Interop -{ - internal static partial class Shell32 - { - [ComImport] - [Guid("EAC04BC0-3791-11d2-BB95-0060977B464C")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public unsafe interface IAutoComplete2 - { - [PreserveSig] - HRESULT Init( - IntPtr hwndEdit, - IEnumString punkACL, - [MarshalAs(UnmanagedType.LPWStr)] string pwszRegKeyPath, - [MarshalAs(UnmanagedType.LPWStr)] string pwszQuickComplete); - - [PreserveSig] - HRESULT Enable( - BOOL fEnable); - - [PreserveSig] - HRESULT SetOptions( - AUTOCOMPLETEOPTIONS dwFlag); - - [PreserveSig] - HRESULT GetOptions( - AUTOCOMPLETEOPTIONS* pdwFlag); - } - } -} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.AutoCompleteWrapper.cs b/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.AutoCompleteWrapper.cs new file mode 100644 index 00000000000..0ebde6ca1e2 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.AutoCompleteWrapper.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Windows.Forms; + +internal partial class Interop +{ + internal unsafe partial class WinFormsComWrappers + { + internal class AutoCompleteWrapper + { + private IntPtr _wrappedInstance; + + public AutoCompleteWrapper(IntPtr wrappedInstance) + { + _wrappedInstance = wrappedInstance.OrThrowIfZero(); + } + + internal IntPtr Instance => _wrappedInstance; + + public void Dispose() + { + Marshal.Release(_wrappedInstance); + _wrappedInstance = IntPtr.Zero; + } + + public HRESULT Init(IntPtr hwndEdit, IEnumString punkACL, IntPtr pwszRegKeyPath, IntPtr pwszQuickComplete) + { + var punkACLPtr = WinFormsComWrappers.Instance.GetOrCreateComInterfaceForObject(punkACL, CreateComInterfaceFlags.None); + return ((delegate* unmanaged)(*(*(void***)_wrappedInstance + 3))) + (_wrappedInstance, hwndEdit, punkACLPtr, pwszRegKeyPath, pwszQuickComplete); + } + + public HRESULT SetOptions(Shell32.AUTOCOMPLETEOPTIONS dwFlag) + { + return ((delegate* unmanaged)(*(*(void***)_wrappedInstance + 5))) + (_wrappedInstance, dwFlag); + } + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.IEnumStringVtbl.cs b/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.IEnumStringVtbl.cs new file mode 100644 index 00000000000..0d129a46969 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.IEnumStringVtbl.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; + +internal partial class Interop +{ + internal unsafe partial class WinFormsComWrappers + { + internal static class IEnumStringVtbl + { + public static IntPtr Create(IntPtr fpQueryInterface, IntPtr fpAddRef, IntPtr fpRelease) + { + IntPtr* vtblRaw = (IntPtr*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(IEnumStringVtbl), IntPtr.Size * 7); + vtblRaw[0] = fpQueryInterface; + vtblRaw[1] = fpAddRef; + vtblRaw[2] = fpRelease; + vtblRaw[3] = (IntPtr)(delegate* unmanaged)&Next; + vtblRaw[4] = (IntPtr)(delegate* unmanaged)&Skip; + vtblRaw[5] = (IntPtr)(delegate* unmanaged)&Reset; + vtblRaw[6] = (IntPtr)(delegate* unmanaged)&Clone; + + return (IntPtr)vtblRaw; + } + + [UnmanagedCallersOnly] + private static int Next(IntPtr thisPtr, int celt, IntPtr* rgelt, int* pceltFetched) + { + IEnumString inst = ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr); + string[] elt = new string[celt]; + var result = inst.Next(celt, elt, (IntPtr)pceltFetched); + for (var i = 0; i < *pceltFetched; i++) + { + rgelt[i] = Marshal.StringToCoTaskMemUni(elt[i]); + } + + return result; + } + + [UnmanagedCallersOnly] + private static int Skip(IntPtr thisPtr, int celt) + { + IEnumString inst = ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr); + return inst.Skip(celt); + } + + [UnmanagedCallersOnly] + private static int Reset(IntPtr thisPtr) + { + try + { + IEnumString inst = ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr); + inst.Reset(); + } + catch (Exception ex) + { + return ex.HResult; + } + + return S_OK; + } + + [UnmanagedCallersOnly] + private static int Clone(IntPtr thisPtr, IntPtr* ppenum) + { + try + { + IEnumString inst = ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr); + inst.Clone(out var cloned); + *ppenum = WinFormsComWrappers.Instance.GetOrCreateComInterfaceForObject(cloned, CreateComInterfaceFlags.None); + } + catch (Exception ex) + { + return ex.HResult; + } + + return S_OK; + } + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs b/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs index a678d27c163..c21ef6e0e28 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; internal partial class Interop { @@ -19,6 +20,7 @@ internal unsafe partial class WinFormsComWrappers : ComWrappers private const int S_OK = (int)Interop.HRESULT.S_OK; private static readonly ComInterfaceEntry* s_streamEntry = InitializeIStreamEntry(); private static readonly ComInterfaceEntry* s_fileDialogEventsEntry = InitializeIFileDialogEventsEntry(); + private static readonly ComInterfaceEntry* s_enumStringEntry = InitializeIEnumStringEntry(); internal static WinFormsComWrappers Instance { get; } = new WinFormsComWrappers(); @@ -48,6 +50,18 @@ private WinFormsComWrappers() { } return wrapperEntry; } + private static ComInterfaceEntry* InitializeIEnumStringEntry() + { + GetIUnknownImpl(out IntPtr fpQueryInterface, out IntPtr fpAddRef, out IntPtr fpRelease); + + IntPtr iEnumStringVtbl = IEnumStringVtbl.Create(fpQueryInterface, fpAddRef, fpRelease); + + ComInterfaceEntry* wrapperEntry = (ComInterfaceEntry*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(WinFormsComWrappers), sizeof(ComInterfaceEntry)); + wrapperEntry->IID = IID.IEnumString; + wrapperEntry->Vtable = iEnumStringVtbl; + return wrapperEntry; + } + protected override unsafe ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count) { if (obj is Interop.Ole32.IStream) @@ -62,6 +76,12 @@ private WinFormsComWrappers() { } return s_fileDialogEventsEntry; } + if (obj is IEnumString) + { + count = 1; + return s_enumStringEntry; + } + throw new NotImplementedException($"ComWrappers for type {obj.GetType()} not implemented."); } @@ -109,6 +129,14 @@ protected override object CreateObject(IntPtr externalComObject, CreateObjectFla return new ShellItemArrayWrapper(shellItemArrayComObject); } + Guid autoCompleteIID = IID.IAutoComplete2; + hr = Marshal.QueryInterface(externalComObject, ref autoCompleteIID, out IntPtr autoCompleteComObject); + if (hr == S_OK) + { + Marshal.Release(externalComObject); + return new AutoCompleteWrapper(autoCompleteComObject); + } + throw new NotImplementedException(); } @@ -143,6 +171,7 @@ private IntPtr GetOrCreateComInterfaceForObject(object obj) { ShellItemWrapper siw => siw.Instance, FileOpenDialogWrapper fodw => fodw.Instance, + FileSaveDialogWrapper fsdw => fsdw.Instance, _ => GetOrCreateComInterfaceForObject(obj, CreateComInterfaceFlags.None), }; } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs b/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs index 3fb25d934b7..35fd697b16f 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using static Interop; @@ -11,7 +9,7 @@ namespace System.Windows.Forms { /// - /// Represents an internal class that is used bu ComboBox and TextBox AutoCompleteCustomSource property. + /// Represents an internal class that is used by ComboBox and TextBox AutoCompleteCustomSource property. /// This class is responsible for initializing the SHAutoComplete COM object and setting options in it. /// The StringSource contains an array of Strings which is passed to the COM object as the custom source. /// @@ -20,41 +18,29 @@ internal class StringSource : IEnumString private string[] strings; private int current; private int size; - private Shell32.IAutoComplete2 _autoCompleteObject2; - - /// - /// SHAutoComplete COM object CLSID. - /// - private static Guid autoCompleteClsid = new Guid("{00BB2763-6A77-11D0-A535-00C04FD7D062}"); + private WinFormsComWrappers.AutoCompleteWrapper? _autoCompleteObject2; /// /// Constructor. /// public StringSource(string[] strings) { - Array.Clear(strings, 0, size); - - if (strings is not null) - { - this.strings = strings; - } + this.strings = strings; current = 0; - size = (strings is null) ? 0 : strings.Length; + size = strings.Length; - Guid iid_iunknown = typeof(Shell32.IAutoComplete2).GUID; - HRESULT hr = Ole32.CoCreateInstance( - ref autoCompleteClsid, + var autoCompleteIID = IID.IAutoComplete2; + Ole32.CoCreateInstance( + in CLSID.AutoComplete, IntPtr.Zero, Ole32.CLSCTX.INPROC_SERVER, - ref iid_iunknown, - out object obj); - if (!hr.Succeeded()) - { - throw Marshal.GetExceptionForHR((int)hr); - } + in autoCompleteIID, + out IntPtr autoComplete2Ptr).ThrowIfFailed(); - _autoCompleteObject2 = (Shell32.IAutoComplete2)obj; + var obj = WinFormsComWrappers.Instance + .GetOrCreateObjectForComInstance(autoComplete2Ptr, CreateObjectFlags.None); + _autoCompleteObject2 = (WinFormsComWrappers.AutoCompleteWrapper)obj; } /// @@ -73,7 +59,7 @@ public bool Bind(HandleRef edit, Shell32.AUTOCOMPLETEOPTIONS options) return false; } - HRESULT hr = _autoCompleteObject2.Init(edit.Handle, (IEnumString)this, null, null); + HRESULT hr = _autoCompleteObject2.Init(edit.Handle, this, IntPtr.Zero, IntPtr.Zero); GC.KeepAlive(edit.Wrapper); return hr.Succeeded(); } @@ -82,20 +68,14 @@ public void ReleaseAutoComplete() { if (_autoCompleteObject2 is not null) { - Marshal.ReleaseComObject(_autoCompleteObject2); + _autoCompleteObject2.Dispose(); _autoCompleteObject2 = null; } } public void RefreshList(string[] newSource) { - Array.Clear(strings, 0, size); - - if (strings is not null) - { - strings = newSource; - } - + strings = newSource; current = 0; size = (strings is null) ? 0 : strings.Length; } From 77c3c50313c3150680d6d84a5e1d1dcd60195885 Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Tue, 15 Feb 2022 08:34:10 +0600 Subject: [PATCH 2/3] Remove not used checks --- .../src/System/Windows/Forms/StringSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs b/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs index 35fd697b16f..d3058fddd33 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs @@ -77,7 +77,7 @@ public void RefreshList(string[] newSource) { strings = newSource; current = 0; - size = (strings is null) ? 0 : strings.Length; + size = strings.Length; } #region IEnumString Members From 9e9ee4497eb932d59e5200bc452074493d04d439 Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Sat, 19 Feb 2022 08:51:35 +0600 Subject: [PATCH 3/3] Apply PR feedback --- .../src/System/Windows/Forms/StringSource.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs b/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs index d3058fddd33..c61ae340f01 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs @@ -25,6 +25,7 @@ internal class StringSource : IEnumString /// public StringSource(string[] strings) { + Array.Clear(strings, 0, size); this.strings = strings; current = 0; @@ -75,6 +76,7 @@ public void ReleaseAutoComplete() public void RefreshList(string[] newSource) { + Array.Clear(strings, 0, size); strings = newSource; current = 0; size = strings.Length;