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

 

 

 

Sometime ago I posted an article about Dotnet Try. This is a wonderful tool to document your code but, since then, a lot has changed.

.NET 6  and C#10 are here, Visual Studio Code is a nice environment for editing your code, and you can even edit your code in  the web. And a new feature has appeared to document your code: .NET Interactive. With it, you can create interactive notebooks and create interactive pieces of code, where you can share and try sample code.

Its use is very simple: just install the >NET Interactive extension in VS Code and you’re ready to go. Just press Ctrl+Shift+Alt+N and open a new notebook (you can choose C#, F# or even Powershell as the default language) and a new notebook is ready to use. If you want to open an existing notebook, just press Ctrl+Shift+P and select .NET Interactive: Open Notebook and open the existing notebook.

When you create a new notebook, you can add markdown or code cells. The markdown cells have text formatted with the Markdown syntax and the code cells can have code, in the selected language. You can run the code by clicking the arrow icon next to the cell or by pressing the Ctrl+Alt+Enter keys.

To display the values, you can use Console.WriteLineor simply declare a variable and put it in a single line like in

if you want to display more than  a single value, you can use the display function, like in

You can even mix languages in the notebook. If you add !#fsharp in the first line, you can add F# code, like in:

If you want to use javascript, you can use something like:

Getting data from an URL

You can get some data from an URL, using the command

#!value --from-url https://raw.githubusercontent.com/ageron/handson-ml2/master/datasets/housing/housing.csv --name housingPath

Once you have the data, you can display it with SandDance, with this code:

using Microsoft.Data.Analysis;
using Microsoft.ML;
using System.Collections.Generic;

var housingData = DataFrame.LoadCsv(housingPath);
housingData.ExploreWithSandDance().Display();

Using Nuget Packages

You can also use Nuget packages with the #r directive. For example, we can get the suppliers dataset as a Json file from the url “https://northwind.vercel.app/api/suppliers”, with this code:

#!value --from-url https://northwind.vercel.app/api/suppliers --name suppliersJson

Then we can show the data as C# classes with:

#!share --from value suppliersJson
#r "nuget:Newtonsoft.Json"

using Newtonsoft.Json;

public record Address(string Street, string City, string Region, string PostalCode, string Country, string Phone);
public record Supplier(string Id, string CompanyName, string ContactName, string ContactTitle, Address Address);
var suppliers = JsonConvert.DeserializeObject<List<Supplier>>(suppliersJson);
suppliers

As you can see there are multiple possibilities. I even created the notebook for this article (you can download it at https://github.com/bsonnino/CSharpNotebooks) based on my last article, about Linq in .NET 6. You can use it as a notebook for you, as a learning tool, as a prototype tool, or even as a tool for sharing knowledge.

The notebook for this article is at https://github.com/bsonnino/CSharpNotebooks

A nice improvement in .NET is the introduction of LINQ, in .NET 4.5. With that, working with data was simplified a lot and, when I go to a language that doesn’t have something like it, I feel lost (having to deal with for and foreach became painful for me :-).

The features available in LINQ made my code more synthetic and readable, but sometimes, there was something that wasn’t easily attained with the default features. Microsoft heard that and introduced new features, many of them I was expecting since a long time. These new features  came with no huge announcements, but, nevertheless, they are very nice improvements.

Index and Range parameters

Index and Ranges were introduced in C#8, they can ease a lot when you must get a subrange of an array or list:

var arr = Enumerable.Range(1,100).ToArray();
Console.WriteLine($"[{string.Join(",",arr[10..15])}]");
Console.WriteLine(arr[^1]);
var list = new List<int>(arr);
Console.WriteLine(list[^1]);
Console.WriteLine(list[^5]);

When you run this code, you will get something like this:

[11,12,13,14,15]
100
100
96

This is nice, but when you wanted to work with Linq, you were not able to use indexes and ranges. Until now. .NET 6 allows using Ranges and Indices in Linq queries. This is very nice, because when you wanted a subrange of an IEnumerable, you should do something like:

list.Skip(10).Take(5)

Now, you can do the same with:

list.Take(10..15)

Or take the last 10 elements with

list.Take(^10..)

You can also take a single element with ElementAt using Indices. To get the last element in the list, you can use:

list.ElementAt(^1)

Chunking

One thing that is very common is to divide our data in chunks, in order to present it in pieces, so the user does not have to scroll long lists of information. Until now, you had to program that by hand, which could lead to errors. With the new Chunk method, you can split your data in chunks. For example, if you want to split the data in blocks of seven elements, you can do:

var chunked = list.Chunk(7);

With this code, you will obtain something like

[1,2,3,4,5,6,7]
[8,9,10,11,12,13,14]
[15,16,17,18,19,20,21]
[22,23,24,25,26,27,28]
[29,30,31,32,33,34,35]
[36,37,38,39,40,41,42]
[43,44,45,46,47,48,49]
[50,51,52,53,54,55,56]
[57,58,59,60,61,62,63]
[64,65,66,67,68,69,70]
[71,72,73,74,75,76,77]
[78,79,80,81,82,83,84]
[85,86,87,88,89,90,91]
[92,93,94,95,96,97,98]
[99,100]

And you can get one chunk with

chunked.ElementAt(3)
chunked.ElementAt(^1)

Zipping

Sometimes you want to combine three Enumerables into one. Combining Enumerables is done using the Zip method, which allowed to combine only two items at once. .NET 6 introduced the possibility of zipping three sequences at once (if you want more sequences, you must chain Zip functions). For example, if you have these three enumerables:

var list1 = Enumerable.Range(1,100).Select(i => $"ID {i}").ToList();
var list2 = Enumerable.Range(1,100).Select(i => $"Name {i}").ToList();
var list3 = Enumerable.Range(1,100).Select(i => $"Address {i}").ToList();

You can combine it into an IEnumerable of tuples with three elements each with:

var zipped = list1.Zip(list2,list3);

One note, here: in .NET 5 you could use a function to zip two sequences int another one and generate anything else than a tuple. .NET 6 didn’t change that and, if you want to zip the three IEnumerables into an IEnumerable of a class, for example, you must still do something like this:

var zipped1 = list1.Zip(list2, (l1, l2) => new { ID = l1, Name = l2 })
    .Zip(list3, (l1, l2) => new { ID = l1.ID, Name = l1.Name, Address = l2 });

DistinctBy, ExceptBy, UnionBy, InterceptBy

One thing that I use a lot is the distinct operator, to get unique values in a sequence. Until now, when I had a class and wanted to get distinct values in a class by some field, and I’m not interested in the other fields, I had to do something like:

public record Person(string Name, int Age);
var people = new List
{
    new Person("John", 30 ),
    new Person("Peter", 40),
    new Person("Mary", 20 ),
    new Person("Jane", 30 ),
    new Person("Larry", 50),
    new Person("Anne", 50 ),
    new Person("Paul", 20),
};
var distinctByAge = people.GroupBy(p => p.Age).Select(g => g.Key);

That worked fine, but lacked clarity – the intent was not explicit and it was hard to understand – Why this GroupBy is there ?

In .NET 6, the DistinctBy comes to solve that. Now, you can use something like this to get the distinct values :

var distinctAges = people.DistinctBy(p => p.Age).Select(p => p.Age);

Now the intent is clear and the code is easier to follow.

You can also use ExceptBy, to filter a sequence depending on another, like in

var excludedAges = new List<int> {30,40};
var people1 = people.ExceptBy(excludedAges, p => p.Age);

One note, here. Due to the way ExceptBy is coded (it uses a HashSet), it will only add the first duplicate element in the result. In our code, it should show:

Person { Name = Mary, Age = 20 }
Person { Name = Larry, Age = 50 }
Person { Name = Anne, Age = 50 }
Person { Name = Paul, Age = 20 }

But it only shows:

Person { Name = Mary, Age = 20 }
Person { Name = Larry, Age = 50 }

If you want all items that don’t match the excluded ages, you should still go with:

var people2 = people.Where(p => !excludedAges.Contains(p.Age));

If you want to join two sequences, removing duplicates between them, you can use the UnionBy method. This code joins the two lists into another, removing the duplicates:

var people3 = new List<Person>
{
    new Person("John", 20 ),
    new Person("Peter", 25),
    new Person("Paul", 20 ),
    new Person("Ringo", 22 ),
    new Person("George", 23),
    new Person("Anne", 50 ),
    new Person("Mark", 20),
};
var people4 = people.UnionBy(people3, p => p.Name);

If you want the have the names present in both lists, you can use the IntersectBy method:

var includedAges = new List<int> {30,40};
var people5 = people.IntersectBy(includedAges, p => p.Age);

In the same way of the ExceptBy, the duplicates are not included. If you want to include them, you should use:

var people6 = people.Where(p => includedAges.Contains(p.Age));

MaxBy and MinBy

When using the methods Max and Min, the sequences should implement the IComparable interface, so they could be compared and the maximum and minimum evaluated. That posed a problem, especially if the class you wanted to compare didn’t implement the IComparable interface. Now, with MinBy and MaxBy you don’t have to use the IComparable and can use something like:

var minByAge = people.MinBy(p => p.Age);
var maxByAge = people.MaxBy(p => p.Age);

This code won’t show all the elements with minimum age. To get that, you should use something like

var minAge = people.Select(p => p.Age).Min();
var allMinByAge = people.Where(p => p.Age == minAge);
var maxAge = people.Select(p => p.Age).Max();
var allMaxByAge = people.Where(p => p.Age == maxAge);

FirstOrDefault, LastOrDefault, SingleOrDefault with a default parameter

These three functions returned Default(T) if the element was not found or the list was empty. This could pose a problem or extra checks if the element was not found. Now, we can set a default value when the element is not found and, in this case, we don’t have to deal with null checks:

var firstOrDefault = people.FirstOrDefault(p => p.Age == 25,new Person("Unknown",25));
var lastOrDefault = people.LastOrDefault(p => p.Age == 25,new Person("Unknown",25));
var singleOrDefault = people.SingleOrDefault(p => p.Age == 25,new Person("Unknown",25));

In all the three cases, the code will return a Person with name Unknown and Age = 25

TryGetNonEnumeratedCount

When you have an IEnumerable and you use the Count() method, it will enumerate the collection, even if it has another method to get the count in another way, thus penalizing the performance. For that, .NET 6 implemented the TryGetNonEnumeratedCount method to try to use another method to get the count, if available. This function will return true if a faster method was available, or false, if not. That way, you can take an action to use something more performant and avoid multiple enumerations. For example:

IEnumerable<Person> people7 = people;
Console.WriteLine(people7.TryGetNonEnumeratedCount(out int count));

Will return true, because The List implements the Count property to get the count. When  you have an IEnumerable, result of a Linq operation, like in

var people6 = people.Where(p => includedAges.Contains(p.Age));
Console.WriteLine(people6.TryGetNonEnumeratedCount(out int count1));

It will return false and the count1 variable will have the actual count of the sequence.

Conclusion

As you can see, there are several improvements to Linq in .NET 6. They were not huge improvements, but brought some ease to the development. I’m sure that I will use them a lot.

The sample code for this article is at https://github.com/bsonnino/LinqImprovements

You have an old .NET app and would like to upgrade to .NET 6, to be up-to date with the latest features and go forward with the app.

Every time you think about it, you notice that you have no time to do it and there are more important things to do. And you see that you’re stuck with an old .NET version. But, in this case, Microsoft has helped you to be able to accomplish your New Year’s resolutions. Now, you have the .NET upgrade assistant to help you to migrate to the latest .NET version: the >NET upgrade assistant.

With it, you can migrate your old .NET app to .NET 6 with very simple steps. It may not be a full migration, but it’s a starting point, which eases a lot the migration.

The first step is have dotnet installed in your machine. If you haven’t done so, you can install it from https://dotnet.microsoft.com/en-us/download/dotnet/6.0. Once installed, you can check the installed version by opening a terminal window and  typing the dotnet --versioncommand.

Then, you should install the upgrade assistant with

dotnet tool install -g upgrade-assistant

Once installed, you can use it with

upgrade-assistant

To demonstrate the procedure, I will use the project DiskAnalisys, from https://github.com/bsonnino/DiskAnalysis. This is a .NET 4.5 project and has many features that need to be upgraded: the FolderPicker, Charts, etc. The first step is to clone the project and run the solution in Visual Studio.

When you click the start button, you will be asked for a folder to analyze and, then, a list of files will be shown, ordered by size. On the other tabs, you can see a chart for the extensions and another for the contribution of each file for the total size:

You can check the .NET version in the properties for the project:

Now we can upgrade the project. Open a terminal window, change to the solution’s directory and type

upgrade-assistant analyze diskanalysis.sln

This will show a report for the upgrade of the project:

If you want the detailed output, you can use the -v option to the command line. In the project’s folder, you can see two files, AnalysisReport.sarif, a json file with the report and upgrade-assistant.clef, a copy of what was output to the screen.

The report shows that the project can be updated, so we will update it with

upgrade-assistant upgrade diskanalysis.sln

That will show the steps that will be followed in the upgrade:

You will move from one step to the other by pressing 1. The steps are:

1. Back up project

Backup the project, so it can be used, if the upgrade fails. The program asks for the path for the backup then copies the project

2. Convert project file to SDK style

Converts the csproj file from the old style to the new style. The new file is changed to something like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net452</TargetFramework>
    <OutputType>WinExe</OutputType>
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
    <UseWPF>true</UseWPF>
    <ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
  </PropertyGroup>
  <ItemGroup>
    <AppDesigner Include="Properties\" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="DotNetProjects.Wpf.Toolkit" Version="5.0.31" />
    <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
    <PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
    <PackageReference Include="WPFFolderBrowser" Version="1.0.2" />
  </ItemGroup>
</Project>

3. Clean up NuGet package references

This task will clean up the NuGet package references and add the reference to Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers

4. Update TFM

This task will change the target framework in the csproj to net6.0-windows

5. Update NuGet Packages

This step will check if there are updates in the NuGet packages, update them and add a reference to Microsoft.Windows.Compatibility in the csproj

When you press 1 to go to the next step, the following steps are skipped

6. Add template files
7. Upgrade app config files
a. Convert Application Settings
b. Convert Connection Strings
c. Disable unsupported configuration sections
8. Update source code

a. Apply fix for UA0002: Types should be upgraded
b. Apply fix for UA0012: ‘UnsafeDeserialize()’ does not exist

And the conversion is finished. You must only press 1 to go to the last step:

9. Move to next project

There were no changes in the source code, just in the csproj. If you have your project still opened in Visual Studio, it will ask you to reload it. If you reload it, you will see that it changed to .NET 6 (if you are using VS 2019 you won’t see that, because VS 2019 doesn’t support .NET 6). If you go to the Solution Explorer, you will see that there are two warnings in the dependency packages:

The WPF Toolkit and the WPF Folder Browser packages don’t have the versions for .NET 6, but we’ll run the project to see if it runs fine. Once you run it, you can see that it woks fine, the same way as it did originally. But this time, you get a .NET 6 app running.

We can fix the issues and have a full .NET 6 app running with some extra effort. The WPF Toolkit has been updated and you can use the new packages from https://github.com/dotnetprojects/WpfToolkit. You just have to remove the old package and add the package DotNetProjects.WPF.Toolkit.DataVisualization. For the WPF Folder Browser, you don’t have an updated package, but you can use the source code from https://github.com/McNeight/WpfFolderBrowser.

Clone the repository, run the Upgrade Assistant in the project, to convert it to .NET 6 the same way you did with the main project and add it to the solution. Remove the WPFFolderBrowser package form the project and add a reference for the modified project. When you rebuild the solution, you will get an error Error CA0052 : No targets were selected.

You must edit the project file and remove this property group:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
   <RunCodeAnalysis>true</RunCodeAnalysis>
   <CodeAnalysisRules>
   </CodeAnalysisRules>
   <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
   <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
 </PropertyGroup>

Once you do that, the project compiles fine, but when you try to run it, you get an error saying that WPFFolderBrowserDialog was not found. The only change here is to change the namespace in MainWindow.xaml.cs to WpfFolderBrowser.

Now you can run the project, it runs fine and there are no more compatibility issues.

As you can see, the upgrade assistant eases the migration to .NET 6 a lot. Sometimes, you will need an extra effort to have your app running, but it’s surely easier than doing it by hand. This is a nice step to evolve your apps and port them to the latest version of .NET.

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

In the last post I showed how to transform your Asp.Net Core MVC app into a PWA, thus allowing to install it and access the OS features. We can go a step further and transform it into a native app, using Electron. You may ask “Electron, what’s it ? I’ve never heard of it”. You may never heard of it, but I’m sure that you’ve already used it: VS Code, Teams and Slack are some examples of Electron apps: they are apps developed with web technologies and packaged with the Electron shell.

The two types of applications, although similar, are completely different. A PWA is an app provided by the browser and has the features that are available on the used browser – for example, if you install it from Chrome, it will have the features offered by Chrome (which may not be the same offered by Edge). An Electron app is a native app and is independent of the browser (as a matter of fact, an Electron app creates its own chromium window). It offers full OS interaction and is a real desktop application.

It also have its downsides: the app size is larger, as it will have all the support needed to create a desktop application and you will have to install it like a desktop application. Which one you should choose? It depends on what do you want – if you want a lightweight app with easy install with mostly web features, the a PWA is a way to go. If, on the other side, you want a desktop app that accesses the full OS and does not depend on the installed browser, then Electron is for you.

We will create a new Dotnet 6 Web App and convert it to an Electron app. For that, we will use Electron.NET, a wrapper around the full Electron, which provides a toolset for transforming your ASP.NET apps into an Electron app.

The first step is to create the app in the command line. Open Windows Terminal and type:

dotnet new mvc -o MvcElectron

This will create the app in the MvcElectron folder. Change to that folder and then install the Electron.NET NuGet package, with this command:

dotnet add package electronnet.api

Once you’ve installed it, we must tell to use Electron in the app. Open VS Code (using the command Code .), and, in Program.cs, add:

using ElectronNET.API;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseElectron(args);

Then, in the end of the file we’ll create the main Electron window:

if (HybridSupport.IsElectronActive)
{
    CreateElectronWindow();
}

app.Run();

async void CreateElectronWindow()
{
    var window = await Electron.WindowManager.CreateWindowAsync();
    window.OnClosed += () => Electron.App.Quit();
}

Then, we must install the Electron CLI with

dotnet tool install ElectronNET.CLI -g

Once we do that, we can use the tool using the electronizecommand. We will initialize the project with

electronize init

You should open a command line in the project’s folder. This command will add a manifest and will add it to your project file. Once you do that, you will be able to run it with

electronize start

This is a native app, with an icon and a full menu. You can notice that, in the View menu, you can open the developer tools for the web app. This app is not dependent on any browser, thus you can have any installed browser and it will run the same. The application is built for the OS where you ran the commands: if you are developing on Windows, it’s a Windows app. If you are developing on a Mac or on Linux, it will be a Mac or a Linux app.

Now, let’s customize it. The first customization is to change the window size. The window size is an option that you will pass when creating the main window. In Program.cs, add this code:

async void CreateElectronWindow()
{
    var options = new BrowserWindowOptions
    {
        Width = 1024,
        Height = 1024
    };
    var window = await Electron.WindowManager.CreateWindowAsync(options);
    window.OnClosed += () => Electron.App.Quit();
}

To use this code, you must add ElectronNET.API.Entities to the usings. After you save and restart the app, you will see that the window has the new size.

The next customization is the app menu. Unfortunately, there is no way to add a single menu option to the main menu, you should replace the entire menu. We create the menu before creating the window:

if (HybridSupport.IsElectronActive)
{
    CreateMenu();
    CreateElectronWindow();
}

The CreateMenu function is:

void CreateMenu()
{
    var fileMenu = new MenuItem[]
    {
        new MenuItem { Label = "Home", 
                                Click = () => Electron.WindowManager.BrowserWindows.First().LoadURL($"http://localhost:{BridgeSettings.WebPort}/") },
        new MenuItem { Label = "Privacy", 
                                Click = () => Electron.WindowManager.BrowserWindows.First().LoadURL($"http://localhost:{BridgeSettings.WebPort}/Privacy") },
        new MenuItem { Type = MenuType.separator },
        new MenuItem { Role = MenuRole.quit }
    };

    var viewMenu = new MenuItem[]
    {
        new MenuItem { Role = MenuRole.reload },
        new MenuItem { Role = MenuRole.forcereload },
        new MenuItem { Role = MenuRole.toggledevtools },
        new MenuItem { Type = MenuType.separator },
        new MenuItem { Role = MenuRole.resetzoom },
        new MenuItem { Role = MenuRole.zoomin },
        new MenuItem { Role = MenuRole.zoomout },
        new MenuItem { Type = MenuType.separator },
        new MenuItem { Role = MenuRole.togglefullscreen }
    };

    var menu = new MenuItem[] 
    {
        new MenuItem { Label = "File", Type = MenuType.submenu, Submenu = fileMenu },
        new MenuItem { Label = "View", Type = MenuType.submenu, Submenu = viewMenu }
    };

    Electron.Menu.SetApplicationMenu(menu);
}

As you can see, we added the two pages in the File menu and recreated the View menu. That way, we don’t need the menu and the footer in the main web page. In _Layout.cshtml, you can remove the header and the footer. That way, we are only using the main menu for changing pages.

We can go further and access the computer’s files. But before that, we’ll setup the watch feature, that will watch for any file changes and will reload the app automatically. For that you must start the app with

electronize start /watch

That way, every change will be detected and the app will be updated with no need to restart. In order to run the Dotnet 6 app in watch mode, a small change must be made in the file Properties\launchsettings.json: change the launch profile to use the port used by Electron:

"MvcElectron": {
    "commandName": "Project",
    "dotnetRunMessages": true,
    "launchBrowser": true,
    "applicationUrl": "http://localhost:8001",
    "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
    }
},

Once you do that, you can launch the app in watch mode. Now we can start making our changes in the main page.

We’ll get the 15 larger files in My Documents folder and display them in the page. For that, in the folder Models, create a new file FilesViewModel.cs and add this code:

namespace MvcElectron.Models;

public class FilesViewModel
{
    public List Files => new DirectoryInfo(Path)
        .GetFiles()
        .OrderByDescending(f => f.Length)
        .Take(15)
        .ToList();
        
    public string Path => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
}

In the HomeController.cs file, we must pass the ViewModel to the view:

public IActionResult Index()
{
    return View(new FilesViewModel());
}

We must change the page Index.cshtml with this code:

@model FilesViewModel
@{
    ViewData["Title"] = "Files list";
}

<div>
    <h3>Files List @Model?.Path</h3>
    <table class="table table-sm table-striped">
        <thead class="thead-dark">
            <tr>
                <th scope="col" class="col-sm-4">Name</th>
                <th scope="col" class="col-sm-3">Size</th>
                <th scope="col" class="col-sm-4">Last Write</th>
                <th scope="col" class="col-sm-1"></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model?.Files ?? new List<FileInfo>())
            {
                <tr class="align-middle">
                    <td scope="col" class="col-sm-4">@item.Name</td>
                    <td scope="col" class="col-sm-3">@item.Length</td>
                    <td scope="col" class="col-sm-4">@item.LastWriteTime</td>
                    <td scope="col" class="col-sm-1">
                        <button type="button" class="btn btn-primary" onclick="location.href='@Url.Action("DeleteFile","Home", new {fileName=item.Name})'">
                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
                            class="bi bi-trash" viewBox="0 0 16 16">
                                <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0
                                V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
                                <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1
                                H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1
                                V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" />
                            </svg>
                        </button>
                    </td>
                </tr>
            }
        </tbody>
    </table>
</div>

It will show the files in a table, ordered (the largest come first):

To delete a file, we must add a new action to the Home controller:

public IActionResult DeleteFile(string fileName)
{
    var viewModel = new FilesViewModel();
    viewModel.DeleteFile(fileName);
    return RedirectToAction("Index");
}

The FilesViewModel class must be changed to delete the file:

namespace MvcElectron.Models;

public class FilesViewModel
{
    public List<FileInfo> Files { get; private set; }

    public FilesViewModel()
    {
        Files = GetFiles();
    }

    private List<FileInfo> GetFiles()
    {
        return new DirectoryInfo(Path)
        .GetFiles()
        .OrderByDescending(f => f.Length)
        .Take(15)
        .ToList();
    }

    public string Path => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

    public void DeleteFile(string fileName)
    {
        var filePath = Path + "\\" + fileName;
        if (System.IO.File.Exists(filePath))
        {
            System.IO.File.Delete(filePath);
        }
        Files = GetFiles();
    }
}

Once you do that, you can click on the button to delete a file and the files list is refreshed. There is only one thing to do, now: generate an executable, so we can distribute the file. To do that, we need to use the command:

electronize build /target win

With that, Electron will build a package for Windows (if you need other platforms, you should change the target). It will generate an install file (the target file is pointed in the output), that can be installed and, then, you can run the program.

As you can see, you can transform your Asp.Net MVC app into a native file that can access the computer’s resources as any native app. This is a multi-platform app, you can generate it for Windows, Linux or Mac, with no change at all.

The full code for the app is at https://github.com/bsonnino/MvcElectron