microsoft / CsWinRT

C# language projection for the Windows Runtime
MIT License
540 stars 102 forks source link

Adding a specific WPF control to the UI causes WinRT API to stop working #381

Closed dotMorten closed 4 years ago

dotMorten commented 4 years ago

This one has me quite stumped. The WinRT breaking change for .NET5 affects us, since we use the GeoLocator on Win8+ via reflection into the WinRT APIs, so I set out to build a workaround for our customers using the new projections instead. To my surprise, getting a position from the locator like this:

var _locator = new Geolocator();
var position = await _locator.GetGeopositionAsync(TimeSpan.FromHours(1), TimeSpan.FromMinutes(1));

would throw:

Exception: Exception has been thrown by the target of an invocation.`
Stack Trace:
   at System.RuntimeFieldHandle.GetValue(RtFieldInfo field, Object instance, RuntimeType fieldType, RuntimeType declaringType, Boolean& domainInitialized)
   at System.Reflection.RtFieldInfo.GetValue(Object obj)
   at WinRT.GuidGenerator.GetIID(Type type)
   at WinRT.IObjectReference.As[T]()
   at WinRT.DefaultComWrappers.CreateObject(IntPtr externalComObject, CreateObjectFlags flags)
   at System.Runtime.InteropServices.ComWrappers.CallCreateObject(ComWrappersScenario scenario, ComWrappers comWrappersImpl, IntPtr externalComObject, CreateObjectFlags flags)
   at System.Runtime.InteropServices.ComWrappers.TryGetOrCreateObjectForComInstanceInternal(ObjectHandleOnStack comWrappersImpl, Int64 wrapperId, IntPtr externalComObject, CreateObjectFlags flags, ObjectHandleOnStack wrapper, ObjectHandleOnStack retValue)
   at System.Runtime.InteropServices.ComWrappers.TryGetOrCreateObjectForComInstanceInternal(ComWrappers impl, IntPtr externalComObject, CreateObjectFlags flags, Object wrapperMaybe, Object& retValue)
   at System.Runtime.InteropServices.ComWrappers.GetOrCreateObjectForComInstance(IntPtr externalComObject, CreateObjectFlags flags)
   at WinRT.ComWrappersSupport.CreateRcwForComObject(IntPtr ptr)
   at WinRT.MarshalInspectable.FromAbi(IntPtr ptr)
   at WinRT.MarshalInterface`1.FromAbi(IntPtr ptr)
   at ABI.Windows.Devices.Geolocation.IGeolocator.GetGeopositionAsync(TimeSpan maximumAge, TimeSpan timeout)
   at Windows.Devices.Geolocation.Geolocator.GetGeopositionAsync(TimeSpan maximumAge, TimeSpan timeout)
   at WpfApp1.MainWindow.<GetLocation>d__1.MoveNext() in E:\sources.tmp\winrt_test_net5\WpfApp1\MainWindow.xaml.cs:line 43

Inner Exception: "The type initializer for 'Vftbl' threw an exception."
Stack Trace: Null

Inner Inner Exception: "The type initializer for 'WinRT.Marshaler`1' threw an exception."
Stack Trace:    at ABI.Windows.Foundation.IAsyncOperation`1.Vftbl..cctor()

Inner Inner Inner Exception: "The type initializer for 'WinRT.MarshalGeneric`1' threw an exception."
StackTrace:   at WinRT.Marshaler`1..cctor()

So I set out to make a reproducer, and couldn't repro. What I found is that when I add a reference to my WPF Control Library, and add that control to the view, the problem occurs. If I just instantiate the control but don't add it to the view, it works.

Repro: Repro app:WpfApp1.zip Or: Create a new WPF project and add the Esri control package:

csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows10.0.17763</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Esri.ArcGISRuntime.WPF" Version="100.8.0" />
  </ItemGroup>
</Project>

Add the following code to MainWindow.xaml.cs (note: I named the root Grid control LayoutRoot in MainWindow.xaml)

       public MainWindow()
        {
            InitializeComponent();            
            var mapView = new Esri.ArcGISRuntime.UI.Controls.MapView();
            this.Content = mapView; // To avoid problem, don't add control to layout:
            GetLocation();
        }

        private async void GetLocation()
        {
            var locator = new Windows.Devices.Geolocation.Geolocator();
            try
            {
                var position = await locator.GetGeopositionAsync(TimeSpan.FromHours(1), TimeSpan.FromMinutes(1));
            }
            catch (System.Exception ex)
            {
                Debugger.Break();
            }
        }

Observe the exception in GetLocation getting caught. Next remove the call this.Content = mapView;, and observe getting the location works.

Unfortunately I can't share the source for the control, but I can't really spot anything especially weird that takes place in Load/OnApplyTemplate that seems to trigger this issue - I'd be happy to troubleshoot one-on-one on a call as well so we can pinpoint the culprit. The static initializers and constructor does cause some code to run that could potentially cause this, but calling the constructor doesn't seem to cause the problem to appear.

dotMorten commented 4 years ago

Update: I narrowed it down a bit more. Instead of creating the MapView control, I instead create this:

var l = new Esri.ArcGISRuntime.Location.SystemLocationDataSource();

The issue still reproduces. But the weird part is if you then take this repro and bring it into a .NET Core console app, it no longer reproduces. There seems to be some weird thing going on between my library and WPF.

So that class doesn't really do much in the constructor. Having said that I do use reflection to get to the WinRT Geolocator types in that class (the thing that now no longer works in net5, so this returns null).

However, after realizing that Type.GetType("Windows.Devices.Geolocation.Geolocator, Windows, ContentType=WindowsRuntime"); returns null, it doesn't really do anything else. I've tried that line of code in the repro, and it doesn't seem to trigger the problem.

jkoritzinsky commented 4 years ago

@dotMorten I'm seeing a Windows.Devices.Geolocation.Geoposition type in the ArcGIS.Runtime assembly. It looks like the type discovery algorithm C#/WinRT uses for parsing the runtime class name is accidentally picking up that type instead of the type in the projection.

Based on this, we need to fix #312 or complete #369 to fix this issue.

jkoritzinsky commented 4 years ago

If the Windows.Devices.Geolocation.Geoposition type in ArcGIS.Runtime is just a reflection-based wrapper around the WinRT object in the old system and you can #if it away on .NET 5, then that should work as a workaround for this issue.

dotMorten commented 4 years ago

@jkoritzinsky I could do that, but that would require adding a .NET 5 target and shipping a new release which is some ways out in the future. Also how come when adding that line of code in the app itself doesn't cause this to get reproduced?

Second, do I understand your first comment correctly that this isn't technically a bug in my code, but a bug in CsWinRT? It definitely seems odd that a simple type lookup can cause the entire projection to stop working. I'm assuming I have far from the only existing library out there that does this, and would break .NET 5 interop.

jkoritzinsky commented 4 years ago

Yes this is a bug in C#/WinRT. It only fails if the ArcGIS.Runtime assembly is loaded in such a way that it is earlier in the runtime's list of loaded assemblies than Microsoft.Windows.SDK.NET.dll.

That's why it doesn't happen when you don't use the ArcGIS APIs before calling into the WinRT APIs.

dotMorten commented 4 years ago

I can confirm that if I add this line to the App constructor to cause it to load, it'll work:

    public App()
    {
        var locator = new Geolocator();
    }

I'm seeing a Windows.Devices.Geolocation.Geoposition type in the ArcGIS.Runtime assembly

Yes I wrote an internal wrapper the matches the API of the WinRT API I used in the UWP target so I had less code-differences between .NET Core and UWP in the consuming code. I guess a simple solution would be to just change the namespace, but again that wouldn't be until we ship the next version, and at that point, I expect we have moved to the new way for consuming WinRT types.

AdamBraden commented 4 years ago

Tagging for RTM - Part of this should be fixed with IDynamicInterfaceCastable and the other part is handled by Module Initializers