Skip to content

Autocomplete using ComWrappers #6685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// 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.

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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IAutoComplete2 has methods Init, Enable, SetOptions, and GetOptions. Here for AutoCompleteWrapper I only see Init and SetOptions. Is that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This is intentional. There no need for declaring methods in RCW if they would not be used in the application. WinForms use just 2 methods, so I leave only these 2 method. That mentality was shown for me in the dotnet/runtime repo, and based on my experience working with WinForms team, same applied here as well. Additional cost (albeit very small) is paid for all WinForms consumers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is good information to know for ComWrappers, I will keep this in mind. Thank you for the insight.

{
_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<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, HRESULT>)(*(*(void***)_wrappedInstance + 3)))
(_wrappedInstance, hwndEdit, punkACLPtr, pwszRegKeyPath, pwszQuickComplete);
}

public HRESULT SetOptions(Shell32.AUTOCOMPLETEOPTIONS dwFlag)
{
return ((delegate* unmanaged<IntPtr, Shell32.AUTOCOMPLETEOPTIONS, HRESULT>)(*(*(void***)_wrappedInstance + 5)))
(_wrappedInstance, dwFlag);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<IntPtr, int, IntPtr*, int*, int>)&Next;
vtblRaw[4] = (IntPtr)(delegate* unmanaged<IntPtr, int, int>)&Skip;
vtblRaw[5] = (IntPtr)(delegate* unmanaged<IntPtr, int>)&Reset;
vtblRaw[6] = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr*, int>)&Clone;

return (IntPtr)vtblRaw;
}

[UnmanagedCallersOnly]
private static int Next(IntPtr thisPtr, int celt, IntPtr* rgelt, int* pceltFetched)
{
IEnumString inst = ComInterfaceDispatch.GetInstance<IEnumString>((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<IEnumString>((ComInterfaceDispatch*)thisPtr);
return inst.Skip(celt);
}

[UnmanagedCallersOnly]
private static int Reset(IntPtr thisPtr)
{
try
{
IEnumString inst = ComInterfaceDispatch.GetInstance<IEnumString>((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<IEnumString>((ComInterfaceDispatch*)thisPtr);
inst.Clone(out var cloned);
*ppenum = WinFormsComWrappers.Instance.GetOrCreateComInterfaceForObject(cloned, CreateComInterfaceFlags.None);
}
catch (Exception ex)
{
return ex.HResult;
}

return S_OK;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

internal partial class Interop
{
Expand All @@ -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();

Expand Down Expand Up @@ -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)
Expand All @@ -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.");
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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),
};
}
Expand Down
50 changes: 16 additions & 34 deletions src/System.Windows.Forms/src/System/Windows/Forms/StringSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
// 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;

namespace System.Windows.Forms
{
/// <summary>
/// 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.
/// </summary>
Expand All @@ -20,41 +18,30 @@ internal class StringSource : IEnumString
private string[] strings;
private int current;
private int size;
private Shell32.IAutoComplete2 _autoCompleteObject2;

/// <summary>
/// SHAutoComplete COM object CLSID.
/// </summary>
private static Guid autoCompleteClsid = new Guid("{00BB2763-6A77-11D0-A535-00C04FD7D062}");
private WinFormsComWrappers.AutoCompleteWrapper? _autoCompleteObject2;

/// <summary>
/// Constructor.
/// </summary>
public StringSource(string[] strings)
{
Array.Clear(strings, 0, size);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the usage this will never ever happens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or better say, this is no-op.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it is true that there are no callers that pass in a null array, not clearing the passed in array is a change in behavior. I wouldn't remove the clear in this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


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;
}

/// <summary>
Expand All @@ -73,7 +60,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();
}
Expand All @@ -82,22 +69,17 @@ 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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also based on the usage this will never happens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or better say, this is no-op.


if (strings is not null)
{
strings = newSource;
}

strings = newSource;
current = 0;
size = (strings is null) ? 0 : strings.Length;
size = strings.Length;
}

#region IEnumString Members
Expand Down