Select XAML file based on build configuration (Visual Studio 2022, WPF)

44 views Asked by At

Using Visual Studio 2022, C#, WPF, .NET Framework 4.7.2 project.

Is there any way to select XAML files at build time, such as based on either build configuration or compilation symbols, or anything else?

The use case: My now mature product was designed to run on a 2K landscape screen, all the UI layout is done in XAML and it auto-adjusts to screen sizes to a reasonable degree. However, I was just given the requirement to make a version that runs on an 800x1200 portrait screen. So, I'm going to have to completely re-design the layout of the screen, BUT it will have all the same objects and functionality. The layout changes are so drastic, and the complexity of the UI is such that it only makes sense to select completely different XMAL. I used the idea in the 2nd post here, to essentially chop my XAML file in half, copy/paste to the second half and change the layout, but because this is a runtime check, at compile time I get a zillion "xyz is already defined..." due to the duplicate objects with the same name, so unfortunately it didn't work for my case. Are there any other ideas short of creating a whole new Visual Studio project with new XAML files but sharing .cs files with the original project?

1

There are 1 answers

5
BionicCode On

I assume you mean run-time? You should detect the screen or let the user select a layout. Then dynamically load the related resource dictionary and merge it into the App.xaml resource dictionary. Depending on the build action you can retrieve XAML files using the APIs of ResourceManager, or Application, or Assembly, or open the file from the filesystem. Then load it using the XamlReaeder, so that you have the final ResourceDictionary instance that you can add to the application's merged dictionray.

The following example is concept or pseudo code:

App.xaml

<ResourcDictionary>
  <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="DefaultLayoutResources.xaml" />
  <ResourceDictionary.MergedDictionaries>
<ResourcDictionary>

DefaultLayoutResources.xaml

<ResourcDictionary>
  <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="DefaultLayoutControlStyles.xaml" />
    <ResourceDictionary Source="DefaultLayoutControlTemplates.xaml" />
  <ResourceDictionary.MergedDictionaries>
<ResourcDictionary>

PortraitLayoutResources.xaml

<ResourcDictionary>
  <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="PortraitLayoutControlStyles.xaml" />
    <ResourceDictionary Source="PortraitLayoutControlTemplates.xaml" />
  <ResourceDictionary.MergedDictionaries>
<ResourcDictionary>

App.xaml.cs


private ResourceDictionary currentLayoutResources;

protected override async void OnStartup(System.Windows.StartupEventArgs e)
{
  // Obtain from database, settings file etc.
  var layoutMode = LayOutMode.Portrait;

  if (layoutMode is LayoutMode.Portrait)
  {
    var uri = new Uri("PortraitLayoutResources.xaml");
    StreamResourceInfo streamResourceInfo = Application.GetResourceStreameManager(uri);
    await using Stream resourceStream = streamResourceInfo.Stream;
    var xamlReader = new System.Windows.Markup.XamlReader();
    xamlReader.LoadCompleted += OnXamlLoaded;
    this.currentLayoutResources = (ResourceDictionary)xamlReader.LoadAsync(resourceStream);
  }
}

private void OnXamlLoaded(object? sender, AsyncCompletedEventArgs e)
{
  // Remove the default layout
  this.Resources.MergedDictionaries.RemoveAt(0);

  this.Resources.MergedDictionaries.Add(this.currentLayoutResources);
}

Making the MainWindow content dynamic

Window is a ContentControl and therefore supports data templating. You define each DataTemplate (e.g. one for each layout style) in a resource dictionary of the actual layout.

Each layout style must have its own root resource dictionary (see example above). Then organize the resources the way you want and merge them into their related root dictionary.
For example, the portrait mode layout is defined by the PortraitLayoutResources.xaml resources. You merge all resources that belong to that layout style to the resource dictionary of PortraitLayoutResources.xaml. Like shown in the above example. All you need are styles and templates.

Then you have to differentiate between static and dynamic layout. The static layout references the dynamic layout resources using {Dynamicresource}. The resource keys must be the same across all layout styles. This way the static layout can use fixed resource keys. But the resource that they reference will change dynamically, e.g. based on the layout style. You can search for articles and information about theming in WPF. All this is highly dynamic so that the user can switch between layouts while the application is running.

The following example shows a working concept to switch the content of the MainWindow at runtime:

github example

MainWindow.xaml

<Window Style="{DynamicResource {x:Static local:LayoutStyleResourceKeys.MainWindowStyleKey}}">

</Window>

App.xaml

<Application StartupUri="MainWindow.xaml">
  <Application.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="DefaultLayoutResources.xaml" />
      </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
  </Application.Resources>
</Application>

DefaultLayoutResources.xaml
The root resource dictionary for the default layout.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="DefaultLayoutMainWindowResources.xaml" />
  </ResourceDictionary.MergedDictionaries>    
</ResourceDictionary>

DefaultLayoutMainWindowResources.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

  <DataTemplate x:Key="MainWindowContent">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
      </Grid.RowDefinitions>
      <TextBlock Grid.Row="0"
                 Text="Default mode layout"
                 FontSize="24" />

      <Rectangle Grid.Row="1"
                 Height="400"
                 Width="400"
                 Fill="Orange" />
      <Button Grid.Row="1"
              Content="Change layout"
              Command="{x:Static local:MainWindow.ChangeLayoutCommand}"
              CommandParameter="{x:Static local:LayoutMode.Portrait}"
              Width="100"
              VerticalAlignment="Top"
              HorizontalAlignment="Left" />
    </Grid>
  </DataTemplate>

  <Style x:Key="{x:Static local:LayoutStyleResourceKeys.MainWindowStyleKey}"
         TargetType="local:MainWindow">
    <Setter Property="ContentTemplate"
            Value="{StaticResource MainWindowContent}" />
  </Style>
</ResourceDictionary>

PortraitLayoutResources.xaml
The root resource dictionary for the portrait layout.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="PortraitLayoutMainWindowResources.xaml" />
  </ResourceDictionary.MergedDictionaries>    
</ResourceDictionary>

PortraitLayoutMainWindowResources.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

  <DataTemplate x:Key="MainWindowContent">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
      </Grid.RowDefinitions>
      <TextBlock Grid.Row="0"
                 Text="Portrait mode layout"
                 FontSize="24" />

      <Ellipse Grid.Row="1"
               Height="400"
               Width="400"
               Fill="Orange" />
      <Button Grid.Row="1"
              Height="50"
              Content="Change layout"
              Command="{x:Static local:MainWindow.ChangeLayoutCommand}"
              CommandParameter="{x:Static local:LayoutMode.Default}"
              Width="100"
              VerticalAlignment="Top"
              HorizontalAlignment="Left" />
    </Grid>
  </DataTemplate>

  <Style x:Key="{x:Static local:LayoutStyleResourceKeys.MainWindowStyleKey}"
         TargetType="local:MainWindow">
    <Setter Property="ContentTemplate"
            Value="{StaticResource MainWindowContent}" />
  </Style>
</ResourceDictionary>

LayoutStyleResourceKeys.cs

public sealed class LayoutStyleResourceKeys
{
  // Define a resource key for each dynamic resource i.e. each resource that appear in each layout style
  public static ComponentResourceKey MainWindowStyleKey = new ComponentResourceKey(typeof(LayoutStyleResourceKeys), nameof(MainWindowStyleKey));
}

LayoutMode.cs

public enum LayoutMode
{
  Default = 0,
  Portrait
}

MainWindow.xaml.cs

public partial class MainWindow : Window
{
  public static RoutedCommand ChangeLayoutCommand { get; } 
    = new RoutedCommand(nameof(MainWindow.ChangeLayoutCommand), typeof(MainWindow));

  public MainWindow()
  {
    InitializeComponent();

    var changeLayoutCommandBinding = new CommandBinding(
      MainWindow.ChangeLayoutCommand, 
      ChangeLayoutCommandExecuted, 
      CanExecuteChangeLayoutCommand);
    this.CommandBindings.Add(changeLayoutCommandBinding);
  }

  private void CanExecuteChangeLayoutCommand(object sender, CanExecuteRoutedEventArgs e) 
    => e.CanExecute = e.Parameter is LayoutMode;

  private async void ChangeLayoutCommandExecuted(object sender, ExecutedRoutedEventArgs e)
  {
    var layoutMode = (LayoutMode)e.Parameter;
    Uri resourceUri = layoutMode switch
    {
      LayoutMode.Portrait => new Uri("PortraitLayoutResources.xaml", UriKind.RelativeOrAbsolute),
      _ => new Uri("DefaultLayoutResources.xaml", UriKind.RelativeOrAbsolute),
    };

    await LoadLayoutAsync(resourceUri);
  }

  private async Task LoadLayoutAsync(Uri uri)
  {
    StreamResourceInfo resourceStreamInfo = Application.GetResourceStream(uri);
    await using Stream bamlResourceStream = resourceStreamInfo.Stream;
    bamlResourceStream.Seek(0, SeekOrigin.Begin);
    using var bamlReader = new Baml2006Reader(bamlResourceStream);
    var resources = (ResourceDictionary)XamlReader.Load(bamlReader);
    Application.Current.Resources.MergedDictionaries.RemoveAt(0);
    Application.Current.Resources.MergedDictionaries.Add(resources);
  }
}