Sometimes, we need to get our display disposition to position windows on them in specific places. The usual way to do it in .NET is to use the Screen class, with a code like this one:

internal record Rect(int X, int Y, int Width, int Height);
internal record Display(string DeviceName, Rect Bounds, Rect WorkingArea, double ScalingFactor);

private void InitializeDisplayCanvas()
{
    
    var minX = 0;
    var minY = 0;
    var maxX = 0;
    var maxY = 0;
    foreach(var screen in Screen.AllScreens)
    {
        if (minX > screen.WorkingArea.X)
            minX = screen.WorkingArea.X;
        if (minY > screen.WorkingArea.Y)
            minY = screen.WorkingArea.Y;
        if (maxX < screen.WorkingArea.X+screen.WorkingArea.Width)
            maxX = screen.WorkingArea.X+screen.WorkingArea.Width;
        if (maxY < screen.WorkingArea.Y+screen.WorkingArea.Height)
            maxY = screen.WorkingArea.Y+screen.WorkingArea.Height;

        _displays.Add(new Display(screen.DeviceName, screen.Bounds, screen.WorkingArea, 1.0));
    }
    DisplayCanvas.Width = maxX - minX;
    DisplayCanvas.Height = maxY - minY;
    DisplayCanvas.RenderTransform = new TranslateTransform(-minX, -minY);
    var background = new System.Windows.Shapes.Rectangle
    {
        Width = DisplayCanvas.Width,
        Height = DisplayCanvas.Height,
        Fill = new SolidColorBrush(System.Windows.Media.Color.FromArgb(1,242,242,242)),
    };
    Canvas.SetLeft(background, minX);
    Canvas.SetTop(background, minY);
    DisplayCanvas.Children.Add(background);
    var numDisplay = 0;
    foreach (var display in _displays)
    {
        numDisplay++;
        var border = new Border
        {
            Width = display.WorkingArea.Width,
            Height = display.WorkingArea.Height,
            Background = System.Windows.Media.Brushes.DarkGray,
            CornerRadius = new CornerRadius(30)
        };
        var text = new TextBlock
        {
            Text = numDisplay.ToString(),
            FontSize = 200,
            FontWeight = FontWeights.Bold,  
            HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center,
        };
        border.Child = text;
        Canvas.SetLeft(border, display.WorkingArea.Left);
        Canvas.SetTop(border, display.WorkingArea.Top);
        DisplayCanvas.Children.Add(border);
    }

}

If you run the code, you will see something like this:

The monitors aren’t contiguous, as you would expect. But, the worst is that it doesn’t work well. If you try to position a window in the center of the middle monitor, with a code like this:

private void Button_Click(object sender, RoutedEventArgs e)
{
    var display = _displays[0];
    var window = new NewWindow
    {
        Top = display.WorkingArea.Top + (display.WorkingArea.Height - 200) / 2,
        Left = display.WorkingArea.Left + (display.WorkingArea.Width - 200) / 2,
    };
    window.Show();
}

You will get something like this:

As you can see, the window is far from centered. And why is that? The reason for these problems are the usage of high DPI. When you set the displays in you system, you set the resolution and the scaling factor:

In my setup, I have three monitors:

  • 1920×1080 125%
  • 3840×2160 150%
  • 1920×1080 100%

This scale factor is not taken in account when you are enumerating the displays and, when I am enumerating them, I have no way of getting this value. That way, everything is positioned in the wrong place. It would work fine if all monitors had a scaling factor of 100%, but most of the time that’s not true.

Researching for High DPI WPF, I came to this page, which shows the use of the appmanifest, so I gave it a try. I added a new item, Application Manifest and uncommented these lines:

<application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
      <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
    </windowsSettings>
  </application>

And nothing happened. Then I added this line:

<windowsSettings>
  <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
  <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
  <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>

Running the project, I got the correct display settings:

But the secondary screen is positioned in the wrong place. If I change the dpiAwareness clause to Unaware, I get the wrong display disposition, but the window is positioned at the center!

We need to get the scale factor for each monitor, to get the correct values in both cases.

Before going further we must notice that this code has also another problem: it’s a WPF app that is using a Winforms class: Screen is declared in System.Windows.Forms and there is no equivalent in WPF. To use, it you must add UseWindowsForms in the csproj:

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>WinExe</OutputType>
		<TargetFramework>net6.0-windows</TargetFramework>
		<Nullable>enable</Nullable>
		<UseWPF>true</UseWPF>
		<UseWindowsForms>true</UseWindowsForms>
	</PropertyGroup>
</Project>

That is something I really don’t like to do: use Winforms in a WPF project. If you want to check the project, the branch I’ve used is here.

So, I tried to find a way to enumerate the displays in WPF and have the right scaling factor, and I found two ways: query the registry or use Win32 API. Yes, the API that is available since the beginning of Windows, and it’s still there.

We could go to http://pinvoke.net/ to get the signatures we need for our project. This is a great site and a nice resource when you want to use Win32 APIs in C#, but we’ll use another resource: CsWin32, a nice source generator that generates the P/Invokes for us. I have already written an article about it, if you didn’t see it, you should check it out.

For that, we should install the NuGet package Microsoft.Windows.CsWin32 (be sure to check the pre-release box). Once installed, you must create a text file and name it NativeMethods.txt. There, we will add the names of all the methods and structures we need.

The first function we need is EnumDisplayMonitors, which we add there. With that, we can use it in our method:

private unsafe void InitializeDisplayCanvas()
{
    Windows.Win32.PInvoke.EnumDisplayMonitors(null, null, enumProc, IntPtr.Zero);
}

We are passing all parameters as null, except for the third one, which is the callback function. As I don’t know the parameters of this function, I will let Visual Studio create it for me. Just press Ctrl+. and Generate method enumProc and Visual Studio will generate the method for us:

private unsafe BOOL enumProc(HMONITOR param0, HDC param1, RECT* param2, LPARAM param3)
{
    throw new NotImplementedException();
}

We can change the names of the parameters and add the return value true to continue the enumeration. This function will be called for each monitor in the system and will pass in the first parameter the handle of the monitor. With that handle we can determine its properties with GetMonitorInfo (which we add in NativeMethods.txt) and use it to get the monitor information. This function uses a MONITORINFO struct as a parameter, and we must declare it before calling the function:

private unsafe BOOL enumProc(HMONITOR monitor, HDC hdc, RECT* clipRect, LPARAM data)
{
    var mi = new MONITORINFO
    {
        cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFO))
    };
    if (Windows.Win32.PInvoke.GetMonitorInfo(monitor, ref mi))
    {

    }
    return true;
}

You have noticed that we’ve set the size of the structure before passing it to the function. This is common to many WinApi functions and forgetting to set this member or setting it with a wrong value is a common source of bugs.

MONITORINFO is declared in Windows.Win32.Graphics.Gdi, which is included in the usings for the file. Now, mi has the data for the monitor:

internal partial struct MONITORINFO
{
	internal uint cbSize;
	internal winmdroot.Foundation.RECT rcMonitor;
	internal winmdroot.Foundation.RECT rcWork;
	internal uint dwFlags;
}

But it doesn’t have the name of the monitor. This was solved by using the structure MONITORINFOEX, which has the name of the monitor. There is a catch, here: although GetMonitorInfo has an overload that uses the MONITORINFOEX structure, it’s not declared in CsWin32, so we must do a trick, here:

private unsafe BOOL enumProc(HMONITOR monitor, HDC hdc, RECT* clipRect, LPARAM data)
{
    var mi = new MONITORINFOEXW();
    mi.monitorInfo.cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFOEXW));
    
    if (Windows.Win32.PInvoke.GetMonitorInfo(monitor, (MONITORINFO*) &mi))
    {

    }
    return true;
}

MONITORINFOEX is not declared automatically, you must use the explicit wide version, MONITORINFOEXW and add the impport to NativeMethods.txt. To use it, you must create the structure, initialize it and then cast the pointer to a pointer of MONITORINFO. It’s not the most beautiful code, but it works. Now we have the code to enumerate the displays:

public class DisplayList
{
    private List<Display> _displays = new List<Display>();

    public DisplayList()
    {
        QueryDisplayDevices();
    }

    public List<Display> Displays => _displays;

    private unsafe void QueryDisplayDevices()
    {
        PInvoke.EnumDisplayMonitors(null, null, enumProc, IntPtr.Zero);
    }

    private unsafe BOOL enumProc(HMONITOR monitor, HDC hdc, RECT* clipRect, LPARAM data)
    {
        var mi = new MONITORINFOEXW();
        mi.monitorInfo.cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFOEXW));

        if (PInvoke.GetMonitorInfo(monitor, (MONITORINFO*)&mi))
        {
            var display = new Display(mi.szDevice.ToString(),
                new Rect(mi.monitorInfo.rcMonitor.left, mi.monitorInfo.rcMonitor.top, 
                    mi.monitorInfo.rcMonitor.Width, mi.monitorInfo.rcMonitor.Height),
                new Rect(mi.monitorInfo.rcWork.left, mi.monitorInfo.rcWork.top, 
                    mi.monitorInfo.rcWork.Width, mi.monitorInfo.rcWork.Height),
                1);
            _displays.Add(display);
        }
        return true;
    }
}

private void InitializeDisplayCanvas()
{
    var displayList = new DisplayList();
    _displays = displayList.Displays;
    InitializeCanvasWithDisplays();
}

private void InitializeCanvasWithDisplays()
{
    var minX = 0;
    var minY = 0;
    var maxX = 0;
    var maxY = 0;
    foreach (var display in _displays)
    {
        if (minX > display.WorkingArea.X)
            minX = display.WorkingArea.X;
        if (minY > display.WorkingArea.Y)
            minY = display.WorkingArea.Y;
        if (maxX < display.WorkingArea.X + display.WorkingArea.Width)
            maxX = display.WorkingArea.X + display.WorkingArea.Width;
        if (maxY < display.WorkingArea.Y + display.WorkingArea.Height)
            maxY = display.WorkingArea.Y + display.WorkingArea.Height;
    }
    DisplayCanvas.Width = maxX - minX;
    DisplayCanvas.Height = maxY - minY;
    DisplayCanvas.RenderTransform = new TranslateTransform(-minX, -minY);
    var background = new System.Windows.Shapes.Rectangle
    {
        Width = DisplayCanvas.Width,
        Height = DisplayCanvas.Height,
        Fill = new SolidColorBrush(System.Windows.Media.Color.FromArgb(1, 242, 242, 242)),
    };
    Canvas.SetLeft(background, minX);
    Canvas.SetTop(background, minY);
    DisplayCanvas.Children.Add(background);
    var numDisplay = 0;
    foreach (var display in _displays)
    {
        numDisplay++;
        var border = new Border
        {
            Width = display.WorkingArea.Width,
            Height = display.WorkingArea.Height,
            Background = System.Windows.Media.Brushes.DarkGray,
            CornerRadius = new CornerRadius(30)
        };
        var text = new TextBlock
        {
            Text = numDisplay.ToString(),
            FontSize = 200,
            FontWeight = FontWeights.Bold,
            HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center,
        };
        border.Child = text;
        Canvas.SetLeft(border, display.WorkingArea.X);
        Canvas.SetTop(border, display.WorkingArea.Y);
        DisplayCanvas.Children.Add(border);
    }
}
private void Button_Click(object sender, RoutedEventArgs e)
{
    var display = _displays[0];
    var window = new NewWindow
    {
        Top = display.Bounds.Y + (display.Bounds.Height - 200) / display.ScalingFactor / 2,
        Left = display.Bounds.X + (display.Bounds.Width - 200) / display.ScalingFactor / 2,
    };
    window.Show();
}

If you run this code, you’ll get the same result we’ve got with the previous version, but at least we don’t have to include WinForms here. But we can go a step further and get the scale for the monitors. For that, we must use the EnumDisplaySettings function, like this:

if (PInvoke.GetMonitorInfo(monitor, (MONITORINFO*)&mi))
{
    var dm = new DEVMODEW
    {
        dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODEW))
    };
    PInvoke.EnumDisplaySettings(mi.szDevice.ToString(), ENUM_DISPLAY_SETTINGS_MODE.ENUM_CURRENT_SETTINGS, ref dm);

    var scalingFactor = Math.Round((double)dm.dmPelsWidth / mi.monitorInfo.rcMonitor.Width, 2);
    var display = new Display(mi.szDevice.ToString(),
        new Rect(mi.monitorInfo.rcMonitor.left, mi.monitorInfo.rcMonitor.top, 
            (int)(mi.monitorInfo.rcMonitor.Width * scalingFactor), (int)(mi.monitorInfo.rcMonitor.Height * scalingFactor)),
        new Rect(mi.monitorInfo.rcWork.left, mi.monitorInfo.rcWork.top, 
            (int)(mi.monitorInfo.rcWork.Width * scalingFactor), (int)(mi.monitorInfo.rcWork.Height * scalingFactor)),
        scalingFactor);
    _displays.Add(display);
}

Now, if you run the code, you’ll see that it runs fine and positions the window in the center of the display.

We now have a WPF application that can detect all installed displays and position a window correctly in any display. The key to that is, besides using the Win32 API to get the display information, to use the App Manifest dpiAwareness setting to Unaware. If you change to any other setting, you will get the wrong positions, because the scaling factors will only be correct when the Unaware setting is used.

The full source code for this article is at https://github.com/bsonnino/EnumeratingDisplays

When you are using the MVVM pattern, at some time, you have to send data between ViewModels. For example, a detail ViewModel must tell the collection ViewModel that the current item should be deleted, or when you have a main view that opens details views and keeps track of them and must be acknowledged of any of them closing.
This is a very common pattern and usually is solved by coupling the two ViewModels.
That’s a solution, but not the best one, because coupling makes difficult maintenance and testing. For example, we’ll use the WPF project at https://github.com/bsonnino/MessengerToolkit/tree/Original.

This is a WPF project with a main window that shows a grid with all the customers:

When you click the button at the right, it opens a secondary window with the details:

This project is composed by two ViewModels, MainViewModel and CustomerViewModel. MainViewModel serves the main view and performs all actions for the buttons: Filter the data, Add a new customer and Save the data:

public partial class MainViewModel : ObservableObject
{
    private readonly ICustomerRepository _customerRepository;
    private readonly INavigationService _navigationService;
    
    public MainViewModel(ICustomerRepository customerRepository, INavigationService navigationService)
    {
        _customerRepository = customerRepository ?? 
                              throw new ArgumentNullException("customerRepository");
        Customers = new ObservableCollection<CustomerViewModel>(
            _customerRepository.Customers.Select(c => new CustomerViewModel(c)));
        _navigationService = navigationService;
    }

    [ObservableProperty]
    private ObservableCollection<CustomerViewModel> _customers;

    [ObservableProperty]
    private int _windowCount;

    [RelayCommand]
    private void Add()
    {
        var customer = new Customer();
        _customerRepository.Add(customer);
        var vm = new CustomerViewModel(customer);
        Customers.Add(vm);
        _navigationService.Navigate(vm);
    }

    [RelayCommand]
    private void Save()
    {
        _customerRepository.Commit();
    }

    [RelayCommand]
    private void Search(string textToSearch)
    {
        var coll = CollectionViewSource.GetDefaultView(Customers);
        if (!string.IsNullOrWhiteSpace(textToSearch))
            coll.Filter = c => ((CustomerViewModel)c).Country?.ToLower().Contains(textToSearch.ToLower()) == true;
        else
            coll.Filter = null;
    }

    [RelayCommand]
    private void ShowDetails(CustomerViewModel vm)
    {
        _navigationService.Navigate(vm);
        WindowCount++;
    }
}

It uses the features available in version 8 of the MVVM toolkit. In this code, we can see two things:

  • The ViewModel maintains the open windows count. When you click on the button to show the details, it will increase the window count. The problem here is to decrease the count once the window is closed. Right now, the count only increases 😃
  • To open the detail window, we could use something like:
private void ShowDetails(CustomerViewModel vm)
{
    var detailWindow = new Detail { DataContext = vm };
    detailWindow.Show();
    WindowCount++;
}

This approach has two flaws:

  • We are coupling the ViewModel to the Detail view
  • We are calling View details in the ViewModel

This makes this code to be completely untestable and defeats all purpose of the MVVM pattern. So, we must find another solution. What I devised is a NavigationService, that will open a Window, depending on the type of the ViewModel that is passed to the Navigate method. This service is very simple, and has only one method,Navigate:

public interface INavigationService
{
    void Navigate(object arg);
}

public class NavigationService : INavigationService
{
    private readonly Dictionary<Type, Type> viewMapping = new()
    {
        [typeof(MainViewModel)] = typeof(MainWindow),
        [typeof(CustomerViewModel)] = typeof(Detail),
    };

    public void Navigate(object arg)
    {
        Type vmType = arg.GetType();
        if (viewMapping.ContainsKey(vmType))
        {
            var windowType = viewMapping[vmType];
            var window = (System.Windows.Window)Activator.CreateInstance(windowType);
            window.DataContext = arg;
            window.Show();
        }
    }
}

We’ve defined an interface, INavigationService, and created a class that implements it, NavigationService. This class has a dictionary with all the ViewModel types that have a corresponding window type, with their corresponding window type as the value of the item.

When we call Navigate and pass the instance of the ViewModel, the method will check its type, verify if it exists in the dictionary and instantiate the window, setting the VM as its DataContext and opening the Window. That way, we decouple the ViewModel from the views. The navigation service is injected in the constructor of MainViewModel by the use of dependency injection.

In App.xaml.cs we are registering the services and the VM. That way, we get the right data injected to the main VM:

public IServiceProvider Services { get; }

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton<INavigationService, NavigationService>();
    services.AddSingleton<ICustomerRepository, CustomerRepository>();
    services.AddSingleton<MainViewModel>();

    return services.BuildServiceProvider();
}

One note is ShowDetails, which in fact is a command that should be sent to the CustomerViewModel, because it’s tied to every CustomerViewModel in the grid. What we did is to tie it to the ViewModel of the main view with:

<DataGridTemplateColumn>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <Button Command="{Binding DataContext.ShowDetailsCommand, 
                RelativeSource={RelativeSource AncestorType=Window}}" 
                    CommandParameter="{Binding}" ToolTip="Details">
                <TextBlock Text="" FontFamily="Segoe UI Symbol" />
            </Button>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

We set the command to ShowDetailsCommand in the datacontext of the parent window, and pass the current CustomerViewModel as a parameter. That suffices to do the trick.

Now, onto our problems:

  • The Window Closing event is just that – an event, not a command and there is no direct way to handle it in the ViewModel
  • Even if we can handle it in the ViewModel, there is no direct way to warn the main viewmodel about the change
  • When we click the Remove button in the Detail view, we cannot remove the item from the list, because the list is on the Main ViewModel, which is not accessible from the Customer ViewModel

For the first problem, we could handle the Closing event in the code behind of the Detail view:

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    var vm = DataContext as CustomerViewModel;
    vm.ClosingCommand.Execute(null);
}

or, better, send the closing signal directly to the main viewmodel:

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    var vm = App.Current.MainVM;
    vm.ClosingCommand.Execute(null);
}

This would work fine, but purists would say that it’s a code smell. So, let’s do it in the MVVM way. For that, we must install the Microsoft.Xaml.Behaviors.WPF NuGet package, that has some behaviors we will need in the detail window:

<Window x:Class="CustomerApp.Detail"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
        mc:Ignorable="d"
        Height="440"
        Width="520"
        Background="AliceBlue">
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="Closing">
            <b:InvokeCommandAction Command="{Binding ClosingCommand}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>

There, we have Interaction.Triggers, to add the triggers we want. We want to use the EventTrigger, that is triggered whe an event is fired. In our case, it’s the Closing event. When it’s fired, it will invoke the command ClosingCommand in the detail window. Problem solved. No more code behind.

To solve the second and third problem, we would have to couple our Detail ViewModel to the Main ViewModel and have a variable that stores its instance. One way would be to pass the VM as parameter:

private readonly MainViewModel _mainvm;

public CustomerViewModel(Customer customer, MainViewModel mainvm)
{
    _customer = customer;
    _mainvm = mainvm;
    CustomerId = _customer.CustomerId;
    CompanyName = _customer.CompanyName;
    ContactName = _customer.ContactName;
    ContactTitle = _customer.ContactTitle;
    Address = _customer.Address;
    City = _customer.City;
    Region = _customer.Region;
    PostalCode = _customer.PostalCode;
    Country = _customer.Country;
    Phone = _customer.Phone;
    Fax = _customer.Fax;
}

One other way would be to get the App property:

private readonly MainViewModel _mainvm = App.Current.MainVM;

or to use the Service Locator to get the instance:

private readonly MainViewModel _mainvm = (MainViewModel)App.Current.Services.GetService(typeof(MainViewModel));

No matter with way we are using it, we are coupling the two viewmodels and that will make testing difficult. Passing the VM as a parameter for the constructor will be the more testable way, the other two would get us in trouble: we should have an App created for the second option and have an App created and a Service Locator in place to get our instance. But wouldn’t it be better if we could get rid of the instance of Main ViewModel and do not have them coupled?

In fact there is: Messages. Messaging is a mechanism that decouples the sender and receiver of a message, there is not even need to have a receiver for the message, or there may be many receivers from it – the sender doesn’t care.

The messenger is based on the Mediator pattern, where the Messenger class acts as the mediator for the messages. The potential receivers subscribe for receiving some kind of messages and, when some sender sends a message, the messenger will send it to the subscribers that are willing to receive that kind of message.

In the MVVM Toolkit, you can define messengers that implement the IMessenger interface, but the class WeakReferenceMessenger exposes a Default property, which offers a thread safe implementation for this interface, which makes it easier to implement. This class has a Register method, which will register the client for some message type and will pass the callback function that will be called when a message of the desired type is received. If you wat toknow more about that, you can take a look at https://docs.microsoft.com/en-us/windows/communitytoolkit/mvvm/messenger.

For our needs, we have to define two types of different messages: a message for when a customer should be deleted and the other one, when the window is closed:

public class WindowClosedMessage: ValueChangedMessage<CustomerViewModel>
{
    public WindowClosedMessage(CustomerViewModel vm) : base(vm)
    {

    }
}

public class ViewModelDeletedMessage : ValueChangedMessage<CustomerViewModel>
{
    public ViewModelDeletedMessage(CustomerViewModel vm) : base(vm)
    {

    }
}

The messages inherit from ValueChangedMessage. This kind of messages are sent to the recipients and the execution goes on. If, on the other way, the Sender needs an answer from the receiver, it will send a message of the RequestMessage type. In this case, the sender will wait the response that the receiver will send using the Reply method of the message. In our case, both messages don’t need a reply, so we use the ValueChangedMessage type. We must create two different types of message, because we want different processing for each one.

Now, we must send the messages in CustomerViewModel, in the Closing and Delete commands:

[RelayCommand]
private void Delete()
{
    WeakReferenceMessenger.Default.Send(new ViewModelDeletedMessage(this));
}

[RelayCommand]
private void Closing()
{
    WeakReferenceMessenger.Default.Send(new WindowClosedMessage(this));
}

The processing for these messages is done in the MainViewModel class:

public MainViewModel(ICustomerRepository customerRepository, INavigationService navigationService)
{
    _customerRepository = customerRepository ?? 
                          throw new ArgumentNullException("customerRepository");
    Customers = new ObservableCollection<CustomerViewModel>(
        _customerRepository.Customers.Select(c => new CustomerViewModel(c)));
    _navigationService = navigationService;
    WeakReferenceMessenger.Default.Register<WindowClosedMessage>(this, (r, m) =>
    {
        WindowCount--;
    });
    WeakReferenceMessenger.Default.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        DeleteCustomer(m.Value);
    });
}

private void DeleteCustomer(CustomerViewModel vm)
{
    Customers.Remove(vm);
    var deletedCustomer = _customerRepository.Customers.FirstOrDefault(c => c.CustomerId == vm.CustomerId);
    if (deletedCustomer != null)
    {
        _customerRepository.Remove(deletedCustomer);
    }
}

Now, when you run the app, you will see that when you close the detail window, the window count decreases. We could even keep a list of open windows, with some code like this:

[ObservableProperty]
private ObservableCollection<CustomerViewModel> _openWindows = new ObservableCollection<CustomerViewModel>();

public MainViewModel(ICustomerRepository customerRepository, INavigationService navigationService)
{
    _customerRepository = customerRepository ?? 
                          throw new ArgumentNullException("customerRepository");
    Customers = new ObservableCollection<CustomerViewModel>(
        _customerRepository.Customers.Select(c => new CustomerViewModel(c)));
    _navigationService = navigationService;
    WeakReferenceMessenger.Default.Register<WindowClosedMessage>(this, (r, m) =>
    {
        WindowCount--;
        _openWindows.Remove(m.Value);
    });
    WeakReferenceMessenger.Default.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        DeleteCustomer(m.Value);
    });
}

....

[RelayCommand]
private void ShowDetails(CustomerViewModel vm)
{
    _navigationService.Navigate(vm);
    WindowCount++;
    _openWindows.Add(vm);
}

When you click the Delete button in the detail window, the corresponding item is deleted in the main list, but the window remains open, and it should not ne there anymore, as the corresponding customer does not exist anymore. We could use some code behind to close the window, but I’d prefer another approach: change the navigation service to allow closing the windows. For that, we should keep the lists of open windows and have another method, Close, to close the selected window:

public interface INavigationService
{
    void Navigate(object arg);
    void Close(object arg);
}

public class NavigationService : INavigationService
{
    private readonly Dictionary<Type, Type> viewMapping = new()
    {
        [typeof(MainViewModel)] = typeof(MainWindow),
        [typeof(CustomerViewModel)] = typeof(Detail),
    };

    private readonly Dictionary<object, List<System.Windows.Window>> _openWindows = new();

    public void Navigate(object arg)
    {
        Type vmType = arg.GetType();
        if (viewMapping.ContainsKey(vmType))
        {
            var windowType = viewMapping[vmType];
            var window = (System.Windows.Window)Activator.CreateInstance(windowType);
            window.DataContext = arg;
            if (!_openWindows.ContainsKey(arg))
            {
                _openWindows.Add(arg, new List<System.Windows.Window>());
            }
            _openWindows[arg].Add(window);
            window.Show();
        }
    }

    public void Close(object arg)
    {
        if (_openWindows.ContainsKey(arg))
        {
            foreach(var window in _openWindows[arg])
            {
                window.Close();
            }
        }
    }
}

In this case, we keep a list of windows open for each customer. If we try to open another window for the same customer, the corresponding window will be added to the corresponding list. When the close method is called, all windows for that customer will be closed at once. The DeleteCustomer in MainViewModel becomes

private void DeleteCustomer(CustomerViewModel vm)
{
    Customers.Remove(vm);
    var deletedCustomer = _customerRepository.Customers.FirstOrDefault(c => c.CustomerId == vm.CustomerId);
    if (deletedCustomer != null)
    {
        _customerRepository.Remove(deletedCustomer);
    }
    _navigationService.Close(vm);
}

And now everything works fine.

But, what about testing? We did all this to ease testing, let’s test the VMs. In the Test project, let’s create a class CustomerViewModelTests.cs and add some tests:

[TestClass]
public class CustomerViewModelTests
{
    [TestMethod]
    public void Constructor_NullCustomer_ShouldThrow()
    {
        Action act = () => new CustomerViewModel(null);

        act.Should().Throw<ArgumentNullException>()
            .Where(e => e.Message.Contains("customer"));
    }
}

If we run this test, we’ll see it doesn’t pass. We haven’t made a check for null customers, so we have to change that in the code:

public CustomerViewModel(Customer customer)
{
    if (customer == null)
        throw new ArgumentNullException("customer");
    _customer = customer;
    CustomerId = _customer.CustomerId;
    CompanyName = _customer.CompanyName;
    ContactName = _customer.ContactName;
    ContactTitle = _customer.ContactTitle;
    Address = _customer.Address;
    City = _customer.City;
    Region = _customer.Region;
    PostalCode = _customer.PostalCode;
    Country = _customer.Country;
    Phone = _customer.Phone;
    Fax = _customer.Fax;
}

The test now passes and we can continue. For the next test, I’d like to have a customer filled with data, and FakeItEasy (which we are using for our test mocks) fills the data with nulls and that’s not what we want. In this article I show how to use Bogus to create fake data. We can use AutoBogus.FakeItEasy to integrate it with FakeItEasy. Install the package AutoBogus.FakeItEasy and let’s create the second test

[TestMethod]
public void Constructor_ShouldSetFields()
{
    var customer = AutoFaker.Generate<Customer>();
    var customerVM = new CustomerViewModel(customer);
    customerVM.CustomerId.Should().Be(customer.CustomerId);
    customerVM.CompanyName.Should().Be(customer.CompanyName);
    customerVM.ContactName.Should().Be(customer.ContactName);
    customerVM.ContactTitle.Should().Be(customer.ContactTitle);
    customerVM.Address.Should().Be(customer.Address);
    customerVM.City.Should().Be(customer.City);
    customerVM.Region.Should().Be(customer.Region);
    customerVM.PostalCode.Should().Be(customer.PostalCode);
    customerVM.Country.Should().Be(customer.Country);
    customerVM.Phone.Should().Be(customer.Phone);
    customerVM.Fax.Should().Be(customer.Fax);
}

For the third test, we will be testing the Delete command, and we want to check if if sends the correct message. For that, we must install the _Community.Toolkit.MVVM in the test project. Then, we can create the tests for the two commands:

[TestMethod]
public void DeleteCommand_ShouldSendMessageWithVM()
{
    var customer = AutoFaker.Generate<Customer>();
    var customerVM = new CustomerViewModel(customer);
    object callbackResponse = null;
    var waitEvent = new AutoResetEvent(false);
    WeakReferenceMessenger.Default.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        callbackResponse = customerVM;
        waitEvent.Set();
    });
    customerVM.DeleteCommand.Execute(null);
    waitEvent.WaitOne(100);
    callbackResponse.Should().Be(customerVM);
}

[TestMethod]
public void CloseCommand_ShouldSendMessageWithVM()
{
    var customer = AutoFaker.Generate<Customer>();
    var customerVM = new CustomerViewModel(customer);
    object callbackResponse = null;
    var waitEvent = new AutoResetEvent(false);
    WeakReferenceMessenger.Default.Register<WindowClosedMessage>(this, (r, m) =>
    {
        callbackResponse = customerVM;
        waitEvent.Set();
    });
    customerVM.ClosingCommand.Execute(null);
    waitEvent.WaitOne(100);
    callbackResponse.Should().Be(customerVM);
}

These two tests have a particularity: the result is set in the callback, so we may not have it immediately, so we can have a flaky test if we don’t wait some time before testing the value – sometimes it may pass and sometimes not. For this issue, I’ve used an AutoResetEvent to be set when the callback is called and wait 100ms to see if it’s called. That makes the trick.

For the MainViewModel tests, we can do this:

[TestClass]
public class MainViewModelTests
{
    [TestMethod]
    public void Constructor_NullRepository_ShouldThrow()
    {
        Action act = () => new MainViewModel(null, null);

        act.Should().Throw<ArgumentNullException>()
            .Where(e => e.Message.Contains("customerRepository"));
    }

    [TestMethod]
    public void Constructor_Customers_ShouldHaveValue()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);

        vm.Customers.Count.Should().Be(customers.Count);
    }

    [TestMethod]
    public void AddCommand_ShouldAddInRepository()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.AddCommand.Execute(null);
        A.CallTo(() => repository.Add(A<Customer>._)).MustHaveHappened();
    }

    [TestMethod]
    public void AddCommand_ShouldAddInCollection()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.AddCommand.Execute(null);
        vm.Customers.Count.Should().Be(11);
    }

    [TestMethod]
    public void AddCommand_ShouldCallNavigate()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.AddCommand.Execute(null);
        A.CallTo(() => navigationService.Navigate(A<CustomerViewModel>.Ignored)).MustHaveHappened();
    }

    [TestMethod]
    public void SaveCommand_ShouldCommitInRepository()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var vm = new MainViewModel(repository, navigationService);
        vm.SaveCommand.Execute(null);
        A.CallTo(() => repository.Commit()).MustHaveHappened();
    }

    [TestMethod]
    public void SearchCommand_WithText_ShouldSetFilter()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.SearchCommand.Execute("text");
        var coll = CollectionViewSource.GetDefaultView(vm.Customers);
        coll.Filter.Should().NotBeNull();
    }

    [TestMethod]
    public void SearchCommand_WithoutText_ShouldSetFilter()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.SearchCommand.Execute("");
        var coll = CollectionViewSource.GetDefaultView(vm.Customers);
        coll.Filter.Should().BeNull();
    }

    [TestMethod]
    public void ShowDetailsCommand_ShouldCallNavigate()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        A.CallTo(() => navigationService.Navigate(customerVM)).MustHaveHappened();
    }

    [TestMethod]
    public void ShowDetailsCommand_ShouldIncrementWindowCount()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        vm.WindowCount.Should().Be(1);
    }

    [TestMethod]
    public void ShowDetailsCommand_ShouldAddToOpenWindows()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        vm.OpenWindows.Count.Should().Be(1);
        vm.OpenWindows[0].Should().Be(customerVM);
    }

    [TestMethod]
    public void CustomerCloseCommand_ShouldDecreaseWindowCount()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.ClosingCommand.Execute(null);
        vm.WindowCount.Should().Be(0);
    }

    [TestMethod]
    public void CustomerCloseCommand_ShouldRemoveFromOpenWindows()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.ClosingCommand.Execute(null);
        vm.OpenWindows.Count.Should().Be(0);
    }

    [TestMethod]
    public void CustomerDeleteCommand_ShouldCallNavigationClose()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.DeleteCommand.Execute(null);
        A.CallTo(() => navigationService.Close(customerVM)).MustHaveHappened();
    }

    [TestMethod]
    public void CustomerDeleteCommand_ShouldRemoveCustomer()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.DeleteCommand.Execute(null);
        vm.Customers.Count.Should().Be(9);
        vm.Customers.Should().NotContain(customerVM);
    }

    [TestMethod]
    public void CustomerDeleteCommand_ShouldCallRemoveFromRepository()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.DeleteCommand.Execute(null);
        A.CallTo(() => repository.Remove(A<Customer>.Ignored)).MustHaveHappened();
    }
}

If you run the tests, all will pass.

As you can see, we have decoupled the two ViewModels with the Messenger implemented in the MVVM Toolkit, our code is completely testable and it runs fine. You can check this code here.

We can still go one step further (yes, we can always do that 😃): inject the IMessenger in the constructor of the ViewModels.

In App.xaml.cs we add the registration for IMessenger:

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton<INavigationService, NavigationService>();
    services.AddSingleton<ICustomerRepository, CustomerRepository>();
    services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
    services.AddSingleton<MainViewModel>();

    return services.BuildServiceProvider();
}

And inject it in the constructor of MainViewModel and CustomerViewModel:

public MainViewModel(ICustomerRepository customerRepository, 
    INavigationService navigationService,
    IMessenger messenger)
{
    _customerRepository = customerRepository ?? 
                          throw new ArgumentNullException("customerRepository");
    _navigationService = navigationService ?? 
        throw new ArgumentNullException("navigationService"); 
    _messenger = messenger ??
        throw new ArgumentNullException("messenger");
    Customers = new ObservableCollection<CustomerViewModel>(
        _customerRepository.Customers.Select(c => new CustomerViewModel(c, messenger)));
    messenger.Register<WindowClosedMessage>(this, (r, m) =>
    {
        WindowCount--;
        _openWindows.Remove(m.Value);
    });
    messenger.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        DeleteCustomer(m.Value);
    });
}
private readonly IMessenger _messenger;

public CustomerViewModel(Customer customer, IMessenger messenger)
{
    if (customer == null)
        throw new ArgumentNullException("customer");
    if (messenger == null)
        throw new ArgumentNullException("messenger");
    _messenger = messenger;
    _customer = customer;
    CustomerId = _customer.CustomerId;
    CompanyName = _customer.CompanyName;
    ContactName = _customer.ContactName;
    ContactTitle = _customer.ContactTitle;
    Address = _customer.Address;
    City = _customer.City;
    Region = _customer.Region;
    PostalCode = _customer.PostalCode;
    Country = _customer.Country;
    Phone = _customer.Phone;
    Fax = _customer.Fax;
}

We have decoupled the ViewModels from the default messenger. There are some changes to do in the code and in the tests, but you can check them in the final project, here. The code now is decoupled, testable and works fine 😃

All the source code for this article is at https://github.com/bsonnino/MessengerToolkit

I was working with the WPF project, converted to UWP in this article, in order to check usage of the new MVVM toolkit, explained in my last article and use the bindings with x:Bind, that are not supported in WPF (in fact, they are, but not natively. You can add the CompiledBindings.WPF package to your WPF project to have them).
I converted it, changed the Views to use the x:Bind bindings and ended up with this project.
There are some notes, here with the conversion:

  • This is an UWP project and, by default, uses C# 7. I wanted to use C# 9 t get the source generator goodies in the MVVM Toolkit. So I upgraded the language version using the LangVersion tag in the project. To do this, open the csproj file (you must unload the project and edit the file) and add this PropertyGroup:
<PropertyGroup>
  <LangVersion>9.0</LangVersion>
</PropertyGroup>
  • I removed the details view, by adding a ContentControl with a template with the Customer details:
<ContentControl Grid.Row="2" Content="{x:Bind master.SelectedItem, Mode=OneWay}" 
                ContentTemplate="{StaticResource DetailTemplate}" Margin="5" />
<Page.Resources>
    <c:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
    <DataTemplate x:Key="DetailTemplate" x:DataType="customerlib:Customer">
        <Grid HorizontalAlignment="Stretch">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="600" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <TextBlock Text="Customer Id:" Grid.Column="0" Grid.Row="0" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="0" Margin="3" Name="customerIdTextBox" 
                     Text="{x:Bind CustomerId, Mode=TwoWay}" VerticalAlignment="Center" />
            <FontIcon Grid.Column="2" FontFamily="Segoe MDL2 Assets" Glyph="" 
                      Margin="20,0" Visibility="{x:Bind IsVip, Converter={StaticResource BooleanToVisibilityConverter}}" />
            <TextBlock Text="Company Name:" Grid.Column="0" Grid.Row="1" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="1" Margin="3" Name="companyNameTextBox" 
                     Text="{x:Bind CompanyName, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Contact Name:" Grid.Column="0" Grid.Row="2" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="2" Margin="3" Name="contactNameTextBox" 
                     Text="{x:Bind ContactName, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Contact Title:" Grid.Column="0" Grid.Row="3" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="3" Margin="3" Name="contactTitleTextBox" 
                     Text="{x:Bind ContactTitle, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Address:" Grid.Column="0" Grid.Row="4" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="4" Margin="3" Name="addressTextBox" 
                     Text="{x:Bind Address, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="City:" Grid.Column="0" Grid.Row="5" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="5" Margin="3" Name="cityTextBox" 
                     Text="{x:Bind City, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Postal Code:" Grid.Column="0" Grid.Row="6" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="6" Margin="3" Name="postalCodeTextBox" 
                     Text="{x:Bind PostalCode, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Region:" Grid.Column="0" Grid.Row="7" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="7" Margin="3" Name="regionTextBox" 
                     Text="{x:Bind Region, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Country:" Grid.Column="0" Grid.Row="8" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="8" Margin="3" Name="countryTextBox" 
                     Text="{x:Bind Country, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Phone:" Grid.Column="0" Grid.Row="9" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="9" Margin="3" Name="phoneTextBox" 
                     Text="{x:Bind Phone, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Fax:" Grid.Column="0" Grid.Row="10" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="10" Margin="3" Name="faxTextBox" 
                     Text="{x:Bind Fax, Mode=TwoWay}" VerticalAlignment="Center" />
        </Grid>
    </DataTemplate>
</Page.Resources>
  • The bindings for the DataGrid columns couldn’t be replaced by x:Bind. I could do it by replacing the DataGridTextColumn with DataGridTemplateColumn like in:
<controls:DataGridTemplateColumn Header="Customer ID">
    <controls:DataGridTemplateColumn.CellTemplate>
        <DataTemplate x:DataType="customerlib:Customer">
            <TextBlock Text="{x:Bind CustomerId, Mode=TwoWay}" />
        </DataTemplate>
    </controls:DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumn> 

But I thought it didn’t worth the extra code and I left it with the bindings. If you find some way to replace the Bindings in the DataGridTextColumn for x:Bind, please leave it in the comments.

You would ask, why change the Binding for x:Bind. There are some good reasons for that:

  • x:Bind is resolved at compile time, while Binding is resolved at runtime, so it should make your code run faster
  • By being resolved at compile time, you know in advance if some binding works or not, while with Binding, you will have something that doesn’t work and you don’t know why
  • You can bind to functions in your ViewModel, thus removing the need of some converters (but not all of them)

On the other side, you have to determine explicitely the data you’re binding with x:Bind:

  • The data templates must have a DataType
  • You cannot use the DataContext property of the View, as it’s an Object and, as so, doesn’t have the properties to bind
  • You must bind to properties/fields in the View. That’s why I created this code in Mainpage.xaml.cs:
private MainViewModel _mainVm;
public MainPage()
{
    this.InitializeComponent();
    _mainVm = App.Current.MainVM;
}
  • You must declare explicitely which property/field you are binding, like in
ItemsSource="{x:Bind _mainVm.Customers}"

With that, we can run the project and this is what you get:

It’s an empty window, the customer data is not there. I tried to figure out what was happening and I set a breakpoint in CustomerRepository.GetCustomersAsync:

Everything is good, there are 91 customers, but none of them is shown in the Datagrid. Why is that? After checking and rechecking the data and the bindings, I remembered one difference between Binding and x:Bind. While Binding uses the OneWay most of the time, x:Bind uses OneTime.

It seems to be a small difference, but it bytes you when you have to deliver some code 😦. OneWay is a binding from the source (the class) to the destination (the view), but every time the class changes its value and raises the PropertyChanged event, the view reflects the change. OneTime is there for performance reasons: it’s set and forget. When the view is first rendered, it will check the binding, set the value and forget. The changes on the class won’t affect the view.

As we are setting the customer’s list in an asynchronous way, when the view is rendered, there is no data available, so it doesn’t show anything. When the data is set, all changes aren’t propagated to the view and nothing is shown. Fortunately, this is a simple change:

<controls:DataGrid AutoGenerateColumns="False" x:Name="master" Grid.Row="1" 
                   ItemsSource="{x:Bind _mainVm.Customers, Mode=OneWay}" SelectedItem="{x:Bind _mainVm.SelectedCustomer, Mode=TwoWay}">

Now, our data shows fine, but when we select a customer, we get this:

Wait a minute, where are our bindings? We just got the first one and nothing else? What is happening here? Is x:Bind broken? Let’s see. I started to change my x:Bind to Binding and see the effect:

<TextBlock Text="Company Name:" Grid.Column="0" Grid.Row="1" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="1" Margin="3" Name="companyNameTextBox" 
         Text="{Binding CompanyName, Mode=TwoWay}" VerticalAlignment="Center" />
<TextBlock Text="Contact Name:" Grid.Column="0" Grid.Row="2" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="2" Margin="3" Name="contactNameTextBox" 
         Text="{Binding ContactName, Mode=TwoWay}" VerticalAlignment="Center" />


I got these bindings working, so x:Bind must be broken, I thought. So, my idea of using x:Bind was broken and I started to change the x:Bind for Binding. That worked fine until I changed this one:

<FontIcon Grid.Column="2" FontFamily="Segoe MDL2 Assets" Glyph="" 
          Margin="20,0" Visibility="{Binding IsVip, Converter={StaticResource BooleanToVisibilityConverter}}" />

When I changed it, I got this runtime error

Wait a minute – Cannot find a Resource with the Name/Key BooleanToVisibilityConverter ? I’m sure I’ve declared it in the Resources section:

<c:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />

Oops – my bad! It’s BoolToVisibilityConverter, not BooleanToVisibilityConverter. I corrected the error and all the bindings started to work with x:Bind:

So, in some way, x:Bind has a bug, but not in the way I expected: instead of pointing the faulty converter with red squiggles, it will crash the bindings that come after it and won’t work anymore. That’s why, it was showing just the customer Id, the star for the IsVip property and nothing else. And it was showing the error at runtime, but I didn’t see it. In the Output window, in the middle of many other messages, a two line message indicated the error:

I think that, being checked as compile time, the converters should also be checked at compile time and not throw an exception in the output window at runtime and blow the rest of the bindings. But that’s just my point of view. Now, everything works and I will make sure that my converters are well defined 😃.

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

Sometime ago, I’ve written this article introducing the MVVM Community Toolkit and developing a CRUD application to show how to use the MVVM pattern with the Community toolkit.

The time has passed and version 8.0 of the MVVM Community toolkit has been released and, with it, a rewrite using incremental generators. This may seem a minor update, but it’s a huge move, as the MVVM pattern is full of boilerplate: implementing the INotifyPropertyChanged interface for the viewmodels, binding commands that implement the ICommand interface, using the RelayCommand class and implementing observable properties that raise the PropertyChanged event when changed. All that make the code cumbersome and repetitive, but that’s what we had to do to implement the MVVM pattern in our apps. Until now.

With the use of source generators, the toolkit removes a lot of the boilerplate and makes the code easier to create and read. In this article, we’ll take the project that we developed in the previous article and will change it to use the new toolkit.

You can clone the code in https://github.com/bsonnino/MvvmApp and open the CustomerApp – Mvvm app in Visual Studio 2022. As the original project is targeted to .NET 5.0, we’ll upgrade it to .NET 6.0. This is an easy task: just open CustomerApp.csproj and change the TargetFramework to .net6.0-windows:

<PropertyGroup>
	<OutputType>WinExe</OutputType>
	<TargetFramework>net6.0-windows</TargetFramework>
	<UseWPF>true</UseWPF>
	<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>

Do the same with CustomerApp.Tests.csproj. There is no need to do it on the CustomerLib project, as it’s a Net Standard project.

The next step is to update the NuGet packages. The package Microsoft.Extensions.DependencyInjection must be upgraded to version 6.0.0. If you try to upgrade the Microsoft.Toolkit.Mvvm package, you’ll see that there is no upgrade to version 8.0. That’s because the package name has changed and you must uninstall this one and install the CommunityToolkit.Mvvm package.

Now, the project is ready to build and, when you do that, you’ll see that it doesn’t work 😦. This is because the namespaces have changed and we need to update the using clauses in MainViewModel.cs. We have to remove the old using clauses and replace with the new ones:

using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

Once you do that and recompile the project, you’ll see that it compiles fine and runs in the same way the original project did. Not bad for a project upgraded from .NET 5 to .NET 6, with a new version of the MVVM framework.

But did I say that with the new toolkit you can remove the boilerplate? We can start doing that now. The first thing is to remove the properties and their getters and setters. We decorate the _selectedCustomer field with the [ObservableProperty] attribute:

[ObservableProperty]
private Customer _selectedCustomer;

When you do that, you’ll see that the class name has a red underline:

That’s because when we add the attribute, the toolkit generates a partial class, and we need to add the partial keyword to the class:

public partial class MainViewModel : ObservableObject

When we add that, SelectedCustomer is underlined:

That’s because we have declared the property in our code and the toolkit has also declared the same property in the generated code. We can now remove the property declaration from our code:

public Customer SelectedCustomer
{
    get => _selectedCustomer;
    set
    {
        SetProperty(ref _selectedCustomer, value);
        RemoveCommand.NotifyCanExecuteChanged();
    }
}

As you can see from the code we are removing, the setter notifies the RemoveCommand. To have the same effect, we add the NotifyCanExecuteChangedFor attribute to the field:

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RemoveCommand))]
private Customer _selectedCustomer;

If you compile the code, you will see that it runs the same way it did before, and we are still using the property name (SelectedCommand) in the commands, even if it’s not explicitly defined in the code. That’s because the toolkit is generating the property in its partial part of the class.

The next steps are to remove the boilerplate from the commands in the code. For that, we must remove all declarations and leave only the command methods, changing their name to the command name (without Command at the end) and adding the [RelayCommand] attribute for the method. For the Add command, we have to change this code:

public IRelayCommand AddCommand { get; }

private void DoAdd()
{
    var customer = new Customer();
    _customerRepository.Add(customer);
    SelectedCustomer = customer;
    OnPropertyChanged("Customers");
}

To this code:

[RelayCommand]
private void Add()
{
    var customer = new Customer();
    _customerRepository.Add(customer);
    SelectedCustomer = customer;
    OnPropertyChanged("Customers");
}

We also have to remove the initialization code:

AddCommand = new RelayCommand(DoAdd);
RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
SaveCommand = new RelayCommand(DoSave);
SearchCommand = new RelayCommand<string>(DoSearch);

If you notice the removed code, you’ll see that the RemoveCommand has a CanExecute method. To solve that, we have to have to add a parameter to RelayCommand:

[RelayCommand(CanExecute = "HasSelectedCustomer")]
private void Remove()
{

The parameter points to HasSelectedCustomer, a method that should be defined in the code:

private bool HasSelectedCustomer() => SelectedCustomer != null;

With that, we have completed our code and now the project runs in the same way it did before. The code is simpler and with no boilerplate:

public partial class MainViewModel : ObservableObject
{
    private readonly ICustomerRepository _customerRepository;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(RemoveCommand))]
    private Customer _selectedCustomer;

    public MainViewModel(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository ??
                              throw new ArgumentNullException("customerRepository");
    }

    public IEnumerable<Customer> Customers => _customerRepository.Customers;

    [RelayCommand]
    private void Add()
    {
        var customer = new Customer();
        _customerRepository.Add(customer);
        SelectedCustomer = customer;
        OnPropertyChanged("Customers");
    }

    [RelayCommand(CanExecute = "HasSelectedCustomer")]
    private void Remove()
    {
        if (SelectedCustomer != null)
        {
            _customerRepository.Remove(SelectedCustomer);
            SelectedCustomer = null;
            OnPropertyChanged("Customers");
        }
    }

    private bool HasSelectedCustomer() => SelectedCustomer != null;

    [RelayCommand]
    private void Save()
    {
        _customerRepository.Commit();
    }

    [RelayCommand]
    private void Search(string textToSearch)
    {
        var coll = CollectionViewSource.GetDefaultView(Customers);
        if (!string.IsNullOrWhiteSpace(textToSearch))
            coll.Filter = c => ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower());
        else
            coll.Filter = null;
    }
}

As you can see, this new version brought a huge improvement. We can use the MVVM pattern with no issues, there is no extra code related to the pattern (except for the attributes) and the code is easier to read and follow. And all tests still run, with no change at all.

The full source code for this article is at https://github.com/bsonnino/MVVMToolkit8

One thing that bothers me when debugging an app in Visual Studio is to get the values of an IEnumerable. For example, when you have a code like this one:

var people = new List<Person>();
for (int i = 1; i <= 100; i++)
{
    people.Add(new Person { Name = $"Name {i}", Address = $"Address {i}" });
}
Console.WriteLine(people);

class Person
{
    public string? Name { get; init; }
    public string? Address { get; init; }
}

If you run it and put a breakpoint in the Console.WriteLine(people), you will get something like this:

This is less than optimal, you have to drill down the list to get the values. If you are searching for a specific value, things get worse. Open each value and take a look if it’s the right one can be a nightmare.

You can improve this experience a lot by using a tool like OzCode (https://oz-code.com/) – take a look at my article at https://blogs.msmvps.com/bsonnino/2016/12/02/debugging-with-ozcode/, but this tool is not free.

One other way to improve the experience is to override the ToString method and show something more useful:

var people = new List<Person>();
for (int i = 1; i <= 100; i++)
{
    people.Add(new Person { Name = $"Name {i}", Address = $"Address {i}" });
}
Console.WriteLine(people);

class Person
{
    public string? Name { get; init; }
    public string? Address { get; init; }
    public override string ToString()
    {
        return $"Name: {Name} Address: {Address}";
    }
}

You can even use the new Records, introduced in C# 9, that implement internally the ToString method:

var people = new List<Person>();
for (int i = 1; i <= 100; i++)
{
    people.Add(new Person($"Name {i}", $"Address {i}"));
}
Console.WriteLine(people);

record Person(string Name, string Address);

But filtering, searching and scrolling through long lists is still cumbersome.

In the Preview Version of Visual Studio 2022, Microsoft introduced a new feature that will improve the debugging experience a lot: the IEnumerable Visualizer.

With this tool (it’s still only in the Preview version, it should came to the production version in one of the next updates), you can get an improved experience for debugging IEnumerables. You can click on the View button in the tooltip and Visual Studio will open a new window with the data:

You can sort the data by clicking on the columns titles and, if you want to work with the data, you can click on the Export to CSV/Export to Excel button and open the results in Excel, so you can work with them. Export to CSV will save the data to a CSV file and Export to Excel will open Excel with the data in a new workbook. Pretty cool, no?

While this is not a full fledged visualizer, it’s still in preview and it’s a work in progress, with room for lots of improvements, like searching and filtering data. But it’s a cool start and a welcome improvement.

Nowadays, a very common scenario is that you have your WPF app ready and running for some time, and your boss tells you that it’s time to go further and port this app to the web, to have a larger market and be run in multiple platforms.

This is not a simple thing, because WPF is not multi-platform and, although it was ported to .NET Core, it’s still a Windows Platform.

You can think of rewriting the whole app, but even if you decide to use Asp.Net Core and Razor pages, or Blazor, you will have a huge effort to do it, because writing the UI in these platforms is completely different than using XAML.

Xamarin is a viable alternative, it’s close to WPF and it uses XAML, but it’s not a web platform, you can only write native apps for iOs, Android or Mac.

But things are not lost, at all. The guys at Uno Platform (https://www.platform.uno) created a nice project that uses WebAssembly to run the code you’ve created on the browser.

And what is WebAssembly? WebAssembly is a technology that allow browsers to run non-javascript code. When people hear this, they usually think of browsers plugins, like Silverlight or Adobe Flash. In fact, these are completely different. While the plugins were apps created to run in the browser, they were installed and supported by the vendors, and could suffer all sort of security vulnerabilities and were, at some point, abandoned by the browsers.

WebAssembly, on the other side, is an open standard fully supported by the browsers and a web standard. You can create code in may languages and compile it into a wasm module, thus running your C++/Rust code in a browser. This link shows a list of languages that are used in WebAssembly.

Uno Platform uses WebAssembly to run UWP code in the web. So, if you have an UWP program, you can compile it with Uno and run it on the web, with minimal changes. That’s great when you want to port your UWP app to the web.

You may have noticed that in the previous paragraph I didn’t mention WPF, but only UWP/WinUI. Uno Platform works with UWP projects, and not WPF. But UWP/WinUI is still XAML and has many similarities with WPF. So, while it’s not a direct port, it’s not a complete rewrite either. Let’s see what must be done to port a WPF program to the web using Uno Platform.

For this article, I will be using the WPF project that shows how to use MVVM with the MVVM Toolkit, which I’ve shown in this article. The source code for this article is here.

Installing UNO Platform

To install the UNO Platform in Visual Studio, you need to have the UWP, Xamarin and Asp.NET workloads installed. You can open the Visual Studio installer and verify if the three workloads are installed:

One other prerequisite is to have .NET 5 SDK or later installed. If you haven’t done so, you can install .NET 5 from https://dotnet.microsoft.com/download/dotnet-core/5.0 or .NET 6 from https://dotnet.microsoft.com/en-us/download/dotnet/6.0.

Once they are installed, in Visual Studio, go to Extensions > Manage Extensions and install Uno Platform Templates.

With the extension installed, you can create a new solution using Uno:

We will create a new Multi-Platform App (Uno Platform|.net 6).

When you create the app, you will see multiple projects there:

As you can see, there are projects for Mobile (Android, iOS and Mac, GTK, WPF, UWP and Wasm). We will be using the Wasm project, so you should select it as the startup project. When you run it, it will open a browser window with the app:

It doesn’t seem too much, but when you think that the underlying code is XAML, that’s pretty nice!

Converting the WPF Project

As you can see in the Solution Explorer, there is a project .Shared, that contains the shared resources for our project. Putting your pages here will share the pages for all projects in the solution.

Before we port the solution, we should port the customer lib, that contains the classes and the repository. In fact this is an easy port, we don’t have to do anything, as it’s already a .NET Standard library.

Just add the files for the project in the same folder of the other projects and add the new project to the solution.

After that, you can add the project as a reference to the other projects you are creating.

Then, you should create the ViewModel for the project. Before that, we will have to add the package Microsoft.Toolkit.Mvvm to all the projects you are using. In the Solution Explorer, right-click the dependencies node in the Wasm project and select Manage Nuget Packages and add the Microsoft.Toolkit.Mvvm package. You must also add the reference to the other projects.

Then, create a folder named ViewModel in the Shared project and add the file MainViewModel.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using CustomerLib;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;

namespace MVVMUnoApp.ViewModel
{
    public class MainViewModel : ObservableObject
    {
        private readonly ICustomerRepository _customerRepository;
        private readonly IEnumerable<Customer> _allCustomers;
        private Customer _selectedCustomer;

        public MainViewModel(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository ??
                                  throw new ArgumentNullException("customerRepository");
            _allCustomers = _customerRepository.Customers;
            AddCommand = new RelayCommand(DoAdd);
            RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
            SaveCommand = new RelayCommand(DoSave);
            SearchCommand = new RelayCommand<string>(DoSearch);
        }

        public IEnumerable<Customer> Customers {get; private set;}

        public Customer SelectedCustomer
        {
            get => _selectedCustomer;
            set
            {
                SetProperty(ref _selectedCustomer, value);
                RemoveCommand.NotifyCanExecuteChanged();
            }
        }

        public IRelayCommand AddCommand { get; }
        public IRelayCommand RemoveCommand { get; }
        public IRelayCommand SaveCommand { get; }
        public IRelayCommand<string> SearchCommand { get; }

        private void DoAdd()
        {
            var customer = new Customer();
            _customerRepository.Add(customer);
            SelectedCustomer = customer;
            OnPropertyChanged("Customers");
        }

        private void DoRemove()
        {
            if (SelectedCustomer != null)
            {
                _customerRepository.Remove(SelectedCustomer);
                SelectedCustomer = null;
                OnPropertyChanged("Customers");
            }
        }

        private void DoSave()
        {
            _customerRepository.Commit();
        }

        private void DoSearch(string textToSearch)
        {
            if (!string.IsNullOrWhiteSpace(textToSearch))
                Customers = _allCustomers.Where(c => ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower()));
            else
                Customers = _allCustomers;
            OnPropertyChanged("Customers");
        }
    }
}

This file is almost the same as the one in the original project, with a change in DoSearch due to the fact that the CollectionViewSource works in a different way in UWP/WinUI than in WPF. For that, we removed the code that filters CollectionViewSource:

private void DoSearch(string textToSearch)
{
    var coll = CollectionViewSource.GetDefaultView(Customers);
    if (!string.IsNullOrWhiteSpace(textToSearch))
        coll.Filter = c =>
            ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower());
    else
        coll.Filter = null;
}

And did an explicit filter on the customer list returned by the repository.

The next step is to add the views to the Shared project. The main view uses a DataGrid that doesn’t exist in UWP/WinUI, but there is a solution, here: add the DataGrid of the Windows Control Toolkit, that works fine in UWP/WinUI. For that, add the NuGet reference to Uno.Microsoft.Toolkit.Uwp.UI.Controls.Datagrid to all the used projects. In the UWP project, you should not add this reference, but add Microsoft.Toolkit.Uwp.UI.Controls.Datagrid.

Then, change the code in MainPage.xaml in the Shared project to:

<Page
    x:Class="MVVMUnoApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
    xmlns:local="using:MVVMUnoApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"    
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="40" />
            <RowDefinition Height="*" />
            <RowDefinition Height="2*" />
            <RowDefinition Height="50" />
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Country" VerticalAlignment="Center" Margin="5"/>
            <TextBox x:Name="searchText" VerticalAlignment="Center" Margin="5,3" Width="250" VerticalContentAlignment="Center"/>
            <Button x:Name="PesqBtn" Content="Find" Width="75" Margin="10,5" VerticalAlignment="Center" 
                    Command="{Binding SearchCommand}" CommandParameter="{Binding ElementName=searchText,Path=Text}"/>
        </StackPanel>
        <controls:DataGrid AutoGenerateColumns="False" x:Name="master" Grid.Row="1" 
                  ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}">
            <controls:DataGrid.Columns>
                <controls:DataGridTextColumn x:Name="customerIDColumn" Binding="{Binding CustomerId}" Header="Customer ID" />
                <controls:DataGridTextColumn x:Name="companyNameColumn" Binding="{Binding CompanyName}" Header="Company Name" Width="160" />
                <controls:DataGridTextColumn x:Name="contactNameColumn" Binding="{Binding ContactName}" Header="Contact Name" Width="160" />
                <controls:DataGridTextColumn x:Name="contactTitleColumn" Binding="{Binding ContactTitle, Mode=TwoWay}" Header="Contact Title"  />
                <controls:DataGridTextColumn x:Name="addressColumn" Binding="{Binding Address}" Header="Address" Width="130" />
                <controls:DataGridTextColumn x:Name="cityColumn" Binding="{Binding City}" Header="City" />
                <controls:DataGridTextColumn x:Name="regionColumn" Binding="{Binding Region}" Header="Region" />
                <controls:DataGridTextColumn x:Name="postalCodeColumn" Binding="{Binding PostalCode}" Header="Postal Code" />
                <controls:DataGridTextColumn x:Name="countryColumn" Binding="{Binding Country}" Header="Country" />
                <controls:DataGridTextColumn x:Name="faxColumn" Binding="{Binding Fax}" Header="Fax" Width="100" />
                <controls:DataGridTextColumn x:Name="phoneColumn" Binding="{Binding Phone}" Header="Phone" Width="100" />
            </controls:DataGrid.Columns>
        </controls:DataGrid>
        <local:Detail Grid.Row="2" DataContext="{Binding ElementName=master, Path=SelectedItem, Mode=OneWay}" Margin="5" x:Name="detail"/>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" Grid.Row="3">
            <Button Width="75" Margin="5,0" Content="Add" Command="{Binding AddCommand}" />
            <Button Width="75" Margin="5,0" Content="Remove" Command="{Binding RemoveCommand}" />
            <Button Width="75" Margin="5,0" Content="Save" Command="{Binding SaveCommand}" />
        </StackPanel>

    </Grid>
</Page>

In this case, there were almost no changes to do: the Toolkit DataGrid is very similar to the one in WPF. We must set the DataContext property in MainPage.xaml.cs:

public MainPage()
{
    this.InitializeComponent();
    DataContext = App.Current.MainVM;
}

We are using the same code we’ve used in the WPF project. For that, we must use the Dependency Injection provided in the MVVM toolkit in App.xaml.cs:

public App()
{
    InitializeLogging();

    this.InitializeComponent();
    
    var services = new ServiceCollection();

    services.AddSingleton<ICustomerRepository, CustomerRepository>();
    services.AddSingleton<MainViewModel>();

    Services = services.BuildServiceProvider();

#if HAS_UNO || NETFX_CORE
    this.Suspending += OnSuspending;
#endif
}
public new static App Current => (App)Application.Current;
public IServiceProvider Services { get; }
public MainViewModel MainVM => Services.GetService<MainViewModel>();

As you can see, there are no changes here.

The next step is to add the Details page, which has almost not changes from the WPF project:

<UserControl
    x:Class="MVVMUnoApp.Detail"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="detailControl"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <Grid>
       
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <TextBlock Text="Customer Id:" Grid.Column="0" Grid.Row="0"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="0"   Margin="3" Name="customerIdTextBox" Text="{Binding CustomerId, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Company Name:" Grid.Column="0" Grid.Row="1"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="1"   Margin="3" Name="companyNameTextBox" Text="{Binding CompanyName, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Contact Name:" Grid.Column="0" Grid.Row="2"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="2"   Margin="3" Name="contactNameTextBox" Text="{Binding ContactName, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Contact Title:" Grid.Column="0" Grid.Row="3"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="3"   Margin="3" Name="contactTitleTextBox" Text="{Binding ContactTitle, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Address:" Grid.Column="0" Grid.Row="4" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="4" Margin="3" Name="addressTextBox" Text="{Binding Address, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="City:" Grid.Column="0" Grid.Row="5"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="5"   Margin="3" Name="cityTextBox" Text="{Binding City, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Postal Code:" Grid.Column="0" Grid.Row="6"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="6"   Margin="3" Name="postalCodeTextBox" Text="{Binding PostalCode, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Region:" Grid.Column="0" Grid.Row="7"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="7"   Margin="3" Name="regionTextBox" Text="{Binding Region, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Country:" Grid.Column="0" Grid.Row="8"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="8"   Margin="3" Name="countryTextBox" Text="{Binding Country, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Phone:" Grid.Column="0" Grid.Row="9"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="9"   Margin="3" Name="phoneTextBox" Text="{Binding Phone, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Fax:" Grid.Column="0" Grid.Row="10"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="10"   Margin="3" Name="faxTextBox" Text="{Binding Fax, Mode=TwoWay}" VerticalAlignment="Center"  />
        </Grid>
</UserControl>

The only change is to remove the ValidatesOnExceptions=true and NotifyOnValidationError=true from the textboxes, because UWP/WinUI doesn’t have native validation. If you want to add validation to UWP/WinUI, you should check the Template10 Validation project.

Now, the project is ok and ready to be run. When you run it, it shows the main window, but doesn’t show any data:

That’s really strange. If you change the startup project to UWP and run it, it runs fine:

We should investigate a little more. When using the Wasm project, we have a web application. In this case, do the web tools work ? Run the Wasm project again and press F12 to open the dev tools:

As you can see, we have the diagnostic tools available and they show us that Customers.xml is not found. That makes sense, as we’re not running a local app, but a web app, and local files aren’t available. We should use another mechanism to get our data. Searching in the internet, we come to this site, which says that the Storage files available in UWP are also available in Wasm, so we should change our code from

var doc = XDocument.Load("Customers.xml");

To

var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Customers.xml"));
var content = await FileIO.ReadTextAsync(file);
var doc = XDocument.Parse(content);

Unfortunately, this way is not supported in Wasm, when using a library file. We have two solutions here:

  • Create a server that will serve the data and use REST calls to the server in the repository
  • Add the repository files to the Shared project and use the UWP storage method

There is no doubt that in a normal project I would go to the first solution. It’s the way to go when you are creating web applications, it’s more flexible and extensible. But, as I’m only showing you how to port a small WPF project to the web and I don’t want to create a server and change the repository now (it would be beyond the scope of the article), I won’t do it (if you want to see that solution, write in the comments, if there are enough requests, I will do it 😃).

So, what we’ll do is to create a new folder named Repository in the Shared project and add all the files that are in the library to this folder. After that, you can delete the library from the solution.

If you run the application, you still won’t see the data, but the error will disappear. The issue now is that we have a synchronization problem: the data now is obtained in async mode (in the original project it was obtained in sync mode). While that is happening, the ViewModel is being built and asks for the customers, which haven’t been read, so the _allCustomers field will be empty and nothing will be shown.

To fix this issue, we must have a function named GetCustomersAsync that retrieves the customers and use it in the ViewModel.

The ICustomerRepository interface will be:

public interface ICustomerRepository
{
    bool Add(Customer customer);
    bool Remove(Customer customer);
    bool Commit();
    Task<IEnumerable<Customer>> GetCustomersAsync();
}

And the CustomerRepository class becomes:

public class CustomerRepository : ICustomerRepository
{
    private IList<Customer> customers;

    public async Task<IEnumerable<Customer>> GetCustomersAsync()
    {
        var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Customers.xml"));
        var content = await FileIO.ReadTextAsync(file);
        var doc = XDocument.Parse(content);
        customers = new ObservableCollection<Customer>((from c in doc.Descendants("Customer")
                                                        select new Customer
                                                        {
                                                            CustomerId = GetValueOrDefault(c, "CustomerID"),
                                                            CompanyName = GetValueOrDefault(c, "CompanyName"),
                                                            ContactName = GetValueOrDefault(c, "ContactName"),
                                                            ContactTitle = GetValueOrDefault(c, "ContactTitle"),
                                                            Address = GetValueOrDefault(c, "Address"),
                                                            City = GetValueOrDefault(c, "City"),
                                                            Region = GetValueOrDefault(c, "Region"),
                                                            PostalCode = GetValueOrDefault(c, "PostalCode"),
                                                            Country = GetValueOrDefault(c, "Country"),
                                                            Phone = GetValueOrDefault(c, "Phone"),
                                                            Fax = GetValueOrDefault(c, "Fax")
                                                        }).ToList());
        return customers;
    }

    #region ICustomerRepository Members

    public bool Add(Customer customer)
    {
        if (customers.IndexOf(customer) < 0)
        {
            customers.Add(customer);
            return true;
        }
        return false;
    }

    public bool Remove(Customer customer)
    {
        if (customers.IndexOf(customer) >= 0)
        {
            customers.Remove(customer);
            return true;
        }
        return false;
    }

    public bool Commit()
    {
        try
        {
            var doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
            var root = new XElement("Customers");
            foreach (Customer customer in customers)
            {
                root.Add(new XElement("Customer",
                                      new XElement("CustomerID", customer.CustomerId),
                                      new XElement("CompanyName", customer.CompanyName),
                                      new XElement("ContactName", customer.ContactName),
                                      new XElement("ContactTitle", customer.ContactTitle),
                                      new XElement("Address", customer.Address),
                                      new XElement("City", customer.City),
                                      new XElement("Region", customer.Region),
                                      new XElement("PostalCode", customer.PostalCode),
                                      new XElement("Country", customer.Country),
                                      new XElement("Phone", customer.Phone),
                                      new XElement("Fax", customer.Fax)
                             ));
            }
            doc.Add(root);
            doc.Save("Customers.xml");
            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }

    #endregion

    private static string GetValueOrDefault(XContainer el, string propertyName)
    {
        return el.Element(propertyName)?.Value ?? string.Empty;
    }
}

To use this repository, we should change the ViewModel to:

private readonly ICustomerRepository _customerRepository;
private IEnumerable<Customer> _allCustomers;
private Customer _selectedCustomer;

public MainViewModel(ICustomerRepository customerRepository)
{
    _customerRepository = customerRepository ??
                          throw new ArgumentNullException("customerRepository");
    GetCustomers();
    AddCommand = new RelayCommand(DoAdd);
    RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
    SaveCommand = new RelayCommand(DoSave);
    SearchCommand = new RelayCommand<string>(DoSearch);
}

private async void GetCustomers()
{
    _allCustomers = await _customerRepository.GetCustomersAsync();
    Customers = _allCustomers;
    OnPropertyChanged("Customers");
}

We must call the async method in the ViewModel constructor and, in this case, we cannot await for the call, so we fire the call and, when the data is ready, it fires the INotifyPropertyChanged event and populates the data.

If we run the program, we see that it opens a new browser window with the program:

We’ve ported our WPF program to the web using Uno Platform. Nice, no ? We can leverage our XAML expertise and port it to the web. It’s not a direct change but, nevertheless, it’s way less trouble than a complete rewrite. Uno Platform did a great work to port UWP to WebAssembly! And you get some extra bonuses: you still have an UWP and a WPF project running with the same code. The original Uno template also allows you to run the code in Android or iOs (but I removed these from the code for this article).

The full code for this article is in https://github.com/bsonnino/MVVMUnoApp

There are some times when you need to get the disk information in your system, to check what’s happening, for inventory check, or even to know the free and available space.
Getting the free and available space is fairly easy in .NET, just use the GetDrives method of the DriveInfo class:

DriveInfo[] drives = DriveInfo.GetDrives();

foreach (DriveInfo drive in drives)
{
    Console.WriteLine($"Name: {drive.Name}");
    Console.WriteLine($"VolumeLabel: {drive.VolumeLabel}");
    Console.WriteLine($"RootDirectory: {drive.RootDirectory}");
    Console.WriteLine($"DriveType: {drive.DriveType}");
    Console.WriteLine($"DriveFormat: {drive.DriveFormat}");
    Console.WriteLine($"IsReady: {drive.IsReady}");
    Console.WriteLine($"TotalSize: {drive.TotalSize}");
    Console.WriteLine($"TotalFreeSpace: {drive.TotalFreeSpace}");
    Console.WriteLine($"AvailableFreeSpace: {drive.AvailableFreeSpace}");
    Console.WriteLine();
}

If you run this code, you will get the info for all drives in your system:

This is ok for most apps, but sometimes you want more than that. If, for example, you want an inventory of your physical disks (not the logical disks, like DriveInfo gives) ?

The physical and logical structure of your disks can differ a lot. For example, if you have a disk with three partitions, you will have one physical disk and three logical disks.

On the other side, you can mount a drive in an empty folder and have two physical disks and one logical disk. And we are not considering multiple OSs, here.

.NET doesn’t have any dedicated class for the physical disk structure, so we should find another way to do it. In fact, there is a way to get this information in Windows, called WMI (Windows Management Instrumentation).

It’s the infrastructure for data management in Windows and you can use it to manage your local and remote systems. You can access it with C++, C# or even PowerShell, to get info and manage your computer.

With WMI, you can use SQL-like queries to query information in your system, using the available classes. For example, to get all the drives in your system, you would use:

SELECT * FROM Win32_DiskDrive

That would return a key-value set with all the properties of the system disks, and you can check the values you want. You can even filter the properties and drives that you want. Something like:

SELECT DeviceID, Model, Name, Size, Status, Partitions, 
  TotalTracks, TotalSectors, BytesPerSector, SectorsPerTrack, TotalCylinders, TotalHeads, 
  TracksPerCylinder, CapabilityDescriptions 
  FROM Win32_DiskDrive WHERE MediaType = 'Fixed hard disk media'

Will get only the device data and geometry for the fixed disks in your system (it won’t get info for the external HDs).

Now that we already know how to get the disk information, let’s see how to do it in C#.

To use WMI in a C# program, we must add the package System.Management to the project and start using it.

Open a new terminal window and create a new console application with

dotnet new console -o DiskInfoCSharp

Once you do that, a new console project will be created in the DiskInfoCSharp folder. You can cd to it and add the new package to the project with

dotnet add package System.Management

This will add the package to the project and will allow you to use WMI in it. You can open VS Code with code . and start editing the project.

The first step is to create a scope and connect to it:

using System.Management;

ManagementScope scope = new ManagementScope("\\\\.\\root\\cimv2");
scope.Connect();

This will allow you to query data in the local computer. If you want to connect to a remote computer, just change the \\. to the name of the remote computer.

Then, you must create the query for getting the disk information:

ObjectQuery query = new ObjectQuery(@"SELECT DeviceID, Model, Name, Size, Status, Partitions, 
  TotalTracks, TotalSectors, BytesPerSector, SectorsPerTrack, TotalCylinders, TotalHeads, 
  TracksPerCylinder, CapabilityDescriptions 
  FROM Win32_DiskDrive WHERE MediaType = 'Fixed hard disk media'");

As you can see, the query has a syntax similar to SQL, you can change it to get different fields (or use * to get all fields) or change (or remove) the WHERE clause. This query will only return the selected fields for the fixed hard disks.

Then, we should instantiate a ManagementObjectSearcher object and start iterating on all instances of fixed disks in your system:

ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
foreach (ManagementObject wmi_HD in searcher.Get())
{
    foreach (PropertyData property in wmi_HD.Properties)
        Console.WriteLine($"{property.Name} = {property.Value}");
    var capabilities = wmi_HD["CapabilityDescriptions"] as string[];
    if (capabilities != null)
    {
        Console.WriteLine("Capabilities");
        foreach (var capability in capabilities)
            Console.WriteLine($"  {capability}");
    }
    Console.WriteLine("-----------------------------------");
}

For each disk, we get all the properties and then we iterate on the capability descriptions, as this is a property that returns an array of strings.

If we run this program, we will get something like

As you can see, it’s very easy to get disk information using WMI in C#. There are some caveats in using this method:

  • You don’t have the properties in advance. Yes, you could create a class with the properties and populate it, but it’s not something that is built in
  • This method only works in Windows. If you are creating a multi-platform program, this method isn’t for you

On the other side, this is a very easy method to get the information in local and remote computers, you can easily filter data and use it as you want.

If you go to the Using WMI page, you will see a note:

As you can see, the System.Management namespace is not recommended anymore, and it must be replaced by the Microsoft.Management.Infrastructure, especially for .NET Core apps.

To port our app, the first thing to do is to ass the package Microsoft.Management.Infrastructure with

dotnet add package Microsoft.Management.Infrastructure

Once you do that, you should change the classes you are using to access the data. We create a CimSession and query the data with QueryInstances:

using Microsoft.Management.Infrastructure;

var instances = CimSession.Create(null)
                .QueryInstances(@"root\cimv2", "WQL", @"SELECT DeviceID, Model, Name, Size, Status, Partitions, 
   TotalTracks, TotalSectors, BytesPerSector, SectorsPerTrack, TotalCylinders, TotalHeads, 
   TracksPerCylinder, CapabilityDescriptions 
   FROM Win32_DiskDrive WHERE MediaType = 'Fixed hard disk media'");

Once you do that, you can iterate on the instances and get the properties with:

foreach (var instance in instances)
{
    foreach (var property in instance.CimInstanceProperties)
        Console.WriteLine($"{property.Name} = {property.Value}");
    var capabilities = instance.CimInstanceProperties["CapabilityDescriptions"].Value as string[];
    if (capabilities != null)
    {
        Console.WriteLine("Capabilities");
        foreach (var capability in capabilities)
            Console.WriteLine($"  {capability}");
    }
    Console.WriteLine("-----------------------------------");
}

If you run the program, you will get properties similar to the ones in the previous figure.

As you can see, WMI can ease the retrieval of information in a Windows machine. Unfortunately, that can’t be used in a Linux machine, that may be a subject for another article!

All the code for this article is at https://github.com/bsonnino/WMICsharp

There are some times when we want to check all installed fonts in the system and test a phrase to check how does it show in the display at some size.

There are many utilities for that, but it’s always better (and most satisfying) to build your own. And, even better, you can do it in WPF with no need of writing code, just use the builtin data binding features in the markup.

To do it, just open a terminal window, and type

dotnet new wpf -o FontsList

That will create a new WPF application in the FontsList folder. Now, you can change to the FontsList folder and type code . to open VS Code in the current folder.

Once you do that, you can edit the MainWindow.xaml file and add this code:

<Window x:Class="FontsList.MainWindow"
    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:local="clr-namespace:FontsList" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Slider Minimum="5" Maximum="100" TickPlacement="BottomRight" x:Name="FontSlider" Value="48" />
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="0.5*"/>
                <ColumnDefinition Width="0.5*"/>
            </Grid.ColumnDefinitions>
            <ListBox HorizontalAlignment="Stretch" x:Name="Listbox" Margin="0,0,5,0" 
                VerticalAlignment="Stretch" ItemsSource="{Binding Source={x:Static Fonts.SystemFontFamilies}}" 
                IsSynchronizedWithCurrentItem="True"/>
            <GridSplitter HorizontalAlignment="Right" VerticalAlignment="Stretch" Width="5"/>
            <TextBox HorizontalAlignment="Stretch" VerticalAlignment="Stretch" 
                FontFamily="{Binding Path=SelectedItem, ElementName=Listbox, Mode=Default}" 
                FontSize="{Binding Path=Value, ElementName=FontSlider}" Grid.Column="1" 
                Text="The quick brown fox jumps over the lazy dog" TextWrapping="Wrap"/>
        </Grid>
    </Grid>
</Window>

We are adding a grid with two rows. In the first row, we add the font size slider, that will be used to change the font size of the preview. In the second row, we will add another grid, with two columns: the font list and the preview.

The font list gets its items from the Windows class Fonts.SystemFontFamilies. The preview is a textbox there you can type the text you want to preview, with the FontFamily bound to the selected item of the listbox (the font you’ve selected) and the FontSize bound to the size slider.

Now we’re ready to run. No extra code is needed. Just type

dotnet run

In the terminal window and you will see the font list with a preview in the selected size:

Nice, no? Now you have your own fonts list in WPF with no code. It even has a splitter in the fonts grid, if you want to change the size of the font list and preview.

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

Once there was a time where Microsoft and Linux were enemies and who was developing in Windows didn’t develop for Linux and vice-versa. Time has passed and things have changed. And how they have changed!

Multi-platform development has evolved, Microsoft released .NET open source, and you can run it both on Linux and Mac (besides Windows, of course). You can use VS Code to develop your code in any of these platforms, and VS Code isn’t restricted to .NET Languages: you can develop in Python, Perl or even Clipper/Harbour, Cobol or Fortran. What a change!

Besides that, you can run a Linux distro directly in Windows, with no need of any VM, dual boot or Live CD. The key for that is WSL, the Windows Subsystem for Linux, that allows you to run a full Linux environment, including any Linux application without modification (you can even run Linux graphical apps with WSLg, that’s in test – the image below shows GIMP running on Windows)

And, just to make sure, it’s not an emulation of Linux. You are running the full distro directly on Windows. WSL uses a translation layer between Linux and Windows, to translate the calls between both OSs and WSL2 uses a Linux kernel for its magic. You can compare both versions of WSL here.

To develop in Linux, the first thing is to get Windows Terminal. This is the successor of the old Windows command line prompt and offers many improvements: customization, tabbed interface and it allows you to use multiple shell types: Windows command, Powershell, Linux prompt or even an Azure Cloud Shell. If you are using Windows 11, you already have it, as it’s installed by default. If you are still using Windows 10, you can get it from the Microsoft Store.

Once you have Windows Terminal installed, you must install WSL. Just open a command prompt window and type

wsl --install

This will install WSL in your machine. If that doesn’t work, you will have to install it manually, using the installation steps listed here.

With WSL installed, you need to install your preferred Linux distro. You can get it from the Microsoft Store:

Or you can install it directly from the command prompt. wsl --list lists all the available distros in your machine (you can install and use many distros) and wsl --list --online lists the available distros online:

To install an online distro, you must use

wsl --install -d <Distro>

If the distro isn’t available online or in the store, you still can install it, by getting the appx packages an manually installing it, as described here. One other way is to check if there is an alternate way to install it in WSL. For example, to install Linux Mint, you can go to the LinuxMintWSL Github and download the installer for it.

When you have WLS and the distros installed in your machine, you can click on the down arrow in the Terminal title bar and it will show them in the list, so you can select and open a command prompt for that distro:

The next step is to install dotnet 6.0. To do that, you should follow the instructions for your distro in

For Linux Mint (based on Ubuntu), you should run these commands on the bash prompt:

wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb

sudo apt-get update; \
  sudo apt-get install -y apt-transport-https && \
  sudo apt-get update && \
  sudo apt-get install -y dotnet-sdk-6.0

Once installed, you can get the installed version with

dotnet --version

Now, you can create and run a dotnet program in Linux. To show the available app types to create, you can run

dotnet new

This will list the common templates for a new app. To list all templates, you should use

dotnet new --list

We can create a new console app using this command:

dotnet new console -o simple-console

This command will create a new console app in the simple-console folder. We can change to this folder with cd simple-console and run the program with

dotnet run

This is already great but, unless you are already a VI or Emacs guru (which I am not 😃), you will have some trouble to modify it. I personally, think that the best way to modify a Linux program in WSL is to use VSCode. By typing code . in the command prompt, VS Code server will be downloaded and installed automatically and VSCode will open in your desktop

In this case, you won’t have VS Code for Linux running. When you are using VS Code from WSL, something different happens: VS Code will be split into two parts, a server (which runs on Linux) and a client (that runs on Windows). You will be editing the files on Windows, but saving them in Linux. You can see in VS Code status bar, at the bottom WSL: Mint, indicating that we are editing files in WSL. If you open a terminal in VS Code with Ctrl+`, you will see that it’s a bash prompt and not a command line or a Powershell prompt.

We can edit program.cs to show the OS version with:

Console.WriteLine($@"Hello World! 
Operating System: {System.Runtime.InteropServices.RuntimeInformation.OSDescription}");

When we save and run the program we will get

This is still great, but I doubt that’s what you want to do when your are developing a Linux app. Many times, we’ll want to develop a web api to serve our data. For that, we must change to the parent folder and create a new app with

dotnet new webapi -o webapi

If you change to the webapi folder and run it with dotnet run, you will see the endpoints for the API:

In our case, if we open a browser window and type

https://localhost:7219/swagger

We will see the swagger interface for our api:

We can test it and see that it works fine:

But we can also use the curl command to get the data. In the bash prompt, we can type

curl -X 'GET' \
  'https://localhost:7219/WeatherForecast' \
  -H 'accept: text/plain'

to get the api data (I got this command line from the swagget test window). If you do that, you will see a certificate error. We didn’t set the certificate for the web site, and we can bypass that with the -k flag:

curl -k -X 'GET' \
  'https://localhost:7219/WeatherForecast' \
  -H 'accept: text/plain'

As you can see, we are getting the json data for the weather forecast sample. You can still consume the api from our console app. All we have to do is change the program.cs file to

using System.Text.Json;

var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback =
              (message, cert, chain, errors) => 
              { return true; };
using var client = new HttpClient(handler);
var json = await client.GetStringAsync("https://localhost:7219/weatherforecast");
var forecasts = JsonSerializer.Deserialize<List<WeatherForecast>>(json);
if (forecasts == null)
    return;
foreach (var forecast in forecasts)
    Console.WriteLine($"{forecast.date:dd/MM/yyyy}  {forecast.temperatureC}  {forecast.summary}");

public class WeatherForecast
{
    public DateTime date { get; set; }
    public int temperatureC { get; set; }
    public int temperatureF => 32 + (int)(temperatureC / 0.5556);
    public string? summary { get; set; }
} 

If you run the program, you will see something like this:

We are consuming the API from our console program in Linux, but we are running .NET, we can run the same program in Windows. One nice thing in WSL is that we can run Windows apps from Linux. If you type

explorer.exe .

It will open Windows Explorer with the current path open. If you copy the address in the address bar, you will see:

\\wsl.localhost\Mint\home\bruno\LinuxProg\simple-console

As you can see, the Linux folder is considered a new network. We can map the drive in Windows to use it, with a command like this one:

net use y: \\wsl.localhost\Mint

If you open a command prompt and type this command, you will have the y: drive available to use. You can then change to the program folder and type dotnet run to run the same program in Windows. If you do that, you will see that we can run the same program we’ve created in Linux in the Windows command prompt and consume the API that is still running in Linux.

As you can see, with .NET you can run the same program in Linux or in Windows and you can interact with the APIs that are running in Linux. You will be using the tools that you still know. You can even access one file system from the other seamlessly (the Windows file system is mounted in /mnt/c/ in Linux). You will be using the same C# language that you know and, most of the time, you don’t even need to know Linux APIs – .NET will take care of that for you.

The full code of this article will be in https://github.com/bsonnino/LinuxProg

Visual Studio Code is a free, multi-platform IDE and has multiple features, like multi-language support, syntax, highlighting, Git integration, among others. It’s open source and can be downloaded here.

One thing that contributes to its success is the fact that anyone can develop an extension for it and add new features to it. In fact, there are thousands of different extensions and, most it’s almost sure that, if you are searching something to do with it, there is an available extension to do it: themes, formatters, highlighters, code editors, or even an integrated interface to work with your favorite tool. You just have to open the extension manager, search what you want and install it.

Although Visual Studio Code has the same name of its older brother, it’s an entirely different code base: while Visual Studio is a .NET application, Windows-only (there is Visual Studio for Mac, based on MonoDevelop and Xamarin Studio, which is another code base), Visual Studio Code is based on the Monaco Editor, a code editor for the web, written in Typescript and it’s a multi-platform Electron app.

The extensions for VS Code are completely different from the Visual Studio ones: they are written in TypeScript (or JavaScript) and run only in VS Code, you cannot install them in Visual Studio.

To develop an extension to VS Code, you must have Node.js and Git installed and install Yeoman and the VS Code Extension Generator:

npm install -g yo generator-code

Then, we can create our extension with

yo code

The code generator will ask questions to direct you to generate the correct extension:

You can choose:

? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? SortLines
? What's the identifier of your extension? sortlines
? What's the description of your extension? Sorts Selected Lines
? Initialize a git repository? Yes
? Bundle the source code with webpack? No
? Which package manager to use? npm

The last question allows you to open the new extension with VS Code:

You can press F5 to compile the extension and open a new instance of VS Code, where you can type Shift+Ctrl+P and type Hello World and the extension will show a message:

Two files are important in the extension: package.json and src\extension.ts. Package.json is the extension manifest, where you will say how your extension will be presented to the world: its name, which commands it implements and information for the marketplace: icon, license, colors, etc. Our extension has this information:

"name": "sortlines",
"displayName": "SortLines",
"description": "Sorts Selected Lines",
"version": "0.0.1",
"engines": {
  "vscode": "^1.64.0"
},
"categories": [
  "Other"
],
"activationEvents": [
  "onCommand:sortlines.helloWorld"
],
"main": "./out/extension.js",
"contributes": {
  "commands": [
    {
      "command": "sortlines.helloWorld",
      "title": "Hello World"
    }
  ]
},

You can recognize the name, displayName and description from your answers when you created the extension. The categories property shows the categories in which your extension fits. In our case, we will stay with Other.

The activationEvents property shows how the extension will be activated. In our case, it will be with the command sortlines.helloWorld. If we were creating a different extension, like an extension for a HTML parser, we could use the onLanguage event.

The main property indicates the entry point for your extension. As we are using TypeScript and it will be transpiled to JavaScript, we are indicating the js file in the out directory. The contributes property indicates where the extension will contribute with VS Code. Our extension will add a command in the command palette, but if the extension would add a new language support to VS Code, we could use the languages property.

The command will have the command identifier and the command title, that will appear in the command palette.

The main code is at src\extension.ts, where we will program what our extension will do. Its basic structure is this:

// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
  
  // Use the console to output diagnostic information (console.log) and errors (console.error)
  // This line of code will only be executed once when your extension is activated
  console.log('Congratulations, your extension "sortlines" is now active!');

  // The command has been defined in the package.json file
  // Now provide the implementation of the command with registerCommand
  // The commandId parameter must match the command field in package.json
  let disposable = vscode.commands.registerCommand('sortlines.helloWorld', () => {
    // The code you place here will be executed every time your command is executed
    // Display a message box to the user
    vscode.window.showInformationMessage('Hello World from SortLines!');
  });

  context.subscriptions.push(disposable);
}

// this method is called when your extension is deactivated
export function deactivate() {}

It declares two methods, activate, called when the extension is being activated and deactivate, called when the extension is deactivated, You use this method when there is some cleanup to be done. If there is no cleanup necessary, you can remove this method.

Let’s start changing the extension  for our needs. Initially, let’s change the package.json file to set our new title and add the commands we need:

"activationEvents": [
    "onCommand:sortlines.sortDescending",
    "onCommand:sortlines.sortAscending"
],
"main": "./out/extension.js",
"contributes": {
    "commands": [{
            "command": "sortlines.sortDescending",
            "title": "Sort Lines: Sort Descending"
        },
        {
            "command": "sortlines.sortAscending",
            "title": "Sort Lines: Sort Ascending"
        }
    ]
},

We are defining two commands: sortDescending and sortAscending. Now we must declare them in the code:

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
  
  // Use the console to output diagnostic information (console.log) and errors (console.error)
  // This line of code will only be executed once when your extension is activated
  console.log('Congratulations, your extension "sortlines" is now active!');

  // The command has been defined in the package.json file
  // Now provide the implementation of the command with registerCommand
  // The commandId parameter must match the command field in package.json
  registerCommand(context, 'sortlines.sortDescending', () => {
    vscode.window.showInformationMessage('You are sorting the lines descending!');
  });
  registerCommand(context, 'sortlines.sortAscending', () => {
    vscode.window.showInformationMessage('You are sorting the lines ascending!');
  });
}

function registerCommand(context: vscode.ExtensionContext, command: string , func : () => void) {
  const disposable = vscode.commands.registerCommand(command, func);

  context.subscriptions.push(disposable);
}

I have refactored the code and created a new function, registerCommand, that will register the command and the callback function that will be invoked when the command is invoked. For now, we will only show the information messages. If you run the extension and open the command palette in the new instance of VS Code, you will see something like:

Selecting one of the options will show the information message. Now, let’s create the code to sort the selected lines in the editor:

// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
  registerCommand(context, 'sortlines.sortDescending', () => sortSelection(true));
  registerCommand(context, 'sortlines.sortAscending', () => sortSelection(false));
}

function sortSelection(isDescending: boolean) {
  //get the active text editor
  const editor = vscode.window.activeTextEditor;
  if (!editor) {
    return;
  }
  //get the selection start and end
  const selection = editor.selection;
  const start = selection.start;
  const end = selection.end;
  // the new selection will extend 
  // from start of the first selected line 
  // to the end of the last selected line
  const newSelection = new vscode.Selection(start.line, 0, end.line + 1, 0);
  // get the selected text
  const text = editor.document.getText(newSelection);
  // split the text into lines
  const lines = text.split('\r\n');
  // sort the lines
  lines.sort((a, b) => isDescending ? b.localeCompare(a) : a.localeCompare(b));
  // replace the text with the sorted lines
  editor.edit((editBuilder) => {
    editBuilder.replace(newSelection, lines.join('\n'));
  });
  // set the new selection
  editor.selection = newSelection;
}

function registerCommand(context: vscode.ExtensionContext, command: string, func: () => void) {
  const disposable = vscode.commands.registerCommand(command, func);
  context.subscriptions.push(disposable);
}

For that, we will use the VS Code Api. We will create a sortSelection function, that will get the current selection, extend it to get the full first and last lines, sort them and replace the text with the sorted lines. The first step is to get the active editor with window.activeTextEditor, then get the current selection and extend it from the first character of the first selected line to the first character of the line just after the last selected line. Then, we sort the lines and replace the selected lines with the sorted lines and set the new selection.

When you run this extension and select some lines in your editor (the sample code has a list of artists obtained from the Chinook database, so you can try it), if you press Shift+Ctrl+P and select the sort, your selected lines will be sorted:

You can see that it works fine, except for one small glitch: if you sort the lines in the ascending order (that doesn’t happen in descending order), a blank line will be inserted in the first line of the selection, and that’s not what we want. That gives us the opportunity of testing another feature of the development of an extension for VS Code: debugging. Let’s debug the extension and check what’s happening:

We can set a break point in the line where the lines will be sorted, select some lines of text in the debugged editor and select the sort ascending. The debugger will stop in that line and we can see what’s happening, by analyzing the lines variable:

There is an extra line in the lines variable due to the last line, that will be sorted and will come first in the selection. And why that doesn’t happen in the descending order ? The empty line will come last and it will add only the last new line character, which will separate the last line of the selection and the next line, not selected.

That’s an easy fix: we can use the trim method to remove the trailing newline and the last line won’t be created when splitting the text:

const lines = text.trim().split('\r\n');

Now, we can run the extension and see that things work fine. There is one last thing to do here: add two keybindings to our extension: we will use Ctrl+K Ctrl+A for the sort ascending and Ctrl+K Ctrl+D for sort descending. For that, we will go to the Contributes section in the package.json file:

"contributes": {
    "commands": [{
            "command": "sortlines.sortDescending",
            "title": "Sort Lines: Sort Descending"
        },
        {
            "command": "sortlines.sortAscending",
            "title": "Sort Lines: Sort Ascending"
        }
    ],
    "keybindings": [{
            "command": "sortlines.sortDescending",
            "key": "ctrl+K ctrl+D",
            "when": "editorHasSelection"
        },
        {
            "command": "sortlines.sortAscending",
            "key": "ctrl+K ctrl+A",
            "when": "editorHasSelection"
        }
    ]
},

We set the command, the keybinding and when the command will be enabled (in this case, we will only enable the keybinding when there is selected text in the editor. Now, if you run the extension, select some text and press Ctrl+K Ctrl+D or Ctrl+K Ctrl+A, you will have the lines sorted.

Our extension is ready and the last step is to install it in VS Code (you can also upload it to the Marketplace, so it will be available for everyone). For that, you need to install the vsce tool. This tool allows you to package the extension, creating a VSIX file that can be installed locally or shared with other users, or publish the extension to the marketplace, so anyone can use it. You can install the tool with

npm install -g vsce

Once installed, you can use the publish or package commands to do what you want with the extension. For now, we’ll just package our extension, so you need to open a terminal window (you can do it in VS Code with Ctrl+`), change to your extension folder and type

vsce package

When you do that, you get an error:

You must edit your readme file to show what your extension will do. This is a markdown file, where you will put the features of your extension.

Once you have edited your readme file, you can package your extension and a VSIX file will be created. You can then go to the Extensions tab, select the “…” icon and select “Install from VSIX”. You’ll have the extension installed in your VS Code. You can distribute the extension to other members of your team by sharing the VSIX file. If you want to publish your extension, you can do it by using the publish command.

As you can see, creating an extension for VS Code is relatively easy, and that’s why there are so many extensions for it

All the source code for this extension is at https://github.com/bsonnino/VSCodeExtension