From 9ea7948b3362053ba90aaffb165b1008515f4e1d Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Fri, 8 Apr 2022 03:13:14 +0600 Subject: [PATCH] Use ComWrappers for IErrorInfo (#6743) * Use ComWrappers for IErrorInfo * Cleanup RCW instance after use * Oops * Fix issue * Add test case for IErrorInfo handling Rely on the MXXMLWriter60 ProgID which implement IDispath and error handling * Add second test which cover actual wrapper * Initialize VsVarsAll when building tests project * Attempt to fix * One more stupid try * Use reg-free COM for tests * Add ARM64 platform * Add configuration * Ignore copying XML files for native projects * Probably I was too smart for ARM64 changes. Attempt to mimic System.Windows.Forms.Interop.Tests * Apply suggestions from code review Co-authored-by: Igor Velikorossov * Add explanation about project file and cleanup of tests * Use IID_IUnknown from Primitives * Apply suggestions from code review Co-authored-by: Igor Velikorossov * Hide GetErrorInfo overload * Add explanation * Reuse CoCreateInstace from Primitives * Apply PR feedback * Bug, stupid bug! * Move tests to Interop.Tests project * Remove not needed file * Remove not used Import * Remove leftovers * Format code Co-authored-by: Igor Velikorossov --- eng/init-vs-env.cmd | 72 +++ .../src/Interop/Interop.IID.cs | 7 +- .../Interop/OleAut32/Interop.GetErrorInfo.cs | 12 +- .../Interop/OleAut32/Interop.IErrorInfo.cs | 37 -- .../WinFormsComWrappers.ErrorInfoWrapper.cs | 49 +++ .../src/Interop/WinFormsComWrappers.cs | 8 + .../src/Properties/AssemblyInfo.cs | 1 + .../COM2Interop/COM2PropertyDescriptor.cs | 15 +- .../InteropTests/NativeTests/App.manifest | 18 + .../InteropTests/NativeTests/CMakeLists.txt | 30 ++ .../InteropTests/NativeTests/ComHelpers.h | 411 ++++++++++++++++++ .../InteropTests/NativeTests/Contract.idl | 38 ++ .../InteropTests/NativeTests/DispatchImpl.cpp | 49 +++ .../InteropTests/NativeTests/DispatchImpl.h | 43 ++ .../NativeTests/DllGetClassObject.cpp | 22 + .../InteropTests/NativeTests/Exports.def | 2 + .../NativeTests/NativeTests.X.manifest | 20 + .../InteropTests/NativeTests/NativeTests.proj | 1 + .../tests/InteropTests/NativeTests/README.md | 18 + .../NativeTests/RawErrorInfoUsageTest.cpp | 44 ++ .../NativeTests/RawErrorInfoUsageTest.h | 49 +++ .../StandardErrorInfoUsageTest.cpp | 40 ++ .../NativeTests/StandardErrorInfoUsageTest.h | 49 +++ .../tests/InteropTests/PropertyGridTests.cs | 146 +++++++ .../System.Windows.Forms.Interop.Tests.csproj | 7 + 25 files changed, 1143 insertions(+), 45 deletions(-) create mode 100644 eng/init-vs-env.cmd delete mode 100644 src/System.Windows.Forms.Primitives/src/Interop/OleAut32/Interop.IErrorInfo.cs create mode 100644 src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.ErrorInfoWrapper.cs create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/App.manifest create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/ComHelpers.h create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/Contract.idl create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/DispatchImpl.cpp create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/DispatchImpl.h create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/DllGetClassObject.cpp create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/Exports.def create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/NativeTests.X.manifest create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/README.md create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/RawErrorInfoUsageTest.cpp create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/RawErrorInfoUsageTest.h create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/StandardErrorInfoUsageTest.cpp create mode 100644 src/System.Windows.Forms/tests/InteropTests/NativeTests/StandardErrorInfoUsageTest.h create mode 100644 src/System.Windows.Forms/tests/InteropTests/PropertyGridTests.cs diff --git a/eng/init-vs-env.cmd b/eng/init-vs-env.cmd new file mode 100644 index 00000000000..18a77332e04 --- /dev/null +++ b/eng/init-vs-env.cmd @@ -0,0 +1,72 @@ +@if not defined _echo @echo off + +:: Initializes Visual Studio developer environment. If a build architecture is passed +:: as an argument, it also initializes VC++ build environment and CMakePath. + +set "__VCBuildArch=" +if /i "%~1" == "x86" (set __VCBuildArch=x86) +if /i "%~1" == "x64" (set __VCBuildArch=x86_amd64) +if /i "%~1" == "arm" (set __VCBuildArch=x86_arm) +if /i "%~1" == "arm64" (set __VCBuildArch=x86_arm64) +if /i "%~1" == "wasm" (set __VCBuildArch=x86_amd64) + +:: Default to highest Visual Studio version available that has Visual C++ tools. +:: +:: For VS2017 and later, multiple instances can be installed on the same box SxS and VS1*0COMNTOOLS +:: is no longer set as a global environment variable and is instead only set if the user +:: has launched the Visual Studio Developer Command Prompt. +:: +:: Following this logic, we will default to the Visual Studio toolset assocated with the active +:: Developer Command Prompt. Otherwise, we will query VSWhere to locate the later version of +:: Visual Studio available on the machine. Finally, we will fail the script if no supported +:: instance can be found. + +if defined VisualStudioVersion goto :VSDetected + +set "__VSWhere=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +set "__VSCOMNTOOLS=" + +if exist "%__VSWhere%" ( + for /f "tokens=*" %%p in ( + '"%__VSWhere%" -latest -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath' + ) do set __VSCOMNTOOLS=%%p\Common7\Tools +) + +if not exist "%__VSCOMNTOOLS%" goto :VSMissing + +:: Make sure the current directory stays intact +set "VSCMD_START_DIR=%CD%" + +call "%__VSCOMNTOOLS%\VsDevCmd.bat" -no_logo + +:: Clean up helper variables +set "__VSWhere=" +set "__VSCOMNTOOLS=" +set "VSCMD_START_DIR=" + +:VSDetected +if "%VisualStudioVersion%"=="16.0" ( + set __VSVersion=vs2019 + set __PlatformToolset=v142 + goto :SetVCEnvironment +) +if "%VisualStudioVersion%"=="17.0" ( + set __VSVersion=vs2022 + set __PlatformToolset=v142 + goto :SetVCEnvironment +) + +:VSMissing +echo %__MsgPrefix%Error: Visual Studio 2019 or 2022 with C++ tools required. ^ +Please see https://github.com/dotnet/runtime/blob/main/docs/workflow/requirements/windows-requirements.md for build requirements. +exit /b 1 + +:SetVCEnvironment + +if "%__VCBuildArch%"=="" exit /b 0 + +:: Set the environment for the native build +call "%VCINSTALLDIR%Auxiliary\Build\vcvarsall.bat" %__VCBuildArch% +if not "%ErrorLevel%"=="0" exit /b 1 + +set "__VCBuildArch=" 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 173dd9c5472..e9ca1e73232 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/Interop.IID.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/Interop.IID.cs @@ -15,11 +15,14 @@ internal static class IID // 00000121-0000-0000-C000-000000000046 public static Guid IDropSource { get; } = new(0x00000121, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + // 00000122-0000-0000-C000-000000000046 + public static Guid IDropTarget { get; } = new(0x00000122, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + // 00000101-0000-0000-C000-000000000046 public static Guid IEnumString { get; } = new(0x00000101, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); - // 00000122-0000-0000-C000-000000000046 - public static Guid IDropTarget { get; } = new(0x00000122, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + // 1CF2B120-547D-101B-8E65-08002B2BD119 + public static Guid IErrorInfo { get; } = new(0x1CF2B120, 0x547D, 0x101B, 0x8E, 0x65, 0x08, 0x00, 0x2B, 0x2B, 0xD1, 0x19); // 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/OleAut32/Interop.GetErrorInfo.cs b/src/System.Windows.Forms.Primitives/src/Interop/OleAut32/Interop.GetErrorInfo.cs index 00b2f3f69b5..010cc14f3fb 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/OleAut32/Interop.GetErrorInfo.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/OleAut32/Interop.GetErrorInfo.cs @@ -9,6 +9,16 @@ internal partial class Interop internal static partial class Oleaut32 { [DllImport(Libraries.Oleaut32, ExactSpelling = true)] - public static extern HRESULT GetErrorInfo(uint dwReserved, out IErrorInfo pperrinfo); + private static extern HRESULT GetErrorInfo(uint dwReserved, out IntPtr pperrinfo); + + public static void GetErrorInfo(out WinFormsComWrappers.ErrorInfoWrapper? errinfo) + { + HRESULT result = GetErrorInfo(0, out IntPtr pperrinfo); + errinfo = null; + if (result.Succeeded() && pperrinfo != IntPtr.Zero) + { + errinfo = (WinFormsComWrappers.ErrorInfoWrapper)WinFormsComWrappers.Instance.GetOrCreateObjectForComInstance(pperrinfo, CreateObjectFlags.Unwrap); + } + } } } diff --git a/src/System.Windows.Forms.Primitives/src/Interop/OleAut32/Interop.IErrorInfo.cs b/src/System.Windows.Forms.Primitives/src/Interop/OleAut32/Interop.IErrorInfo.cs deleted file mode 100644 index 273282b5a5e..00000000000 --- a/src/System.Windows.Forms.Primitives/src/Interop/OleAut32/Interop.IErrorInfo.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; - -internal partial class Interop -{ - internal static partial class Oleaut32 - { - [ComImport] - [Guid("1CF2B120-547D-101B-8E65-08002B2BD119")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public unsafe interface IErrorInfo - { - [PreserveSig] - HRESULT GetGUID( - Guid* pguid); - - [PreserveSig] - HRESULT GetSource( - out string pBstrSource); - - [PreserveSig] - HRESULT GetDescription( - out string pBstrDescription); - - [PreserveSig] - HRESULT GetHelpFile( - out string pBstrHelpFile); - - [PreserveSig] - HRESULT GetHelpContext( - uint* pdwHelpContext); - } - } -} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.ErrorInfoWrapper.cs b/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.ErrorInfoWrapper.cs new file mode 100644 index 00000000000..a88da959b34 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.ErrorInfoWrapper.cs @@ -0,0 +1,49 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +internal partial class Interop +{ + internal unsafe partial class WinFormsComWrappers + { + internal class ErrorInfoWrapper + { + private IntPtr _wrappedInstance; + + public ErrorInfoWrapper(IntPtr wrappedInstance) + { + _wrappedInstance = wrappedInstance.OrThrowIfZero(); + } + + internal IntPtr Instance => _wrappedInstance; + + public void Dispose() + { + Marshal.Release(_wrappedInstance); + _wrappedInstance = IntPtr.Zero; + } + + public bool GetDescription([NotNullWhen(true)] out string? pBstrDescription) + { + IntPtr descriptionPtr; + var result = ((delegate* unmanaged)(*(*(void***)_wrappedInstance + 5 /* IErrorInfo.GetDescription */))) + (_wrappedInstance, &descriptionPtr); + if (result.Succeeded()) + { + pBstrDescription = Marshal.PtrToStringUni(descriptionPtr); + Marshal.FreeBSTR(descriptionPtr); + } + else + { + pBstrDescription = null; + } + + return result.Succeeded(); + } + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs b/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs index a82eecd98e8..6254bab2641 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs @@ -135,6 +135,14 @@ protected override object CreateObject(IntPtr externalComObject, CreateObjectFla return new PictureWrapper(pictureComObject); } + Guid errorInfoIID = IID.IErrorInfo; + hr = Marshal.QueryInterface(externalComObject, ref errorInfoIID, out IntPtr errorInfoComObject); + if (hr == S_OK) + { + Marshal.Release(externalComObject); + return new ErrorInfoWrapper(errorInfoComObject); + } + Guid fileOpenDialogIID = IID.IFileOpenDialog; hr = Marshal.QueryInterface(externalComObject, ref fileOpenDialogIID, out IntPtr fileOpenDialogComObject); if (hr == S_OK) diff --git a/src/System.Windows.Forms/src/Properties/AssemblyInfo.cs b/src/System.Windows.Forms/src/Properties/AssemblyInfo.cs index 464f8bc3936..c8394778a9d 100644 --- a/src/System.Windows.Forms/src/Properties/AssemblyInfo.cs +++ b/src/System.Windows.Forms/src/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ [assembly: InternalsVisibleTo("System.Windows.Forms.Tests, PublicKey=00000000000000000400000000000000")] [assembly: InternalsVisibleTo("System.Windows.Forms.Primitives.TestUtilities, PublicKey=00000000000000000400000000000000")] +[assembly: InternalsVisibleTo("System.Windows.Forms.Interop.Tests, PublicKey=00000000000000000400000000000000")] [assembly: InternalsVisibleTo("System.Windows.Forms.UI.IntegrationTests, PublicKey=00000000000000000400000000000000")] [assembly: InternalsVisibleTo("MauiImageListTests, PublicKey=00000000000000000400000000000000")] [assembly: InternalsVisibleTo("MauiListViewTests, PublicKey=00000000000000000400000000000000")] diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ComponentModel/COM2Interop/COM2PropertyDescriptor.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ComponentModel/COM2Interop/COM2PropertyDescriptor.cs index 4934acbe739..dc43ed0ba6b 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/ComponentModel/COM2Interop/COM2PropertyDescriptor.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ComponentModel/COM2Interop/COM2PropertyDescriptor.cs @@ -1306,13 +1306,18 @@ public unsafe override void SetValue(object component, object value) g = typeof(Oleaut32.IDispatch).GUID; if (iSupportErrorInfo.InterfaceSupportsErrorInfo(&g) == HRESULT.S_OK) { - Oleaut32.IErrorInfo pErrorInfo; - Oleaut32.GetErrorInfo(0, out pErrorInfo); + WinFormsComWrappers.ErrorInfoWrapper pErrorInfo; + Oleaut32.GetErrorInfo(out pErrorInfo); - string info; - if (pErrorInfo is not null && pErrorInfo.GetDescription(out info).Succeeded()) + if (pErrorInfo is not null) { - errorInfo = info; + string info; + if (pErrorInfo.GetDescription(out info)) + { + errorInfo = info; + } + + pErrorInfo.Dispose(); } } } diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/App.manifest b/src/System.Windows.Forms/tests/InteropTests/NativeTests/App.manifest new file mode 100644 index 00000000000..800dce828cf --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/App.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/CMakeLists.txt b/src/System.Windows.Forms/tests/InteropTests/NativeTests/CMakeLists.txt index 0ad0f58f982..457fb7d11d5 100644 --- a/src/System.Windows.Forms/tests/InteropTests/NativeTests/CMakeLists.txt +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/CMakeLists.txt @@ -4,6 +4,24 @@ set(CMAKE_MACOSX_RPATH 1) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Compile IDL file using MIDL +set(IDL_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/Contract.idl) +get_filename_component(IDL_NAME ${IDL_SOURCE} NAME_WE) + + +FIND_PROGRAM( MIDL midl.exe ) +set(IDL_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Contract) +add_custom_command( + OUTPUT ${IDL_OUTPUT_DIRECTORY}/${IDL_NAME}_i.c ${IDL_OUTPUT_DIRECTORY}/${IDL_NAME}.h + COMMAND ${MIDL} ${MIDL_INCLUDE_DIRECTORIES} + /h ${IDL_OUTPUT_DIRECTORY}/${IDL_NAME}.h ${MIDL_DEFINITIONS} + /out ${IDL_OUTPUT_DIRECTORY} + /tlb $/NativeTests.tlb + ${IDL_SOURCE} + DEPENDS ${IDL_SOURCE} + COMMENT "Compiling ${IDL_SOURCE}") + +include_directories(${IDL_OUTPUT_DIRECTORY}) # [[! Microsoft.Security.SystemsADM.10086 !]] - SQL required warnings add_compile_options($<$:/W3>) # warning level 3 @@ -15,5 +33,17 @@ add_compile_options($<$:/we4055>) # 'conversion' : from add_library(NativeTests SHARED AccessibleObjectTests.cpp WebBrowserSiteBaseInteropTests.cpp + DllGetClassObject.cpp + DispatchImpl.cpp + StandardErrorInfoUsageTest.cpp + RawErrorInfoUsageTest.cpp + Exports.def + ${IDL_OUTPUT_DIRECTORY}/${IDL_NAME}_i.c ) +file(GENERATE OUTPUT $/NativeTests.X.manifest INPUT ${CMAKE_CURRENT_SOURCE_DIR}/NativeTests.X.manifest) +file(GENERATE OUTPUT $/App.manifest INPUT ${CMAKE_CURRENT_SOURCE_DIR}/App.manifest) + install(TARGETS NativeTests) +install(FILES $/NativeTests.tlb TYPE BIN) +install(FILES $/NativeTests.X.manifest TYPE BIN) +install(FILES $/App.manifest TYPE BIN) diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/ComHelpers.h b/src/System.Windows.Forms/tests/InteropTests/NativeTests/ComHelpers.h new file mode 100644 index 00000000000..93ae4a3a09f --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/ComHelpers.h @@ -0,0 +1,411 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma once + +#include +#include +#include +#include +#include +#include + +// Common macro for working in COM +#define RETURN_IF_FAILED(exp) { hr = exp; if (FAILED(hr)) { return hr; } } + +namespace Internal +{ + template + HRESULT __QueryInterfaceImpl( + /* [in] */ REFIID riid, + /* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject, + /* [in] */ I obj) + { + if (riid == __uuidof(I)) + { + *ppvObject = static_cast(obj); + } + else + { + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + return S_OK; + } + + template + HRESULT __QueryInterfaceImpl( + /* [in] */ REFIID riid, + /* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject, + /* [in] */ I1 i1, + /* [in] */ IR... remain) + { + if (riid == __uuidof(I1)) + { + *ppvObject = static_cast(i1); + return S_OK; + } + + return __QueryInterfaceImpl(riid, ppvObject, remain...); + } +} + +// Implementation of IUnknown operations +class UnknownImpl +{ +public: + UnknownImpl() = default; + virtual ~UnknownImpl() = default; + + UnknownImpl(const UnknownImpl&) = delete; + UnknownImpl& operator=(const UnknownImpl&) = delete; + + UnknownImpl(UnknownImpl&&) = default; + UnknownImpl& operator=(UnknownImpl&&) = default; + + template + HRESULT DoQueryInterface( + /* [in] */ REFIID riid, + /* [iid_is][out] */ _COM_Outptr_ void **ppvObject, + /* [in] */ I1 i1, + /* [in] */ IR... remain) + { + if (ppvObject == nullptr) + return E_POINTER; + + if (riid == __uuidof(IUnknown)) + { + *ppvObject = static_cast(i1); + } + else + { + HRESULT hr = Internal::__QueryInterfaceImpl(riid, ppvObject, i1, remain...); + if (hr != S_OK) + return hr; + } + + DoAddRef(); + return S_OK; + } + + ULONG DoAddRef() + { + assert(_refCount > 0); + return (++_refCount); + } + + ULONG DoRelease() + { + assert(_refCount > 0); + ULONG c = (--_refCount); + if (c == 0) + delete this; + return c; + } + +protected: + ULONG GetRefCount() + { + return _refCount; + } + +private: + std::atomic _refCount = 1; +}; + +// Macro to use for defining ref counting impls +#define DEFINE_REF_COUNTING() \ + STDMETHOD_(ULONG, AddRef)(void) { return UnknownImpl::DoAddRef(); } \ + STDMETHOD_(ULONG, Release)(void) { return UnknownImpl::DoRelease(); } + +// Templated class factory +template +class ClassFactoryBasic : public UnknownImpl, public IClassFactory +{ +public: // static + static HRESULT Create(_In_ REFIID riid, _Outptr_ LPVOID FAR* ppv) + { + try + { + auto cf = new ClassFactoryBasic(); + HRESULT hr = cf->QueryInterface(riid, ppv); + cf->Release(); + return hr; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + } + +public: // IClassFactory + STDMETHOD(CreateInstance)( + _In_opt_ IUnknown *pUnkOuter, + _In_ REFIID riid, + _COM_Outptr_ void **ppvObject) + { + if (pUnkOuter != nullptr) + return CLASS_E_NOAGGREGATION; + + try + { + auto ti = new T(); + HRESULT hr = ti->QueryInterface(riid, ppvObject); + ti->Release(); + return hr; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + } + + STDMETHOD(LockServer)(/* [in] */ BOOL fLock) + { + assert(false && "Not impl"); + return E_NOTIMPL; + } + +public: // IUnknown + STDMETHOD(QueryInterface)( + /* [in] */ REFIID riid, + /* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject) + { + return DoQueryInterface(riid, ppvObject, static_cast(this)); + } + + DEFINE_REF_COUNTING(); +}; + +// Templated class factory for aggregation +template +class ClassFactoryAggregate : public UnknownImpl, public IClassFactory +{ +public: // static + static HRESULT Create(_In_ REFIID riid, _Outptr_ LPVOID FAR* ppv) + { + try + { + auto cf = new ClassFactoryAggregate(); + HRESULT hr = cf->QueryInterface(riid, ppv); + cf->Release(); + return hr; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + } + +public: // IClassFactory + STDMETHOD(CreateInstance)( + _In_opt_ IUnknown *pUnkOuter, + _In_ REFIID riid, + _COM_Outptr_ void **ppvObject) + { + if (pUnkOuter != nullptr && riid != IID_IUnknown) + return CLASS_E_NOAGGREGATION; + + try + { + auto ti = new T(pUnkOuter); + HRESULT hr = ti->QueryInterface(riid, ppvObject); + ti->Release(); + return hr; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + } + + STDMETHOD(LockServer)(/* [in] */ BOOL fLock) + { + assert(false && "Not impl"); + return E_NOTIMPL; + } + +public: // IUnknown + STDMETHOD(QueryInterface)( + /* [in] */ REFIID riid, + /* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject) + { + return DoQueryInterface(riid, ppvObject, static_cast(this)); + } + + DEFINE_REF_COUNTING(); +}; + +// Templated class factory +// Supplied type must have the following properties to use this template: +// 1) Have a static method with the following signature: +// - HRESULT RequestLicKey(BSTR *key); +// 2) Have a constructor that takes an optional BSTR value as the key +template +class ClassFactoryLicense : public UnknownImpl, public IClassFactory2 +{ +public: // static + static HRESULT Create(_In_ REFIID riid, _Outptr_ LPVOID FAR* ppv) + { + try + { + auto cf = new ClassFactoryLicense(); + HRESULT hr = cf->QueryInterface(riid, ppv); + cf->Release(); + return hr; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + } + +public: // IClassFactory + STDMETHOD(CreateInstance)( + _In_opt_ IUnknown *pUnkOuter, + _In_ REFIID riid, + _COM_Outptr_ void **ppvObject) + { + return CreateInstanceLic(pUnkOuter, nullptr, riid, nullptr, ppvObject); + } + + STDMETHOD(LockServer)(/* [in] */ BOOL fLock) + { + assert(false && "Not impl"); + return E_NOTIMPL; + } + +public: // IClassFactory2 + STDMETHOD(GetLicInfo)( + /* [out][in] */ __RPC__inout LICINFO *pLicInfo) + { + // The CLR does not call this function and as such, + // returns an error. Note that this is explicitly illegal + // in a proper implementation of IClassFactory2. + return E_UNEXPECTED; + } + + STDMETHOD(RequestLicKey)( + /* [in] */ DWORD dwReserved, + /* [out] */ __RPC__deref_out_opt BSTR *pBstrKey) + { + if (dwReserved != 0) + return E_UNEXPECTED; + + return T::RequestLicKey(pBstrKey); + } + + STDMETHOD(CreateInstanceLic)( + /* [annotation][in] */ _In_opt_ IUnknown *pUnkOuter, + /* [annotation][in] */ _Reserved_ IUnknown *pUnkReserved, + /* [annotation][in] */ __RPC__in REFIID riid, + /* [annotation][in] */ __RPC__in BSTR bstrKey, + /* [annotation][iid_is][out] */ __RPC__deref_out_opt PVOID *ppvObj) + { + if (pUnkOuter != nullptr) + return CLASS_E_NOAGGREGATION; + + if (pUnkReserved != nullptr) + return E_UNEXPECTED; + + try + { + auto ti = new T(bstrKey); + HRESULT hr = ti->QueryInterface(riid, ppvObj); + ti->Release(); + return hr; + } + catch (HRESULT hr) + { + return hr; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + } + +public: // IUnknown + STDMETHOD(QueryInterface)( + /* [in] */ REFIID riid, + /* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject) + { + return DoQueryInterface(riid, ppvObject, static_cast(this), static_cast(this)); + } + + DEFINE_REF_COUNTING(); +}; + +template +struct ComSmartPtr +{ + T* p; + + ComSmartPtr() + : p{ nullptr } + { } + + ComSmartPtr(_In_ T* t) + : p{ t } + { + if (p != nullptr) + (void)p->AddRef(); + } + + ComSmartPtr(_In_ const ComSmartPtr&) = delete; + + ComSmartPtr(_Inout_ ComSmartPtr&& other) + : p{ other.Detach() } + { } + + ~ComSmartPtr() + { + Release(); + } + + ComSmartPtr& operator=(_In_ const ComSmartPtr&) = delete; + + ComSmartPtr& operator=(_Inout_ ComSmartPtr&& other) + { + Attach(other.Detach()); + return (*this); + } + + operator T*() + { + return p; + } + + T** operator&() + { + return &p; + } + + T* operator->() + { + return p; + } + + void Attach(_In_opt_ T* t) noexcept + { + Release(); + p = t; + } + + T* Detach() noexcept + { + T* tmp = p; + p = nullptr; + return tmp; + } + + void Release() noexcept + { + if (p != nullptr) + { + (void)p->Release(); + p = nullptr; + } + } +}; diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/Contract.idl b/src/System.Windows.Forms/tests/InteropTests/NativeTests/Contract.idl new file mode 100644 index 00000000000..168f76d8b13 --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/Contract.idl @@ -0,0 +1,38 @@ +import "oaidl.idl"; +import "ocidl.idl"; + +[ + object, + uuid(F4611744-02AF-47D4-A10F-9E692368DEFD) +] +interface IBasicTest : IDispatch +{ + [propget] HRESULT Int_Property([out, retval] int *ret); + [propput] HRESULT Int_Property([in] int val); +}; + +[ + uuid(0971AD7E-3D4A-4C44-B0A3-A518AC88DFE1) +] +library NativeTests +{ + importlib("stdole2.tlb"); + + [ + uuid(0ED8EE0D-22E3-49EA-850C-E69B20D1F296) + ] + coclass RawErrorInfoUsageTest + { + [default] interface IBasicTest; + interface ISupportErrorInfo; + } + + [ + uuid(EA1FCB3A-277C-4C79-AB85-E2ED3E858201) + ] + coclass StandardErrorInfoUsageTest + { + [default] interface IBasicTest; + interface ISupportErrorInfo; + } +} diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/DispatchImpl.cpp b/src/System.Windows.Forms/tests/InteropTests/NativeTests/DispatchImpl.cpp new file mode 100644 index 00000000000..0e4a167a471 --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/DispatchImpl.cpp @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "DispatchImpl.h" + +namespace +{ + const wchar_t* s_tlbDefault = L"NativeTests.tlb"; +} + +DispatchImpl::DispatchImpl(GUID guid, void *instance, const wchar_t* tlb) + : _typeLib{ nullptr } + , _typeInfo{ nullptr } + , _instance{ instance } +{ + const wchar_t* tlbToLoad = tlb == nullptr ? s_tlbDefault : tlb; + HRESULT hr = ::LoadTypeLibEx(tlbToLoad, REGKIND::REGKIND_NONE, &_typeLib); + if (FAILED(hr)) + throw hr; + + hr = _typeLib->GetTypeInfoOfGuid(guid, &_typeInfo); + if (FAILED(hr)) + throw hr; +} + +HRESULT DispatchImpl::DoGetTypeInfoCount(UINT* pctinfo) +{ + *pctinfo = 1; + return S_OK; +} + +HRESULT DispatchImpl::DoGetTypeInfo(UINT iTInfo, ITypeInfo** ppTInfo) +{ + if (iTInfo != 0) + return DISP_E_BADINDEX; + + assert(_typeInfo != nullptr); + return _typeInfo->QueryInterface(__uuidof(*ppTInfo), (void**)ppTInfo); +} + +HRESULT DispatchImpl::DoGetIDsOfNames(LPOLESTR* rgszNames, UINT cNames, DISPID* rgDispId) +{ + return _typeInfo->GetIDsOfNames(rgszNames, cNames, rgDispId); +} + +HRESULT DispatchImpl::DoInvoke(DISPID dispIdMember, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) +{ + return _typeInfo->Invoke(_instance, dispIdMember, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr); +} diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/DispatchImpl.h b/src/System.Windows.Forms/tests/InteropTests/NativeTests/DispatchImpl.h new file mode 100644 index 00000000000..563e621c284 --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/DispatchImpl.h @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma once + +#include "ComHelpers.h" +#include + +// Implementation of IDispatch operations +class DispatchImpl : public UnknownImpl +{ +public: + DispatchImpl(GUID guid, void *instance, const wchar_t* tlb = nullptr); + virtual ~DispatchImpl() = default; + + DispatchImpl(const DispatchImpl&) = delete; + DispatchImpl& operator=(const DispatchImpl&) = delete; + + DispatchImpl(DispatchImpl&&) = default; + DispatchImpl& operator=(DispatchImpl&&) = default; + +protected: + HRESULT DoGetTypeInfoCount(UINT* pctinfo); + HRESULT DoGetTypeInfo(UINT iTInfo, ITypeInfo** ppTInfo); + HRESULT DoGetIDsOfNames(LPOLESTR* rgszNames, UINT cNames, DISPID* rgDispId); + HRESULT DoInvoke(DISPID dispIdMember, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr); + +private: + ComSmartPtr _typeLib; + ComSmartPtr _typeInfo; + void *_instance; +}; + +// Macro to use for defining dispatch impls +#define DEFINE_DISPATCH() \ + STDMETHOD(GetTypeInfoCount)(UINT *pctinfo) \ + { return DispatchImpl::DoGetTypeInfoCount(pctinfo); } \ + STDMETHOD(GetTypeInfo)(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo) \ + { return DispatchImpl::DoGetTypeInfo(iTInfo, ppTInfo); } \ + STDMETHOD(GetIDsOfNames)(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId) \ + { return DispatchImpl::DoGetIDsOfNames(rgszNames, cNames, rgDispId); } \ + STDMETHOD(Invoke)(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) \ + { return DispatchImpl::DoInvoke(dispIdMember, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr); } diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/DllGetClassObject.cpp b/src/System.Windows.Forms/tests/InteropTests/NativeTests/DllGetClassObject.cpp new file mode 100644 index 00000000000..2b007eb83a3 --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/DllGetClassObject.cpp @@ -0,0 +1,22 @@ +// 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. + +#include "ocidl.h" +#include +#include "ComHelpers.h" +#include + +#include "RawErrorInfoUsageTest.h" +#include "StandardErrorInfoUsageTest.h" + +STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID FAR* ppv) +{ + if (rclsid == __uuidof(RawErrorInfoUsageTest)) + return ClassFactoryBasic::Create(riid, ppv); + + if (rclsid == __uuidof(StandardErrorInfoUsageTest)) + return ClassFactoryBasic::Create(riid, ppv); + + return CLASS_E_CLASSNOTAVAILABLE; +} diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/Exports.def b/src/System.Windows.Forms/tests/InteropTests/NativeTests/Exports.def new file mode 100644 index 00000000000..2e9253049fd --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/Exports.def @@ -0,0 +1,2 @@ +EXPORTS + DllGetClassObject PRIVATE diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/NativeTests.X.manifest b/src/System.Windows.Forms/tests/InteropTests/NativeTests/NativeTests.X.manifest new file mode 100644 index 00000000000..5fdd87552cb --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/NativeTests.X.manifest @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/NativeTests.proj b/src/System.Windows.Forms/tests/InteropTests/NativeTests/NativeTests.proj index fe624ca5788..8b5b6b4132c 100644 --- a/src/System.Windows.Forms/tests/InteropTests/NativeTests/NativeTests.proj +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/NativeTests.proj @@ -1,5 +1,6 @@ + call "$(RepoRoot)eng\init-vs-env.cmd" $(TargetArchitecture) CMakeLists.txt diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/README.md b/src/System.Windows.Forms/tests/InteropTests/NativeTests/README.md new file mode 100644 index 00000000000..631085a9090 --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/README.md @@ -0,0 +1,18 @@ +Supplementary native parts for WinForms Interop tests +================================================== + +This project is native DLL which provide unmanaged parts required for testing WinForms components. Most interesting part of the tests would be COM objects which given to controls. +This project builds as a native DLL, which can be used as a reg-free COM host. That allow running tests without worrying about "polluting" developer/build machines. + +Preferably each test case should have their separate COM objects. + +Manifests used in the application: + +- `App.manifest` This file used during running test to provide information about this DLL. Developers do not need to modify that file, just run it inside activation context. +- `NativeTests.X.manifest` This is manifest where all reg-free COM objects registered. + +## How to add new COM object + +1. Declare COM object in `Contract.idl` file +2. Implement COM object in C++. +3. Add CoClass defined in `Contract.idl` inside `NativeTests.X.manifest` diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/RawErrorInfoUsageTest.cpp b/src/System.Windows.Forms/tests/InteropTests/NativeTests/RawErrorInfoUsageTest.cpp new file mode 100644 index 00000000000..c0e48822396 --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/RawErrorInfoUsageTest.cpp @@ -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. + +#include "RawErrorInfoUsageTest.h" +#include + +HRESULT STDMETHODCALLTYPE RawErrorInfoUsageTest::get_Int_Property( + /* [retval][out] */ int *ret) +{ + *ret = _int; + return S_OK; +} + +HRESULT STDMETHODCALLTYPE RawErrorInfoUsageTest::put_Int_Property( + /* [in] */ int val) +{ + return DISP_E_MEMBERNOTFOUND; +} + +HRESULT STDMETHODCALLTYPE RawErrorInfoUsageTest::InterfaceSupportsErrorInfo( + /* [in] */ __RPC__in REFIID riid) +{ + // This is hack, in order to not implement IDispatch. + // By default implementations wrap any error during invoke into DISP_E_EXCEPTION + // and consume IErrorInfo. Some implementation behave differently, so this is emulation + // of that behaviour. + ComSmartPtr cei; + if (SUCCEEDED(::CreateErrorInfo(&cei))) + { + if (SUCCEEDED(cei->SetGUID(IID_IBasicTest))) + { + if (SUCCEEDED(cei->SetDescription(L"Error From RawErrorInfoUsageTest"))) + { + ComSmartPtr errorInfo; + if (SUCCEEDED(cei->QueryInterface(IID_IErrorInfo, (void**)&errorInfo))) + { + ::SetErrorInfo(0, errorInfo); + } + } + } + } + + return S_OK; +} diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/RawErrorInfoUsageTest.h b/src/System.Windows.Forms/tests/InteropTests/NativeTests/RawErrorInfoUsageTest.h new file mode 100644 index 00000000000..0cc1aa5924e --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/RawErrorInfoUsageTest.h @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma once + +#include "ComHelpers.h" +#include +#include "DispatchImpl.h" +#include + +class RawErrorInfoUsageTest : public DispatchImpl, public IBasicTest, public ISupportErrorInfo +{ +public: + RawErrorInfoUsageTest() + : DispatchImpl(IID_IBasicTest, static_cast(this)) + { + } + +public: // IBasicTest + + virtual HRESULT STDMETHODCALLTYPE get_Int_Property( + /* [retval][out] */ int *ret); + + virtual HRESULT STDMETHODCALLTYPE put_Int_Property( + /* [in] */ int val); + +public: // ISupportErrorInfo + virtual HRESULT STDMETHODCALLTYPE InterfaceSupportsErrorInfo( + /* [in] */ __RPC__in REFIID riid); + +public: // IDispatch + DEFINE_DISPATCH(); + +public: // IUnknown + STDMETHOD(QueryInterface)( + /* [in] */ REFIID riid, + /* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject) + { + return DoQueryInterface(riid, ppvObject, + static_cast(this), + static_cast(this), + static_cast(this)); + } + + DEFINE_REF_COUNTING(); + +private: + int _int; +}; diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/StandardErrorInfoUsageTest.cpp b/src/System.Windows.Forms/tests/InteropTests/NativeTests/StandardErrorInfoUsageTest.cpp new file mode 100644 index 00000000000..9609f93ab7d --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/StandardErrorInfoUsageTest.cpp @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "StandardErrorInfoUsageTest.h" +#include + +HRESULT STDMETHODCALLTYPE StandardErrorInfoUsageTest::get_Int_Property( + /* [retval][out] */ int *ret) +{ + *ret = _int; + return S_OK; +} + +HRESULT STDMETHODCALLTYPE StandardErrorInfoUsageTest::put_Int_Property( + /* [in] */ int val) +{ + ComSmartPtr cei; + if (SUCCEEDED(::CreateErrorInfo(&cei))) + { + if (SUCCEEDED(cei->SetGUID(IID_IBasicTest))) + { + if (SUCCEEDED(cei->SetDescription(L"Error From StandardErrorInfoUsageTest"))) + { + ComSmartPtr errorInfo; + if (SUCCEEDED(cei->QueryInterface(IID_IErrorInfo, (void**)&errorInfo))) + { + ::SetErrorInfo(0, errorInfo); + } + } + } + } + + return DISP_E_MEMBERNOTFOUND; +} + +HRESULT STDMETHODCALLTYPE StandardErrorInfoUsageTest::InterfaceSupportsErrorInfo( + /* [in] */ __RPC__in REFIID riid) +{ + return S_OK; +} diff --git a/src/System.Windows.Forms/tests/InteropTests/NativeTests/StandardErrorInfoUsageTest.h b/src/System.Windows.Forms/tests/InteropTests/NativeTests/StandardErrorInfoUsageTest.h new file mode 100644 index 00000000000..3418d3ee36b --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/NativeTests/StandardErrorInfoUsageTest.h @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma once + +#include "ComHelpers.h" +#include +#include "DispatchImpl.h" +#include + +class StandardErrorInfoUsageTest : public DispatchImpl, public IBasicTest, public ISupportErrorInfo +{ +public: + StandardErrorInfoUsageTest() + : DispatchImpl(IID_IBasicTest, static_cast(this)) + { + } + +public: // IBasicTest + + virtual HRESULT STDMETHODCALLTYPE get_Int_Property( + /* [retval][out] */ int *ret); + + virtual HRESULT STDMETHODCALLTYPE put_Int_Property( + /* [in] */ int val); + +public: // ISupportErrorInfo + virtual HRESULT STDMETHODCALLTYPE InterfaceSupportsErrorInfo( + /* [in] */ __RPC__in REFIID riid); + +public: // IDispatch + DEFINE_DISPATCH(); + +public: // IUnknown + STDMETHOD(QueryInterface)( + /* [in] */ REFIID riid, + /* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject) + { + return DoQueryInterface(riid, ppvObject, + static_cast(this), + static_cast(this), + static_cast(this)); + } + + DEFINE_REF_COUNTING(); + +private: + int _int; +}; diff --git a/src/System.Windows.Forms/tests/InteropTests/PropertyGridTests.cs b/src/System.Windows.Forms/tests/InteropTests/PropertyGridTests.cs new file mode 100644 index 00000000000..1f412edaa31 --- /dev/null +++ b/src/System.Windows.Forms/tests/InteropTests/PropertyGridTests.cs @@ -0,0 +1,146 @@ +// 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.ComponentModel; +using System.Runtime.InteropServices; +using Xunit; +using static Interop; + +namespace System.Windows.Forms.Interop.Tests; + +[Collection("Sequential")] +public class PropertyGridTests +{ + [Fact] + public void ISupportErrorInfo_Supported_ButNoIErrorInfoGiven() + { + ExecuteWithActivationContext( + "App.manifest", + () => + { + using PropertyGrid propertyGrid = new PropertyGrid(); + var target = CreateComObjectWithStandardIErrorInfoUsage(); + propertyGrid.SelectedObject = target; + var entries = propertyGrid.GetCurrentEntries(); + var encodingEntry = entries[0].Children.First(_ => _.PropertyName == "Int_Property"); + try + { + encodingEntry.SetPropertyTextValue("333"); + Assert.False(true, "Invalid set values should produce ExternalException which will be presenttted to the user."); + } + catch (ExternalException ex) + { + // Most default C++ implementation when Invoke return error code + // implementation consults IErrorInfo object and populates EXCEPINFO structure. + // From EXCEPINFO grid entry reads error code and message. + // IErrorInfo consulted too, but it does not hold error message anymore. + Assert.Equal((int)HRESULT.DISP_E_MEMBERNOTFOUND, ex.HResult); + Assert.Equal("Error From StandardErrorInfoUsageTest", ex.Message); + } + finally + { + propertyGrid.SelectedObject = null; + Marshal.ReleaseComObject(target); + } + }); + } + + [Fact] + public void ISupportErrorInfo_Supported_WithIErrorInfoGiven() + { + ExecuteWithActivationContext( + "App.manifest", + () => + { + using PropertyGrid propertyGrid = new PropertyGrid(); + var target = CreateComObjectWithRawIErrorInfoUsage(); + propertyGrid.SelectedObject = target; + var entries = propertyGrid.GetCurrentEntries(); + var encodingEntry = entries[0].Children.First(_ => _.PropertyName == "Int_Property"); + try + { + encodingEntry.SetPropertyTextValue("123"); + Assert.False(true, "Invalid set values should produce ExternalException which will be presenttted to the user."); + } + catch (ExternalException ex) + { + // If C++ implementation of Invoke did not populate EXCEPINFO structure + // from IErrorInfo, then we read that information about error call and display that error message to the user. + Assert.Equal("Error From RawErrorInfoUsageTest", ex.Message); + } + finally + { + propertyGrid.SelectedObject = null; + Marshal.ReleaseComObject(target); + } + }); + } + + private unsafe void ExecuteWithActivationContext(string applicationManifest, Action action) + { + var context = new Kernel32.ACTCTXW(); + IntPtr handle; + fixed (char* p = applicationManifest) + { + context.cbSize = (uint)sizeof(Kernel32.ACTCTXW); + context.lpSource = p; + + handle = Kernel32.CreateActCtxW(ref context); + } + + if (handle == IntPtr.Zero) + { + throw new Win32Exception(); + } + + try + { + if (Kernel32.ActivateActCtx(handle, out var cookie).IsFalse()) + { + throw new Win32Exception(); + } + + try + { + action(); + } + finally + { + if (Kernel32.DeactivateActCtx(0, cookie).IsFalse()) + { + throw new Win32Exception(); + } + } + } + finally + { + ReleaseActCtx(handle); + } + } + + private object CreateComObjectWithRawIErrorInfoUsage() + { + Guid clsidRawErrorInfoUsageTest = new("0ED8EE0D-22E3-49EA-850C-E69B20D1F296"); + Ole32.CoCreateInstance(ref clsidRawErrorInfoUsageTest, + IntPtr.Zero, + Ole32.CLSCTX.INPROC_SERVER, + ref NativeMethods.ActiveX.IID_IUnknown, + out object result); + return result; + } + + private object CreateComObjectWithStandardIErrorInfoUsage() + { + Guid clsidStandardErrorInfoUsageTest = new("EA1FCB3A-277C-4C79-AB85-E2ED3E858201"); + Ole32.CoCreateInstance(ref clsidStandardErrorInfoUsageTest, + IntPtr.Zero, + Ole32.CLSCTX.INPROC_SERVER, + ref NativeMethods.ActiveX.IID_IUnknown, + out object result); + return result; + } + + [DllImport("kernel32", SetLastError = true)] + private static extern void ReleaseActCtx(IntPtr hActCtx); +} diff --git a/src/System.Windows.Forms/tests/InteropTests/System.Windows.Forms.Interop.Tests.csproj b/src/System.Windows.Forms/tests/InteropTests/System.Windows.Forms.Interop.Tests.csproj index d5e5f680164..da36d96df85 100644 --- a/src/System.Windows.Forms/tests/InteropTests/System.Windows.Forms.Interop.Tests.csproj +++ b/src/System.Windows.Forms/tests/InteropTests/System.Windows.Forms.Interop.Tests.csproj @@ -23,4 +23,11 @@ + + + + + + +