Skip to content
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

Test AxHost ICustomTypeDescriptor implementation #3366

Merged
merged 1 commit into from
Jun 19, 2020

Conversation

hughbe
Copy link
Contributor

@hughbe hughbe commented May 29, 2020

Proposed Changes

  • Test AxHost ICustomTypeDescriptor implementation
Microsoft Reviewers: Open in CodeFlow

@hughbe hughbe requested a review from a team as a code owner May 29, 2020 17:17
@ghost ghost assigned hughbe May 29, 2020
@hughbe hughbe force-pushed the AxHost-PropertyDescriptor-Tests branch 3 times, most recently from f908216 to 1895219 Compare June 3, 2020 21:34
@RussKie RussKie added 📖 documentation: breaking A change in behavior that could be breaking for applications. Needs to be documented. test-enhancement Improvements of test source code labels Jun 4, 2020
RussKie
RussKie previously approved these changes Jun 4, 2020
@codecov
Copy link

codecov bot commented Jun 4, 2020

Codecov Report

Merging #3366 into master will decrease coverage by 30.40673%.
The diff coverage is 62.50000%.

@@                 Coverage Diff                  @@
##              master       #3366          +/-   ##
====================================================
- Coverage   64.68320%   34.27647%   -30.40673%     
====================================================
  Files           1318         890         -428     
  Lines         484169      253833      -230336     
  Branches       39912       36794        -3118     
====================================================
- Hits          313176       87005      -226171     
+ Misses        165621      162049        -3572     
+ Partials        5372        4779         -593     
Flag Coverage Δ
#Debug 34.27647% <62.50000%> (-30.40673%) ⬇️
#production 34.27647% <62.50000%> (+0.43405%) ⬆️
#test ?

@hughbe
Copy link
Contributor Author

hughbe commented Jun 6, 2020

@weltkante @AaronRobinsonMSFT

Hey you guys I'm trying to test AxHost a little more and its interactions with native interfaces/com objects.

I have a self-contained here https://github.com/hughbe/AxHost-tests (run dotnet test in the System.Windows.Forms.Tests directory or running dotnet run >> output.txt and looking at output.txt)

Basically in a separate assembly (ComClass) I have defined some ComVisible classes with guids. I'm using regfree COM:

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <EnableComHosting>true</EnableComHosting>
    <EnableRegFreeCom>true</EnableRegFreeCom>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <NoWarn>CS0649;$(NoWarn)</NoWarn>
  </PropertyGroup>

This has a class Server that implements IOleObject (https://github.com/hughbe/AxHost-tests/blob/master/ComClass/Server.cs#L11)

Then I have a separate test project (System.Windows.Forms.Tests) that references ComClass. In the program code I have the following code

    [STAThread]
    static void Main()
    {
        try
        {
            var o = Activator.CreateInstance(Type.GetTypeFromCLSID(typeof(Server).GUID));
            Console.WriteLine(o.GetType());

            Guid clsid = typeof(Server).GUID;
            Guid iid = new Guid("{00000000-0000-0000-C000-000000000046}");
            HRESULT hr = CoCreateInstance(
                ref clsid,
                IntPtr.Zero,
                Ole32.CLSCTX.INPROC_SERVER,
                ref iid,
                out object res);
            Console.WriteLine(res.GetType());

            var control = new SubAxHost(typeof(Server).GUID.ToString());
            Assert.NotEqual(IntPtr.Zero, control.Handle);
            int invalidatedCallCount = 0;
            control.Invalidated += (sender, e) => invalidatedCallCount++;
            int styleChangedCallCount = 0;
            control.StyleChanged += (sender, e) => styleChangedCallCount++;
            int createdCallCount = 0;
            control.HandleCreated += (sender, e) => createdCallCount++;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }

        Console.WriteLine("Please Love Me");
    }

    private class SubAxHost : AxHost
    {
        public SubAxHost(string clsidString) : base(clsidString)
        {
        }
    }
}

private class SubAxHost : AxHost
{
    public SubAxHost(string clsidString) : base(clsidString)
    {
    }
}

First things first there appears a bug if we don't manually copy ComClass.runtimeconfig.json (i.e. remove the code in https://github.com/hughbe/AxHost-tests/blob/master/ComClass/ComClass.csproj#L15-L25)

System.Runtime.InteropServices.COMException (0x80008093): Retrieving the COM class factory for component with CLSID {0C0AF4A2-D038-4B82-B2C9-DED4D9435659} failed due to the following error: 80008093 0x80008093.
   at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean wrapExceptions, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& hasNoDefaultCtor)
   at System.RuntimeType.CreateInstanceDefaultCtorSlow(Boolean publicOnly, Boolean wrapExceptions, Boolean fillCache)
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, Boolean wrapExceptions)
   at System.Activator.CreateInstance(Type type, Boolean nonPublic, Boolean wrapExceptions)
   at System.Activator.CreateInstance(Type type)
   at System.Windows.Forms.Tests.Program.Main() in C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\Program.cs:line 23

and

  X System.Windows.Forms.Tests.AxHostTests.AxHost_Handle_GetNotIOleObject_ThrowsInvalidCastException [25ms]
  Error Message:
   Assert.Throws() Failure
Expected: typeof(System.InvalidCastException)
Actual:   typeof(System.Runtime.InteropServices.COMException): The specified runtimeconfig.json [C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\bin\Debug\netcoreapp3.1\ComClass.runtimeconfig.json] does not exist
---- System.Runtime.InteropServices.COMException : The specified runtimeconfig.json [C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\bin\Debug\netcoreapp3.1\ComClass.runtimeconfig.json] does not exist
  Stack Trace:
     at System.Windows.Forms.UnsafeNativeMethods.CoCreateInstance(Guid& clsid, Object punkOuter, Int32 context, Guid& iid)
   at System.Windows.Forms.AxHost.CreateWithLicense(String license, Guid clsid)
   at System.Windows.Forms.AxHost.CreateInstanceCore(Guid clsid)
   at System.Windows.Forms.AxHost.CreateInstance()
   at System.Windows.Forms.AxHost.GetOcxCreate()
   at System.Windows.Forms.AxHost.TransitionUpTo(Int32 state)
   at System.Windows.Forms.AxHost.CreateHandle()
   at System.Windows.Forms.Control.get_Handle()
   at System.Windows.Forms.Tests.AxHostTests.<>c__DisplayClass3_0.<AxHost_Handle_GetNotIOleObject_ThrowsInvalidCastException>b__1() in C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\AxHostTests.cs:line 60
----- Inner Stack Trace -----
   at System.Windows.Forms.UnsafeNativeMethods.CoCreateInstance(Guid& clsid, Object punkOuter, Int32 context, Guid& iid)
   at System.Windows.Forms.AxHost.CreateWithLicense(String license, Guid clsid)
   at System.Windows.Forms.AxHost.CreateInstanceCore(Guid clsid)
   at System.Windows.Forms.AxHost.CreateInstance()
   at System.Windows.Forms.AxHost.GetOcxCreate()
   at System.Windows.Forms.AxHost.TransitionUpTo(Int32 state)
   at System.Windows.Forms.AxHost.CreateHandle()
   at System.Windows.Forms.Control.get_Handle()
   at System.Windows.Forms.Tests.AxHostTests.<>c__DisplayClass3_0.<AxHost_Handle_GetNotIOleObject_ThrowsInvalidCastException>b__1() in C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\AxHostTests.cs:line 60

The error ---- System.Runtime.InteropServices.COMException : The specified runtimeconfig.json [C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\bin\Debug\netcoreapp3.1\ComClass.runtimeconfig.json] does not exist seems odd - is this an oversight/bug in registration free com? I'd expect this runtimeconfig.json to be copied over in the same way that the manifest, comhost.pdb, dll and pdb are...

When I do copy the runtimeconfig.json over, I get the following test failureerror

  X System.Windows.Forms.Tests.AxHostTests.AxHost_Handle_Get_Success [48ms]
  Error Message:
   System.InvalidCastException : Unable to cast object of type 'ComClass.Server' to type 'IOleObject'.
  Stack Trace:
     at System.Windows.Forms.AxHost.RealizeStyles()
   at System.Windows.Forms.AxHost.GetOcxCreate()
   at System.Windows.Forms.AxHost.TransitionUpTo(Int32 state)
   at System.Windows.Forms.AxHost.CreateHandle()
   at System.Windows.Forms.Control.get_Handle()
   at System.Windows.Forms.Tests.AxHostTests.<>c.<AxHost_Handle_Get_Success>b__1_0() in
C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\AxHostTests.cs:line 37
   at System.Windows.Forms.Tests.ActivationContext.UsingManifestDo(String manifest, Action action) in C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\ActivationContext.cs:line 41
   at System.Windows.Forms.Tests.AxHostTests.AxHost_Handle_Get_Success() in C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\AxHostTests.cs:line 34

It seems like in the following code

Guid clsid = typeof(Server).GUID;
Guid iid = new Guid("{00000000-0000-0000-C000-000000000046}");
HRESULT hr = CoCreateInstance(
    ref clsid,
    IntPtr.Zero,
    Ole32.CLSCTX.INPROC_SERVER,
    ref iid,
    out object res);
Console.WriteLine(res.GetType());

rest is of type Server, however, I would have expected it to be of type __ComObject which could be casted to interface types.

Is there a way to force this behaviour? Am I misunderstanding COM a bit. I would have expected that if you create a COM class (defined in C# in this case) using CoCreateInstance (which AxHost does under the hood) then we would get __ComObject back

Also, separately I found a bug in AxHost implementation. If the AxHost clsid exists but does not implement IOleObject, then we throw InvalidCastException on ocx creation. This is fine. However, the object can not be disposed as disposal crashes.

  X System.Windows.Forms.Tests.AxHostTests.AxHost_Handle_GetNotIOleObject_ThrowsInvalidCastException [9ms]
  Error Message:
   System.ArgumentException : The object's type must be __ComObject or derived from __ComObject. (Parameter 'o')
  Stack Trace:
     at System.Runtime.InteropServices.Marshal.FinalReleaseComObject(Object o)
   at System.Windows.Forms.AxHost.ReleaseAxControl()
   at System.Windows.Forms.AxHost.TransitionDownTo(Int32 state)
   at System.Windows.Forms.AxHost.Dispose(Boolean disposing)
   at System.ComponentModel.Component.Dispose()
   at System.Windows.Forms.Tests.AxHostTests.<>c.<AxHost_Handle_GetNotIOleObject_ThrowsInvalidCastException>b__3_0() in C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\AxHostTests.cs:line 62
   at System.Windows.Forms.Tests.ActivationContext.UsingManifestDo(String manifest, Action action) in C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\ActivationContext.cs:line 41
   at System.Windows.Forms.Tests.AxHostTests.AxHost_Handle_GetNotIOleObject_ThrowsInvalidCastException() in C:\Users\hughbe\source\repos\AxHost\System.Windows.Forms.Tests\AxHostTests.cs:line 57

The fix would be to guard the call to FinalReleaseComObject with a check for IsComObject. Should we fix this?

@weltkante
Copy link
Contributor

weltkante commented Jun 6, 2020

I would have expected it to be of type __ComObject which could be casted to interface types

__ComObject is for native RCWs, RCWs for managed objects are unpacked to preserve reference identity. You get __ComObject within the same process only when you marshal across apartments and the object is not agile (inherits from StandardOleMarshalObject). However that requires a TLB which is no longer supported in .NET Core. You cannot test __ComObject with a managed object implementation out of the box in .NET Core as far as I'm aware.

@hughbe
Copy link
Contributor Author

hughbe commented Jun 6, 2020

:( I guess i could write these tests against a native implementation. Or I could do some dodgy InternalsVisibleTo stuff to avoid the ICE - not a fan though.

Got to really really really get #1932 working haha

@weltkante
Copy link
Contributor

weltkante commented Jun 6, 2020

If you want to implement something to put into an AxHost your best bet is using ATL/MFC. The MFC wizards for creating ActiveX controls may still be functional and clicking through the wizard may create a basic control you can put in an AxHost.

@weltkante
Copy link
Contributor

weltkante commented Jun 6, 2020

The fix would be to guard the call to FinalReleaseComObject with a check for IsComObject. Should we fix this?

Probably not worth it, I'd rather fail earlier before the instance gets into a bad state. Trying to properly dispose a bad state is not something you should bother doing.

@weltkante
Copy link
Contributor

weltkante commented Jun 6, 2020

your best bet is using ATL/MFC

Also be warned that if you try going that route, the default VS project settings are to register the build output globally. The first thing I do with new projects is turn that off (unsetting "Register output" in "Linker" category of project settings) and then use regfree COM manifests in the consuming application instead.

Feel free to ping me on gitter btw. if you want to chat about this, its pretty hard to get started with this kind of stuff if you don't know anything about it.

@weltkante
Copy link
Contributor

weltkante commented Jun 6, 2020

Don't know if it helps you, but created a sample repo with an MFC ActiveX control hosted in Desktop/Core WinForms projects. The wizard creates a simple control which draws a circle. You can start from there and add properties to the control which will be settable in the AxHost design time.

steps taken
  • create an empty solution
  • create project from template "MFC ActiveX control"
  • In wizard, second page ("settings") keep the defaults, but you may want multiple controls with different settings here if you want to cover all the different features of AxHost/ActiveX (for example at least one unlicensed and once licensed control because their creation is different)
  • Go to project properties, linker page, set "Register output" to "No" (make sure you have "All Configurations" and "All platforms" selected in the dropdown)
  • Add a WinForms project
  • Add a file reference to the build output of the native project and copy it to your output directory of the WinForms project
  • Create an AxHost subclass which configures the CLSID (classically these AxHost subclasses were automatically generated by the tlbimp tool which auto-generated a managed assembly and exposed the Class/Interface of the Control on the AxHost subclass)
  • Add the AxHost subclass to a form
  • Add a manifest with the TLB and CoClass (look in the native projects IDL file for the GUIDs to use)

@hughbe
Copy link
Contributor Author

hughbe commented Jun 18, 2020

I'll do that in another PR then. I've managed to find a good way to make it work with cmake

@RussKie
Copy link
Member

RussKie commented Jun 18, 2020

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@codecov
Copy link

codecov bot commented Jun 18, 2020

Codecov Report

Merging #3366 into master will decrease coverage by 31.14041%.
The diff coverage is 62.50000%.

@@                 Coverage Diff                  @@
##              master       #3366          +/-   ##
====================================================
- Coverage   66.58991%   35.44950%   -31.14042%     
====================================================
  Files           1338         895         -443     
  Lines         501564      253524      -248040     
  Branches       40847       36761        -4086     
====================================================
- Hits          333991       89873      -244118     
+ Misses        162033      158803        -3230     
+ Partials        5540        4848         -692     
Flag Coverage Δ
#Debug 35.44950% <62.50000%> (-31.14042%) ⬇️
#production 35.44950% <62.50000%> (+0.08633%) ⬆️
#test ?

@RussKie RussKie removed the 📖 documentation: breaking A change in behavior that could be breaking for applications. Needs to be documented. label Jun 19, 2020
@RussKie RussKie merged commit 2a1ea08 into dotnet:master Jun 19, 2020
@ghost ghost added this to the 5.0 Preview7 milestone Jun 19, 2020
@hughbe hughbe deleted the AxHost-PropertyDescriptor-Tests branch June 19, 2020 08:27

namespace System.Windows.Forms.Tests
{
public class AxHostPropertyDescriptorTests : IClassFixture<ThreadExceptionFixture>
Copy link
Contributor

Choose a reason for hiding this comment

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

This would have needed [Collection("Sequential")] since otherwise WebBrowser currently is corrupting memory. I'm getting crashes locally again after this PR was merged, will extend the workaround by adding the attribute.

@hughbe @RussKie please keep this in mind if there are more AxHost tests coming.

@ghost ghost locked as resolved and limited conversation to collaborators Jan 31, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
test-enhancement Improvements of test source code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants