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

When you have a multi-monitor device, you usually want to write code in one monitor and debug the program in another one. This is especially helpful when you want to debug some visual interface that is rendered by the code (or debug the Paint event, for WinForms apps).

But the program you’re debugging insists to open in the same monitor you are writing code. If you try to find some setting in Visual Studio to set the monitor to open the program, you won’t find any. So, what can be done in this case?

I’ve found two options, the code one, and the Windows one. Let’s start with the code option:

Add some code in the closing of the main form to save the window position and in the constructor of the form to restore the window position. This code could be used as a feature for your program: that way, the user can reposition and resize the window and the next time he opens it, it will be in the same position. If you don’t want this feature for the released version, enclose the code in the conditional compiler directive #if DEBUG ..#endif. You can check something like that for WPF in this CodeProject article: https://www.codeproject.com/Articles/50761/Save-and-Restore-WPF-Window-Size-Position-and-or-S (things should be similar for WinForms). With this code, you can move the window to the other monitor and start debugging there. Just remember to close the app normally, to save the current position. The next time you will debug the program, the window it will be in the same place.

For UWP, saving the last window position is the default behavior, so you don’t have to do anything in the code: just move the window to the new position and close it normally and everything is set.

The Windows option is very simple, but not quite at sight: when you have a multi-monitor disposition, there is a checkbox in the display settings that says “Make this my main display”. All you have to do in this case is to right click the desktop of any monitor, select “ Display settings”, select the monitor you want to open your programs, check this box and voilà, all programs will open by default on the selected monitor. The only side effect in this case is that the search bar and the system tray will move to this monitor, but I think that this is minimal and does not affect my daily use. Easy, no?

image

That way, you can edit code in one screen and run the program in the other. So, happy debugging!

Os novos dispositivos 2-em-1 Windows 10 trouxeram uma novidade: você pode usar os seus diversos sensores para aumentar a experiência do usuário em seus programas.

O Delphi trouxe a possibilidade de usar estes sensores em seus programas, tanto usando a VCL como no FireMonkey. A minha palestra na CodeRage Brasil mostra como você pode criar seus programas Delphi que utilizam a classe TSensor para melhorar a experiência do usuário. Confira em  https://www.youtube.com/watch?v=U2uKyuKwJcY

After showing the desktop icons (see my last post), I right clicked the desktop and hid again the desktop icons. That way I’ve missed the context menu again (If you are running Build 10547 and hide the desktop icons, you lose the context menu).

But this time, Shift-F10 didn’t work for me. So, I decided to use another solution. I remember I’ve developed an utility for PCMag.com, Wintidy (http://www.pcmag.com/article2/0,2817,2372867,00.asp) which, among other things, shows or hides the desktop icons. I ran it and clicked on the “Show desktop icons” and voilà, the icons were back again!

Now I have a permanent solution, no more flaky hotkeys. I hope the Windows team solves this bug soon.

 

 

I am running Windows 10, build 10547, and I was having a strange problem: I had disabled my icons in the desktop and the right click wasn’t working in the desktop – it seems to be a known problem – if you have your icons hidden, right clicking on the desktop don’t show the context menu for the desktop. I didn’t know it at that time and, after doing the wrong searches and not finding anything useful, I thought it could be a Windows extension that might be causing this.

So, I decided to boot my computer in Safe Mode. I pressed Windows+R to open the Run window and typed msconfig to open the System Configuration. Then, I went to the Boot tab and selected Safe Boot with network:

MsConfig

I restarted my computer and then my saga began. The computer restarted fine, asked my password and entered the desktop, showing Safe Mode. But it was unusable – the wait cursor was spinning and I couldn’t do anything. I decided to wait. After one hour, nothing had happened, the cursor was still spinning.

I decided that the icons in my desktop didn’t worth the wait and decided to go back to normal mode. But how? With Ctrl-Alt-Del I could restart the computer and return to Safe mode again, but there was no way to return to normal mode. Pressing Shift while clicking on the “Restart” menu didn’t work – my computer was showing a message that there was no boot. I went to the boot options and selected the Uefi boot again and was back again to Safe mode (oh, no).

Then I thought – I have another computer with Windows 10, so I’ll make a recovery disk in a USB drive. It took more than one hour to make this recovery disk (I don’t know why did it take so long – Windows takes less than half hour to reinstall itself, but at that time I was becoming a little desperate). With the recovery disk, I booted my computer and it booted to recovery – good, at least I have hope. But there wasn’t any options to restore to normal mode. I tried to do a system restore, but it showed me a cryptic error message to select an operating system and nothing else.

I opened a command prompt and typed “bcdedit”, in the hope I could have some light – nothing, it just showed me an error message and no entries. I scanned all drives in the command prompt, but my C drive wasn’t there. My options were narrowing. Then, I thought I could create a bootable Windows disk in the USB drive and try to use it, if not to repair my Windows, at least to reinstall it (so you can see how desperate I was).

I downloaded the Windows ISO from the MSDN site (I have a subscription) and then downloaded the Windows USB tool from https://www.microsoft.com/en-us/download/windows-usb-dvd-download-tool and followed the instructions to create the USB. The ddownload of the ISO and creation of the bootable USB was way faster than creating the recovery drive.

I changed my computer boot to boot from the USB and there it was – the prompt to install. I went through the first screen and there was a prompt to Repair my computer:

Install

I clicked on it and now I tried a system restore again – It showed me the Windows 10 system and showed me a restore point. Now we’re running – I selected the restore point and it began to work and rebooted. When the system booted, I saw with despair that it was still on Safe Mode – it had done a system restore, but hadn’t changed the boot.

Back to the install drive again, I had now some hope – the repair in the drive was able to show me my system. So I opened the command prompt again and typed “bcdedit”. It showed me the entries, and the {default}  one was the one I was searching, it had the clause “safeboot Network” there. I had to delete it. Then, I typed:

bcdedit /deletevalue {default} safeboot

After seeing that the command was issued, I restarted the computer and there it was – normal mode again. After 4 hours of struggling with boots, recovery disks, and install usbs, I was back at where I had started – still no desktop icons, but happy again.

My conclusion? Safe mode in Windows 10 didn’t work for me. If it’s to be like that, it’s better to remove it. Recovery disks didn’t work either – slow to create and no use for it. The only thing that worked was the install disk – this one is precious – you should keep an USB drive with a copy of Windows ready to boot.

And did I solve my icons’ problem? Yes. After doing a good search (“show desktop icons windows 10 build 10547”), I saw that it was a bug in this build and saw the fix – press Shift-F10 to show the context menu. It worked!