-
Notifications
You must be signed in to change notification settings - Fork 678
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
Comments
I have the same issue. |
This may be related to 5536 |
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 |
@MikeHillberg @RealTommyKlein I was not successful replacing the generated 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? |
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 |
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? |
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. 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: 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:
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. |
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. |
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 [3] Also, the plugin can't define a |
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. |
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. |
It's still an issue in 1.1 full release :( I can't create my application with that error. |
@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 :( |
No, this is a bona fide feature request. |
Have you tried with a |
This may be related to #7788 |
@axodox Just wondering if you have tested
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. |
@harvinders We have not tested those yet, I will definitely look into the dependency properties next week. |
@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! :) |
@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. |
Did you create a new |
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. |
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.. |
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. :) |
I've managed to get this working with some success in C#, however am still having problems where 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 :)) |
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, |
I had problems at first with PRI as well with UWP / WinUI 2 / C++/WinRT. I had to do the following to fix it:
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. |
@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. |
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. |
@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. |
@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. |
@dnchattan Have you been able to get this to work with .NET 7? I tried upgrading today, but ran into XAML parsing exceptions. |
@cody3b
<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':
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 |
Now that I wrote my workaround for that problem, I really think this is a problem that should be managed by the |
@Juansero29 Many thanks! I ended up with this, after reading your post. Working for simple libraries, using <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> |
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. |
As for me, stuff from other answers helped me a bit and:
This may or may not help somebody, but well... it at least might be worth trying. |
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. |
Describe the bug
When instantiating a UserControl from outside the application package an XamlParseException is thrown from the UserControl constructor at
this.InitializeComponent()
.Steps to reproduce the bug
See minimal reproducing repository https://github.com/williamfigtree/WinUIPluginIssue. Key steps to recreate:
Create a class library "Plugin" using template "Class Library (WinUI 3 in Desktop)"
Add a UserControl "PluginUserControl" using template "User Control (WinUI 3)" to Plugin.csproj
Create an application "HostApp" using template "Black App, Packaged (WinUI 3 in Desktop)"
Add the "PluginLoadContext" class from this tutorial https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support to HostApp.csproj
Add the following code to the App constructor. You may need to modify the path to Plugin.dll.
Build Plugin.csproj
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
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.
The text was updated successfully, but these errors were encountered: