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

WinUI 3 Cant instantiate a UserControl from a location outside the application package or directory (XamlParseException) #6299

Open
2 tasks
williamfigtree opened this issue Nov 15, 2021 · 53 comments
Labels
bug Something isn't working feature proposal New feature proposal product-winui3 WinUI 3 issues team-Markup Issue for the Markup team wct

Comments

@williamfigtree
Copy link

Describe the bug

When instantiating a UserControl from outside the application package an XamlParseException is thrown from the UserControl constructor at this.InitializeComponent().

Exception thrown at 0x773B35D2 (KernelBase.dll) in HostApp.exe: WinRT originate error - 0x80004005 : 'Cannot locate resource from 'ms-appx:///Plugin/PluginUserControl.xaml'.'.
Exception thrown: 'Microsoft.UI.Xaml.Markup.XamlParseException' in WinRT.Runtime.dll
WinRT information: Cannot locate resource from 'ms-appx:///Plugin/PluginUserControl.xaml'.
XAML parsing failed.

Steps to reproduce the bug

See minimal reproducing repository https://github.com/williamfigtree/WinUIPluginIssue. Key steps to recreate:

  1. Create a class library "Plugin" using template "Class Library (WinUI 3 in Desktop)"

  2. Add a UserControl "PluginUserControl" using template "User Control (WinUI 3)" to Plugin.csproj

  3. Create an application "HostApp" using template "Black App, Packaged (WinUI 3 in Desktop)"

  4. Add the "PluginLoadContext" class from this tutorial https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support to HostApp.csproj

    using System;
    using System.Reflection;
    using System.Runtime.Loader;
    
    namespace HostApp
    {
        class PluginLoadContext : AssemblyLoadContext
        {
            private AssemblyDependencyResolver _resolver;
    
            public PluginLoadContext(string pluginPath)
            {
                _resolver = new AssemblyDependencyResolver(pluginPath);
            }
    
            protected override Assembly Load(AssemblyName assemblyName)
            {
                string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
                if (assemblyPath != null)
                {
                    return LoadFromAssemblyPath(assemblyPath);
                }
    
                return null;
            }
    
            protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
            {
                string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
                if (libraryPath != null)
                {
                    return LoadUnmanagedDllFromPath(libraryPath);
                }
    
                return IntPtr.Zero;
            }
        }
    }
  5. Add the following code to the App constructor. You may need to modify the path to Plugin.dll.

        public App()
        {
            this.InitializeComponent();
    
            // Attempt to load a UserControl from a plugin dll
            // PluginUserControl throws an XamlParsingExcpetion during instantiation
    
            // Locate plugin dll in the Plugin project bin directory
            var rootPath = Path.GetFullPath(@"..\..\..\..\..\..\..\..\", typeof(Program).Assembly.Location);
            var pluginDllPath = Path.Combine(rootPath, @"Plugin\bin\x86\Debug\net5.0-windows10.0.19041.0\Plugin.dll");
    
            // Instantiate PluginUserControl
            var pluginLoadContext = new PluginLoadContext(pluginDllPath);
            using (pluginLoadContext.EnterContextualReflection())
            {
                var pluginUserControl = Activator.CreateInstance("Plugin", "Plugin.PluginUserControl");
            }
        }
  6. Build Plugin.csproj

  7. Deploy and debug HostApp.csproj

Expected behavior

The UserControl is instantiated and the UserControl XAML resources supplied by the external project, package, or directory are located.

Screenshots

No response

NuGet package version

No response

Windows app type

  • UWP
  • Win32

Device form factor

Desktop

Windows version

November 2019 Update (18363)

Additional context

This is a blocking issue for applications which require plugins which supply their own UI.

Observed the issue with WinUI 3 1.0.0-preview3.

#3888 describes a similar requirement but does not come to an appropriate solution as it requires files from the plugin to be copied into the main package. This prevents separate build and distribution of applications and plugins.

#1365 (comment) and https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support suggest that AssemblyLoadContext is the intended mechanism for plugin support and do not indicate any separate system for XAML resources.

@ghost ghost added the needs-triage Issue needs to be triaged by the area owners label Nov 15, 2021
@mbyerska
Copy link

mbyerska commented Nov 29, 2021

I have the same issue.
I followed the https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support example, but it only does console stuff. When trying to get the plugin to have UI components, it just doesn't work.

@Daniellled
Copy link

Daniellled commented Dec 7, 2021

This may be related to 5536

@MikeHillberg
Copy link
Contributor

Dynamic loading of a UserControl isn't supported yet, unless we can find a workaround. The restriction is in the code for the UserControl that's generated by the Xaml compiler, where it assumes that its markup is stored in the app's resources (ms-appx):

public void InitializeComponent()
{
    if (_contentLoaded)
        return;

    _contentLoaded = true;

    global::System.Uri resourceLocator = new global::System.Uri("ms-appx:///UserControl1.xaml");
    global::Microsoft.UI.Xaml.Application.LoadComponent(this, resourceLocator, global::Microsoft.UI.Xaml.Controls.Primitives.ComponentResourceLocation.Application);
}

There might be a way to work around it by not calling this generated InitializeComponent and instead writing that code with a different Uri for the resourceLocaler. @RealTommyKlein might have ideas. I know that that URI needs to be ms-appx, ms-appdata, or ms-resource.

@williamfigtree
Copy link
Author

@MikeHillberg @RealTommyKlein I was not successful replacing the generated InitializeComponent() with a different implementation. That seems like it should work though, perhaps I'm doing something wrong...

Using an optional package to include my plugin project in the app dependency graph I expected to be able to use a URI like "ms-appx://PluginPackageIdentityName/UserControl1.xaml" per these docs: https://docs.microsoft.com/en-us/windows/uwp/app-resources/uri-schemes#authority-ms-appx-and-ms-appx-web. Unfortunately the same "Cannot locate resource..." exception is thrown.

What am I missing?

@RealTommyKlein
Copy link
Contributor

At least for the InitializeComponent replacement, something like this should work:

        public MainWindow()
        {
            // Remove default compiler-generated InitializeComponent implementation, and use our hand-written one instead
            //this.InitializeComponent();
            this.InitializeComponentCustom();
        }

        public void InitializeComponentCustom()
        {
            if (_contentLoaded)
                return;

            _contentLoaded = true;

            global::System.Uri resourceLocator = new global::System.Uri("ms-appx:///SomeCustomLocation/PluginUserControl.xaml");
            global::Microsoft.UI.Xaml.Application.LoadComponent(this, resourceLocator, global::Microsoft.UI.Xaml.Controls.Primitives.ComponentResourceLocation.Application);
        }

But I suspect the LoadComponent call is failing because PluginUserControl.xaml isn't present in the application's resources.pri. Normally there's an entry generated for it during the build if the application references the "Plugin" library directly, but since it's loaded by reflection instead, the class library's resources.pri with the PluginUserControl.xaml entry isn't merged into the application's resources.pri. I don't think using a control from a library loaded via reflection is currently supported for that reason - @evelynwu-msft and @marb2000 since we were discussing this yesterday.

@iomismo
Copy link

iomismo commented Jan 13, 2022

What about resource merge at run-time?

I tried it some time ago, but I don't remember the results. But for sure it didn't end working. Just thinking...

The first approach was:

Plugin:

public static List<ResourceDictionary> GetResources()
{
	return new List<ResourceDictionary>()
	{
		new ResourceDictionary
		{
			Source = new Uri("ms-appx:///Views/ModulePage.xaml", UriKind.Relative)
		}
	};
}

Host:

foreach (ResourceDictionary re in plugin.GetResources())
{
	Application.Current.Resources.MergedDictionaries.Add(re);
}

The second approach was mannually getting resources (at least string values and only for testing) and setting control text properties ... no luck, as the MainResourceMap does not contain plugin resources, no matter if trying at plugin or host code.

var resourceContext = new Windows.ApplicationModel.Resources.Core.ResourceContext();// not using ResourceContext.GetForCurrentView
resourceContext.QualifierValues["Language"] = "en-US";
var resourceMap = Windows.ApplicationModel.Resources.Core.ResourceManager.Current.MainResourceMap.GetSubtree("Resources");
string resourceValue = resourceMap.GetValue("AppDescription", resourceContext).ValueAsString;

Then I ended up playing with makepri with no results.

Maybe something more drastic like resource merge at plugin install time, involving makepri?

Or try to use another resource system type/workflow in plugins?

@williamfigtree
Copy link
Author

I discovered that I am able to successfuly load the UserControl if it is included in an optional package and the URI specifies the full path to the installed package location e.g.
ms-appx:///C:/Program Files/WindowsApps/[Package full name]/HostApp.SimplePlugin/SimpleUI.xaml.

This is a viable workaround for simple things but leaves a big outstanding issue with resource location. Features like Resource.resw files and 3rd party controls with embedded xaml data do not work from the plugin package. So more on that:

@RealTommyKlein The plugin's resources.pri file does seem to be loaded into the application's resources although not into MainResourceMap. I can see this by inspecting AllResourceMaps on ResourceManager.Current at runtime:
plugin resource map loaded at runtime

Perhaps the plugin's resources.pri is incorrect? I am using the resources.pri file that VisualStudio generates for a WinUI3 class library. Does it need to contain a named resource for the UserControl xaml and not the xbf? Is it formatted incorrectly? Here's the plugin pri dump resources.pri.xml.zip and a trimmed down version with just the relevant resource map:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<PriInfo>
	<PriHeader>
		<WindowsEnvironment name="WinCore" version="1.2" checksum="1912541329"/>
		<AutoMerge>false</AutoMerge>
		<IsDeploymentMergeable>true</IsDeploymentMergeable>
		<TargetOS version="10.0.0"/>
		<ReverseMap>false</ReverseMap>
	</PriHeader>
	<QualifierInfo>
		<Qualifiers>
			<Qualifier name="Language" value="EN-US" priority="700" scoreAsDefault="1.0" index="1"/>
		</Qualifiers>
		<QualifierSets>
			<QualifierSet index="1">
				<Qualifier name="Language" value="EN-US" priority="700" scoreAsDefault="1.0" index="1"/>
			</QualifierSet>
		</QualifierSets>
		<Decisions>
			<Decision index="2">
				<QualifierSet index="1">
					<Qualifier name="Language" value="EN-US" priority="700" scoreAsDefault="1.0" index="1"/>
				</QualifierSet>
			</Decision>
		</Decisions>
	</QualifierInfo>
	<ResourceMap name="HostApp.SimplePlugin" primary="true" uniqueName="ms-appx://HostApp.SimplePlugin/" version="1.0">
		<VersionInfo version="1.0" checksum="1169589394" numScopes="16" numItems="15"/>
		<ResourceMapSubtree index="1" name="Files">
			<ResourceMapSubtree index="15" name="HostApp.SimplePlugin">
				<NamedResource name="SimpleUI.xbf" index="14" uri="ms-resource://HostApp.SimplePlugin/Files/HostApp.SimplePlugin/SimpleUI.xbf">
					<Decision index="1">
						<QualifierSet index="0"/>
					</Decision>
					<Candidate type="Path">
						<QualifierSet index="0"/>
						<Value>HostApp.SimplePlugin\SimpleUI.xbf</Value>
					</Candidate>
				</NamedResource>
			</ResourceMapSubtree>
		</ResourceMapSubtree>
	</ResourceMap>
</PriInfo>

And here is the repo I'm using to test this with optional packages https://github.com/williamfigtree/WinUI3-plugin-sample. I've had to use a custom build script on the optional package project due to lack of VisualStudio tooling for WinUI3.

@harvinders
Copy link

I have tried #6299 (comment) by @williamfigtree and it works for a single UserControl. However, if that UserControl contains another UserControl, then it fails.

So if you have a MainApp that tries to load a UserControlA from a AssemblyX, you would be able to load UserControlA as mentioned. However if there is another UserControlB in AssemblyX and UserControlA uses it in the XAML file, then we get XAML Parsing exception when loading the UserControlA. @RealTommyKlein do you have any suggestion or work around that you can think off.

Also, @marb2000 has mentioned that the WinUI team is concentrating on the core infrastructure this year, instead of adding more controls. Would you consider this issue/feature among core infrastructure tickets that needs to be addressed in 1.1 or 1.2? This issue is preventing us from developing a class of application (plugins with UIs). I am sure many others in the community too would like to see it addressed soon. @marb2000, @MikeHillberg, A guidance would help us immensely to determine the direction for our project.

@harvinders
Copy link

WE have further found few more things

[1] It is possible to load multiple UserControls, however you would need to do it in the code and then assign then to the Content properties, something like

<Page>
     <ContentControl x:Name="ControlA"/>
....
     <ContentControl x:Name="ControlB"/>
</Page>
ControlA.Content = new MyUserControl();

[2] Also, these UserControls can't have a DependencyProperty.

[3] Also, the plugin can't define a AttachedProperty that can be used in the UserControls XAML

@cody3b
Copy link

cody3b commented Apr 28, 2022

Issue still present in 1.1.0-preview2. The plugin architecture is a critical piece of my companies application and this needs to be resolved before we can move forward with WinUI 3.

@michael-hawker
Copy link
Collaborator

This would be useful for XAML Studio in the future as well to be able to load custom diagnostic tools from other assemblies based on some plug-in API. This seems like a pretty common scenario we see a lot of folks build into apps for mods/customizations/plugins.

@Pietro228
Copy link

It's still an issue in 1.1 full release :( I can't create my application with that error.

@Scottj1s
Copy link
Member

Scottj1s commented Jun 15, 2022

@evelynwu-msft is this related to http://task.ms/36925415? mention the workaround?

(update: since this is dynamically activated, rather than project-referenced, looks like some of the approaches above will be necessary)

@Pietro228
Copy link

@evelynwu-msft is this related to http://task.ms/36925415? mention the workaround?

(update: since this is dynamically activated, rather than project-referenced, looks like some of the approaches above will be necessary)

I can't open that link because it gives me an error :(

@evelynwu-msft
Copy link
Contributor

@evelynwu-msft is this related to http://task.ms/36925415? mention the workaround?

(update: since this is dynamically activated, rather than project-referenced, looks like some of the approaches above will be necessary)

No, this is a bona fide feature request.

@OculiViridi
Copy link

Describe the bug

When instantiating a UserControl from outside the application package an XamlParseException is thrown from the UserControl constructor at this.InitializeComponent().

Have you tried with a CustomControl using template Custom Control (WinUI 3)? Do you have the same issue?

@djonasdev
Copy link

This may be related to #7788

@harvinders
Copy link

@axodox Just wondering if you have tested

  • Does the dependency properties in the UserControls work?
  • Also have you been able to use x:Uid in XAML to display information from the resource file? I suppose it has something to do with the pri files and a separate story.

I tried in a C# application, the above points are not working, however I am able to place a UserControl within a XAML of another UserControl. We were not able to do that before your work around. Thanks to you we would be able to simplify the code a little bit.

@axodox
Copy link

axodox commented Jan 15, 2023

@harvinders We have not tested those yet, I will definitely look into the dependency properties next week.

@williamfigtree
Copy link
Author

@Pietro228 in addition to what axodox's mentioned my repo has a powershell script that builds a WinUI 3 library as an optional package invoked from the post build target and a user control with the modified InitializeComponent code. None of it is beautiful but it might help you get started.

@Pietro228
Copy link

@Pietro228 in addition to what axodox's mentioned my repo has a powershell script that builds a WinUI 3 library as an optional package invoked from the post build target and a user control with the modified InitializeComponent code. None of it is beautiful but it might help you get started.

Thank you, I'll have a look! :)

@axodox
Copy link

axodox commented Jan 16, 2023

@harvinders So I have tested the dependency properties, and they seem to work properly. I have tested it with an updating OneWay binding bound to a INotifyPropertyChanged capable class and it was updating on the UI. Both the source and the target of the binding was defined in the plugin. We are using x:Bind almost exclusively, so I only tested with that. Can you describe how it failed for you?

I have also added one more change to the above example: I added recursion guards to PluginXamlMetadataProvider, as infinite recursion happens if you reference the DLL carrying PluginXamlMetadataProvider in both the plugin and your app.

@harvinders
Copy link

@axodox

Did you create a new DependencyProperty in your control or are you taking about existing WinUI control's dependency properties. I had trouble with defining and using new Dependency Properties and Attached Properties.

@axodox
Copy link

axodox commented Jan 17, 2023

@axodox

Did you create a new DependencyProperty in your control or are you taking about existing WinUI control's dependency properties. I had trouble with defining and using new Dependency Properties and Attached Properties.

I did create my own property, in fact I have not even tested built-in ones. I created a static field to store it, and initialized it like this:

  DependencyProperty SampleControl::_textProperty =
    DependencyProperty::Register(
      L"Text",
      xaml_typename<hstring>(),
      xaml_typename<TestLibrary::SampleControl>(),
      PropertyMetadata{ box_value(L"Default") } );
}

This is my IDL:

  [default_interface]
  runtimeclass SampleControl : Windows.UI.Xaml.Controls.UserControl
  {
    SampleControl();

    String Text;
    static Windows.UI.Xaml.DependencyProperty TextProperty{ get; };
  }

The static variables are initialized from the DLL main when the plugin is loaded. But basically codewise it is same as usual for me.

Maybe there is a difference since we are using C++? There were certainly some differences between C# and C++ XAML functionality before on our project I have run into.

@axodox
Copy link

axodox commented Jan 17, 2023

The bindings in C++ are done with autogenerated code like the one I have attached:

SamplePanelExtension.xaml.g.zip

I am not sure where this is with C#, maybe it uses reflection instead and that causes it to not find something. You could look into if you have similar files with C#, and compare them.

Or it could be that we are on UWP..

@Pietro228
Copy link

Pietro228 commented Feb 6, 2023

@Pietro228 in addition to what axodox's mentioned my repo has a powershell script that builds a WinUI 3 library as an optional package invoked from the post build target and a user control with the modified InitializeComponent code. None of it is beautiful but it might help you get started.

I was trying to run your code but I don't know what should I write to "my_certificate_path" argument in you post build PS script :(

How can I get my certificate path and which should I use?

@williamfigtree
Copy link
Author

@Pietro228 in addition to what axodox's mentioned my repo has a powershell script that builds a WinUI 3 library as an optional package invoked from the post build target and a user control with the modified InitializeComponent code. None of it is beautiful but it might help you get started.

I was trying to run your code but I don't know what should I write to "my_certificate_path" argument in you post build PS script :(

How can I get my certificate path and which should I use?

The post build step packages the plugin assembly as a signed msix. To do the signing you need a signing certificate (.pfx file). The argument "my_certificate_path" is the path to your signing certificate. You can create your own signing certificate for local development using the steps I've outlined in the repo readme which also links the MS docs. You will need to change a few details e.g. publisher name when you apply this to your own projects. :)

@dnchattan
Copy link

I've managed to get this working with some success in C#, however am still having problems where GetXamlType is getting called from the native Xaml interop with a null type argument if I reference certain things (like custom dependency properties using custom types). Best I can figure is that it is because the host apps PRI file doesn't include references to the extension. I got it to work by merging the PRI files, but that isn't a shippable solution for user-installed extensions.

For reference, I have thrown a quick working library and example to do this in C#, and also does some special magic to keep hot reload working if your extension is not packaged with the application. (in my own use-case, I can launch the shipped host application with a cmdline argument pointing it at an extension output directory and have a lovely time with hot reload working :))

https://github.com/dnchattan/winui-extensions

@harvinders
Copy link

Just wondering if the new API in the experimental build https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/experimental-channel#new-features-from-across-winappsdk, ResourceManagerInitializing could be of help to load the PRI resources from the plugins

@axodox
Copy link

axodox commented Feb 12, 2023

I had problems at first with PRI as well with UWP / WinUI 2 / C++/WinRT. I had to do the following to fix it:

  • Rename the file to resources.pri and put it in the root of the msix
  • Create a package mapping file with a [ResourceMetadata] section
  • Make sure that the tooling processed xaml files are also added to the msix under the directory named as target name
  • Use makeappx.exe to pack the files with the mapping

There is a tool which can dump your pri file to a human readable format, you can use it to make sure that it has all the content needed. I needed no modification in the code (except for the initialize component URI to have the package name) as discussed above. The runtime will locate the resources.pri in the package and load it, by default it has a different filename for me, which will not get loaded.

This way I could produce working plugins, including custom controls with custom dependency properties. Now we have it tested and working, the plugins are built after the main app from a separate git repo, so the main app has no knowledge of them.

@dnchattan
Copy link

@axodox thanks for the additional details. I'm trying to do this with an unpackaged app, so I'm a little off the rails when it comes to using an MSIX for the extension. This gives me some good info though, so I have a little more I can try out.

@dnchattan
Copy link

actually, now that I'm trying more scenarios with dependency properties and whatnot, it's still working for me where it definitely wasn't in my other project. will try porting my example to my actual code, but this is looking promising.

@cody3b
Copy link

cody3b commented Feb 14, 2023

@dnchattan Wanted to give a huge thanks to you (and all the others here). I was able to use your example to successfully get plugins working in our main app. I haven't ran into any issues so far but will report in if I do.

@dnchattan
Copy link

@cody3b glad I could be of help! The one thing I'm running into right now is where resources need to be loaded from the PRI (such as string resources). Feels like it's close to working but can't get the last piece.
FWIW I've pushed the shared library part of my solution to nuget if you want to consume it there. Open to any PRs if you need to change anything.
https://www.nuget.org/packages/CustomExtensions.WinUI/

@cody3b
Copy link

cody3b commented Mar 9, 2023

@dnchattan Have you been able to get this to work with .NET 7? I tried upgrading today, but ran into XAML parsing exceptions.

@Juansero29
Copy link

Juansero29 commented Aug 3, 2023

@cody3b
Don't know if this can help you or others but in my case could finally find a solution for this using .NET 7, it involves:

  1. Using these tags in the .csproj
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<GenerateLibraryLayout>true</GenerateLibraryLayout>
<!--FORCE INCLUSION OF *.pri FILE SO THAT STRING RESOURCES FROM THIS NUGET CAN BE ACCESSIBLE TO APPS THAT REFERENCE IT-->
<ItemGroup>
    <None Update="bin/$(Configuration)/$(TargetFramework)/$(PackageId).pri" CopyToOutputDirectory="CopyAlways" CopyToPublishDirectory="CopyAlways" />
</ItemGroup>

If 'None' tag doesn't succed alone on copying the PRI of the project inside the nuget (that's what happened to me), then we need to do it 'manually':

  1. This can be done by copying the .pri file that is generated when I built my library (at bin/$(Configuration)/$(TargetFramework)/$(PackageId).pri), then putting it inside the nuget at the path nuget/lib/net7.0-windows10.0.22621/My.Project.Name.pri then using this nuget (I tested it locally first by manuallly dragging and dropping the .pri file inside the nuget, then referencing the local nuget from my app to see it worked)
  2. If you want to integrate this into CI/CD, then implement it on the Devops build pipeline of theproject so that you no longer have to do this manually: it involves doing the same process but with a PITA .yml code to write in your build pipeline.

This is what my pipeline looked like in the end:

trigger:
- main

pool:
  vmImage: 'windows-latest'

variables:
  projectName: 'NameOfYourProject'
  solution: '**/$(projectName).sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
  major: '1'
  minor: '0'
  revision: $[counter(variables['minor'], 1)] # This will get reset every time minor gets bumped.
  nugetVersion: '$(major).$(minor).$(revision)'
  targetFramework: 'net7.0-windows10.0.22621'

steps:
- task: NuGetToolInstaller@1

- task: DotNetCoreCLI@2
  displayName: 'Restore Nuget'
  inputs:
    command: 'restore'
    projects: '**/$(projectName)/$(projectName).csproj'
    feedsToUse: 'config'
    nugetConfigPath: '.\nuget.config'

- task: VSBuild@1
  displayName: 'Build'
  inputs:
    platform: 'Any CPU'
    solution: '$(solution)'
    configuration: '$(buildConfiguration)'

- task: DotNetCoreCLI@2
  displayName: 'Pack Nuget'
  inputs:
    command: 'pack'
    packagesToPack: '**/$(projectName)/$(projectName).csproj'
    packDirectory: '$(Build.ArtifactStagingDirectory)/package'
    includesymbols: true
    includesource: true
    versioningScheme: 'byEnvVar'
    versionEnvVar: 'nugetVersion'
    buildProperties: 'SymbolPackageFormat=snupkg'
    verbosityPack: 'Diagnostic'

- task: PowerShell@2
  displayName: 'Rename .nupkg to .zip'
  inputs:
    targetType: 'inline'
    script: |
      Write-Host "Renaming file $(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).nupkg to $(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).zip"
      Rename-Item -Path "$(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).nupkg" -NewName "$(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).zip"
      Write-Host "File $(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).nupkg successfully renamed to $(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).zip"
    showWarnings: true
    
- task: ExtractFiles@1
  displayName: 'Extract files from .zip into a folder'
  inputs:
    archiveFilePatterns: $(Build.ArtifactStagingDirectory)/package/$(projectName).$(nugetVersion).zip
    destinationFolder: $(Build.ArtifactStagingDirectory)/package/$(projectName).$(nugetVersion)/
    cleanDestinationFolder: true
    overwriteExistingFiles: false

- task: CopyFiles@2
  displayName: 'Copy PRI into folder'
  inputs:
    SourceFolder: '$(projectName)/bin/$(buildConfiguration)/$(targetFramework)/'
    Contents: '$(projectName).pri'
    TargetFolder: '$(Build.ArtifactStagingDirectory)/package/$(projectName).$(nugetVersion)/lib/$(targetFramework)/'

- task: ArchiveFiles@2
  displayName: 'Archive folder back into .zip'
  inputs:
    rootFolderOrFile: $(Build.ArtifactStagingDirectory)/package/$(projectName).$(nugetVersion)
    includeRootFolder: false
    archiveType: 'zip'
    archiveFile: $(Build.ArtifactStagingDirectory)/package/$(projectName).$(nugetVersion).zip
    replaceExistingArchive: true

- task: PowerShell@2
  displayName: 'Rename .zip back to .nupkg'
  inputs:
    targetType: 'inline'
    script: |
      Write-Host "Renaming file $(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).zip to $(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).nupkg"
      Rename-Item -Path "$(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).zip" -NewName "$(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).nupkg"
      Write-Host "File $(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).zip successfully renamed to $(Build.ArtifactStagingDirectory)\package\$(projectName).$(nugetVersion).nupkg"
    showWarnings: true

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact'
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)/package'

This fixed the exception WinRT information: Cannot locate resource from 'ms-appx:///Plugin/*.xaml'. for me. This was kind of like 2 days of work for me, so I really hope it saves that from you!

@Juansero29
Copy link

Juansero29 commented Aug 3, 2023

Now that I wrote my workaround for that problem, I really think this is a problem that should be managed by the dotnet pack command, instead of having devs writing ape code to do the job that the nuget command line is supposed to do in order to create clean nuget packages for WinUI 3. Please fix this @microsoft

@blake-fm
Copy link

blake-fm commented Sep 9, 2023

@Juansero29 Many thanks! I ended up with this, after reading your post. Working for simple libraries, using dotnet pack.

  <EnableMsixTooling>true</EnableMsixTooling>
  <ItemGroup Condition="$(TargetFramework.EndsWith('-windows10.0.19041.0'))">
    <None Include="bin/$(Platform)/$(Configuration)/$(TargetFramework)/$(AssemblyName).pri" Pack="true" PackagePath="lib/$(TargetFramework.TrimEnd('.0'))/" />
  </ItemGroup>

@dsnorat
Copy link

dsnorat commented Aug 15, 2024

Great! But is there any way how to do this in C# with System.Reflection.Assembly? Because I don't know how should I replicate this in C# :(

I think there should be - at least for UWP and WinUI 2 (somebody should verify WinUI 3). You could start with @williamfigtree's repo which shows how to load an assembly in C#, of course with UWP you would have separate packages, but that would work the same way as with C++. Then implement PluginXamlMetadataProvider in C# the same way as I did above (you might need to do this in a separate project, as with C++ we had an issues that it did not work when it was in the same project / assembly as the host). You should use the activator to instantiate the XamlMetadataProvider of the plug-in and add it to the provider's list, this should happen before instantiating the control. If you use UWP and multiple packages you would need to modify InitalizeComponent code as well to use the proper URI.

Yes, I can confirm this works for WinUI3 as well. We have had a very similar solution in place since WinUI3 was released in 2021. However, I'm really looking forward to the WinUI3 team addressing this issue and supporting it natively in the UI framework. There are other SW products waiting of this in order to move confidently to WinUI3. BTW we had to put in place a similar workaround for UWP solutions and OP, same timeline as when UWP was launched.

@LucaCris
Copy link

Any news on this? I have a lot of plugins (config dialogs) and dedicated developers must implement all the UI by code... not ideal!

Image

@KimihikoAkayasaki
Copy link

KimihikoAkayasaki commented Oct 15, 2024

Any news on this? I have a lot of plugins (config dialogs) and dedicated developers must implement all the UI by code... not ideal!

As for me, stuff from other answers helped me a bit and:

  • I'm using MEF for plugin imports, this works for both packaged and unpackaged app with unpackaged plugins.
    For packaged plugins, you may omit the resource merge, and it should work without additional setup.

  • The host app prompts the plugins to create their UI elements upon loading (so they can do that on UI thread),
    the Pages are then returned by them for the host app to display in settings, using a ContentPresenter.

  • Also worth noting - I am not using pri string resources, a MEF import with a "translator" class is used instead.

  1. Replace InitializeComponent(); with code similar to this. You will need to merge the resource dictionaries and manually load the XAML component (as InitializeComponent would do by default, however using other paths). This workaround is used basically everywhere.
    public DeviceSettings()
    {
        // Get the root folder of the plugin - mine are unpackaged and contain published files
        var pluginDir = Directory.GetParent(Assembly.GetAssembly(GetType())!.Location);
    
        // Find the resources.pri file and load it synchronously
        var priFile = StorageFile.GetFileFromPathAsync(
            Path.Join(pluginDir!.FullName, "resources.pri")).GetAwaiter().GetResult();
    
        // Manually load your resources into the app
        ResourceManager.Current.LoadPriFiles([priFile]);
    
        // My plugin has XAML and XBF files copied from release in ./Pages (relative to its .dll)
        // Those files also have their names the same as the classes - DeviceSettings.xaml/xaml.cs here
        Application.LoadComponent(this, new Uri($"ms-appx:///{
            Path.Join(pluginDir!.FullName, "Pages", $"{GetType().Name}.xaml")}"),
            ComponentResourceLocation.Application);
    }
  2. For certain setups, one additional step may be needed; resources.pri, .xaml, and .xbf files appear for me only in debug and release mode, there's none when I publish the plugins. As a workaround, I copy them "manually" by abusing post-build steps.
    Again, note that in my case the UI stuff in in ./Pages (and plugin_Relay shoud probably be $(ProjectName) instead)
    The .pri file is also renamed here so the handler has one thing less to worry about during its workaround execution.
    <Target Name="CopyResources" AfterTargets="Publish">
    	<ItemGroup>
    		<_CustomFiles1 Include="$(ProjectDir)$(OutDir)plugin_Relay\Pages\*.*" />
    		<_CustomFiles2 Include="$(ProjectDir)$(OutDir)plugin_Relay.pri" />
    	</ItemGroup>
    	<Copy SourceFiles="@(_CustomFiles1)" DestinationFolder="$(PublishDir)\Pages" />
    	<Copy SourceFiles="@(_CustomFiles2)" DestinationFiles="$(PublishDir)\resources.pri" />
    </Target>

This may or may not help somebody, but well... it at least might be worth trying.

Posted code is from here, a plugin for this app.

@LucaCris
Copy link

My plugins are just dlls, standard net 8 dll with WinUI support enabled and released as nuget packages... LoadAssembly and assembly.CreateInstance() to load and execute them when downloaded on disk...

@KimihikoAkayasaki
Copy link

KimihikoAkayasaki commented Oct 22, 2024

My plugins are just dlls, standard net 8 dll with WinUI support enabled and released as nuget packages... LoadAssembly and assembly.CreateInstance() to load and execute them when downloaded on disk...

So it's quite the same as in here, although without the extra steps for MEF support. The "workaround" code should be similar, or event the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working feature proposal New feature proposal product-winui3 WinUI 3 issues team-Markup Issue for the Markup team wct
Projects
None yet
Development

No branches or pull requests