Some time ago, computer makers introduced new classes of Windows devices: the 2-in-1 and the tablets. These kinds of computers bring a lot of innovation – the 2-in-1 computers can be used as notebooks and tablets, while still maintaining their power: you can have very powerful devices, that can be used at the office and on the road, thus avoiding the need of having a tablet and a notebook.

But this kind of devices have another kind of innovations that are not visible at first sight: sensors. With them, you can have the same experience you have with a smartphone – you can use the accelerator for games or gestures, the GPS for location and routing and many others. This series of posts will show how to use these sensors in UWP, so you can give your user a better experience, so he can use all the resources of his device. And all this with an added bonus, thanks to UWP development – the same project will work both on desktop devices and smartphones.

Working with sensors in UWP

Basically, working with sensors in UWP is pretty much the same:

  • You get an instance of the sensor with the GetDefault method
  • You set its properties
  • You set a handler for the ReadingChanged

That’s all that is needed to work with sensors in UWP. So, let’s go and start with the first sensor: the light sensor.

Working with the light sensor

The light sensor can be used to detect the environment lighting and adjust the screen brightness according to the external light, so the use has the best experience. In this post I will show a simple way to create the effect used in GPS devices: when it’s day, it shows the display with a white background and when it’s night, it shows it with a black background.

To get this effect, the simplest way is to use themes. UWP has three default themes, Dark, Light and High Contrast and you can set them by using the RequestedTheme property of the control (in our case, the main window). So, let’s start.

In Visual Studio create a UWP application and add this XAML to the main page:

[sourcecode language=”xml” padlinenumbers=”true”]

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<TextBlock Text="Day" HorizontalAlignment="Center" x:Name="MainText"
VerticalAlignment="Center" FontSize="48" />
</Grid>
[/sourcecode]

Then, in the MainPage constructor, we will initialize the light sensor and process its readings:

[sourcecode language=”csharp”]
public MainPage()
{
this.InitializeComponent();
var lightSensor = LightSensor.GetDefault();
if (lightSensor == null)
{
MainText.Text = "There is no light sensor in this device";
return;
}
lightSensor.ReportInterval = 15;
lightSensor.ReadingChanged += async (s, e) =>
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
if (e.Reading.IlluminanceInLux < 30)
{

RequestedTheme = ElementTheme.Dark;
MainText.Text = "Night";
}
else
{
RequestedTheme = ElementTheme.Light;
MainText.Text = "Day";
}
});
};
}
[/sourcecode]

We must use the Dispatcher to change the theme and text, because the ReadingChanged can be called in a different thread of the UI thread. When the Illuminance reading is less than 30 Lux, we change to the Dark theme and change the text to Night.

Now, when you run the program in a device that has a light sensor, you will see the default view in daylight like this:

Day

If you cover the light sensor to simulate the lack of light, you will see something like this:

Night

Conclusions

As you can see, it’s very easy to work with sensors. With them, you can give a better experience for your users, so they can enjoy using your apps. And a added benefit is that you can use the same app in a desktop or in a Windows Phone.

The source code for this app is in https://github.com/bsonnino/LightSensor

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:

[sourcecode language='csharp'  padlinenumbers='true']
private void LoadThemes()
{
    themes.ItemsSource = Directory.EnumerateDirectories("Themes").Select(t => t.Substring(7));
}
[/sourcecode]

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:

[sourcecode language='csharp'  padlinenumbers='true']
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);
        }
    }
}
[/sourcecode]

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.

image

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:

[sourcecode language='xml' ]
<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>
[/sourcecode]

We can create a default resource dictionary and call it DefaultDict.xaml:

[sourcecode language='xml' ]
<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>
[/sourcecode]

And add this dictionary to App.xaml:

[sourcecode language='xml' ]
<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="DefaultDict.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>
[/sourcecode]

When we run the application, the default style is applied to the view:

image

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:

[sourcecode language='xml' ]
<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>
[/sourcecode]
[sourcecode language='xml' ]
<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>
[/sourcecode]
[sourcecode language='xml' ]
<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>
[/sourcecode]

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:

[sourcecode language='xml' ]
<ComboBox x:Name="StylesCombo" Height="30" Width="200" 
            SelectionChanged="SelectedStyleChanged" HorizontalAlignment="Right" Margin="5" />
[/sourcecode]

The SelectionChanged event handler for this box is:

[sourcecode language='csharp' ]
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);
            }
        }
}
[/sourcecode]

When you run the program and try to change the styles, you will see a strange thing:

image

The font that 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:

image

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).