Skip to content

Resource Management APIs for all project types #11

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

Closed
jonwis opened this issue May 18, 2020 · 15 comments
Closed

Resource Management APIs for all project types #11

jonwis opened this issue May 18, 2020 · 15 comments
Assignees

Comments

@jonwis
Copy link
Member

jonwis commented May 18, 2020

Proposal: Enable MRT for unpackaged applications

Summary

Provide a version of the MRT resource management & packaging system even for unpackaged applications. While MUI is supported, MRT is more powerful and supported directly by various "visual" tooling, and generates code for direct use by apps.

Rationale

https://docs.microsoft.com/en-us/windows/uwp/app-resources/ is a powerful system for packaged applications to deliver resources (images, XAML, html, xml, or any other data) to enable localization or customized content for form factors. Unpackaged applications would benefit from the same ability to easily use Visual Studio and other tools to build and consume resources. https://docs.microsoft.com/en-us/previous-versions/windows/apps/jj552947(v=win.10) describes the Resource Manager in depth.

Scope

Capability Priority
Use .PRI files in unpackaged applications Must
Use the same API surface for resources between packaged, unpackaged, UWP, and Win32 Must
Let developers use custom-defined resource qualifiers in their apps Should
Provide sample hooks for common app frameworks (WinUI3, Electron, CEF, Flutter, React*) Could
Provide drop-in replacements for the Win32 *Resource family of APIs Could
Require apps to get packaged to use the features Won't
@huichen123
Copy link
Contributor

huichen123 commented Jun 12, 2020

A list of flat C APIs will provide core MRT functionality. A WinRT wrapper will build on top the core MRT to provide APIs similar to Window.ApplicationModel.Resources(.Core) and some helper APIs (e.g. locate merged resource file for resource bundle).

C APIs

DECLARE_HANDLE(MrmManagerHandle);
DECLARE_HANDLE(MrmContextHandle);
DECLARE_HANDLE(MrmMapHandle);

enum MrmType
{
    MrmType_Unknown,
    MrmType_String,
    MrmType_Path,
    MrmType_Embedded
};

struct MrmResourceData
{
    UINT32 size;
    void*  data;
};

STDAPI MrmCreateResourceManager(_In_ PCWSTR priFileName, _Out_ MrmManagerHandle* resourceManager);    
STDAPI_(void) MrmDestroyResourceManager(_In_opt_ MrmManagerHandle resourceManager);

STDAPI MrmCreateResourceContext(_In_ MrmManagerHandle resourceManager, _Out_ MrmContextHandle* resourceContext);
STDAPI_(void) MrmFreeQualifierNames(UINT32 size, _In_reads_(size) PWSTR* names);
STDAPI MrmGetAllQualifierNames(_In_ MrmContextHandle resourceContext, _Out_ UINT32* size, _Outptr_result_buffer_(*size) PWSTR** names);
STDAPI MrmGetQualifier(_In_ MrmContextHandle resourceContext, _In_ PCWSTR qualifierName, _Outptr_ PWSTR* qualifierValue);
STDAPI MrmSetQualifier(_In_ MrmContextHandle resourceContext, _In_ PCWSTR qualifierName, _In_ PCWSTR qualifierValue);
STDAPI_(void) MrmDestroyResourceContext(_In_opt_ MrmContextHandle resourceContext);

// Resource maps are owned by the resource manager and so do not need to be destroyed.
STDAPI MrmGetChildResourceMap(
    _In_ MrmManagerHandle resourceManager,
    _In_opt_ MrmMapHandle resourceMap,
    _In_ PCWSTR childResourceMapName,
    _Out_ MrmMapHandle* childResourceMap);

STDAPI MrmGetResourceCount(
    _In_ MrmManagerHandle resourceManager,
    _In_opt_ MrmMapHandle resourceMap,
    _Out_ UINT32* count);

STDAPI MrmLoadStringResource(
    _In_ MrmManagerHandle resourceManager,
    _In_opt_ MrmContextHandle resourceContext,
    _In_opt_ MrmMapHandle resourceMap,
    _In_ PCWSTR resourceId,
    _Outptr_ PWSTR* resourceString);

STDAPI MrmLoadStringResourceFromResourceUri(
    _In_ MrmManagerHandle resourceManager,
    _In_opt_ MrmContextHandle resourceContext,
    _In_ PCWSTR resourceUri,
    _Outptr_ PWSTR* resourceString);

STDAPI MrmLoadEmbeddedResource(
    _In_ MrmManagerHandle resourceManager,
    _In_opt_ MrmContextHandle resourceContext,
    _In_opt_ MrmMapHandle resourceMap,
    _In_ PCWSTR resourceId,
    _Out_ MrmResourceData* data);

STDAPI MrmLoadEmbeddedResourceFromResourceUri(
    _In_ MrmManagerHandle resourceManager,
    _In_opt_ MrmContextHandle resourceContext,
    _In_ PCWSTR resourceUri,
    _Out_ MrmResourceData* data);

STDAPI MrmLoadStringOrEmbeddedResource(
    _In_ MrmManagerHandle resourceManager,
    _In_opt_ MrmContextHandle resourceContext,
    _In_opt_ MrmMapHandle resourceMap,
    _In_ PCWSTR resourceId,
    _Out_ MrmType* resourceType,
    _Outptr_result_maybenull_ PWSTR* resourceString,
    _Out_ MrmResourceData* data);

STDAPI MrmLoadStringOrEmbeddedFromResourceUri(
    _In_ MrmManagerHandle resourceManager,
    _In_opt_ MrmContextHandle resourceContext,
    _In_ PCWSTR resourceUri,
    _Out_ MrmType* resourceType,
    _Outptr_result_maybenull_ PWSTR* resourceString,
    _Out_ MrmResourceData* data);

STDAPI MrmLoadStringOrEmbeddedResourceByIndex(
    _In_ MrmManagerHandle resourceManager,
    _In_opt_ MrmContextHandle resourceContext,
    _In_opt_ MrmMapHandle resourceMap,
    UINT32 index,
    _Out_ MrmType* resourceType,
    _Outptr_ PWSTR* resourceName,
    _Outptr_result_maybenull_ PWSTR* resourceString,
    _Out_ MrmResourceData* data);

STDAPI_(void*) MrmAllocateBuffer(size_t size); 
STDAPI_(void) MrmFreeResource(_In_opt_ void* resource);

STDAPI MrmGetFilePathFromName(_In_ PCWSTR filename, _Outptr_ PWSTR* filePath);

WinRT APIs

namespace Microsoft.ApplicationModel.Resources
{
    [contractversion(1.0)]
    apicontract MrtContract{};

    [contract(MrtContract, 1.0)]
    [default_interface]
    runtimeclass ResourceLoader
    {
        ResourceLoader();
        ResourceLoader(String fileName);
        ResourceLoader(String fileName, String resourceMap);

        static String GetDefaultResourceFilePath();

        String GetString(String resourceId);
        String GetStringForUri(Windows.Foundation.Uri resourceUri);
    }

    [contract(MrtContract, 1.0)]
    runtimeclass ResourceNotFoundEventArgs
    {
        ResourceContext Context { get; };
        String Name { get; };
        void SetResolvedCandidate(ResourceCandidate candidate);
    }

    [contract(MrtContract, 1.0)]
    runtimeclass ResourceManager
    {
        ResourceManager();
        ResourceManager(String fileName);

        ResourceMap MainResourceMap { get; };
        ResourceContext DefaultResourceContext { get; };

        event Windows.Foundation.TypedEventHandler<ResourceManager, ResourceNotFoundEventArgs> ResourceNotFound;
    }

    [contract(MrtContract, 1.0)]
    runtimeclass ResourceMap
    {
        UInt32 ResourceCount { get; }; 

        ResourceMap GetSubtree(String reference);
        ResourceCandidate GetValue(String resource);
        ResourceCandidate GetValue(ResourceContext context, String resource);
        Windows.Foundation.Collections.IKeyValuePair<String, ResourceCandidate> GetValueByIndex(UInt32 index);
        Windows.Foundation.Collections.IKeyValuePair<String, ResourceCandidate> GetValueByIndex(ResourceContext context, UInt32 index);
    }

    [contract(MrtContract, 1.0)]
    runtimeclass ResourceContext
    {
        Windows.Foundation.Collections.IMap<String, String> QualifierValues { get; };
    }

    [contract(MrtContract, 1.0)]
    enum ResourceCandidateKind
    {
        Unknown = 0,
        String,
        FilePath,
        EmbeddedData,
    };

    [contract(MrtContract, 1.0)]
    runtimeclass ResourceCandidate
    {
        ResourceCandidate(ResourceCandidateKind kind, String data);
        ResourceCandidate(byte[] data);

        String ValueAsString { get; };
        byte[] ValueAsBytes { get; };
        ResourceCandidateKind Kind { get; };
    }
} // namespace Microsoft.ApplicationModel.Resources

Samples

Load resources with C APIs

MrmManagerHandle resourceManager; 
MrmCreateResourceManager(L"resources.pri", &resourceManager); 
  
MrmContextHandle resourceContext; 
MrmCreateResourceContext(resourceManager, &resourceContext); 
  
MrmSetQualifier(resourceContext, L"Contrast", L"WHITE"); 
MrmSetQualifier(resourceContext, L"TargetSize", L"96"); 
  
// Load the asset file path
wchar_t* resourceString; 
MrmLoadStringResource( 
    resourceManager,  
    resourceContext,  
    nullptr,  
    L"Files/Assets/AppList.png",  
    &resourceString);  
MrmFreeResource(resourceString); 
  
MrmMapHandle childResourceMap; 
MrmGetChildResourceMap( 
    resourceManager,  
    nullptr,  
    L"Files",  
    &childResourceMap); 
  
MrmMapHandle childChildResourceMap; 
MrmGetChildResourceMap( 
    resourceManager,  
    childResourceMap,  
    L"Assets",  
    &childChildResourceMap); 
  
// Load the asset file path
MrmLoadStringResource( 
    resourceManager,  
    resourceContext,  
    childChildResourceMap,  
    L"AppList.png",  
    &resourceString); 
MrmFreeResource(resourceString); 
  
MrmDestroyResourceContext(resourceContext); 
MrmDestroyResourceManager(resourceManager); 

Load resources with WinRT APIs

var resourceManager = new ResourceManager();
var candidate = resourceManager.MainResourceMap.GetValue("resources/IDS_MANIFEST_MUSIC_APP_NAME");
var value = candidate.ValueAsString;

var resourceContext = resourceManager.DefaultResourceContext;
resourceContext.QualifierValues["Language"] = "fr-FR";
candidate = resourceManager.MainResourceMap.GetValue(resourceContext, "resources/IDS_MANIFEST_MUSIC_APP_NAME");

@wjk
Copy link

wjk commented Jun 15, 2020

One nit: I don’t think you would want to load PNG data into a variable of type wchar_t *. I think you would want to use the MrmLoadEmbeddedResource() function in your sample instead. Thanks!

@axelandrejs
Copy link
Contributor

axelandrejs commented Jun 15, 2020

Thanks for taking a look at the API!

The string resource lookup gives you back a path to the file. You then load it however you like. That is parity behavior with the OS API. We should put a comment into the sample, and maybe have a sample for the embedded case as well.

The embedded resource is for truly embedded resources, not loose files on disk (the dev can pick whether to embed files or keep them loose). We are planning to provide wrappers that abstract this away, something like

MrmLoadStringOrEmbeddedResource(type, resource)
if (MrmType_Path == type)
{
CreateFile(resource);
ret = // load data from file
}
else
{
ret = resource;
}

return ret;

@huichen123
Copy link
Contributor

Thanks for the comment! I have added a comment in the sample.

@wjk
Copy link

wjk commented Dec 3, 2020

When will this code be packaged and be made available on some (public) NuGet feed? Currently, I can only reference MRT Core if I use WinUI 3, since Reunion is bundled in the WinUI 3 NuGet package, or if I build it myself.

@wjk
Copy link

wjk commented Dec 3, 2020

Guidance on how to use MRT Core in other components of Project Reunion (in code in this repository) would also be welcome.

@axelandrejs
Copy link
Contributor

Yes, our current release vehicle is WinUI. The code itself works from any context, but right now our build scripts are tied to the WinUI ones. We are working on standalone support, and will have that ready before the first full Reunion release. But I don't have a hard date to share right now.

@nickrandolph
Copy link

@axelandrejs is there any documentation on how to use this in an unpackaged application. All the options for WinUI rely on the packaging project. It would be good to see an example of using MRT Core without packaging.

@wjk
Copy link

wjk commented Jan 31, 2021

@nickrandolph I use MRT Core in unpackaged applications all the time; it’s part of my standard development technique now. First, you will need to vendor (include in your solution) the MRT Core projects (until and unless they are posted on NuGet independently of WinUI). I use the following technique in my private projects:

First, paste the following code into a Directory.Build.props or other file that will be included at the top of your csproj:

<Project>
  <PropertyGroup>
    <MRTCoreResourceRoot>$(MSBuildProjectDirectory)\PRIResources</MRTCoreResourceRoot>
  </PropertyGroup>
</Project>

Second, create a folder called PRIResources next to your csproj and dump what resources you want in there. You can include arbitrary files, and they will be embedded within the PRI file once built (assuming you use the instructions below to create your PRI file; embedding is not the default). Use *.resjson files to localize strings, since this extension is handed specially by MRT Core. (These files are simple; there is one top-level JSON object, where its keys are identifiers used from code and the values are the localized strings.) To actually implement localization (or high-DPI support), create files named according to the following examples:

some\picture.png
some\picture.scale-200.png
some\strings.resjson
some\strings.lang-de.resjson
some\other_image.lang-de_scale-125.png

some\picture.png and some\strings.resjson are the neutral (fallback) versions. You put what are called qualifiers in the filename after a second dot, before the filename extension according to the above syntax (I hope I’ve made it clear what the semantics are). You can find more information about qualifiers here.

Then, create a file called priconfig.xml somewhere you can get from MSBuild. Paste the following contents into it:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources targetOsVersion="10.0.0" majorVersion="1">
  <index root="\" startIndexAt="\">
    <default>
      <qualifier name="Language" value="en-US"/>
      <qualifier name="Contrast" value="standard"/>
      <qualifier name="Scale" value="100"/>
      <qualifier name="HomeRegion" value="001"/>
      <qualifier name="LayoutDirection" value="LTR"/>
      <qualifier name="Theme" value="light"/>
      <qualifier name="AlternateForm" value=""/>
      <qualifier name="DXFeatureLevel" value="DX9"/>
      <qualifier name="Configuration" value=""/>
      <qualifier name="DeviceFamily" value="Universal"/>
    </default>

    <indexer-config type="folder" foldernameAsQualifier="true" filenameAsQualifier="true" qualifierDelimiter="."/>
    <indexer-config type="resw" convertDotsToSlashes="false" initialPath=""/>
    <indexer-config type="resjson" initialPath=""/>
    <indexer-config type="resfiles" qualifierDelimiter="."/>
    <indexer-config type="EmbedFiles"/>
  </index>

  <index root="\" startIndexAt="\Resources">
    <default>
      <qualifier name="Language" value="en-US"/>
      <qualifier name="Contrast" value="standard"/>
      <qualifier name="Scale" value="100"/>
      <qualifier name="HomeRegion" value="001"/>
      <qualifier name="LayoutDirection" value="LTR"/>
      <qualifier name="Theme" value="light"/>
      <qualifier name="AlternateForm" value=""/>
      <qualifier name="DXFeatureLevel" value="DX9"/>
      <qualifier name="Configuration" value=""/>
      <qualifier name="DeviceFamily" value="Universal"/>
    </default>

    <indexer-config type="EmbedFiles"/>
  </index>
</resources>

This file (a slightly modified copy of the default used in WinUI apps) instructs makepri.exe on how to process the contents of the $(MRTCoreResourceRoot). Finally, paste the following code in a Directory.Build.targets file or some other place it will be included at the end of your csproj:

<Project>
  <PropertyGroup>
    <UseMRTCore Condition="'$(UseMRTCore)' == '' and '$(MRTCoreResourceRoot)' != ''">true</UseMRTCore>
    <UseMRTCore Condition="'$(UseMRTCore)' == ''">false</UseMRTCore>
  </PropertyGroup>

  <PropertyGroup Condition="'$(UseMRTCore)' == 'true'">
    <MRTCoreConfigurationFile Condition="'$(MRTCoreConfigurationFile)' == ''">path\to\priconfig.xml</MRTCoreConfigurationFile>
  </PropertyGroup>

  <Target Name="LocateMakePriExe" Condition="'$(UseMRTCore)' == 'true'" DependsOnTargets="PrepareForBuild">
    <PropertyGroup>
      <WindowsSdkDir Condition="'$(WindowsSdkDir)' == ''">$(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v10.0@InstallationFolder)</WindowsSdkDir>
      <WindowsSdkDir Condition="'$(WindowsSdkDir)' == ''">$(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows\v10.0@InstallationFolder)</WindowsSdkDir>
    </PropertyGroup>

    <PropertyGroup>
      <MakePRIVersion Condition="'$(MakePRIVersion)' == ''">10.0.18362.0</MakePRIVersion>
      <MakePRIPath>$(WindowsSdkDir)bin\$(MakePRIVersion)\x64\makepri.exe</MakePRIPath>
    </PropertyGroup>
  </Target>

  <Target Name="CreatePRIFile" Condition="'$(UseMRTCore)' == 'true'" DependsOnTargets="LocateMakePriExe"
          Inputs="$(MRTCoreResourceRoot)\**\*.*" Outputs="$(IntermediateOutputPath)$(TargetName).manual.pri">
    <Error Condition="'$(MRTCoreResourceRoot)' == ''" Text="MRTCoreResourceRoot must be set" />
    <Error Condition="HasTrailingSlash('$(MRTCoreResourceRoot)')" Text="MRTCoreResourceRoot property cannot end in a backslash" />

    <PropertyGroup>
      <MRTCoreResourceRootAbsolute>$([System.IO.Path]::GetFullPath('$(MRTCoreResourceRoot)'))</MRTCoreResourceRootAbsolute>
    </PropertyGroup>
    
    <Exec Command="&quot;$(MakePRIPath)&quot; new /pr &quot;$(MRTCoreResourceRootAbsolute)&quot; /cf &quot;$(MRTCoreConfigurationFile)&quot; /of &quot;$(IntermediateOutputPath)$(TargetName).manual.pri&quot; /o" />
    <Exec Command="&quot;$(MakePRIPath)&quot; resourcepack /pr &quot;$(MRTCoreResourceRootAbsolute)&quot; /cf &quot;$(MRTCoreConfigurationFile)&quot; /if &quot;$(IntermediateOutputPath)$(TargetName).manual.pri&quot; /of &quot;$(IntermediateOutputPath)$(TargetName).manual.pri&quot; /o" />

    <ItemGroup>
      <_PriFile Include="$(IntermediateOutputPath)$(TargetName).manual.pri" />
      <FileWrites Include="$(IntermediateOutputPath)$(TargetName).manual.pri" />
    </ItemGroup>

    <Copy Condition="'$(PriProjTaskAssembly)' == ''" SkipUnchangedFiles="true" UseHardlinksIfPossible="true"
          SourceFiles="$(IntermediateOutputPath)$(TargetName).manual.pri" DestinationFiles="$(IntermediateOutputPath)$(TargetName).pri" />
    <ItemGroup Condition="'$(PriProjTaskAssembly)' == ''">
      <Content Include="$(IntermediateOutputPath)$(TargetName).pri">
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
        <CopyToPublishDirectory>Always</CopyToPublishDirectory>
      </Content>
      <FileWrites Include="$(IntermediateOutputPath)$(TargetName).pri" />
    </ItemGroup>
  </Target>
  <PropertyGroup>
    <PrepareResourcesDependsOn>
      CreatePRIFile;
      $(PrepareResourcesDependsOn)
    </PrepareResourcesDependsOn>
  </PropertyGroup>

You’ll need to update the value of the MRTCoreConfigurationFile property to point to where your priconfig.xml file is. When this MSBuild code is run, it will generate a PRI file named after your assembly, located next to the DLL on disk. You can then use the MRT Core APIs to extract data from it. It is completely self-contained; the source files do not need to be distributed, only the generated PRI file.

Unfortunately, the C# code required for retrieving data from the PRI file (the code that calls the MRT Core APIs) uses a path syntax that is difficult to explain here, because it depends on the structure and contents of that PRIResources folder you created earlier. If you want to see the contents of your PRI file, run makepri.exe dump /if AssemblyName.pri /of pri.xml /o and examine the resulting XML file. I recommend always doing this at the start, as the contents of that XML file will give you the name/URI syntax that is required to lookup resources.

As a finishing touch, the above code will not conflict with WinUI’s usage of MRT Core; if you ever add WinUI to your project, the contents of your manually generated PRI file will be merged into WinUI’s automatically generated PRI file during the build. The two chunks of data will coexist perfectly; the names/URIs you use to retrieve the resources won’t even change. Let me know if you have any more questions. Hope this helps!

@nickrandolph
Copy link

@wjk that's awesome - thanks so much for this information. You mentioned adding WinUI to the project in the final paragraph - is this something you've done successfully and been able to run the app without packaging? If so, would you be willing to share a sample as I think this is an important scenario that I would love to see working.

@wjk
Copy link

wjk commented Feb 1, 2021

WinUI still requires MSIX packaging. I generally delete the automatically generated packaging project and use one I create myself. I was talking about merging WinUI's PRI content (which is substantial) with the manually created PRI data, in the normal context of a WinUI app running in a packaged environment. Glad I could be of service nonetheless!

@axelandrejs
Copy link
Contributor

axelandrejs commented Feb 1, 2021

@axelandrejs is there any documentation on how to use this in an unpackaged application. All the options for WinUI rely on the packaging project. It would be good to see an example of using MRT Core without packaging.

We're working finalizing this support, and should have something to share very soon. The basic usage will be no different from packaged. You add Reunion to your project, and then you add assets and call APIs like you would in a WinUI app. The APIs themselves work the same.

Once we are done you won't need to do the manual steps outlined above.

@nickrandolph
Copy link

@jonwis I noted that this is now "code complete" yet according to the roadmap support for unpackaged apps won't appear until v0.8 (preview). Should we be expecting these issues to be in sync with the roadmap?

@MarkIngramUK
Copy link

0.8 Preview is now available, and there is an unpackaged sample app available - https://github.com/microsoft/WindowsAppSDK-Samples/tree/main/MrtCore/console_unpackaged_app However, it only demonstrates how to work with strings (resw), and not images or other resources. It'd be good to have some additional on how to work with MRT Core in this scenario.

@btueffers
Copy link
Contributor

Hi everyone! Since this was shipped in 1.0 we're going to go ahead and close the issue. However, if there is additional functionality you would like to see in the product please submit further feedback to our feature portal!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants