In the last post, I showed how to load a complete view dynamically, but that’s not the most common way to change the visualization of data. Most of the time, you will want to just change the appearance of the view, customizing the style for a client.
If we want to apply the styles for all controls, we must use a Theme. A Theme provides the default appearance for all controls of an application. The default theme is provided by the OS the app is running, but that can be changed by using an alternate theme file, a resource dictionary that has default styles for all controls.
Loading a theme dynamically
One way to load themes dynamically is to use any theme dll available for our use (many of them are free). We can add the themes to our project using Nuget and then change the theme as we want, but that’s not the focus, here. We want to load the themes from loose XAML files dynamically.
For that, I used an open source project, named WpfThemes (https://wpfthemes.codeplex.com/), that has several themes available and creates a themes dll that can be used in our project. As it is open source, I won’t use the dll, but I will use the theme structure and its demo to show how to load the theme from its files.
I downloaded the project and restructured it in this way:
- I created a single project, LoadThemes, and added the content of the demo main window to the new project’s main window
- I created a new folder on the project, named Themes and added all themes folders from the WPF.Themes project
- I set all Theme.xaml file Build Action to None and Copy to output Directory to “Copy if newer”
With this structure, we can load all available theme names to the theme combobox using this code:
private void LoadThemes()
{
themes.ItemsSource = Directory.EnumerateDirectories("Themes").Select(t => t.Substring(7));
}
I just enumerate all folders under the Themes folder and get the folder names, removing the initial “Themes\”. Now, we must load the theme dynamically. This is done in the SelectionChanged event for the theme combobox:
private void themes_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0)
return;
using (FileStream fs = new FileStream($"Themes\\{e.AddedItems[0]}\\Theme.xaml", FileMode.Open))
{
var dict = XamlReader.Load(fs) as ResourceDictionary;
if (dict != null)
{
Application.Current.Resources.Clear();
Application.Current.Resources.MergedDictionaries.Add(dict);
}
}
}
The program gets the selected item and opens the Theme.xaml located in the selected folder. Then, it loads the file as a ResourceDictionary and sets the MergedDictionaries for the current Application to it. This is everything that’s needed to change the style of all controls at once.
Loading styles dynamically
Usually, you do not want such a radical change, like the theme change, but you want to change the way some controls are drawn (colors, margins, etc). For that, you just need to change some styles. One way to do it is to apply some styles to your controls and then load the resource dictionaries dynamically. For example, the project from previous post could be redesigned by using styles. We can take the Master/Detail file and apply styles to the file, setting something like this:
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800" Foreground="{StaticResource PageForeground}">
<UserControl.Resources>
<Style TargetType="TextBlock" >
<Setter Property="Foreground" Value="{StaticResource PageForeground}" />
</Style>
</UserControl.Resources>
<Grid Background="{StaticResource PageBackground}">
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="*" />
<RowDefinition Height="2*" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Country" VerticalAlignment="Center" Margin="5"/>
<TextBox Height="25"
VerticalAlignment="Center" Margin="5,3" Width="250" Text="{Binding SearchText, Mode=TwoWay}" />
<Button Content="Search" Width="75" Height="25" Margin="10,5" VerticalAlignment="Center"
Command="{Binding SearchCommand}" Style="{StaticResource ButtonStyle}"/>
</StackPanel>
<DataGrid AutoGenerateColumns="False" x:Name="master" CanUserAddRows="False" CanUserDeleteRows="True" Grid.Row="1"
ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}" >
<DataGrid.Columns>
<DataGridTextColumn x:Name="customerIDColumn" Binding="{Binding Path=CustomerId}" Header="Customer ID" Width="80" />
<DataGridTextColumn x:Name="companyNameColumn" Binding="{Binding Path=CompanyName,ValidatesOnDataErrors=True}" Header="Company Name" Width="300" />
<DataGridTextColumn x:Name="cityColumn" Binding="{Binding Path=City}" Header="City" Width="100" />
<DataGridTextColumn x:Name="countryColumn" Binding="{Binding Path=Country}" Header="Country" Width="100" />
</DataGrid.Columns>
</DataGrid>
<Grid DataContext="{Binding SelectedCustomer}" Grid.Row="2">
<Grid Name="grid1" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Customer Id:" Grid.Column="0" Grid.Row="0" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="0" Margin="3" Name="customerIdTextBox" Text="{Binding Path=CustomerId, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<TextBlock Text="Company Name:" Grid.Column="0" Grid.Row="1" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="1" Margin="3" Name="companyNameTextBox" Text="{Binding Path=CompanyName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<TextBlock Text="Contact Name:" Grid.Column="0" Grid.Row="2" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="2" Margin="3" Name="contactNameTextBox" Text="{Binding Path=ContactName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<TextBlock Text="Contact Title:" Grid.Column="0" Grid.Row="3" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="3" Margin="3" Name="contactTitleTextBox" Text="{Binding Path=ContactTitle, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<TextBlock Text="Address:" Grid.Column="0" Grid.Row="4" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="4" Margin="3" Name="addressTextBox" Text="{Binding Path=Address, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<TextBlock Text="City:" Grid.Column="0" Grid.Row="5" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="5" Margin="3" Name="cityTextBox" Text="{Binding Path=City, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<TextBlock Text="Postal Code:" Grid.Column="0" Grid.Row="6" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="6" Margin="3" Name="postalCodeTextBox" Text="{Binding Path=PostalCode, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<TextBlock Text="Region:" Grid.Column="0" Grid.Row="7" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="7" Margin="3" Name="regionTextBox" Text="{Binding Path=Region, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<TextBlock Text="Country:" Grid.Column="0" Grid.Row="8" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="8" Margin="3" Name="countryTextBox" Text="{Binding Path=Country, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<TextBlock Text="Phone:" Grid.Column="0" Grid.Row="9" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="9" Margin="3" Name="phoneTextBox" Text="{Binding Path=Phone, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<TextBlock Text="Fax:" Grid.Column="0" Grid.Row="10" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="10" Margin="3" Name="faxTextBox" Text="{Binding Path=Fax, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
</Grid>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" Grid.Row="3">
<Button Width="75" Height="25" Margin="5" Content="Add" Command="{Binding AddCommand}" Style="{StaticResource ButtonStyle}"/>
<Button Width="75" Height="25" Margin="5" Content="Remove" Command="{Binding RemoveCommand}" Style="{StaticResource ButtonStyle}"/>
<Button Width="75" Height="25" Margin="5" Content="Save" Command="{Binding SaveCommand}" Style="{StaticResource ButtonStyle}"/>
</StackPanel>
</Grid>
</UserControl>
We can create a default resource dictionary and call it DefaultDict.xaml:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomerApp">
<SolidColorBrush x:Key="PageForeground" Color="Black" />
<SolidColorBrush x:Key="PageBackground" Color="AliceBlue" />
<Style x:Key="ButtonStyle" TargetType="Button">
<Setter Property="Background" Value="BlanchedAlmond" />
<Setter Property="Margin" Value="10,0" />
</Style>
</ResourceDictionary>
And add this dictionary to App.xaml:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="DefaultDict.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
When we run the application, the default style is applied to the view:
Now, we want to apply dynamic styles to the view, so we can create loose files with new resource dictionaries. In the project, create a new folder and name it Styles. In this folders, add three files and name them as Blue.xaml, Red.xaml and Yellow.xaml:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomerApp">
<SolidColorBrush x:Key="PageForeground" Color="White" />
<SolidColorBrush x:Key="PageBackground" Color="Navy" />
<Style x:Key="ButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Azure" />
<Setter Property="Margin" Value="10,0" />
</Style>
<Style TargetType="TextBlock" >
<Setter Property="FontFamily" Value="Arial" />
</Style>
</ResourceDictionary>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomerApp">
<SolidColorBrush x:Key="PageForeground" Color="White" />
<SolidColorBrush x:Key="PageBackground" Color="Red" />
<Style x:Key="ButtonStyle" TargetType="Button">
<Setter Property="Background" Value="LightPink" />
<Setter Property="Margin" Value="10,0" />
</Style>
<Style TargetType="TextBlock" >
<Setter Property="FontFamily" Value="Times New Roman" />
</Style>
</ResourceDictionary>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomerApp">
<SolidColorBrush x:Key="PageForeground" Color="Black" />
<SolidColorBrush x:Key="PageBackground" Color="Yellow" />
<Style x:Key="ButtonStyle" TargetType="Button">
<Setter Property="Background" Value="BlanchedAlmond" />
<Setter Property="Margin" Value="10,0" />
</Style>
<Style TargetType="TextBlock" >
<Setter Property="FontFamily" Value="Comic Sans MS" />
</Style>
</ResourceDictionary>
And set their Build Action to None and Copy to output directory to “Copy if newer”. As you can see, these files are very similar to the default dictionary: they override some colors and styles and add a new one, setting the font for all TextBlocks. Then, add a new Combobox in the main window, to select the new styles:
<ComboBox x:Name="StylesCombo" Height="30" Width="200"
SelectionChanged="SelectedStyleChanged" HorizontalAlignment="Right" Margin="5" />
The SelectionChanged event handler for this box is:
private void SelectedStyleChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0)
return;
using (FileStream fs = new FileStream($"Styles\\{e.AddedItems[0]}", FileMode.Open))
{
var dict = XamlReader.Load(fs) as ResourceDictionary;
if (dict != null)
{
Application.Current.Resources.MergedDictionaries.Add(dict);
}
}
}
To load the styles, we will use the LoadStyles function, called in the constructor:
public MainWindow()
{
InitializeComponent();
DataContext = new CustomersViewModel();
LoadStyles();
}
private void LoadStyles()
{
StylesCombo.ItemsSource = Directory
.EnumerateFiles("Styles", "*.xaml")
.Select(s => s.Substring(7));
}
When you run the program and try to change the styles, you will see a strange thing:
The font, which was not defined in the default style is changed, but the colors have not changed. This happens because we have set the styles as Static, using StaticResource when we applied the styles. In order for the style be applied dynamically, we must apply it using DynamicResource. When we change all StaticResource to DynamicResource in the master/detail file, we get this:
All the colors and styles have changed.
Conclusions
As you can see, WPF gives you a great flexibility to change you views dynamically. You can change the entire view, restyle all controls or only change the style of part of the controls. Everything can be done at runtime, and you can load the files from anywhere you want: loose files in a selected folder, from a database or from a request to a server, you can load the XAML as a string, parse and load it into your program. The files for this project are in https://github.com/bsonnino/LoadThemes (runtime themes) and https://github.com/bsonnino/DynamicXamlViews (dynamic styles).