On last post, I’ve shown how to publish a .NET app to the store. This is quite easy with Visual Studio, but the app must be a .NET app and it must be ported to .NET 4.6.1 or later. Many times, this is not possible – you have a .NET app that targets an older version of the framework and cannot be updated or you have a Win32 app (have I heard VB6 or Delphi?) that you want to publish to the store. Sometimes, even the source code is lost and all you have is the installer or executable. Even with this case there is no problem: you can still publish it to the store, the only thing is that you won’t be able to use Visual Studio.

To demonstrate it, I will use an old Delphi game sample, Swat!, which source code is in the Delphi samples since the earlier versions of Delphi (I think it’s there since Delphi 1, in 1994).

In this game, you must kill the ants with a hammer, this is an old game and I used it as an example of what you can convert to the Store.

The first step is to download the Desktop app converter from the store

Once you have it installed, you should run it with admin rights: in the start menu, right click in its icon and select “More/Run as administrator”. That will open a console window where you will run the converter app.

This window shows some examples of command line for packaging your app, but the first example must always be run once to setup the environment. You must download a base image for your system and run the setup. The base image can be downloaded from here, it must be compatible with your system version. You can check the system version by pressing Win+R and running winver:

My Windows version is 16299. so I have to donwload the base image for that version. It will be downloaded in my downloads folder, with the name “Windows_InsiderPreview_DAC_16299.wim”. So, the first step is to run the command line:

DesktopAppConverter.exe -Setup -BaseImage "E:\Downloads\Windows_InsiderPreview_DAC_16299.wim"

You may have an error after running this command line. This is because the Containers feature must be enabled in your system. Note: you must have Windows 10 Pro or Enterprise to enable this feature – if you have Windows 10 Home, you won’t be able to use the packager:

The converter will add this feature, but you must restart your machine. When you restart it, the Desktop App Converter will reopen and re-run the setup. You are then ready to package your app. The program we are running doesn’t have an installer, it is a single file. If you have an installer, you can point DesktopAppConverter to it, but in this case, we just need to create a folder and put all the files we need there (in this case, just the executable) and another folder for the output. Note: don’t create an output folder as a subfolder from the install folder – I did that and got an error “Path too long” – this is because the output folder is added as an install folder and this is recursive – newbie mistake. Then we must run this command line:

DesktopAppConverter.exe -Installer D:\Temp\Swat\ -AppExecutable Swat.exe -Destination D:\Temp\SwatUWP -PackageName "SwatUWP" -Publisher "CN=RevolutionSoft" -Version 0.0.0.1 -MakeAppx -Sign -Verbose -Verify

This will add all the files in D:\Temp\Swat and create the output file in D:\Temp\SwatUWP, creating and signing an appx file. You can go to the output folder and double click the Appx file. It will open the installer for the app:

If it’s the first time you are installing the app, you will get an error, because the certificate is not installed. You can install it by going to the output folder and double clicking the cer file. That will open the install certificate window:

You must click on the Install Certificate button and select Local Machine as the store location. Then click on Next and then in Place all certificates in the following store, clicking on Browse and selecting Trusted People. The certificate will be installed and you can click again in the Appx file to install it. Now you have your app running as a UWP app.

If you have an installer (it can be an MSI or an EXE installer) that can be run unattended, you can use it to convert your app. In this case, all the files in the installer will be added to the package.

Now it’s time to customize the package, but this is something for another post.

 

 

You have an old app (maybe a WPF or even a VB6 app), it still works fine, but the plans to rewrite it to UWP and get all the features for Windows 10 are always postponed (yes, there is always something more urgent than rewrite an app that is working fine). But, at the same time, you would like to publish it to the Store to get the benefits of being there: easy install and uninstall, worldwide discovery (you always knew there was somebody in the other side of the world that would benefit of the app), easy deploy, checking against viruses and other threats, and even monetization of the app – it would be nice to sell the app and receive the revenues at the end of the month without worrying with that.

Until some time ago, that would be impossible to do, unless you did a complete rewrite of the app, but with the Desktop Bridge, this is possible. And you can do it in many ways: you can just convert your app without any changes (you don’t even need the source code for the app), you can add some Windows 10 features to it, rewrite some parts of the app to UWP and so on. That way, you can start immediately and convert the app at your own pace. That will give you all the benefits of the store at once and you don’t need to worry with the conversion until you are ready for it.

Can your app be converted?

Before converting your app, you must be sure it can be converted and, if not, you must make some changes to make it compliant with the conversion. These are good practices that should be done even before thinking to convert:

  • Running in elevated mode – converted apps cannot run in elevated mode. Do you really need elevated mode? If not, you must change your app before converting.
  • Write in the install folder – this is one of the most common mistakes done by old apps. This is not allowed in UWP apps and, unless you change this to write in another folder, your app won’t be approved.
  • Write in system folders – yes, Microsoft has something to do with this issue. When you were using ini files to save your configuration, the default folder for them was in the Windows folder
  • Write in the HKLM registry hive. If you have this in your app, you must change it to HKCU instead

There are some other issues that may prevent your app to be submitted to the Store. You can check them here.

Starting the conversion process

Once you are sure that your app can be converted, you can start the conversion. The first step for the conversion is filling a form here. This is needed because the submission process will be different from normal UWP apps. The converted apps will have full thrust in the machine they are installed and they must be double-checked. One other issue is that you don’t need the source code, so you must be the legal owner of the app to publish it (and you must have some kind of proof of that). Once you have that in place, you can convert your app. You have three ways to convert it:

  • using Visual Studio (you must have the source code for that and the project must be opened in Visual Studio (in this case, you cannot convert your VB6 projects or any project that cannot be opened in Visual Studio)
  • using the Desktop App Converter – this is a Store app that must be run to convert your app. In this case, you have more flexibility than using Visual Studio: you can convert any app, including Win32 ones. You just need to have an installer that runs silently (with no user interaction)
  • Doing a manual conversion – this is the most flexible way, you can choose the files that will be packaged and how they will be, but this way will require more work and knowledge.

Using Visual Studio to convert a WPF app

We will use Visual Studio to convert my Data Structures app, discussed in the posts from 2016 (the last one in the series is here). You can get the full project in https://github.com/bsonnino/DataStructures. Once you download it and open it in Visual Studio, you are ready to convert it to the Store.

The first step here is to convert it to use the .NET Framework version 4.6.1 or later. You need that to convert the app to the store, as this is the minimum version supported there. You can do that by going to Project/Options/Application:

The next step is to add a new project to the solution, of type Windows Application Packaging Project (under Windows Universal):

Name it DataStructuresUWP. That will be the name of your UWP app. You must select the Windows version 14393 as the minimum version:

When the project is created, you must right click the Applications node and add the application as a reference

Once the app is added, you can right-click in the node under Applications and select “Set as Entry Point”. You are now ready to create the UWP app, by selecting the conversion project in the Solution Explorer, right-clicking on it and selecting “Store/Create app packages”.

When you select to build the package for the App Store, you need to have an account there (you can create an account at http://dev.windows.com) and select a name for your app:

You can also create packages that won’t go to the store and will be installed in the local machine. When you create the package, a link where the files will be located is shown and you must right click the Add-AppDevPackage.ps1 file and select “Run With Powershell”.

By distributing these files, you can sideload the app in any machine that has the developer mode enabled (in the Windows Settings). In our case, we have a faster way to install and run the app: just right click the conversion project in the Solution Explorer and select “Set as Startup Project”. Now, when you run the project, you will install and run the converted app, which is an UWP app. If you see the icon for the app in the taskbar, you will see the default UWP icon (the square with an X) and opening the start menu, you will see the installed app there.

That was easy, no? Now, you can customize your app to make it better for the store.

Customizing the app for the store

Now that the app is converted, you can customize it for the store by setting its name and assets (icons, tiles, etc). This is done by double clicking the Package.appxmanifest file in the Solution Explorer:

There, you can change its display name, icons and tiles and make it ready for sending to the store. With this, your app is ready for the store and you already have all the benefits of being there. As you can see, the changes were minimal and your app still runs the same. If you still want to use your WPF app, the only change that was made is to retarget for the 4.6.1 .NET Framework. In this case, we had the source code to build the app, but in the next article I will show how to convert a Win32 app with no source code, so you can get the same benefits. See you there!

Sometimes you need to enumerate the files in a folder and its subfolders and that can be a very long task, especially with large folders. This article will show a fast and easy way to enumerate all files in a folder when using UWP.

Enumerating files the traditional way

UWP runs in a sandbox and you don’t have access to all files in the system. One way to get access to a folder and its subfolders is to use the FolderPicker. You can’t access the folders directly (unless for some libraries when you ask for their use in the app manifest), and using the folder picker gives you access to other folders: you open it, the user selects a folder and you have access to it and its subfolders. That can be done with a code like this:

private static async Task<StorageFolder> SelectFolderAsync()
{
    var folderPicker = new Windows.Storage.Pickers.FolderPicker
    {
        SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.Desktop
    };
    folderPicker.FileTypeFilter.Add("*");
    StorageFolder folder = await folderPicker.PickSingleFolderAsync();
    return folder;
}

Once you get the folder reference, you can enumerate all files using a code like this:

private List<StorageFile> _allFiles;

private async Task GetFilesInFolder(StorageFolder folder)
{
    var items = await folder.GetItemsAsync();
    foreach (var item in items)
    {
        if (item is StorageFile)
            _allFiles.Add(item as StorageFile);
        else
            await GetFilesInFolder(item as StorageFolder);
    }
}

This recursive function will get all files and folders in the folder and will add the files to the files list and will call the function for all subfolders. Notice that you must use the StorageFolder and StorageFile classes to enumerate the files. While you can use .NET Standard 2.0 FileIO classes, this will be blocked by access permissions to UWP programs (for example, you will have access denied on the Documents library if you try to enumerate any file there with these classes even if you got the folder through the FolderPicker).

Using this function, I can get all files in a folder and fill a GridView in the main page with this function:

 

private async void GetFolderSlow(object sender, RoutedEventArgs e)
{
    StorageFolder folder = await SelectFolderAsync();
    if (folder != null)
    {
        DocsGrid.ItemsSource = null;
        StatusTxt.Text = "";
        var sw = new Stopwatch();
        sw.Start();
        _allFiles = new List<StorageFile>();
        await GetFilesInFolder(folder);
        DocsGrid.ItemsSource = _allFiles;
        sw.Stop();
        StatusTxt.Text = $"Ellapsed time: {sw.ElapsedMilliseconds}  {_allFiles.Count} files";
    }
}

In my machine, it takes about 2 minutes to enumerate the Documents Library (about 38,000 files). That is way more than using a .NET console program (in my machine, it took about 5 seconds to get all file names and process them to get their sizes), but there is no better way (until now – maybe there will be some kind of Full Thrust UWP app in the future :-)) to enumerate the files in UWP. Or there is?

Enumerating files the fast way

Yes, there is a faster way. You can use the Windows indexing service and query the files faster, when they are indexed. For that, you just need to create a QueryOptions instance and tell that you want to use the indexer. This code makes all the work:

 

private async void GetFolder(object sender, RoutedEventArgs e)
{
    var folder = await SelectFolderAsync();
    if (folder != null)
    {
        DocsGrid.ItemsSource = null;
        StatusTxt.Text = "";
        var sw = new Stopwatch();
        sw.Start();
        var queryOptions = new QueryOptions
        {
            FolderDepth = FolderDepth.Deep,
            IndexerOption = IndexerOption.UseIndexerWhenAvailable
        };
        var query = folder.CreateFileQueryWithOptions(queryOptions);
        var allFiles = await query.GetFilesAsync();
        DocsGrid.ItemsSource = allFiles;
        sw.Stop();
        StatusTxt.Text = $"Ellapsed time: {sw.ElapsedMilliseconds}  {allFiles.Count} files";
    }
}

As you can see, there is no need to have a recursive function. Setting the FolderDepth property to FolderDepth.Deep does all the recursive work for you. An you can use the index if it’s available and query for some files (like – only pictures or files created by an author). While this is not as fast as the console enumeration, it’s still twice as fast as the previous version.

Conclusions

Enumerating files in UWP is not very fast, but when you need it, you have a faster alternative to the default, using the indexer. That can be a handy alternative, especially when you need to filter the files.

All the source code for the article is in https://github.com/bsonnino/DocumentsAccess

 

After finishing my last post, I saw I could improve it a little bit. The UI was too raw (my goal at the time was not to show the UI, but show how to add logging to a UWP app) and could be improved: instead of listviews, why not use a DataGrid to show the client’s data?

Although there is no native DataGrid in the SDK, Telerik open sourced its UWP components, so you can use them in your UWP apps. If you want to know more,  you can go to the Telerik’s GitHub page (https://github.com/telerik/UI-For-UWP) and take a look at their components for UWP.

So, let’s start where we left: you can go to my GitHub page (https://github.com/bsonnino/LoggingSerilog) and download the project developed in the last post.  Then, add the NuGet package Telerik.UI.for.UniversalWindowsPlatform.

The first step in our change is to change the ListView in the Customers view to a RadDataGrid:

<grid:RadDataGrid ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer}"/>

You should add the namespace Telerik.UI.Xaml.Controls.Grid in the beginning pf the XAML file:

<UserControl
    x:Class="TelerikGrid.View.Customers"
    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"
    xmlns:grid="using:Telerik.UI.Xaml.Controls.Grid"
    mc:Ignorable="d"
    d:DesignHeight="500"
    d:DesignWidth="700"
    DataContext="{Binding Customers, Source={StaticResource Locator}}">

With this simple change, you get a full featured Data Grid, that allows sorting, filtering or grouping. You can group by any column by dragging it to the group box, at the left:

Really nice, no? If you take a look at the grid, you will see that all the columns are being shown, including InDesignMode, a property introduced in ViewModelBase, but we don’t want that. To set the columns, we have to set the property AutoGenerateColumns to False and set the columns we want in the Columns property. If you want, you can also set the CanUserChooseColumns to True, so the user can choose the columns he wants to display:

<grid:RadDataGrid ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer}" 
                  AutoGenerateColumns="False" CanUserChooseColumns="True">
    <grid:RadDataGrid.Columns>
        <grid:DataGridTextColumn PropertyName="Id"/>
        <grid:DataGridTextColumn PropertyName="Name"/>
        <grid:DataGridTextColumn PropertyName="City"/>
        <grid:DataGridTextColumn PropertyName="Country" />
    </grid:RadDataGrid.Columns>
</grid:RadDataGrid>

One extra twist is to add alternating columns to the grid. This is very easy, just set the AlternationStep property to a value greater than 1.

Now that we have the grid in place, let’s go to the second step: use a DataForm for the detail view, That way you can have a single control for easy editing of objects.

Adding a DataForm to edit the selected item

The DataForm is an easy way to edit objects in UWP. With it, you don’t need to add the editors for each field, you just need to add it and set the Item property to the item you want to edit:

<data:RadDataForm Item="{Binding SelectedCustomer}" />

As you can see, it works the same way as it did with all the TextBoxes, but the labels are not there. To fix this, we must add an attribute to the ViewModel properties to display the header:

public class CustomerViewModel : ViewModelBase
{
    private readonly Customer _customer;

    public CustomerViewModel(Customer customer)
    {
        _customer = customer;
    }

    [Display(Header = "Id", PlaceholderText = "Customer Id")]
    public string Id
    {
        get => _customer.Id;
        set
        {
            Log.Verbose("Customer Id changed from {OldId} to {NewId}", _customer.Id,value);
            _customer.Id = value;
            RaisePropertyChanged();
        }
    }

    [Display(Header = "Name", PlaceholderText = "Customer name")]
    public string Name
    {
        get => _customer.Name;
        set
        {
            Log.Verbose("Customer Name changed from {OldName} to {NewName}", _customer.Name, value);
            _customer.Name = value;
            RaisePropertyChanged();
        }
    }

    [Display(Header = "Address", PlaceholderText = "Customer address")]
    public string Address
    {
        get => _customer.Address;
        set
        {
            Log.Verbose("Customer Address changed from {OldAddress} to {NewAddress}", _customer.Address, value);
            _customer.Address = value;
            RaisePropertyChanged();
        }
    }

    [Display(Header = "City", PlaceholderText = "Customer city")]
    public string City
    {
        get => _customer.City;
        set
        {
            Log.Verbose("Customer City changed from {OldCity} to {NewCity}", _customer.City, value);
            _customer.City = value;
            RaisePropertyChanged();
        }
    }

    [Display(Header = "Country", PlaceholderText = "Customer country")]
    public string Country
    {
        get => _customer.Country;
        set
        {
            Log.Verbose("Customer Country changed from {OldCountry} to {NewCountry}", _customer.Country, value);
            _customer.Country = value;
            RaisePropertyChanged();
        }
    }

    [Display(Header = "Phone", PlaceholderText = "Customer phone")]
    public string Phone
    {
        get => _customer.Phone;
        set
        {
            Log.Verbose("Customer Phone changed from {OldPhone} to {NewPhone}", _customer.Phone, value);
            _customer.Phone = value;
            RaisePropertyChanged();
        }
    }
}

The Display attribute will tell the form what is the label that must be shown and the placeholder to show in the edit box when it’s empty. One note here is that the Display attribute that must be used isn’t in System.ComponentModel.DataAnnotations, but it is in Telerik.Data.Core. You must add the correct namespace to use the Header and PlaceHolderText properties. Once you make these changes, the labels appear in the form:

Adding a chart to the view

Now that our program is working like the old one with the Telerik controls, let’s enhance  it with a Pie Chart that shows the percentage of customers by country. To do that, we must create a new property in the CustomersViewModel,  CustomersByCountry. It will be initialized in the ViewModel’s constructor:

public CustomersViewModel()
{
    _selectedCustomer = _customers.Count > 0 ? _customers[0] : null;
    _newCustomerCommand = new RelayCommand(AddCustomer);
    _deleteCustomerCommand = new RelayCommand(DeleteCustomer, () => SelectedCustomer != null);
    CustomersByCountry = _customers.GroupBy(c => c.Country)
        .Select(g => new CustomerCountry(g.Key, g.Count()))
        .OrderByDescending(c => c.NumCustomers);
}

public IEnumerable<CustomerCountry> CustomersByCountry { get; }

We use LINQ to group the customers by country and generate the data ordered in descending order by the customer count. We have created a new class named CustomerCountry to store the total data for each country:

public class CustomerCountry
{
    public CustomerCountry(string country, int numCustomers)
    {
        Country = country;
        NumCustomers = numCustomers;
    }

    public string Country { get; }
    public int NumCustomers { get; }
}

Once we have that in place, we can create our chart view. In the View folder, create a new UserControl and name it CustomersChart. In the chart, add this code:

<UserControl
    x:Class="TelerikGrid.View.CustomersChart"
    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"
    xmlns:chart="using:Telerik.UI.Xaml.Controls.Chart"
    xmlns:primitives="using:Telerik.UI.Xaml.Controls.Primitives"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400"
    DataContext="{Binding Customers, Source={StaticResource Locator}}">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="20">
        <chart:RadPieChart x:Name="Chart" PaletteName="DefaultLight" >
            <chart:PieSeries ItemsSource="{Binding CustomersByCountry}" ShowLabels="True" RadiusFactor="0.8">
                <chart:PieSeries.ValueBinding>
                    <chart:PropertyNameDataPointBinding PropertyName="NumCustomers" />
                </chart:PieSeries.ValueBinding>
                <chart:PieSeries.LegendTitleBinding>
                    <chart:PropertyNameDataPointBinding PropertyName="Country" />
                </chart:PieSeries.LegendTitleBinding>
            </chart:PieSeries>
        </chart:RadPieChart>
        <primitives:RadLegendControl LegendProvider="{Binding ElementName=Chart}">
            <primitives:RadLegendControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <ItemsWrapGrid Orientation="Vertical"/>
                </ItemsPanelTemplate>
            </primitives:RadLegendControl.ItemsPanel>
            <primitives:RadLegendControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" Width="110">
                        <Ellipse Fill="{Binding Fill}" Stroke="{Binding Stroke}"
                                 StrokeThickness="1" Width="10" Height="10"/>
                        <TextBlock Text="{Binding Title}" Foreground="{Binding Fill}"
                                   Margin="10" />
                    </StackPanel>
                </DataTemplate>
            </primitives:RadLegendControl.ItemTemplate>
        </primitives:RadLegendControl>
    </Grid>
</UserControl>

We are adding a RadPieChart with a PieSeries in it. This PieSeries has the ItemsSource property set to the ViewModel’s CustomersByCountry property. Its values are set to the NumCustomers property and the Legends are set to the country names. To add a legend, we must add a RadLegendControl and set its LegendProvider property bound to the chart. The ItemsPanel property is set to a ItemsWrapGrid, in a way that the items span to a new column if there is no available space at the bottom. The labels of the legend have the same color of the pie.

Now, we must add the new view to the main view. I’ve chosen to replace the log view with this new view, In MainPage.xaml, put this code:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="2*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <view:Customers />
    <!--<view:Log Grid.Row="1"/>-->
    <view:CustomersChart Grid.Row="1"/>
</Grid>

When you run the app, you will see something like this:

Note: this chart is not dynamic – if you change the country for a customer or add a new customer, the chart doesn’t update, because it’s calculated in the constructor, To make it dynamic, you should recalculate it when the country of a customer has changed.

With these changes, we have an improved application, with a lot of new features.

Conclusions

As you can see, with small changes we got a lot of improvements for our app. With the DataGrid we gor sorting, filtering and grouping for our data with almost no effort. With the DataForm we can edit the customer without having to add TexBlocks and TextBoxes. If we change the underlying class, the form will be updated automatically. With the chart we could show a new visualization for our data. The Telerik controls for UWP are open source and free for you to use.

All the source code in this article is in https://github.com/bsonnino/TelerikGrid

Introduction

One important step in developing an application is to debug it. This task will take you a lot of time and it will be especially difficult to debug an application that is live in the field (that doesn’t happen in my machine ;-)). One solution is to add logging to the application and analyze the logs after the error occurred.

You can develop your own logging framework, but this will take some time and will be cumbersome and not standard, so I tend to search a tested framework out there to do this task for me. The most used frameworks, like Log4Net don’t work for UWP, and you cannot log to some places, like a Sql Server database.  I’ve found two that work nicely with UWP apps: MetroLog (https://github.com/onovotny/MetroLog) and Serilog (https://serilog.net/)

I’ve used Metrolog for a long time, it’s very easy to use and very flexible, but Serilog has two features that make it better: Structured Logging and Sinks (logging providers).

Structured Logging

When you are logging, you usually do something like this:

Log.Information("The current time is " + DateTime.Now);

That’s fine when you have a small log, but what about a log that has thousands of lines? How can you extract some useful information from the log? You end up with some program that will use Regex patterns to extract the information you want from a large log. With Serilog you will log something like this:

 

Log.Information("The current time is {CurrentTime}", DateTime.Now);

This will log a property with name CurrentTime along with the log record that can be retrieved later, when you are analyzing the log. This can be used for simple objects and for complex classes.  Let’s say you have a class named Customer like this one:

 

public class Customer
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    ...
}

If you use something like this:

 

Customer customer = new Customer();
...
Log.Information("Customer Data: {@Customer}", customer);

Serilog will generate a log record with a Customer property and will list all properties for that customer. Notice we are using the @ character before the variable name. That instructs Serilog to preserve the structure of the class, instead of flattening it in a single string. That way it will be easier to extract this information from the log file.

Sinks

The second feature in Serilog that makes it a good choice is the concept of Sinks, or logging providers. There are many logging providers for Serilog and you can even log to more than one place at once. A fast search of “Serilog Sinks” lists 342 packages, including some common ones, like files, console, event viewer, MS Sql Server, email, some to No-SQL DBs, like DocumentDB, RavenDB and MongoDB, and some not so common like Slack, Seq, or even a Reactive Extensions sink, that uses an Observable sequence of events for the logging.

As I’m always interested in Rx, I thought it would be very nice to use this sink to show the logging in UWP with Serilog. So, let’s start our project.

Creating an UWP app

In Visual Studio, create a new UWP blank project. In the solution explorer, right click on the References node and select “Manage NuGet packages”. Install the Serilog, Serilog.Sinks.Observable and System.Reactive packages, then install the MVVM Light package – we will use it as a MVVM framework for this project.

Usually, adding the MVVM Light package adds a ViewModelLocator and a Main ViewModel automatically, but this doesn’t happen in UWP, so we must add them manually. In the Solution Explorer, create a ViewModel folder and add a MainViewModel class:

public class MainViewModel
{
}

Then add a ViewModelLocator class in this folder:

public class ViewModelLocator
{
    public ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
        SimpleIoc.Default.Register();
    }
    public MainViewModel Main => ServiceLocator.Current.GetInstance();
}

The next step is to add a reference to the ViewModelLocator in App.xaml:

<Application
    x:Class="LoggingSerilog.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vm="using:LoggingSerilog.ViewModel"
    RequestedTheme="Light">
    <Application.Resources>
        <vm:ViewModelLocator x:Key="Locator" />
    </Application.Resources>
</Application>

Finally, we set the DataContext of the Main View to the MainViewModel in MainPage.xaml:

<Page
    x:Class="LoggingSerilog.MainPage"
    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"
    DataContext="{Binding Main, Source={StaticResource Locator}}">

With all this in place, we can start our project. We will create a small project that allows you to add, modify or delete Customers from a list. Every change will be logged to a view below the main view.

We will create our model, that will be used for the project. Create a new folder named Model and insert a new class named Customer:

public class Customer
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    public string Phone { get; set; }


    public static IEnumerable<Customer> GetCustomers()
    {
        string customerXml = Path.Combine(Package.Current.InstalledLocation.Path, "Customers.xml");
        return XDocument.Load(customerXml).Descendants("Customer").Select(c =>
            new Customer
            {
                Id = c.Element("CustomerID")?.Value,
                Name = c.Element("CompanyName")?.Value,
                Address = c.Element("Address")?.Value,
                City = c.Element("City")?.Value,
                Country = c.Element("Country")?.Value,
                Phone = c.Element("Phone")?.Value,
            });
    }
}

This class also has a static method to get all the customers from a xml file based on the Customers table in the Northwind database. You can get this file from the source code for this article. When you get the file, you must add it to the project, so it will be copied to the install folder for the app. One note here: as the install folder is read-only, you will not be able to save the data once it’s updated. To do that, you must copy the file to the local app data store and work from there, but I let this for you.

As you can see, this class is a POCO (Plain Old Clr Object) class, it doesn’t have any mechanism to trigger updates when it’s changed, so we’ll create a ViewModel for it. In the ViewModel folder, create a new CustomerViewModel class:

public class CustomerViewModel : ViewModelBase
{
    private readonly Customer _customer;

    public CustomerViewModel(Customer customer)
    {
        _customer = customer;
    }

    public string Id
    {
        get => _customer.Id;
        set
        {
            _customer.Id = value;
            RaisePropertyChanged();
        }
    }

    public string Name {
        get => _customer.Name;
        set
        {
            _customer.Name = value;
            RaisePropertyChanged();
        }
    }

    public string Address
    {
        get => _customer.Address;
        set
        {
            _customer.Address = value;
            RaisePropertyChanged();
        }
    }

    public string City
    {
        get => _customer.City;
        set
        {
            _customer.City = value;
            RaisePropertyChanged();
        }
    }

    public string Country
    {
        get => _customer.Country;
        set
        {
            _customer.Country = value;
            RaisePropertyChanged();
        }
    }

    public string Phone
    {
        get => _customer.Phone;
        set
        {
            _customer.Phone = value;
            RaisePropertyChanged();
        }
    }
}

The ViewModel is derived from the MVVM Light’s ViewModelBase class, which implements the INotifyPropertyChanged interface. That way, all changes in the values for the customer will be reflected in the UI. We also need to create a CustomersViewModel class to expose all customers to the UI:

public class CustomersViewModel : ViewModelBase
{
    readonly ObservableCollection<CustomerViewModel> _customers = 
        new ObservableCollection<CustomerViewModel>(
            Customer.GetCustomers().Select(c => new CustomerViewModel(c)));

    public CustomersViewModel()
    {
        _selectedCustomer = _customers.Count > 0 ? _customers[0] : null;
    }

    private CustomerViewModel _selectedCustomer;

    public ObservableCollection<CustomerViewModel> Customers => _customers;

    public CustomerViewModel SelectedCustomer
    {
        get => _selectedCustomer;
        set
        {
          _selectedCustomer = value;
            RaisePropertyChanged();
        }
    }
}

The ViewModel has two properties: Customers, an ObservableCollection of CustomerViewModel, and SelectedCustomer, the customer selected in the list.

The next step is to create a new view to show the data. Create a new folder called View and add a new UserControl named Customers. I am using here a UserControl because this will be added to the Main page – the main page will be the shell for the customer view and the logging view. The view will be like this:

<UserControl
    x:Class="LoggingSerilog.View.Customers"
    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="500"
    d:DesignWidth="700"
    DataContext="{Binding Customers, Source={StaticResource Locator}}">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ListView ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding Id}" FontWeight="Bold"/>
                        <TextBlock Text="{Binding Name}" />
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Grid Grid.Column ="1" Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Grid DataContext="{Binding SelectedCustomer}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="35"/>
                    <RowDefinition Height="35"/>
                    <RowDefinition Height="35"/>
                    <RowDefinition Height="35"/>
                    <RowDefinition Height="35"/>
                    <RowDefinition Height="35"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <TextBlock Text ="Id" Grid.Row="0" VerticalAlignment="Center"/>
                <TextBlock Text ="Name" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center"/>
                <TextBlock Text ="Address" Grid.Row="2" Grid.Column="0" VerticalAlignment="Center"/>
                <TextBlock Text ="City" Grid.Row="3" Grid.Column="0" VerticalAlignment="Center"/>
                <TextBlock Text ="Country" Grid.Row="4" Grid.Column="0" VerticalAlignment="Center"/>
                <TextBlock Text ="Phone" Grid.Row="5" Grid.Column="0" VerticalAlignment="Center"/>
                <TextBox Text ="{Binding Id, Mode=TwoWay}" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center"/>
                <TextBox Text ="{Binding Name, Mode=TwoWay}" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center"/>
                <TextBox Text ="{Binding Address, Mode=TwoWay}" Grid.Row="2" Grid.Column="1" VerticalAlignment="Center"/>
                <TextBox Text ="{Binding City, Mode=TwoWay}" Grid.Row="3" Grid.Column="1" VerticalAlignment="Center"/>
                <TextBox Text ="{Binding Country, Mode=TwoWay}" Grid.Row="4" Grid.Column="1" VerticalAlignment="Center"/>
                <TextBox Text ="{Binding Phone, Mode=TwoWay}" Grid.Row="5" Grid.Column="1" VerticalAlignment="Center"/>
            </Grid>
            <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom">
                <Button Content="New" Margin="5,0" Width="65" Command="{Binding NewCustomerCommand}"/>
                <Button Content="Delete" Margin="5,0" Width="65" Command="{Binding DeleteCustomerCommand}"/>
            </StackPanel>
        </Grid>
    </Grid>
</UserControl>

The main grid has two columns: the first one shows the customers list and the second one shows the details for the selected customer. At the bottom of the second column there are two buttons to create or delete a customer. These two buttons are bound to two commands in the CustomersViewModel:

public class CustomersViewModel : ViewModelBase
{
    readonly ObservableCollection<CustomerViewModel> _customers = 
        new ObservableCollection<CustomerViewModel>(
            Customer.GetCustomers().Select(c => new CustomerViewModel(c)));

    public CustomersViewModel()
    {
        _selectedCustomer = _customers.Count > 0 ? _customers[0] : null;
        _newCustomerCommand = new RelayCommand(AddCustomer);
        _deleteCustomerCommand = new RelayCommand(DeleteCustomer, () => SelectedCustomer != null);
    }
    private CustomerViewModel _selectedCustomer;
    private readonly RelayCommand _newCustomerCommand;
    private readonly RelayCommand _deleteCustomerCommand;

    public ObservableCollection<CustomerViewModel> Customers => _customers;

    public CustomerViewModel SelectedCustomer
    {
        get => _selectedCustomer;
        set
        {
            _selectedCustomer = value;
            RaisePropertyChanged();
            _deleteCustomerCommand.RaiseCanExecuteChanged();
        }
    }

    public ICommand NewCustomerCommand => _newCustomerCommand ;

    private void AddCustomer()
    {
        var customer = new CustomerViewModel(new Customer());
        _customers.Add(customer);
        SelectedCustomer = customer;
    }

    public ICommand DeleteCustomerCommand => _deleteCustomerCommand;

    private void DeleteCustomer()
    {
        _customers.Remove(SelectedCustomer);
        SelectedCustomer = null;
    }
}

One note here: we want to disable the delete command if there is no selected customer. This is done with the overload when we create the delete command, but we must tell Windows that the command has changed when the selected customer has changed. We do that by calling the RaiseCanExecuteChanged method in the setter of the SelectedCustomer property.

As you can see in the top of the usercontrol, I’ve set the DataContext of the view to a property in the locator. We must create this property in ViewModelLocator.cs:

public class ViewModelLocator
{
    public ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

        SimpleIoc.Default.Register<MainViewModel>();
        SimpleIoc.Default.Register<CustomersViewModel>();
    }

    public MainViewModel Main => ServiceLocator.Current.GetInstance<MainViewModel>();
    public CustomersViewModel Customers => ServiceLocator.Current.GetInstance<CustomersViewModel>();
}

Then we must add the usercontrol to the Main page. In MainPage.xaml add this code:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="2*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <view:Customers />
</Grid>

Now, when you run the application, you will see something like this:

Adding logging to our app

Once we have our app ready, we can add logging to it. I want to add some kind of live logging, where the log records are shown in the main window when they are generated. For that, we must create a new view. In the View folder, add a new UserControl and name it Log. Add this code in the UserControl:

<UserControl
    x:Class="LoggingSerilog.View.Log"
    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"
    xmlns:events="using:Serilog.Events"
    xmlns:l="using:LoggingSerilog"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400"
    DataContext="{Binding Log, Source={StaticResource Locator}}">
    <Grid>
        <ListView ItemsSource="{Binding LogItems}" Margin="0,10,0,0"
                ItemContainerStyleSelector="{StaticResource LogStyleSelector}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="events:LogEvent">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{x:Bind Timestamp.ToString('hh:MM:ss.fff', x:Null)}" Margin="10,0"/>
                        <TextBlock Text="[" Margin="10,0,0,0"/>
                        <TextBlock Text="{x:Bind Level}"/>
                        <TextBlock Text="]" />
                        <TextBlock Text="{x:Bind RenderMessage(x:Null)}"  Margin="10,0"/>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</UserControl>

We’ve added a listview to show the log items. To display the items we’ve added an ItemTemplate with a horizontal StackPanel with five TextBlocks in it. As you can see, I’ve not used the Binding markup for data binding, but I’ve used the x:Bind markup. I’ve done it because when I use it I can bind to functions, and not only to properties. That way, I can use the RenderMessage and the ToString messages in the bindings. To use this feature, you must set the project to use the Windows 10 Anniversary update. You can change that in the project properties:

The next step is to create the LogViewModel to store the log events, that will be used as the data context for the view. In the ViewModel folder, add a new class and name it LogViewModel:

 public class LogViewModel :ViewModelBase
 {
     public LogViewModel()
     {
         LogItems = new ObservableCollection<LogEvent>();
         Log.Logger = new LoggerConfiguration()
             .MinimumLevel.Verbose()
             .WriteTo.Observers(events => events
                 .Do(evt => LogItems.Add(evt))
                 .Subscribe())
             .CreateLogger();
     }
     public ObservableCollection<LogEvent> LogItems { get; }
 }

This class has only one property, LogItems, an ObservableCollection that stores the log events. In the constructor of the ViewModel, I initialize the logger, setting the minimum level to be logged to Verbose and writing the log events to the log items inside the observer. This is enough to capture all the log events emitted everywhere in the program. We must also declare it in the ViewModelLocator:

public class ViewModelLocator
{
    public ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

        SimpleIoc.Default.Register<MainViewModel>();
        SimpleIoc.Default.Register<CustomersViewModel>();
        SimpleIoc.Default.Register<LogViewModel>();
    }

    public MainViewModel Main => ServiceLocator.Current.GetInstance<MainViewModel>();
    public CustomersViewModel Customers => ServiceLocator.Current.GetInstance<CustomersViewModel>();
    public LogViewModel Log => ServiceLocator.Current.GetInstance<LogViewModel>();
}

With all the infrastructure in place, we can add the logging in CustomerViewModel.cs and CustomersViewModel.cs:

public class CustomerViewModel : ViewModelBase
{
    private readonly Customer _customer;

    public CustomerViewModel(Customer customer)
    {
        _customer = customer;
    }

    public string Id
    {
        get => _customer.Id;
        set
        {
            Log.Verbose("Customer Id changed from {OldId} to {NewId}", _customer.Id,value);
            _customer.Id = value;
            RaisePropertyChanged();
        }
    }

    public string Name {
        get => _customer.Name;
        set
        {
            Log.Verbose("Customer Name changed from {OldName} to {NewName}", _customer.Name, value);
            _customer.Name = value;
            RaisePropertyChanged();
        }
    }

    public string Address
    {
        get => _customer.Address;
        set
        {
            Log.Verbose("Customer Address changed from {OldAddress} to {NewAddress}", _customer.Address, value);
            _customer.Address = value;
            RaisePropertyChanged();
        }
    }

    public string City
    {
        get => _customer.City;
        set
        {
            Log.Verbose("Customer City changed from {OldCity} to {NewCity}", _customer.City, value);
            _customer.City = value;
            RaisePropertyChanged();
        }
    }

    public string Country
    {
        get => _customer.Country;
        set
        {
            Log.Verbose("Customer Country changed from {OldCountry} to {NewCountry}", _customer.Country, value);
            _customer.Country = value;
            RaisePropertyChanged();
        }
    }

    public string Phone
    {
        get => _customer.Phone;
        set
        {
            Log.Verbose("Customer Phone changed from {OldPhone} to {NewPhone}", _customer.Phone, value);
            _customer.Phone = value;
            RaisePropertyChanged();
        }
    }
public class CustomersViewModel : ViewModelBase
{
    readonly ObservableCollection<CustomerViewModel> _customers = 
        new ObservableCollection<CustomerViewModel>(
            Customer.GetCustomers().Select(c => new CustomerViewModel(c)));

    public CustomersViewModel()
    {
        _selectedCustomer = _customers.Count > 0 ? _customers[0] : null;
        _newCustomerCommand = new RelayCommand(AddCustomer);
        _deleteCustomerCommand = new RelayCommand(DeleteCustomer, () => SelectedCustomer != null);
    }
    private CustomerViewModel _selectedCustomer;
    private readonly RelayCommand _newCustomerCommand;
    private readonly RelayCommand _deleteCustomerCommand;

    public ObservableCollection<CustomerViewModel> Customers => _customers;

    public CustomerViewModel SelectedCustomer
    {
        get => _selectedCustomer;
        set
        {
            _selectedCustomer = value;
            RaisePropertyChanged();
            _deleteCustomerCommand.RaiseCanExecuteChanged();
            Log.Debug("Customer changed to {@Customer}", SelectedCustomer);
        }
    }

    public ICommand NewCustomerCommand => _newCustomerCommand ;

    private void AddCustomer()
    {
        var customer = new CustomerViewModel(new Customer());
        _customers.Add(customer);
        SelectedCustomer = customer;
        Log.Information("Added new Customer");
    }

    public ICommand DeleteCustomerCommand => _deleteCustomerCommand;

    private void DeleteCustomer()
    {
        Log.Warning("Deleting Customer {@Customer}", SelectedCustomer);
        _customers.Remove(SelectedCustomer);
        SelectedCustomer = null;
    }
}

Note that we are using Serilog’s structured logging, where we have the logging message and level (like any other logging framework) and the properties attached to the log message, that can be used to process the log files in a later time. With that, you can run the program and get something like this:

Coloring the log list

While running the app, I’ve seen that all messages have the same color, and it’s difficult to differentiate between message levels, so I decided to color the list items, depending on their level. For that, I’ve used a resource called StyleSelector. You must declare a class inherited from StyleSelector, overrride its method SelectStyleCore and return a style that will be used to render the current item. Then you set the ListView’s ItemContainerStyleSelector property to the instance of the class.

To declare the class. you must create a new class and name it LogStyleSelector and add this code:

public class LogStyleSelector : StyleSelector
{
    public Style VerboseStyle { get; set; }
    public Style DebugStyle { get; set; }
    public Style InformationStyle { get; set; }
    public Style WarningStyle { get; set; }
    public Style ErrorStyle { get; set; }
    public Style FatalStyle { get; set; }
    protected override Style SelectStyleCore(object item, DependencyObject container)
    {
        var logEvent = item as LogEvent;
        if (logEvent == null)
            return null;

        switch (logEvent.Level)
        {
            case LogEventLevel.Verbose:
                return VerboseStyle;
            case LogEventLevel.Debug:
                return DebugStyle;
            case LogEventLevel.Information:
                return InformationStyle;
            case LogEventLevel.Warning:
                return WarningStyle;
            case LogEventLevel.Error:
                return ErrorStyle;
            case LogEventLevel.Fatal:
                return FatalStyle;
            default:
                return null;
        }
    }
}

In this class, I have declared six Style properties, each one pointing to a log message level. These properties will be filled in the xaml files with the styles wanted for each kind of message. Then, in  SelectStyleCore, we get the level for the current item and return the corresponding style.

In Log.xaml we must declare the selector and the six styles in the Resources section:

<UserControl.Resources>
    <l:LogStyleSelector x:Key="LogStyleSelector" >
        <l:LogStyleSelector.VerboseStyle>
            <Style TargetType="ListViewItem">
                <Setter Property="Background" Value="Beige" />
            </Style>
        </l:LogStyleSelector.VerboseStyle>
        <l:LogStyleSelector.DebugStyle>
            <Style TargetType="ListViewItem">
                <Setter Property="Background" Value="LightGreen" />
            </Style>
        </l:LogStyleSelector.DebugStyle>
        <l:LogStyleSelector.InformationStyle>
            <Style TargetType="ListViewItem">
                <Setter Property="Background" Value="Green" />
                <Setter Property="Foreground" Value="White" />
            </Style>
        </l:LogStyleSelector.InformationStyle>
        <l:LogStyleSelector.WarningStyle>
            <Style TargetType="ListViewItem">
                <Setter Property="Background" Value="Yellow" />
            </Style>
        </l:LogStyleSelector.WarningStyle>
        <l:LogStyleSelector.ErrorStyle>
            <Style TargetType="ListViewItem">
                <Setter Property="Background" Value="Red" />
                <Setter Property="Foreground" Value="White" />
            </Style>
        </l:LogStyleSelector.ErrorStyle>
        <l:LogStyleSelector.FatalStyle>
            <Style TargetType="ListViewItem">
                <Setter Property="Background" Value="DarkRed" />
                <Setter Property="Foreground" Value="White" />
            </Style>
        </l:LogStyleSelector.FatalStyle>
    </l:LogStyleSelector>
</UserControl.Resources>

and set the ItemContainerStyleSelector property of the listview to it:

<ListView ItemsSource="{Binding LogItems}" Margin="0,10,0,0"
          ItemContainerStyleSelector="{StaticResource LogStyleSelector}">

That’s all we need to color the list items depending on the item’s level. Much better, no?

Conclusions

It’s been a long journey, but we’ve come to an end. We’ve seen how to use Serilog to use logging in a UWP app, and we’ve used a Rx sink, getting all log messages in an observable – it would be a simple change to log the messages to another place, like a Sqlite database, a flat file on disk or even a remote destination, like HoockeyApp. You can take advantage of the structured logging, where you can check the properties logged (that can be a big timesaver when examining long logs).

In the middle of the project I’ve added some extra features, like the x:Bind compiled data binding, and use functions and other kinds of bindings, and the style selector to change the background color of the list items depending on the value of the level property.

All the source code in this project is available on https://github.com/bsonnino/LoggingSerilog

 

Introduction

Every journey begins with the first step. But, sometimes, the first step is the hardest to give. Once you started, things fly and everything becomes easier. Starting a new project is always difficult: you don’t have a clear view of the parts and don’t know how to add them to your project in a way they interact seamlessly.

This is worse when you are using a new technology like UWP: what do I add to my project, what are the best practices and so on. To help us on this task and give us a starting point, the Windows team has created the Windows Template Studio, a new Visual Studio template that will help us to create a full UWP project, using only the parts we want.

In this article, we will create a UWP project to manipulate customer data with a master/detail view and use the MVVM pattern with the MVVM Light Framework.

Installing the template

The installation of the template is very simple: in Visual Studio 2017, go to Tools/Extensions and Updates and search for Windows Template Studio:

image

Once you install the template, a new option appears when you select File/New Project under Windows Universal:

image

Creating the project

Once you select this option and give the project name, a new screen will open:

image

In this screen, you can select the kind of the project you want. We will check the Navigation Pane and the MVVM Light Framework and then click in the Next button. Then we select the pages and features.

Select the Master/Detail page and name it Customer. Also select the Settings page:

image

Under the Features, select the Settings Storage, the Live Tile and the Toast Notifications, then click on Create. The new project will be created.

image

As you can see, a full project has been created, separated by folders, with all the needed files for the selected options. This project also is localized, if you want to translate it, you just need to add the localized resources. If you run the project, you will have a page with a hamburger menu and two options:

image

The project has some sample data and all the code to handle navigation, settings (including setting a dark theme), toast notifications and live tiles.

Now it’s time to customize the project for our needs.

Customizing the project

The first step in customizing our app is to set its description in the settings page. If you open SettingsPage.xaml you will see it uses the x:Uid tag to localize strings.

<TextBlock
    x:Uid="Settings_AboutDescription"
    Style="{ThemeResource BodyTextBlockStyle}"/>

To modify the description we need to open the Resources.resw file under Strings\en-US and edit the description:

image

I’ve also changed some margins in the text StackPanel in the page:

<StackPanel Grid.Row="2" Margin="30,16,30,0">
    <TextBlock
        x:Uid="Settings_About"
        Style="{ThemeResource TitleTextBlockStyle}"/>
    <TextBlock
        Text="{x:Bind ViewModel.AppDescription, Mode=OneWay}"
        Style="{ThemeResource SubtitleTextBlockStyle}"
        Margin="0,10"/>
    <TextBlock Margin="0,10"
        x:Uid="Settings_AboutDescription"
        Style="{ThemeResource BodyTextBlockStyle}"/>
    <HyperlinkButton 
        x:Uid="Settings_PrivacyTermsLink"
        Margin="0,10"/>
</StackPanel>

After changing that and running the application again, you can see the new description in the settings page:

image

Now, let’s customize the app for our needs. We don’t need the main page, so let’s remove it. You can also remove MainViewModel.cs. Removing these files, you must go to ViewModelLocator.cs and remove the references to the main viewmodel:

public class ViewModelLocator
{
    NavigationServiceEx _navigationService = new NavigationServiceEx();

    public ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

        SimpleIoc.Default.Register(() => _navigationService);
        SimpleIoc.Default.Register<ShellViewModel>();
        Register<CustomerViewModel, CustomerPage>();
        Register<CustomerDetailViewModel, CustomerDetailPage>();
        Register<SettingsViewModel, SettingsPage>();
    }

    public SettingsViewModel SettingsViewModel => 
        ServiceLocator.Current.GetInstance<SettingsViewModel>();

    public CustomerDetailViewModel CustomerDetailViewModel => 
        ServiceLocator.Current.GetInstance<CustomerDetailViewModel>();

    public CustomerViewModel CustomerViewModel => 
        ServiceLocator.Current.GetInstance<CustomerViewModel>();

    public ShellViewModel ShellViewModel => 
        ServiceLocator.Current.GetInstance<ShellViewModel>();

    public void Register<VM, V>() where VM : class
    {
        SimpleIoc.Default.Register<VM>();
        _navigationService.Configure(typeof(VM).FullName, typeof(V));
    }
}

If you build the project now you will get many errors due to the removal of the main page. Let’s fix them:

The first one is in App.xaml.cs, for the activation of the window:

private ActivationService CreateActivationService()
{
    return new ActivationService(this, typeof(ViewModels.MainViewModel), new Views.ShellPage());
}

We have to change the reference to CustomerViewModel:

private ActivationService CreateActivationService()
{
    return new ActivationService(this, typeof(ViewModels.CustomerViewModel), new Views.ShellPage());
}

The second one is in ShellViewModel.cs, where the items in the NavBar are populated:

private void PopulateNavItems()
{
    _primaryItems.Clear();
    _secondaryItems.Clear();

    // More on Segoe UI Symbol icons: 
    //      https://docs.microsoft.com/windows/uwp/style/segoe-ui-symbol-font
    // Edit String/en-US/Resources.resw: Add a menu item title for each page
    _primaryItems.Add(new ShellNavigationItem("Shell_Main".GetLocalized(), 
        Symbol.Document, typeof(MainViewModel).FullName));
    _primaryItems.Add(new ShellNavigationItem("Shell_Customer".GetLocalized(), 
        Symbol.Document, typeof(CustomerViewModel).FullName));
    _secondaryItems.Add(new ShellNavigationItem("Shell_Settings".GetLocalized(), 
        Symbol.Setting, typeof(SettingsViewModel).FullName));
}

We remove that reference and, while changing that, we also change the Customer’s icon (you can see the reference in the comments):

private void PopulateNavItems()
{
    _primaryItems.Clear();
    _secondaryItems.Clear();

    // More on Segoe UI Symbol icons: 
    //    https://docs.microsoft.com/windows/uwp/style/segoe-ui-symbol-font
    // Edit String/en-US/Resources.resw: Add a menu item title for each page
    _primaryItems.Add(new ShellNavigationItem("Shell_Customer".GetLocalized(), 
        Symbol.People, typeof(CustomerViewModel).FullName));
    _secondaryItems.Add(new ShellNavigationItem("Shell_Settings".GetLocalized(), 
        Symbol.Setting, typeof(SettingsViewModel).FullName));
}

Now, when we run the app, the main page is not there anymore and the customer page has a new symbol in the NavBar:

image

Adding data to the app

We need some customer data, so I added a json file with the customers (this file should be added to the project as Content and Copy if newer):

{
  "Customers": [
    {
      "Id": "ALFKI",
      "CompanyName": "Alfreds Futterkiste",
      "ContactName": "Maria Anders",
      "ContactTitle": "Sales Representative",
      "Address": "Obere Str. 57",
      "City": "Berlin",
      "PostalCode": "12209",
      "Country": "Germany",
      "Phone": "030-0074321",
      "Fax": "030-0076545"
    },
...

If you take a look at the Models folder you will see that the template has added a sample model:

public class SampleModel
{
    public string Title { get; set; }
    public string Description { get; set; }
    public Symbol Symbol { get; set; }

    public char SymbolAsChar
    {
        get { return (char)Symbol; }
    }
}

We don’t want that, so we can remove it and add our model, Customer.cs:

public class Customer
{
    public string Id { get; set; }
    public string CompanyName { get; set; }
    public string ContactName { get; set; }
    public string ContactTitle { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
    public string Country { get; set; }
    public string Phone { get; set; }
    public string Fax { get; set; }
}

When we do that, a lot of code will break, as it is dependent of SampleModel. The first place is in CustomerDetailViewModel, where there is an Item property:

private SampleModel _item;
public SampleModel Item
{
    get { return _item; }
    set { Set(ref _item, value); }
}

We change that to:

private Customer _item;
public Customer Item
{
    get { return _item; }
    set { Set(ref _item, value); }
}

The second place to change is in CustomerViewModel, where we change all references from SampleModel to Customer. There is also a property SampleItems that we will use the Rename refactoring (CTRL+R+R) to rename it to Customers.

In CustomerViewModel, there is a reference to SampleModelService, the service used to serve data to the ViewModel, we have to change it to read data from our json file and rename it to CustomerService (by now, we will make it a read-only service, we will change that later):

public class CustomerService
{
    public async Task<IEnumerable<Customer>> GetDataAsync()
    {
        StorageFile customerFile = await StorageFile.GetFileFromApplicationUriAsync(
            new Uri("ms-appx:///Customers.json"));
        var customerJson = await FileIO.ReadTextAsync(customerFile);
        return await Json.ToObjectAsync<List<Customer>>(customerJson);
    }
}

We are using the Json.cs helper class to read the data and return the list of customers.

The next step is to change the references in the views. Change all references of SampleModel in CustomerDetailControl.xaml.cs and CustomerDetailPage.xaml.cs. Then, we must change the views to point to the Customer properties. The first change is in the MasterViewItemTemplate in CustomerPage.xaml:

<DataTemplate x:Key="MasterListViewItemTemplate" x:DataType="model:Customer">
    <Grid Margin="12,12,12,12">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock 
            Text="{x:Bind CompanyName}" 
            FontSize="16" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" TextWrapping="NoWrap"/>
        <TextBlock
            Grid.Row="1"
            Opacity="0.6"
            Text="{x:Bind ContactName}"
            FontSize="16" FontWeight="Normal" TextTrimming="CharacterEllipsis" TextWrapping="NoWrap"/>
    </Grid>
</DataTemplate>

Then we must replace the data in CustomDetailControl.xaml to show the customer data (there is even a comment in the file for that):

<TextBlock
    x:Name="TitlePage"
    Text="{x:Bind MasterMenuItem.CompanyName, Mode=OneWay}"
    FontSize="28" FontWeight="SemiLight" TextTrimming="CharacterEllipsis" 
    TextWrapping="NoWrap" VerticalAlignment="Center"
    Margin="0,0,12,7"/>

<ScrollViewer
    Grid.Row="1"
    ScrollViewer.HorizontalScrollBarVisibility="Disabled"
    ScrollViewer.VerticalScrollBarVisibility="Auto"
    ScrollViewer.VerticalScrollMode="Auto">

    <!--The SystemControlPageBackgroundChromeLowBrush background represents where you should place your detail content.-->
    <Grid Background="{ThemeResource SystemControlPageBackgroundChromeLowBrush}">

        <!--Replate FontIcon and TextBlock with your detail content.-->
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition Height="40"/>
            <RowDefinition Height="40"/>
            <RowDefinition Height="40"/>
            <RowDefinition Height="40"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Text="Contact Name" VerticalAlignment="Center" Margin="10,0" Grid.Row="0" Grid.Column="0"/>
        <TextBlock Text="Contact Title" VerticalAlignment="Center" Margin="10,0" Grid.Row="1" Grid.Column="0"/>
        <TextBlock Text="Address" VerticalAlignment="Center" Margin="10,0" Grid.Row="2" Grid.Column="0"/>
        <TextBlock Text="City" VerticalAlignment="Center" Margin="10,0" Grid.Row="3" Grid.Column="0"/>
        <TextBlock Text="Country" VerticalAlignment="Center" Margin="10,0" Grid.Row="4" Grid.Column="0"/>
        <TextBox Text="{x:Bind MasterMenuItem.ContactName, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="0" Grid.Column="1"/>
        <TextBox Text="{x:Bind MasterMenuItem.ContactTitle, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="1" Grid.Column="1"/>
        <TextBox Text="{x:Bind MasterMenuItem.Address, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="2" Grid.Column="1"/>
        <TextBox Text="{x:Bind MasterMenuItem.City, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="3" Grid.Column="1"/>
        <TextBox Text="{x:Bind MasterMenuItem.Country, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="4" Grid.Column="1"/>
    </Grid>
</ScrollViewer>
<TextBlock
    x:Name="TitlePage"
    Text="{x:Bind MasterMenuItem.CompanyName, Mode=OneWay}"
    FontSize="28" FontWeight="SemiLight" TextTrimming="CharacterEllipsis" 
    TextWrapping="NoWrap" VerticalAlignment="Center"
    Margin="0,0,12,7"/>

<ScrollViewer
    Grid.Row="1"
    ScrollViewer.HorizontalScrollBarVisibility="Disabled"
    ScrollViewer.VerticalScrollBarVisibility="Auto"
    ScrollViewer.VerticalScrollMode="Auto">

    <!--The SystemControlPageBackgroundChromeLowBrush background represents where you should place your detail content.-->
    <Grid Background="{ThemeResource SystemControlPageBackgroundChromeLowBrush}">

        <!--Replate FontIcon and TextBlock with your detail content.-->
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition Height="40"/>
            <RowDefinition Height="40"/>
            <RowDefinition Height="40"/>
            <RowDefinition Height="40"/>
            <RowDefinition Height="40"/>
            <RowDefinition Height="40"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Text="Id" VerticalAlignment="Center" Margin="10,0" Grid.Row="0" Grid.Column="0"/>
        <TextBlock Text="Company Name" VerticalAlignment="Center" Margin="10,0" Grid.Row="1" Grid.Column="0"/>
        <TextBlock Text="Contact Name" VerticalAlignment="Center" Margin="10,0" Grid.Row="2" Grid.Column="0"/>
        <TextBlock Text="Contact Title" VerticalAlignment="Center" Margin="10,0" Grid.Row="3" Grid.Column="0"/>
        <TextBlock Text="Address" VerticalAlignment="Center" Margin="10,0" Grid.Row="4" Grid.Column="0"/>
        <TextBlock Text="City" VerticalAlignment="Center" Margin="10,0" Grid.Row="5" Grid.Column="0"/>
        <TextBlock Text="Country" VerticalAlignment="Center" Margin="10,0" Grid.Row="6" Grid.Column="0"/>
        <TextBox Text="{x:Bind MasterMenuItem.Id, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="0" Grid.Column="1"/>
        <TextBox Text="{x:Bind MasterMenuItem.CompanyName, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="1" Grid.Column="1"/>
        <TextBox Text="{x:Bind MasterMenuItem.ContactName, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="2" Grid.Column="1"/>
        <TextBox Text="{x:Bind MasterMenuItem.ContactTitle, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="3" Grid.Column="1"/>
        <TextBox Text="{x:Bind MasterMenuItem.Address, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="4" Grid.Column="1"/>
        <TextBox Text="{x:Bind MasterMenuItem.City, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="5" Grid.Column="1"/>
        <TextBox Text="{x:Bind MasterMenuItem.Country, Mode=TwoWay}" 
                 VerticalAlignment="Center" Margin="10,5" Grid.Row="6" Grid.Column="1"/>
    </Grid>
</ScrollViewer>

Now, if you run the app, you will see the customer data:

image

Adding, updating and deleting customers

Until now, the data is read-only, we can’t update it. To allow updating the data, we need to save the file in the isolated storage; the actual file is located in the installation folder and it’s read-only. To copy the file to the isolated storage, we must change the Customer Service:

public class CustomerService
{
    public async Task<IEnumerable<Customer>> GetDataAsync()
    {
        StorageFolder localFolder = ApplicationData.Current.LocalFolder;
        var customerData = await localFolder.ReadAsync<List<Customer>>("Customers");
        if (customerData == null)
        {
            customerData = await LoadInitialCustomerDataAsync();
            await localFolder.SaveAsync("Customers", customerData);
        }
        return customerData;
    }

    private static async Task<List<Customer>> LoadInitialCustomerDataAsync()
    {
        StorageFile customerFile = await StorageFile.GetFileFromApplicationUriAsync(
            new Uri("ms-appx:///Customers.json"));
        var customerJson = await FileIO.ReadTextAsync(customerFile);
        return await Json.ToObjectAsync<List<Customer>>(customerJson);
    }
}
 public class CustomerService
 {
     public async Task<IEnumerable<Customer>> GetDataAsync()
     {
         StorageFolder localFolder = ApplicationData.Current.LocalFolder;
         var customerData = await localFolder.ReadAsync<List<Customer>>("Customers");
         if (customerData == null)
         {
             customerData = await LoadInitialCustomerDataAsync();
             await localFolder.SaveAsync("Customers", customerData);
         }
         return customerData;
     }

     private static async Task<List<Customer>> LoadInitialCustomerDataAsync()
     {
         StorageFile customerFile = await StorageFile.GetFileFromApplicationUriAsync(
             new Uri("ms-appx:///Customers.json"));
         var customerJson = await FileIO.ReadTextAsync(customerFile);
         return await Json.ToObjectAsync<List<Customer>>(customerJson);
     }

     public async Task SaveDataAsync(IEnumerable<Customer> customerData)
     {
         StorageFolder localFolder = ApplicationData.Current.LocalFolder;
         await localFolder.SaveAsync("Customers", customerData);
     }
 }

We are using the SettingsStorageExtensions helper class to load the data from the isolated storage. If there is no data, we load the customers from the installation file. We also created a SaveDataAsync method to save the data.

Now we must create some buttons to allow adding, deleting and saving customers. In CustomerPage.xaml we will add the buttons for these actions:

<!--The SystemControlPageBackgroundChromeLowBrush background represents where you should place your master content.-->
<StackPanel Orientation="Horizontal" Grid.Row="1" Margin="0,4">
    <Button Width="48" Height="48" BorderThickness="0" Background="Transparent" 
            Command="{x:Bind ViewModel.AddCustomerCommand}">
        <Grid>
            <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="" FontSize="16"/>
            <Grid Background="White" Margin="16,16,0,0" Width="8" Height="8">
                <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="" FontSize="8" Foreground="Red" />
            </Grid>
        </Grid>
    </Button>
    <Button Width="48" Height="48" BorderThickness="0" Background="Transparent" 
            Command="{x:Bind ViewModel.DeleteCustomerCommand}">
        <Grid>
            <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="" FontSize="16"/>
            <Grid Background="White" Margin="16,16,0,0" Width="8" Height="8">
                <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="" FontSize="8" Foreground="Red" />
            </Grid>
        </Grid>
    </Button>
    <Button Width="48" Height="48" BorderThickness="0" Background="Transparent" 
            Command="{x:Bind ViewModel.SaveCustomersCommand}">
        <Grid>
            <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="" FontSize="16"/>
            <Grid Background="White" Margin="16,16,0,0" Width="8" Height="8">
                <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="" FontSize="8" Foreground="Red" />
            </Grid>
        </Grid>
    </Button>
</StackPanel>

We must add the three commands in CustomerViewModel:

public ICommand AddCustomerCommand { get; }
public ICommand DeleteCustomerCommand { get; }
public ICommand SaveCustomersCommand { get; }

public CustomerViewModel()
{
    ItemClickCommand = new RelayCommand<ItemClickEventArgs>(OnItemClick);
    StateChangedCommand = new RelayCommand<VisualStateChangedEventArgs>(OnStateChanged);
    AddCustomerCommand = new RelayCommand(DoAddCustomer);
    DeleteCustomerCommand = new RelayCommand(DoDeleteCustomer);
    SaveCustomersCommand = new RelayCommand(DoSaveCustomers);
}

private async void DoSaveCustomers()
{
    var customerService = new CustomerService();
    await customerService.SaveDataAsync(Customers);
}

private void DoDeleteCustomer()
{
    if (Selected != null)
        Customers.Remove(Selected);
}

private void DoAddCustomer()
{
    var customer = new Customer();
    Customers.Add(customer);
    Selected = customer;
}
public ICommand AddCustomerCommand { get; }
public ICommand DeleteCustomerCommand { get; }
public ICommand SaveCustomersCommand { get; }

public CustomerViewModel()
{
    ItemClickCommand = new RelayCommand<ItemClickEventArgs>(OnItemClick);
    StateChangedCommand = new RelayCommand<VisualStateChangedEventArgs>(OnStateChanged);
    AddCustomerCommand = new RelayCommand(DoAddCustomer);
    DeleteCustomerCommand = new RelayCommand(DoDeleteCustomer);
    SaveCustomersCommand = new RelayCommand(DoSaveCustomers);
}

private async void DoSaveCustomers()
{
    var customerService = new CustomerService();
    await customerService.SaveDataAsync(Customers);
}

private void DoDeleteCustomer()
{
    if (Selected != null)
        Customers.Remove(Selected);
    Selected = Customers.FirstOrDefault();
}

private void DoAddCustomer()
{
    var customer = new Customer();
    Customers.Add(customer);
    Selected = customer;
}

Now, when you run the program, you will see three icons at the top of the customer list and you are able to create update and delete customers. If you click the Save button, the data will be saved and will be available in the next run.

Saving and Loading the current state

One thing that is nice in an application is that the current state is saved when the app is closed. We want to save the selected customer when the app is closed and restore it when it is reopened. To do that, we will save the id of the selected customer to the application settings, using the SettingsStorageExtensions class:

public async Task LoadDataAsync(VisualState currentState)
{
    _currentState = currentState;
    Customers.Clear();

    var service = new CustomerService();
    var data = await service.GetDataAsync();

    foreach (var item in data)
    {
        Customers.Add(item);
    }
    await LoadSettingsAsync();
}

public async void SaveSettings()
{
    if (Selected != null)
    {
        var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;

        var container =
            localSettings.CreateContainer("CustSettings",
                Windows.Storage.ApplicationDataCreateDisposition.Always);
        await container.SaveAsync("LastCust", Selected.Id);
    }
}

public async Task LoadSettingsAsync()
{
    if (Selected != null)
    {
        var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;

        var container =
            localSettings.CreateContainer("CustSettings",
                Windows.Storage.ApplicationDataCreateDisposition.Always);
        var lastCust = await container.ReadAsync<string>("LastCust");
        if (!string.IsNullOrEmpty(lastCust))
        Selected = Customers.FirstOrDefault(c => c.Id == lastCust);
    }
}
public async Task LoadDataAsync(VisualState currentState)
{
    _currentState = currentState;
    Customers.Clear();

    var service = new CustomerService();
    var data = await service.GetDataAsync();

    foreach (var item in data)
    {
        Customers.Add(item);
    }
    await LoadSettingsAsync();
}

private async void SaveSettings()
{
    if (Selected != null)
    {
        var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;

        var container =
            localSettings.CreateContainer("CustSettings",
                Windows.Storage.ApplicationDataCreateDisposition.Always);
        await container.SaveAsync("LastCust", Selected.Id);
    }
}

private async Task LoadSettingsAsync()
{
    var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;

    var container =
        localSettings.CreateContainer("CustSettings",
            Windows.Storage.ApplicationDataCreateDisposition.Always);
    var lastCust = await container.ReadAsync<string>("LastCust");
    Selected = !string.IsNullOrEmpty(lastCust) ? 
        Customers.FirstOrDefault(c => c.Id == lastCust) : 
        Customers.FirstOrDefault();
}
public Customer Selected
{
    get { return _selected; }
    set
    {
        Set(ref _selected, value);
        SaveSettings();
    }
}

public async Task LoadDataAsync(VisualState currentState)
{
    _currentState = currentState;
    Customers.Clear();

    var service = new CustomerService();
    var data = await service.GetDataAsync();

    foreach (var item in data)
    {
        Customers.Add(item);
    }
    await LoadSettingsAsync();
}

private async void SaveSettings()
{
    if (Selected != null)
    {
        var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;

        var container =
            localSettings.CreateContainer("CustSettings",
                Windows.Storage.ApplicationDataCreateDisposition.Always);
        await container.SaveAsync("LastCust", Selected.Id);
    }
}

private async Task LoadSettingsAsync()
{
    var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;

    var container =
        localSettings.CreateContainer("CustSettings",
            Windows.Storage.ApplicationDataCreateDisposition.Always);
    var lastCust = await container.ReadAsync<string>("LastCust");
    Selected = !string.IsNullOrEmpty(lastCust) ? 
        Customers.FirstOrDefault(c => c.Id == lastCust) : 
        Customers.FirstOrDefault();
}

When the data is loaded, the id of the last selected customer is retrieved from the storage settings. This information is saved every time the selected customer changes. That way, when the user opens the app, the last selected customer is shown in the screen.

Working with toasts and tiles

You should have noticed that every time you run the project, a toast notification is shown. As we selected to add the Toast Notifications to the project, these were added and we can customize them. The sample toast is called from DefaultLaunchActivationHandler.cs:

protected override async Task HandleInternalAsync(LaunchActivatedEventArgs args)
{
    // When the navigation stack isn't restored navigate to the first page,
    // configuring the new page by passing required information as a navigation
    // parameter
    NavigationService.Navigate(_navElement, args.Arguments);

    // TODO UWPTemplates: This is a sample on how to show a toast notification.
    // You can use this sample to create toast notifications where needed in your app.
    Singleton<ToastNotificationsService>.Instance.ShowToastNotificationSample();
    await Task.CompletedTask;
}

We will remove this call and add it at the end of the load process, to show how many customers were loaded and after saving the customer file, to show how many customers were saved. Before that, we must change the sample toast (in ToastNotificationsService.Samples.cs) to show a custom message:

public void ShowToastNotificationSample(string message)
{
    var customerService = new CustomerService();
    // Create the toast content
    var content = new ToastContent()
    {
        // TODO UWPTemplates: Check this documentation to know more about the Launch property
        // Documentation: https://developer.microsoft.com/en-us/windows/uwp-community-toolkit/api/microsoft_toolkit_uwp_notifications_toastcontent
        Launch = "ToastContentActivationParams",

        Visual = new ToastVisual()
        {
            BindingGeneric = new ToastBindingGeneric()
            {
                Children =
                {
                    new AdaptiveText()
                    {
                        Text = "Customer CRUD"
                    },

                    new AdaptiveText()
                    {
                         Text = message
                    }
                }
            }
        },

        Actions = new ToastActionsCustom()
        {
            Buttons =
            {
                // TODO UWPTemplates: Check this documentation to know more about Toast Buttons
                // Documentation: https://developer.microsoft.com/en-us/windows/uwp-community-toolkit/api/microsoft_toolkit_uwp_notifications_toastbutton
                new ToastButton("OK", "ToastButtonActivationArguments")
                {
                    ActivationType = ToastActivationType.Foreground
                },

                new ToastButtonDismiss("Cancel")
            }
        }
    };

    // Create the toast
    var toast = new ToastNotification(content.GetXml())
    {
        // TODO UWPTemplates: Gets or sets the unique identifier of this notification within the notification Group. Max length 16 characters.
        // Documentation: https://docs.microsoft.com/uwp/api/windows.ui.notifications.toastnotification
        Tag = "ToastTag"
    };

    // And show the toast
    ShowToastNotification(toast);
}

Then we can change the methods to load and save data in CustomerViewModel.cs to show the toast:

 private async void DoSaveCustomers()
 {
     var customerService = new CustomerService();
     await customerService.SaveDataAsync(Customers);
     Singleton<ToastNotificationsService>.Instance.ShowToastNotificationSample($"Saved {Customers.Count} customers");
 }

 public async Task LoadDataAsync(VisualState currentState)
 {
     _currentState = currentState;
     Customers.Clear();

     var service = new CustomerService();
     var data = await service.GetDataAsync();

     foreach (var item in data)
     {
         Customers.Add(item);
     }
     await LoadSettingsAsync();
     
     Singleton<ToastNotificationsService>.Instance.ShowToastNotificationSample($"Loaded {Customers.Count} customers");
 }

Now, when you run the app, every time the data is loaded or saved, a toast is shown with the number of customers in the file. You can even capture the click of the OK button in the HandleInternalAsync method in ToastNotificationsService.

One final change should be made in our app: it is still showing a default tile.We will change that to reflect the number of customers in the file. The sample tile is shown in the StartupAsync method in ActivationService.cs.

private async Task StartupAsync()
{
    Singleton<LiveTileService>.Instance.SampleUpdate();
    Services.ThemeSelectorService.SetRequestedTheme();
    await Task.CompletedTask;
}

We will change the SampleUpdate in LiveTileService.Samples.cs to show a custom message:

public void SampleUpdate(string message)
{
    // See more information about Live Tiles Notifications
    // Documentation: https://docs.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-sending-a-local-tile-notification

    // These would be initialized with actual data
    string title = "Customer CRUD";
   
    // Construct the tile content
    TileContent content = new TileContent()
    {
        Visual = new TileVisual()
        {
            Arguments = "Customer CRUD",
            TileMedium = new TileBinding()
            {
                Content = new TileBindingContentAdaptive()
                {
                    Children =
                    {
                        new AdaptiveText()
                        {
                            Text = title
                        },
                        new AdaptiveText()
                        {
                            Text = message,
                            HintStyle = AdaptiveTextStyle.CaptionSubtle
                        }
                    }
                }
            },

            TileWide = new TileBinding()
            {
                Content = new TileBindingContentAdaptive()
                {
                    Children =
                    {
                        new AdaptiveText()
                        {
                            Text = title,
                            HintStyle = AdaptiveTextStyle.Subtitle
                        },
                        new AdaptiveText()
                        {
                            Text = message,
                            HintStyle = AdaptiveTextStyle.CaptionSubtle
                        }
                    }
                }
            }
        }
    };

    // Then create the tile notification            
    var notification = new TileNotification(content.GetXml());
    UpdateTile(notification);
}

With that change, we can change CustomerViewModel to update the live tile every time the customer file is loaded or saved:

private async void DoSaveCustomers()
{
    var customerService = new CustomerService();
    await customerService.SaveDataAsync(Customers);
    Singleton<ToastNotificationsService>.Instance.ShowToastNotificationSample($"Saved {Customers.Count} customers");
    Singleton<LiveTileService>.Instance.SampleUpdate($"{Customers.Count} customers in the database");
}

public async Task LoadDataAsync(VisualState currentState)
{
    _currentState = currentState;
    Customers.Clear();

    var service = new CustomerService();
    var data = await service.GetDataAsync();

    foreach (var item in data)
    {
        Customers.Add(item);
    }
    await LoadSettingsAsync();
    
    Singleton<ToastNotificationsService>.Instance.ShowToastNotificationSample($"Loaded {Customers.Count} customers");
    Singleton<LiveTileService>.Instance.SampleUpdate($"{Customers.Count} customers in the database");
}

Conclusions

As you can see, we went from 0 to a full UWP app, using the MVVM pattern, toast and live tiles, using best practices. The Windows Template Studio gives us a good starting point for it: just select what you want and a sample project is created, where you can fully customize it. In this case, I used a json file, but CustomerService could get its data from an external service with minimal changes. There is a lot of room to improve this app, but the basis is there, it’s not impossible to create a LOB app using UWP.

The full project source is in https://github.com/bsonnino/CustomerCrud

protected override async Task HandleInternalAsync(LaunchActivatedEventArgs args)
{
    // When the navigation stack isn't restored navigate to the first page,
    // configuring the new page by passing required information as a navigation
    // parameter
    NavigationService.Navigate(_navElement, args.Arguments);

    // TODO UWPTemplates: This is a sample on how to show a toast notification.
    // You can use this sample to create toast notifications where needed in your app.
    Singleton<ToastNotificationsService>.Instance.ShowToastNotificationSample();
    await Task.CompletedTask;
}

Some time ago, I’ve written a post about Surface Dial programming. With the introduction of the Creator’s Update, some things have improved. In this post, I will show some changes and will revisit the program that was developed and add these to the app. To use these changes, you need to have the Windows 10 Creator’s Update installed (you will have it if you are using the Windows Insider builds), Visual Studio 2017 and the Creator’s Update SDK (build 15063) installed. Once you have these pre requisites installed, go to the project properties in Visual Studio and change the target version to 15063:

image

With that, the new APIs will be opened.

Menu icons from font glyphs

A welcome change was the possibility to use font glyphs for the menu items. In the previous version, we had to add PNG files to the project, create the icon and then create the menu item, with some code like this:

var iconResize = RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///Assets/Resize.png"));
var itemResize = RadialControllerMenuItem.CreateFromIcon("Resize", iconResize);

This is not needed anymore: now, we can use the CreateFromFontGlyph method, that creates the menu item from a font glyph, that can come both from an installed font or for a custom font for the app. This code creates the menu icons for the app:

// Create the items for the menu
var itemResize = RadialControllerMenuItem.CreateFromFontGlyph("Resize", "\xE8B9", "Segoe MDL2 Assets");
var itemRotate = RadialControllerMenuItem.CreateFromFontGlyph("Rotate", "\xE7AD", "Segoe MDL2 Assets");
var itemMoveX = RadialControllerMenuItem.CreateFromFontGlyph("MoveX", "\xE8AB", "Segoe MDL2 Assets");
var itemMoveY = RadialControllerMenuItem.CreateFromFontGlyph("MoveY", "\xE8CB", "Segoe MDL2 Assets");
var itemColor = RadialControllerMenuItem.CreateFromFontGlyph("Color", "\xE7E6", "Segoe MDL2 Assets"); 

We only have to add the Unicode character and the font name and that’s all. The new Surface Dial menu is like this:

image

Once we have the new menu, we can add new features to the app.

Changing behavior when the button is pressed

One change that we can make to the project is to detect when the button is pressed and change the behavior when the dial is rotated. This is done by using the new IsButtonPressed property in the RadialControllerRotationChangedEventArgs class. We can use it in the RotationChanged event to move diagonally when the button is pressed. We only need to do a small change to the code:

private void ControllerRotationChanged(RadialController sender, 
    RadialControllerRotationChangedEventArgs args)
{
    switch (_currentTool)
    {
        case CurrentTool.Resize:
            Scale.ScaleX += args.RotationDeltaInDegrees / 10;
            Scale.ScaleY += args.RotationDeltaInDegrees / 10;
            break;
        case CurrentTool.Rotate:
            Rotate.Angle += args.RotationDeltaInDegrees;
            break;
        case CurrentTool.MoveX:
            Translate.X += args.RotationDeltaInDegrees;
            if (args.IsButtonPressed)
                Translate.Y += args.RotationDeltaInDegrees;
            break;
        case CurrentTool.MoveY:
            Translate.Y += args.RotationDeltaInDegrees;
            if (args.IsButtonPressed)
                Translate.X += args.RotationDeltaInDegrees;
            break;
        case CurrentTool.Color:
            _selBrush += (int)(args.RotationDeltaInDegrees / 10);
            if (_selBrush >= _namedBrushes.Count)
                _selBrush = 0;
            if (_selBrush < 0)
                _selBrush = _namedBrushes.Count-1;
            Rectangle.Fill = _namedBrushes[(int)_selBrush];
            break;
        default:
            break;
    }
}

 

When the tool is MoveX or MoveY, we check for IsButtonPressed and, if it’s on, we move both in the X and Y directions at the same time. Now, if you run the code and press the button while rotating the dial, you will see that the rectangle moves in the diagonal.

Hiding the menu

The menu is a great tool, but some times, it’s too intrusive and you would like to hide it. Until now, there was no way to hide it, but the Creator’s Update introduced the possibility to do that. You just have to set the IsMenuSuppressed property of the RadialControllerConfiguration class to true and the menu will not appear anymore.

In this case, there will be an extra problem – the controller won’t send messages to the app anymore. You will need some extra steps to regain control:

  • Set the ActiveControllerWhenMenuIsSuppressed property of RadialControllerConfiguration to point to the controller
  • Capture the ButtonHolding event of the controller to set the actions when the user is holding the button
  • Give feedback to the user, as he won’t see the menu anymore

With these steps, you’re ready and don’t need to show the menu.

We will add a new TextBlock to the window to show the tool the user is using. In MainPage.xaml, add this code:

 

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Rectangle ...
    </Rectangle>
    <TextBlock x:Name="ToolText" HorizontalAlignment="Right" VerticalAlignment="Bottom" 
               Margin="5" Foreground="Red" FontSize="20" FontWeight="Bold"/>
</Grid>

Then, in the constructor of MainPage, add this code:

// Leave only the Volume default item - Zoom and Undo won't be used
RadialControllerConfiguration config = RadialControllerConfiguration.GetForCurrentView();
config.SetDefaultMenuItems(new[] { RadialControllerSystemMenuItemKind.Volume });
config.ActiveControllerWhenMenuIsSuppressed = controller;
config.IsMenuSuppressed = true;
controller.ButtonHolding += (s, e) => _isButtonHolding = true;
controller.ButtonReleased += (s, e) => _isButtonHolding = false;
ToolText.Text = _currentTool.ToString();

It will suppress the menu, set the controller as the active controller when the menu is suppressed and set the _isButtonHolding field to true when the button is pressed. The next step is to change tools when the button is pressed. This is done in the RotationChanged event:

private void ControllerRotationChanged(RadialController sender,
    RadialControllerRotationChangedEventArgs args)
{
    if (_isButtonHolding)
    {
        _currentTool = args.RotationDeltaInDegrees > 0 ? 
            MoveNext(_currentTool) : MovePrevious(_currentTool); 
        ToolText.Text = _currentTool.ToString();
        return;
    }
    switch (_currentTool)
    ...

When the button is pressed, we change the current tool by calling the MoveNext and MovePrevious (depending of the direction of the rotation) methods:

private CurrentTool MoveNext(CurrentTool currentTool)
{
    return Enum.GetValues(typeof(CurrentTool)).Cast()
        .FirstOrDefault(t => (int)t > (int)currentTool);
}

private CurrentTool MovePrevious(CurrentTool currentTool)
{
    return currentTool == CurrentTool.Resize ? CurrentTool.Color :
        Enum.GetValues(typeof(CurrentTool)).Cast()
        .OrderByDescending(t => t)
        .FirstOrDefault(t => (int)t < (int)currentTool);
}

These methods use LINQ to get the next tool and, if there isn’t one, reset to the first (or last). With this code in place, you can run the app and see that the menu isn’t shown, but the tool is changed when the dial is pressed and rotated, and the TextBlock reflects the current tool.

image

Haptic Feedback

Another improvement in the SDK is the ability to control the haptic feedback, the vibration the dial sends for every change in the rotation. You can disable this feedback by setting the UseAutomaticHapticFeedback property of the controller to false. You can then send your feedback using the methods SendHapticFeedback, SendHapticFeedbackForDuration and SendHapticFeedbackForPlayCount of the SimpleHapticsController class. You can get an instance of this class using the SimpleHapticsController property of the RadialControllerRotationChangedEventArgs class in the RotationChanged event.

We will add the feedback when the user tries to move the rectangle beyond the window’s limits. The first step is to remove the feedback, in the constructor of MainPage:

controller.UseAutomaticHapticFeedback = false;

The next step is to check if the movement is beyond the window’s limits and send the feedback if it is:

case CurrentTool.MoveX:
    if (CanMove(Translate, Scale, args.RotationDeltaInDegrees))
    {
        Translate.X += args.RotationDeltaInDegrees;
        if (args.IsButtonPressed)
            Translate.Y += args.RotationDeltaInDegrees;
    }
    else
        SendHapticFeedback(args.SimpleHapticsController,3);
    break;
case CurrentTool.MoveY:
    if (CanMove(Translate, Scale, args.RotationDeltaInDegrees))
    {
        Translate.Y += args.RotationDeltaInDegrees;
        if (args.IsButtonPressed)
            Translate.X += args.RotationDeltaInDegrees;
    }
    else
        SendHapticFeedback(args.SimpleHapticsController,3);
    break;

The code uses the CanMove method to check if the user is trying to move outside the limits and then, if that’s the case, it calls the SendHapticFeedback method to send the feedback. The CanMove method uses the translation, scale and rotation delta to check it the rectangle can be moved:

private bool CanMove(TranslateTransform translate, ScaleTransform scale,
    double delta)
{
    var canMove = delta > 0 ?
        translate.X + 60 * scale.ScaleX + delta < ActualWidth / 2 &&
        translate.Y + 60 * scale.ScaleY + delta < -ActualWidth / 2 &&
        translate.X - 60 * scale.ScaleX + delta > -ActualWidth / 2 &&
        translate.Y - 60 * scale.ScaleY + delta > -ActualHeight / 2;
    return canMove;
}

SendHapticFeedback receives the SimpleHapticsController as a parameter and sends the feedback:

private void SendHapticFeedback(SimpleHapticsController simpleHapticsController, int count)
{
    var feedback = simpleHapticsController.SupportedFeedback.FirstOrDefault(f =>; 
        f.Waveform == KnownSimpleHapticsControllerWaveforms.Click);
    if (feedback != null)
        simpleHapticsController.SendHapticFeedbackForPlayCount(feedback, 1, count, 
            TimeSpan.FromMilliseconds(100));
}

This method uses LINQ to get the feedback of type Click and, if it finds it, it uses the SendHapticFeedbackForPlayCount to send the feedback three times with an interval of 100ms. We just need to make an extra change: as we removed completely the feedback, there will be no feedback when the user changes tools. One way to restore it is to send the feedback every time we change tools:

if (_isButtonHolding)
{
    _currentTool = args.RotationDeltaInDegrees > 0 ?
        MoveNext(_currentTool) : MovePrevious(_currentTool);
    ToolText.Text = _currentTool.ToString();
    SendHapticFeedback(args.SimpleHapticsController, 1);
    return;
}

Now, if you run the app, you will see that there is no more feedback for every rotation, but you still get feedback when you change tools or when you try to move beyond window’s limits.

Conclusions

We could see a lot of improvements in the Surface Dial API, now we have a lot of control on the menu and on the feedback sent to the user. The Surface Dial brings a new way to input data into your apps, and using it can improve the user experience for your users and make your app stand above the competition.

The full source code for this project is at https://github.com/bsonnino/SurfaceDial2

Recently, Microsoft introduced the new version of its test framework, MS-Test 2. With this new version, they introduced a new feature that I was waiting for a long time: parametrized tests (yes, NUnit and XUnit have had this for a long time, I know).

And what are parametrized tests? Let me show you with an example. Let’s say we have this routine to return Fibonacci numbers (source: https://www.dotnetperls.com/fibonacci)

 

public static int Fibonacci(int n)
{
    int a = 0;
    int b = 1;
    // In N steps compute Fibonacci sequence iteratively.
    for (int i = 0; i &lt; n; i++)
    {
        int temp = a;
        a = b;
        b = temp + b;
    }
    return a;
}

And we want to test it. We would like to test it with the numbers 0, 1, 2 and 80 (the first two are special cases, the third is a normal case and 80 is a large number to be sure that the routine works with large numbers). We should create a test like this:

[TestMethod]
public void Given0FibonacciReturns0()
{
   var fib = new Fib();
    var actual = fib.Fibonacci(0);
    Assert.AreEqual(0,actual);
}

This is not a bad test, but we must copy and paste to test the other results. You may argue that we could create a test like this one:

[TestMethod]
public void GivenDataFibonacciReturnsResultsOk()
{
    var numbers = new[] { 0, 1, 2, 80 };
    var results = new[] { 0L, 1L, 1L, 23416728348467685L };
    var fib = new Fib();
    for (int i = 0; i &lt; numbers.Length; i++)
    {
        var actual = fib.Fibonacci(numbers[i]);
        Assert.AreEqual(results[i], actual);
    }
}

But this has some problems:

  • If a test fails, it’s difficult to know which number failed
  • If one number fails, the next ones are not tested
  • You don’t have a clear view of what is being tested

MS-Test has had for a long time Data Driven tests (https://msdn.microsoft.com/en-us/library/ms182527.aspx), but this is very cumbersome. You must create a data file, assign it to the test and run the test using the TestContext. It’s too much work for just four tests, no?

Then it comes MS-Test 2. With it, you can create a DataTestMethod, with DataRows for each test. Let’s see how do you create a test with this new feature.

Creating Parametrized tests with MS-Test 2

In Visual Studio, create a new Console Project. In this project, create a new class and name it Fib.cs. Add this code to the class:

 public class Fib
 {
     public int Fibonacci(int n)
     {
         int a = 0;
         int b = 1;
         // In N steps compute Fibonacci sequence iteratively.
         for (int i = 0; i &lt; n; i++)
         {
             int temp = a;
             a = b;
             b = temp + b;
         }
         return a;
     }
 }

Then, in the solution, add a new Class Library project. Right click the References node in the Solution Explorer and add a reference to the console project. Then right click in the References node again and select “Manage NuGet packages”. Add the packages MsTest.TestAdapter and MsTest.TestFramework.

image

With that, you have a test project with MS-Test 2. If you are using Visual Studio 2017, the Test Project template already includes these two packages, but you must update them to the latest version, as the parametrized tests didn’t run well with the default packages.

Then, we can create our test:

[TestClass]
public class FibonacciTests
{
    [DataRow(0, 0)]
    [DataRow(1, 1)]
    [DataRow(2, 1)]
    [DataRow(80, 23416728348467685)]
    [DataTestMethod]
    public void GivenDataFibonacciReturnsResultsOk(int number, Int64 result)
    {
        var fib = new Fib();
        var actual = fib.Fibonacci(number);
        Assert.AreEqual(result, actual);
    }
}

The test is very similar to the ones we are used to create, it just has some differences:

 

  • Instead of the TestMethod attribute, it is decorated with the DataTestMethod attribute
  • The method receives two parameters
  • Each test has a DataRow attribute associated to it.

 

 

If we run this test, we get these results:

image

As you can see, we have three tests that passed and one that failed. We didn’t take in account in our routine that the results could be very large and overflow. So, we must change the routine to take this in account:

public Int64 Fibonacci(int n)
{
    Int64 a = 0;
    Int64 b = 1;
    // In N steps compute Fibonacci sequence iteratively.
    for (int i = 0; i &lt; n; i++)
    {
        Int64 temp = a;
        a = b;
        b = temp + b;
    }
    return a;
}

Now, when you run the tests, you get this:

image

All tests are passing, and we can have a clear view of which tests were run, without the need of extra files or any other tricks. Cool, no? This was a very welcome addition to MS-Test and can improve a lot our testing.

The source code for this article is in https://github.com/bsonnino/Fibonacci

Recently, Microsoft introduced the new version of its test framework, MS-Test 2. With this new version, they introduced a new feature that I was waiting for a long time: parametrized tests (yes, NUnit and XUnit have had this for a long time, I know).

And what are parametrized tests? Let me show you with an example. Let’s say we have this routine to return Fibonacci numbers (source: https://www.dotnetperls.com/fibonacci)

 

public static int Fibonacci(int n)
{
    int a = 0;
    int b = 1;
    // In N steps compute Fibonacci sequence iteratively.
    for (int i = 0; i &lt; n; i++)
    {
        int temp = a;
        a = b;
        b = temp + b;
    }
    return a;
}

And we want to test it. We would like to test it with the numbers 0, 1, 2 and 80 (the first two are special cases, the third is a normal case and 80 is a large number to be sure that the routine works with large numbers). We should create a test like this:

[TestMethod]
public void Given0FibonacciReturns0()
{
   var fib = new Fib();
    var actual = fib.Fibonacci(0);
    Assert.AreEqual(0,actual);
}

This is not a bad test, but we must copy and paste to test the other results. You may argue that we could create a test like this one:

[TestMethod]
public void GivenDataFibonacciReturnsResultsOk()
{
    var numbers = new[] { 0, 1, 2, 80 };
    var results = new[] { 0L, 1L, 1L, 23416728348467685L };
    var fib = new Fib();
    for (int i = 0; i &lt; numbers.Length; i++)
    {
        var actual = fib.Fibonacci(numbers[i]);
        Assert.AreEqual(results[i], actual);
    }
}

But this has some problems:

  • If a test fails, it’s difficult to know which number failed
  • If one number fails, the next ones are not tested
  • You don’t have a clear view of what is being tested

MS-Test has had for a long time Data Driven tests (https://msdn.microsoft.com/en-us/library/ms182527.aspx), but this is very cumbersome. You must create a data file, assign it to the test and run the test using the TestContext. It’s too much work for just four tests, no?

Then it comes MS-Test 2. With it, you can create a DataTestMethod, with DataRows for each test. Let’s see how do you create a test with this new feature.

Creating Parametrized tests with MS-Test 2

In Visual Studio, create a new Console Project. In this project, create a new class and name it Fib.cs. Add this code to the class:

 public class Fib
 {
     public int Fibonacci(int n)
     {
         int a = 0;
         int b = 1;
         // In N steps compute Fibonacci sequence iteratively.
         for (int i = 0; i &lt; n; i++)
         {
             int temp = a;
             a = b;
             b = temp + b;
         }
         return a;
     }
 }

Then, in the solution, add a new Class Library project. Right click the References node in the Solution Explorer and add a reference to the console project. Then right click in the References node again and select “Manage NuGet packages”. Add the packages MsTest.TestAdapter and MsTest.TestFramework.

image

With that, you have a test project with MS-Test 2. If you are using Visual Studio 2017, the Test Project template already includes these two packages, but you must update them to the latest version, as the parametrized tests didn’t run well with the default packages.

Then, we can create our test:

[TestClass]
public class FibonacciTests
{
    [DataRow(0, 0)]
    [DataRow(1, 1)]
    [DataRow(2, 1)]
    [DataRow(80, 23416728348467685)]
    [DataTestMethod]
    public void GivenDataFibonacciReturnsResultsOk(int number, Int64 result)
    {
        var fib = new Fib();
        var actual = fib.Fibonacci(number);
        Assert.AreEqual(result, actual);
    }
}

The test is very similar to the ones we are used to create, it just has some differences:

 

  • Instead of the TestMethod attribute, it is decorated with the DataTestMethod attribute
  • The method receives two parameters
  • Each test has a DataRow attribute associated to it.

 

 

If we run this test, we get these results:

image

As you can see, we have three tests that passed and one that failed. We didn’t take in account in our routine that the results could be very large and overflow. So, we must change the routine to take this in account:

public Int64 Fibonacci(int n)
{
    Int64 a = 0;
    Int64 b = 1;
    // In N steps compute Fibonacci sequence iteratively.
    for (int i = 0; i &lt; n; i++)
    {
        Int64 temp = a;
        a = b;
        b = temp + b;
    }
    return a;
}

Now, when you run the tests, you get this:

image

All tests are passing, and we can have a clear view of which tests were run, without the need of extra files or any other tricks. Cool, no? This was a very welcome addition to MS-Test and can improve a lot our testing.

The source code for this article is in https://github.com/bsonnino/Fibonacci

After using the program developed in the last post, I was thinking about some ways to optimize it. Then I went to the FileFinder class and saw this:

class FileFinder
{
    public async Task<ConcurrentDictionary<string, List>> GetFiles(string[] paths, 
        Regex excludeFilesRegex, Regex excludePathsRegex, bool incremental)
    {
        var files = new ConcurrentDictionary<string, List>();
        var tasks = paths.Select(path =>
            Task.Factory.StartNew(() =>
            {
                var rootDir = "";
                var drive = Path.GetPathRoot(path);
                if (!string.IsNullOrWhiteSpace(drive))
                {
                    rootDir = drive[0] + "_drive";
                    rootDir = rootDir + path.Substring(2);
                }
                else
                    rootDir = path;
                var selectedFiles = GetFilesInDirectory(path, excludeFilesRegex, excludePathsRegex, incremental);
                files.AddOrUpdate(rootDir, selectedFiles.ToList(), (a, b) => b);
            }));
        await Task.WhenAll(tasks);
        return files;
    }

    private List GetFilesInDirectory(string directory, Regex excludeFilesRegex, 
        Regex excludePathsRegex,bool incremental)
    {
        var files = new List();
        try
        {
            var directories = Directory.GetDirectories(directory);
            try
            {
                var selectedFiles = Directory.EnumerateFiles(directory).Where(f => !excludeFilesRegex.IsMatch(f.ToLower()));
                if (incremental)
                    selectedFiles = selectedFiles.Where(f => (File.GetAttributes(f) & FileAttributes.Archive) != 0);
                files.AddRange(selectedFiles);
            }
            catch
            {
            }
            foreach (var dir in directories.Where(d => !excludePathsRegex.IsMatch(d.ToLower())))
            {
                files.AddRange(GetFilesInDirectory(Path.Combine(directory, dir), excludeFilesRegex, excludePathsRegex, incremental));
            }
        }
        catch
        {
        }

        return files;
    }
}

I pass the filters to the GetFilesInDirectory method and do my filter there. That way, the folders I don’t want aren’t enumerated. For that, I had to make a change in the Config class, adding a new property for the path Regex and initializing it:

public class Config
{
    public Config(string fileName)
    {
        if (!File.Exists(fileName))
            return;
        var doc = XDocument.Load(fileName);
        if (doc.Root == null)
            return;
        IncludePaths = doc.Root.Element("IncludePaths")?.Value.Split(';');
        ExcludeFiles = doc.Root.Element("ExcludeFiles")?.Value.Split(';') ?? new string[0] ;
        ExcludePaths = doc.Root.Element("ExcludePaths")?.Value.Split(';') ?? new string[0];
        BackupFile = $"{doc.Root.Element("BackupFile")?.Value}{DateTime.Now:yyyyMMddhhmmss}.zip";
        ExcludeFilesRegex = new Regex(string.Join("|", ExcludeFiles));
        ExcludePathRegex = new Regex(string.Join("|", ExcludePaths));
    }

    public Regex ExcludeFilesRegex { get; }
    public Regex ExcludePathRegex { get; }
    public IEnumerable IncludePaths { get; }
    public IEnumerable ExcludeFiles { get; }
    public IEnumerable ExcludePaths { get; }
    public string BackupFile { get; }
}

With this changes, I could run the program again and measure the differences. That made a great difference. Before the change, the program was taking 160s to enumerate the files and give me 470000 files. After the change, enumerating the files took only 14.5s to give me the same files (I ran the programs three times each to avoid distortions). That’s a huge difference, no?

Then I started to think a little bit more and thought that the Regex could be compiled. So, I made two simple changes in the Config class:

ExcludeFilesRegex = new Regex(string.Join("|", ExcludeFiles),RegexOptions.Compiled);
ExcludePathRegex = new Regex(string.Join("|", ExcludePaths), RegexOptions.Compiled);

When I ran again the program, it took me 11.5s to enumerate the files. It doesn’ t seem much, but it’s a 25% improvement with just a simple change. That was really good. That way, I have a backup program that enumerates files way faster than before.

All the source code for the project is at https://github.com/bsonnino/BackupData