How could one to merge resource dictionaries either dynamically or in compile time?
Asked Answered
D

1

2

Let's say we have a resource dictionary defined like

<Application.Resources>
   <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
          <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
          <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
      </ResourceDictionary.MergedDictionaries>
  </ResourceDictionary>
</Application.Resources>

If I would like to switch Android specific libraries, as for the sake of example define this like

<Application.Resources>
   <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
          <!-- There seem to be no way to choose based on platform.
          <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
          <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
          -->
          <ResourceDictionary Source="Resources/Styles/Android/Colors.xaml" />
          <ResourceDictionary Source="Resources/Styles/Android/Styles.xaml" />
      </ResourceDictionary.MergedDictionaries>
  </ResourceDictionary>
</Application.Resources>

Is there an official way to do it on compile time based on target (so, multitargeting and compiling to Android)?

If I try multitargeting via MSBuild, it requires the whole path to be present.

Also multitargeting using OnPlatform is not available (even if not compile-time now, but maybe in the future).

I could imagine some kind of a regex in MSBuild that reads this App.xaml file paths, switches files before the steps operating on this file and then back (so that version control is not affected). But seems a laborious path to figure out how to do... And maybe there is a better way? Or is there?

This is related to How to properly add Microsoft Fluent Design colors to Maui application?. It appears Material definitions are quite different from Fluent ones, so it would make sense not to create fat resources that have them all but ship only those that are actually needed.

Dreg answered 5/11, 2022 at 20:5 Comment(0)
I
3

Multi-targeting is a valid approach here, actually. You just need to merge the dictionaries during runtime, just like you would when setting a custom color theme.

In your application's .csproj file, add the following <ItemGroup> elements (showing Android and Windows only, but the same applies for iOS, etc.):

 <!-- Platform specific XAML Android -->
  <ItemGroup Condition="$(TargetFramework.StartsWith('net')) == true AND $(TargetFramework.Contains('-android')) == true">
    <MauiXaml Update="Resources\Styles\Platform\SpecialStyles.android.xaml">
      <Generator>MSBuild:Compile</Generator>
    </MauiXaml>
  </ItemGroup>
  <ItemGroup Condition="$(TargetFramework.StartsWith('net')) == true AND $(TargetFramework.Contains('-android')) != true">
    <MauiXaml Remove="Resources\Styles\Platform\SpecialStyles.android.xaml" />
    <None Include="Resources\Styles\Platform\SpecialStyles.android.xaml" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
    <Compile Remove="Resources\Styles\Platform\SpecialStyles.android.xaml.cs" />
    <None Include="Resources\Styles\Platform\SpecialStyles.android.xaml.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
  </ItemGroup>

  <!-- Platform specific XAML Windows -->
  <ItemGroup Condition="$(TargetFramework.StartsWith('net')) == true AND $(TargetFramework.Contains('-windows')) == true">
    <MauiXaml Update="Resources\Styles\Platform\SpecialStyles.windows.xaml">
      <Generator>MSBuild:Compile</Generator>
    </MauiXaml>
  </ItemGroup>
  <ItemGroup Condition="$(TargetFramework.StartsWith('net')) == true AND $(TargetFramework.Contains('-windows')) != true">
    <MauiXaml Remove="Resources\Styles\Platform\SpecialStyles.windows.xaml" />
    <None Include="Resources\Styles\Platform\SpecialStyles.windows.xaml" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
    <Compile Remove="Resources\Styles\Platform\SpecialStyles.windows.xaml.cs" />
    <None Include="Resources\Styles\Platform\SpecialStyles.windows.xaml.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
  </ItemGroup>

Then, under the Resources/Styles folder, you could create a new folder called Platform or similar and add a new Resource XAML file, e.g. SpecialStyles.android.xaml (and SpecialStyles.android.xaml.cs). Make sure to rename the class and remove the "android" from the name and add some special style you want to apply only on Android, e.g. red background for a Button:

<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    x:Class="MauiSamples.Resources.Styles.SpecialStyles">

  <Style ApplyToDerivedTypes="True" TargetType="Button">
    <Setter Property="BackgroundColor" Value="Red" />
  </Style>

</ResourceDictionary>

Also rename the class in the code-behind:

namespace MauiSamples.Resources.Styles;

public partial class SpecialStyles : ResourceDictionary
{
    public SpecialStyles()
    {
        InitializeComponent();
    }
}

Do the same for Windows but make the Button green instead:

<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiSamples.Resources.Styles.SpecialStyles">

  <Style ApplyToDerivedTypes="True" TargetType="Button">
    <Setter Property="BackgroundColor" Value="Green" />
  </Style>

</ResourceDictionary>

Finally, in your App.xaml.cs, you can merge the resource dictionary with the default resource dictionary as follows:

#if ANDROID || WINDOWS
    ICollection<ResourceDictionary> mergedDictionaries = Current.Resources.MergedDictionaries;
    if (mergedDictionaries != null)
    {
        mergedDictionaries.Clear();
        mergedDictionaries.Add(new MauiSamples.Resources.Styles.SpecialStyles());
    }
#endif

Attention: Mind the preprocessor directive, this is only necessary, if the SpecialStyles class only exists for Android or Windows. If you provide a SpecialStyles class for each platform respectively with their own suffix (e.g. SpecialStyles.ios.xaml, etc.), then you wouldn't need the preprocessor directive.

As I was curious about this, I quickly implemented the full sample in my open source MAUI samples GitHub repo: https://github.com/ewerspej/maui-samples

Result on Android:

enter image description here

Result on Windows:

enter image description here

UPDATE

This approach works, I only noticed that the newly created XAML files might disappear from the Solution Explorer, but they still get processed during the build. Apparently, the <MauiXaml> build action does not get reevaluated in the Solution Explorer according to the selected target framework. I regard this as an inconvenience and I'm happy to update the answer if I find a better solution or if someone has suggestions to improve it.

Insecure answered 6/11, 2022 at 17:54 Comment(2)
This is super-great! I think I failed something in setting up the csproj file since, well, something didn't work. I think I will start using this! A good point that note too! One issue I see are the acrylic brushes that do not exist in Android but exist Windows/Fluent. They need to be set up specifically on Windows. One would wish this sort of information were easily available since "platform native UX" is one selling point for native. :)Dreg
I'm happy to help. You can check out the sample repository for a full working example. If and when I figure out the issue with the <MauiXaml> build step not correctly being reflected in the Solution Explorer, I might write a blog post about this. Until then, the answer hopefully serves as inspiration.Insecure

© 2022 - 2024 — McMap. All rights reserved.