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

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

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

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

}

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

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

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

You will get something like this:

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

In my setup, I have three monitors:

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

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

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

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

And nothing happened. Then I added this line:

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

Running the project, I got the correct display settings:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    }
    return true;
}

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

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

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

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

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

    }
    return true;
}

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

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

    public DisplayList()
    {
        QueryDisplayDevices();
    }

    public List<Display> Displays => _displays;

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

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

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

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

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

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

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

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

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

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

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

Introduction

Some time ago, I wrote this post  with this accompanying source code. It showed the way to use NTFS file structures to enumerate files and show the files that take more space in your hard disk. Then a user posted a note asking if it would be possible to know if, instead of knowing the files, I could show the directories that consume more space, and I said “of course, you can do it with some minor changes!”, and showed him a sample of the code.

Then I realized that that could be an interesting way to improve the program and show a little bit about working with the data we have – gather the data is important, but work with it to extract every bit of information is even more! So, let’s go to it (if you haven’t already read the original article, do it now).

Working with the data

The original code to retrieve all the files size using the NTFS file structures is:

var ntfsReader =
    new NtfsReader(driveToAnalyze, RetrieveMode.All);
nodes =
    ntfsReader.GetNodes(driveToAnalyze.Name)
        .Where(n => (n.Attributes &
                     (Attributes.Hidden | Attributes.System |
                      Attributes.Temporary | Attributes.Device |
                      Attributes.Directory | Attributes.Offline |
                      Attributes.ReparsePoint | Attributes.SparseFile)) == 0)
        .OrderByDescending(n => n.Size).ToList();

The reader will get all nodes from the selected drive, will filter them, removing the hidden, system, temporary and directories and will sort it by descending order. The ordering will not be necessary for our needs, because we want the directories sizes.

Before working with the data, let’s create a new WPF project that will show our data in a list. In Visual Studio (you should open Visual Studio in elevated mode, because to use the NTFS structures you must have admin rights), create a new WPF project (.NET Framework) and add the dll NTFSReader from this site as a reference to the project. Then, add this code in MainWindow.xaml for our UI:

<Window x:Class="NtfsDirectories.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"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal" Margin="5">
            <TextBlock Text="Drive" VerticalAlignment="Center"/>
            <ComboBox x:Name="DrvCombo" Margin="5,0" Width="100" 
                      VerticalContentAlignment="Center"/>
        </StackPanel>
        <StackPanel Grid.Row="0" HorizontalAlignment="Right" Orientation="Horizontal" Margin="5">
            <TextBlock Text="Level" VerticalAlignment="Center"/>
            <ComboBox x:Name="LevelCombo" Margin="5,0" Width="100" 
                      VerticalContentAlignment="Center" SelectedIndex="3" 
                      SelectionChanged="LevelCombo_OnSelectionChanged">
                <ComboBoxItem>1</ComboBoxItem>
                <ComboBoxItem>2</ComboBoxItem>
                <ComboBoxItem>3</ComboBoxItem>
                <ComboBoxItem>Max</ComboBoxItem>
            </ComboBox>
        </StackPanel>
        <ListBox x:Name="DirectoriesList" Grid.Row="1" 
                 VirtualizingPanel.IsVirtualizing="True"
                 VirtualizingPanel.IsVirtualizingWhenGrouping="True" >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Name}" 
                                   Margin="5,0" Width="450"/>
                        <TextBlock Text="{Binding Size,StringFormat=N0}" 
                                   Margin="5,0" Width="150" TextAlignment="Right"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <TextBlock x:Name="StatusTxt" Grid.Row="2" HorizontalAlignment="Center" Margin="5"/>
    </Grid>
</Window>

That will add two comboboxes, to select the drive and level to drill down and a listbox to show the results. Then, we should add the code to get the drives in the constructor of MainWindow:

public MainWindow()
{
    InitializeComponent();
    var ntfsDrives = DriveInfo.GetDrives()
        .Where(d => d.DriveFormat == "NTFS").ToList();
    DrvCombo.ItemsSource = ntfsDrives;
    DrvCombo.SelectionChanged += DrvCombo_SelectionChanged;
}

This code will fill the drives combo with all the NTFS drives in the system. When the user changes the selection, the drive will be scanned and will get all files:

private IEnumerable<INode> nodes = null;
private async void DrvCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (DrvCombo.SelectedItem == null) return;
    var driveToAnalyze = (DriveInfo)DrvCombo.SelectedItem;
    DrvCombo.IsEnabled = false;
    LevelCombo.IsEnabled = false;
    StatusTxt.Text = "Analyzing drive";
    
    await Task.Factory.StartNew(() => { nodes = GetFileNodes(driveToAnalyze); });

    LevelCombo.SelectedIndex = -1;
    LevelCombo.SelectedIndex = 3;

    DrvCombo.IsEnabled = true;
    LevelCombo.IsEnabled = true;
    StatusTxt.Text = "";
}

You will notice the fact that I’m setting the LevelCombo’s SelectedIndex to -1 then to 3.I do that to achieve two things: clean the directories list when the drive changes and trigger the change event for the level combo. The GetFileNodes method will scan the drive and will return a list of files in that drive:

private static IEnumerable<INode> GetFileNodes(DriveInfo driveToAnalyze)
{
    var ntfsReader =
        new NtfsReader(driveToAnalyze, RetrieveMode.All);
    return
        ntfsReader.GetNodes(driveToAnalyze.Name)
            .Where(n => (n.Attributes &
                         (Attributes.Hidden | Attributes.System |
                          Attributes.Temporary | Attributes.Device |
                          Attributes.Directory | Attributes.Offline |
                          Attributes.ReparsePoint | Attributes.SparseFile)) == 0);
}

Now, we must get the directories sizes depending on the level the user selected and add them to the listbox. This is done when the level changes in the combobox:

private void LevelCombo_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (DirectoriesList == null)
        return;
    int level = Int32.MaxValue-1;
    if (LevelCombo.SelectedIndex < 0 || nodes == null)
        DirectoriesList.ItemsSource = null;
    else if (LevelCombo.SelectedIndex < 3)
        level = LevelCombo.SelectedIndex;
    DirectoriesList.ItemsSource = GetDirectorySizes(nodes, level+1);
}

As you can see, when the level is negative, I clean the results list, and when there is a level set, I call the GetDirectoriesSizes method. If the user selects the Max level, I set the level to Int32.MaxValue. The GetDirectoriesSizes method is:

private IList<DirectorySize> GetDirectorySizes(IEnumerable<INode> nodes, int level)
{
     var directories = nodes
        .Select(n => new
        {
            Path = string.Join(Path.DirectorySeparatorChar.ToString(),
                Path.GetDirectoryName(n.FullName)
                    .Split(Path.DirectorySeparatorChar)
                    .Take(level)),
            Node = n
        })
        .GroupBy(g => g.Path)
        .Select(g => new DirectorySize(g.Key, g.Sum(d => (Int64)d.Node.Size)))
        .OrderByDescending(d => d.Size)
        .ToList();
      return directories;
}

In this case, I use LINQ to group the files in the desired level. The first Select will break the path into its parts, then will take the parts only to the level we want and rejoin the parts. This will allow us to group the files according to the wanted level. One gotcha is that, when we select level 3, for example, we will want to show all files grouped to the level 3, but we will also want those that are on the upper levels. For example, for the level 3, we will want to show the files in C:\, C:\Temp, C:\Program Files\Windows and so on. If the file is under a path more than 3 levels down, it will be grouped with the upper directory.

When you run this project, you will have something like this:

Conclusion

As you can see, once we have the data, we can work on it and obtain different views. One thing that helps a lot in C# is the use of LINQ, which allows to transform the data in a simple way. The function that we used can be extended to any level you want and will bring the good results.

All the source code for this project is in https://github.com/bsonnino/NtfsDirectories

A very common issue when dealing with console applications is to parse the command line. I am a huge fan of command line applications, especially when I want to add an operation to my DevOps pipeline that is not provided by the tool I’m using, Besides that, there are several applications that don’t need user interaction or any special UI, so I end up creating a lot of console applications. But one problem that always arises is parsing the command line. Adding switches and help to the usage is always a pain and many times I resort to third party utils (CommandLineUtils is one of them, for example).

But this make me resort to non-standard utilities and, many times, I use different libraries, in a way that it poses me a maintenance issue: I need to remember the way that the library is used in order to make changes in the app. Not anymore. It seems that Microsoft has seen this as a problem and has designed its own library: System.CommandLine.

Using System.CommandLine

The first step to use this library is to create a console application. Open Visual Studio and create a new console application. The default application is a very simple one that prints Hello World on the screen.

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

If you want to process the command line arguments, you could do something like this:

static void Main(string[] args)
{
    Console.WriteLine($"Hello {(args.Length > 0 ? args[0] : "World")}!");
}

This program will take the first argument (if it exists) and print a Hello to it:

This is ok, but I’m almost sure that’s not what you want.

If you want to add a switch, you will have  to parse the arguments, check which ones start with a slash or an hyphen, check the following parameter (for something like “–file Myfile.txt”). Not an easy task, especially if there are a lot of switches available.

Then, there is System.CommandLine that comes to the rescue. You must add the System.CommandLine NuGet package to the project. Then, you can add a command handler to add your options and process them, like this:

static async Task Main(string[] args)
{
    var command = new RootCommand
    {
        new Option(new [] {"--verbose", "-v"}),
        new Option("--numbers") { Argument = new Argument<int[]>() }
    };

    command.Handler = CommandHandler.Create(
        (bool verbose, int[] numbers) =>
        {
            if (numbers != null)
            {
                if (verbose)
                    Console.WriteLine($"Adding {string.Join(' ', numbers)}");

                Console.WriteLine(numbers.Sum());
            }
            else
            {
                if (verbose)
                    Console.WriteLine("No numbers to sum");
            }
        });
    await command.InvokeAsync(args);
}

As you can see, there are several parts to the process:

Initially you declare a RootCommand with the options you want. The parameter to the Option constructor has the aliases to the option. The first option is a switch with to aliases, –verbose  and -v. That way, you can enable it using any of the two ways or passing a boolean argument (like in -v false). The second parameter will be an array of ints. This is specified in the generic type of the argument. In our case, we expect to have an array of ints. In this case, if you pass a value that cannot be parsed to an int, you will receive an error:

As you can see in the image above, you get the help and version options for free.

The second part is the handler, a function that will receive the parsed parameters and use them in the program. Finally, you will call the handler with:

await command.InvokeAsync(args);

Once you execute the program, it will parse your command line and will pass the arguments to the handler, where you can use them. In our handler, I am using the verbose switch to show extra info, and I’m summing the numbers. If anything is wrong, like unknown parameters or invalid data, the program will show an error message and the help.

Right now, I’ve used simple parameters, but we can also pass some special ones. As it’s very common to pass file or directory names in the command line, you can accept FileInfo and DirectoryInfo parameters, like in this code:

static async Task Main(string[] args)
{
    var command = new RootCommand
    {
        new Argument<DirectoryInfo>("Directory", 
            () => new DirectoryInfo("."))
            .ExistingOnly()
    };

    command.Handler = CommandHandler.Create(
        (DirectoryInfo directory) =>
        {
            foreach (var file in directory.GetFiles())
            {
               Console.WriteLine($"{file.Name,-40} {file.Length}");
            }
        });
    await command.InvokeAsync(args);
}

Here, you will have only one argument, a DirectoryInfo. If it isn’t passed, the second parameter in the Argument constructor is the function that will get a default argument to the handler, a DirectoryInfo pointing to the current directory. Then, it will list all the files in the selected directory. One interesting thing is the ExistingOnly method. It will ensure that the directory exists. If it doesn’t exist, an error will be generated:

If the class has a constructor receiving a string, you can also use it as an argument, like in the following code:

static async Task Main(string[] args)
{
    var command = new RootCommand
    {
        new Argument<StreamReader>("stream")
    };

    command.Handler = CommandHandler.Create(
        (StreamReader stream) =>
        {
            var fileContent = stream.ReadToEnd();
            Console.WriteLine(fileContent);
        });
    await command.InvokeAsync(args);
}

In this case, the argument is a StreamReader, that is created directly from a file name. Then, I use the ReadToEnd method to read the file into a string, and then show the contents in the console.

You can also pass a complex class and minimize the number of parameters that are passed to the handler.

All this is really fine, we now have a great method to parse the command line, but it’s a little clumsy: create a command, then declare a handler to process the commands and then call the InvokeAsync method to process the parameters and execute the code. Wouldn’t it be better to have something easier to parse the command line? Well, there is. Microsoft went further and created DragonFruit – with it, you can add your parameters directly in the Main method.

Instead of adding the System.CommandLine NuGet package, you must add the System.CommandLine.DragonFruit package and, all of a sudden, your Main method can receive the data you want as parameters. For example, the first program will turn into:

static void Main(bool verbose, int[] numbers)
{
    if (numbers != null)
    {
        if (verbose)
            Console.WriteLine($"Adding {string.Join(' ', numbers)}");

        Console.WriteLine(numbers.Sum());
    }
    else
    {
        if (verbose)
            Console.WriteLine("No numbers to sum");
    }
}

If you run it, you will have something like this:

If you notice the code, it is the same as the handler. What Microsoft is doing, under the hood, is to hide all this boilerplate from you and calling your main program with the parameters you are defining. If you want to make a parameter an argument, with no option, you should name it as argumentargs or arguments:

static void Main(bool verbose, int[] args)
{
    if (args != null)
    {
        if (verbose)
            Console.WriteLine($"Adding {string.Join(' ', args)}");

        Console.WriteLine(args.Sum());
    }
    else
    {
        if (verbose)
            Console.WriteLine("No numbers to sum");
    }
}

 

You can also define default values with the exactly same way you would do with other methods:

static void Main(int number = 10, bool verbose = false)
{
    var primes = GetPrimesLessThan(number);
    Console.WriteLine($"Found {primes.Length} primes less than {number}");
    Console.WriteLine($"Last prime last than {number} is {primes.Last()}");
    if (verbose)
    {
        Console.WriteLine($"Primes: {string.Join(' ',primes)}");
    }
}

private static int[] GetPrimesLessThan(int maxValue)
{
    if (maxValue <= 1)
        return new int[0];
    ;
    var primeArray = Enumerable.Range(0, maxValue).ToArray();
    var sizeOfArray = primeArray.Length;

    primeArray[0] = primeArray[1] = 0;

    for (int i = 2; i < Math.Sqrt(sizeOfArray - 1) + 1; i++)
    {
        if (primeArray[i] <= 0) continue;
        for (var j = 2 * i; j < sizeOfArray; j += i)
            primeArray[j] = 0;
    }

    return primeArray.Where(n => n > 0).ToArray();
}

If you want more help in your app, you can add xml comments in the app, they will be used when the help is requested:

/// <summary>
/// Lists files of the selected directory.
/// </summary>
/// <param name="argument">The directory name</param>
static void Main(DirectoryInfo argument)
{
    argument ??= new DirectoryInfo(".");
    foreach (var file in argument.GetFiles())
    {
        Console.WriteLine($"{file.Name,-40} {file.Length}");
    }
}

As you can see, there are a lot of options for command line processing, and DragonFruit makes it easier to process them. But that is not everything, the same team has also made some enhancements to make use of the new Ansi terminal features, in System.CommandLine.Rendering. For example, if you want to list the files in a table, you can use code like this:

static void Main(InvocationContext context, DirectoryInfo argument= null)
{
    argument ??= new DirectoryInfo(".");
    var consoleRenderer = new ConsoleRenderer(
        context.Console,
        context.BindingContext.OutputMode(),
        true);

    var tableView = new TableView<FileInfo>
    {
        Items = argument.EnumerateFiles().ToList()
    };

    tableView.AddColumn(f => f.Name, "Name");

    tableView.AddColumn(f => f.LastWriteTime, "Modified");

    tableView.AddColumn(f => f.Length, "Size");

    var screen = new ScreenView(consoleRenderer, context.Console) { Child = tableView };
    screen.Render();
}

As you can see, the table is formatted for you. You may have noticed another thing: the first parameter, context, is not entered by the user. This is because you can have some variables injected for you by DragonFruit.

There are a lot of possibilities with System.CommandLine and this is a very welcome addition. I really liked it and, although it’s still on preview, it’s very nice and I’m sure I’ll use it a lot. And you, what do you think?

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

Many times I need to enumerate the files in my disk or in a folder and subfolders, but that always has been slow. All the file enumeration techniques go through the disk structures querying the file names and going to the next one. With the Windows file indexing, this has gone to another level of speed: you can query and filter your data almost instantaneously, with one pitfall: it only works in the indexed parts of the disk (usually the libraries and Windows folders), being unusable for your data folders, unless you add them to the indexer:

Wouldn’t it be nice to have a database that stores all files in the system and is updated as the files change? Some apps, like Copernic Desktop Search or X1 Search do exactly that: they have a database that indexes your system and can do fast queries for you. The pitfall is that you don’t have an API to integrate to your programs, so the only way you have is to query the files is to use their apps.

At some time, Microsoft thought of doing something like a database of files, creating what was called WinFS – Windows Future Storage, but the project was cancelled. So, we have to stick with our current APIs to query files. Or no? As a matter of fact there is something in the Windows system that allows us to query the files in a very fast way, and it’s called the NTFS MFT (NT file system master file table).

The NTFS MFT is a file structure use internally by Windows that allows querying files in a very fast way. It was designed to be fast and safe (there are two copies of the MFT, in case one of them gets corrupt), and we can access it to get our files enumerated. But some things should be noted when accessing the MFT:

  • The MFT is only available for NTFS volumes. So, you cannot access FAT drives with this API
  • To access the MFT structures, you must have elevated privileges – a normal user won’t be able to access it
  • With great power comes great responsibility (this is a SpiderMan comic book quote), so you should know that accessing the internal NTFS structures may harm you system irreversively – use the code with care, and don’t blame me if something goes wrong (but here’s a suggestion to Windows API designers: why not create some APIs that query the NTFS structures safely for normal users? That could be even be added to UWP programming).

Acessing the MFT structure

There is no formal API to access the MFT structure. You will have to decipher the structure (there is a lot of material here) and access the data using raw disk data read (that’s why you need elevated privileges). This is a lot of work and hours of trial and error.

Fortunately, there are some libraries that do that in C#, and I’ve used this one, which is licensed as LGPL. You can use the library in your compiled work as a library, with no restriction. If you include the library source code in your code, it will be “derived work” and you must distribute all code as LGPL.

We will create a WPF program that will show the disk usage. It will enumerate all files and show them in the list, so you can see what’s taking space in your disk. You will be able to select any of the NTFS disks in your machine.

Open Visual Studio with administrative rights (this is very important, or you won’t be able to debug your program). Then create a new WPF project and add a new item. Choose Application Manifest File, you will have an app.manifest file added to your project. Then, you must change the requestedExecutionLevel tag of the file to:

<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />

The next step is to detect the NTFS disks in your system. This is done with this code:

var ntfsDrives = DriveInfo.GetDrives()
    .Where(d => d.DriveFormat == "NTFS").ToList();

Then, add the NtfsReader project to the solution and add a reference to it in the WPF project. Then, add the following UI in MainWindow.xaml.cs:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="40"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="30"/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal" Margin="5">
        <TextBlock Text="Drive" VerticalAlignment="Center"/>
        <ComboBox x:Name="DrvCombo" Margin="5,0" Width="100" 
                  VerticalContentAlignment="Center"/>
    </StackPanel>
    <ListBox x:Name="FilesList" Grid.Row="1" 
             VirtualizingPanel.IsVirtualizing="True"
             VirtualizingPanel.IsVirtualizingWhenGrouping="True"
             >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding FullName}" 
                               Margin="5,0" Width="450"/>
                    <TextBlock Text="{Binding Size,StringFormat=N0}" 
                               Margin="5,0" Width="150" TextAlignment="Right"/>
                    <TextBlock Text="{Binding LastChangeTime, StringFormat=g}" 
                               Margin="5,0" Width="200"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <TextBlock x:Name="StatusTxt" Grid.Row="2" HorizontalAlignment="Center" Margin="5"/>
</Grid>

We will have a combobox with all drives in the first row and a listbox with the files. The listbox has an ItemTemplate that will show the name, size and date of last change of each file. To fill this data, you will have to add this code in MainWindow.xaml.cs:

public MainWindow()
{
    InitializeComponent();
    var ntfsDrives = DriveInfo.GetDrives()
        .Where(d => d.DriveFormat == "NTFS").ToList();
    DrvCombo.ItemsSource = ntfsDrives;
    DrvCombo.SelectionChanged += DrvCombo_SelectionChanged;
}

private void DrvCombo_SelectionChanged(object sender, 
    System.Windows.Controls.SelectionChangedEventArgs e)
{
    if (DrvCombo.SelectedItem != null)
    {
        var driveToAnalyze = (DriveInfo) DrvCombo.SelectedItem;
        var ntfsReader =
            new NtfsReader(driveToAnalyze, RetrieveMode.All);
        var nodes =
            ntfsReader.GetNodes(driveToAnalyze.Name)
                .Where(n => (n.Attributes & 
                             (Attributes.Hidden | Attributes.System | 
                              Attributes.Temporary | Attributes.Device | 
                              Attributes.Directory | Attributes.Offline | 
                              Attributes.ReparsePoint | Attributes.SparseFile)) == 0)
                .OrderByDescending(n => n.Size);
        FilesList.ItemsSource = nodes;
    }
}

It gets all NTFS drives in your system and fills the combobox. In the SelectionChanged event handler, the reader gets all nodes in the drive. These nodes are filtered to remove all that are not normal files and then ordered descending by size and added to the listbox.

If you run the program you will see some things:

  • If you look at the output window in Visual Studio, you will see these debug messages:
1333.951 MB of volume metadata has been read in 26.814 s at 49.748 MB/s
1324082 nodes have been retrieved in 2593.669 ms

This means that it took 2.6s to read and analyze all files in the disk (pretty fast for 1.3 million files, no?).

  • When you change the drive in the combobox, the program will freeze for some time and the list will be filled with the files. The freezing is due to the fact that you are blocking the main thread while you are analyzing the disk. To avoid this, you should run the code in a secondary thread, like this code:
private async void DrvCombo_SelectionChanged(object sender, 
    System.Windows.Controls.SelectionChangedEventArgs e)
{
    if (DrvCombo.SelectedItem != null)
    {
        var driveToAnalyze = (DriveInfo) DrvCombo.SelectedItem;
        DrvCombo.IsEnabled = false;
        StatusTxt.Text = "Analyzing drive";
        List<INode> nodes = null;
        await Task.Factory.StartNew(() =>
        {
            var ntfsReader =
                new NtfsReader(driveToAnalyze, RetrieveMode.All);
            nodes =
                ntfsReader.GetNodes(driveToAnalyze.Name)
                    .Where(n => (n.Attributes &
                                 (Attributes.Hidden | Attributes.System |
                                  Attributes.Temporary | Attributes.Device |
                                  Attributes.Directory | Attributes.Offline |
                                  Attributes.ReparsePoint | Attributes.SparseFile)) == 0)
                    .OrderByDescending(n => n.Size).ToList();
        });
        FilesList.ItemsSource = nodes;
        DrvCombo.IsEnabled = true;
        StatusTxt.Text = $"{nodes.Count} files listed. " +
                         $"Total size: {nodes.Sum(n => (double)n.Size):N0}";
    }
}

This code creates a task and runs the analyzing code in it, and doesn’t freeze the UI. I just took care of disabling the combobox and putting a warning for the user. After the code is run, the nodes list is assigned to the listbox and the UI is re-enabled.

This code can show you the list of the largest files in your disk, but you may want to analyze it by other ways, like grouping by extension or by folder. WPF has an easy way to group and show data: the CollectionViewSource. With it, you can do grouping and sorting in the ListBox. We will change our UI to add a new ComboBox to show the new groupings:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="40"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="30"/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal" Margin="5">
        <TextBlock Text="Drive" VerticalAlignment="Center"/>
        <ComboBox x:Name="DrvCombo" Margin="5,0" Width="100" 
                  VerticalContentAlignment="Center"/>
    </StackPanel>
    <StackPanel Grid.Row="0" HorizontalAlignment="Right" Orientation="Horizontal" Margin="5">
        <TextBlock Text="Sort" VerticalAlignment="Center"/>
        <ComboBox x:Name="SortCombo" Margin="5,0" Width="100" 
                  VerticalContentAlignment="Center" SelectedIndex="0" 
                  SelectionChanged="SortCombo_OnSelectionChanged">
            <ComboBoxItem>Size</ComboBoxItem>
            <ComboBoxItem>Extension</ComboBoxItem>
            <ComboBoxItem>Folder</ComboBoxItem>
        </ComboBox>
    </StackPanel>
    <ListBox x:Name="FilesList" Grid.Row="1" 
             VirtualizingPanel.IsVirtualizing="True"
             VirtualizingPanel.IsVirtualizingWhenGrouping="True" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding FullName}" 
                               Margin="5,0" Width="450"/>
                    <TextBlock Text="{Binding Size,StringFormat=N0}" 
                               Margin="5,0" Width="150" TextAlignment="Right"/>
                    <TextBlock Text="{Binding LastChangeTime, StringFormat=g}" 
                               Margin="5,0" Width="200"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
        <ListBox.GroupStyle>
            <GroupStyle>
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Name}" FontSize="15" FontWeight="Bold" 
                                       Margin="5,0"/>
                            <TextBlock Text="(" VerticalAlignment="Center" Margin="5,0,0,0" />
                            <TextBlock Text="{Binding Items.Count}" VerticalAlignment="Center"/>
                            <TextBlock Text=" files - " VerticalAlignment="Center"/>
                            <TextBlock Text="{Binding Items, 
                                Converter={StaticResource ItemsSizeConverter}, StringFormat=N0}"
                                         VerticalAlignment="Center"/>
                            <TextBlock Text=" bytes)" VerticalAlignment="Center"/>
                        </StackPanel>
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </ListBox.GroupStyle>
    </ListBox>
    <TextBlock x:Name="StatusTxt" Grid.Row="2" HorizontalAlignment="Center" Margin="5"/>
</Grid>

The combobox has three options, Size, Extension and Folder. The first one is the same thing we’ve had until now; the second will group the files by extension and the third will group the files by top folder. We’ve also added a GroupStyle to the listbox. If we don’t do that, the data will be grouped, but the groups won’t be shown. If you notice the GroupStyle, you will see that we’re adding the name, then the count of the items (number of files in the group), then we have a third TextBox where we pass the Items and a converter. That’s because we want to show the total size in bytes of the group. For that, I’ve created a converter that converts the Items in the group to the sum of the bytes of the file:

public class ItemsSizeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, 
        CultureInfo culture)
    {
        var items = value as ReadOnlyObservableCollection<object>;
        return items?.Sum(n => (double) ((INode)n).Size);
    }

    public object ConvertBack(object value, Type targetType, object parameter, 
        CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

The code for the SelectionChanged for the sort combobox is:

private void SortCombo_OnSelectionChanged(object sender, 
    SelectionChangedEventArgs e)
{
    if (_view == null)
        return;
    _view.GroupDescriptions.Clear();
    _view.SortDescriptions.Clear();
    switch (SortCombo.SelectedIndex)
    {
        case 1:
            _view.GroupDescriptions.Add(new PropertyGroupDescription("FullName", 
                new FileExtConverter()));
            break;
        case 2:
            _view.SortDescriptions.Add(new SortDescription("FullName",
                ListSortDirection.Ascending));
            _view.GroupDescriptions.Add(new PropertyGroupDescription("FullName", 
                new FilePathConverter()));
            break;
    }
}

We add GroupDescriptions for each kind of group. As we don’t have the extension and top path properties in the nodes shown in the listbox, I’ve created two converters to get these from the full name. The converter that gets the extension from the name is:

class FileExtConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, 
        CultureInfo culture)
    {
        var fileName = value as string;
        return string.IsNullOrWhiteSpace(fileName) ? 
            null : 
            Path.GetExtension(fileName).ToLowerInvariant();
    }

    public object ConvertBack(object value, Type targetType, object parameter, 
        CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

The converter that gets the top path of the file is:

class FilePathConverter :IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, 
        CultureInfo culture)
    {
        var fileName = value as string;
        return string.IsNullOrWhiteSpace(fileName) ?
            null :
            GetTopPath(fileName);
    }

    private string GetTopPath(string fileName)
    {
        var paths = fileName.Split(Path.DirectorySeparatorChar).Take(2);
        return string.Join(Path.DirectorySeparatorChar.ToString(), paths);
    }

    public object ConvertBack(object value, Type targetType, object parameter, 
        CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

One last thing is to create the _view field, when we are filling the listbox:

 private async void DrvCombo_SelectionChanged(object sender, 
     System.Windows.Controls.SelectionChangedEventArgs e)
 {
     if (DrvCombo.SelectedItem != null)
     {
         var driveToAnalyze = (DriveInfo) DrvCombo.SelectedItem;
         DrvCombo.IsEnabled = false;
         StatusTxt.Text = "Analyzing drive";
         List<INode> nodes = null;
         await Task.Factory.StartNew(() =>
         {
             var ntfsReader =
                 new NtfsReader(driveToAnalyze, RetrieveMode.All);
             nodes =
                 ntfsReader.GetNodes(driveToAnalyze.Name)
                     .Where(n => (n.Attributes &
                                  (Attributes.Hidden | Attributes.System |
                                   Attributes.Temporary | Attributes.Device |
                                   Attributes.Directory | Attributes.Offline |
                                   Attributes.ReparsePoint | Attributes.SparseFile)) == 0)
                     .OrderByDescending(n => n.Size).ToList();
         });
         FilesList.ItemsSource = nodes;
         _view = (CollectionView)CollectionViewSource.GetDefaultView(FilesList.ItemsSource);

         DrvCombo.IsEnabled = true;
         StatusTxt.Text = $"{nodes.Count} files listed. " +
                          $"Total size: {nodes.Sum(n => (double)n.Size):N0}";
     }
     else
     {
         _view = null;
     }
 }

With all these in place, you can run the app and get a result like this:

Conclusions

As you can see, there is a way to get fast file enumeration for your NTFS disks, but you must have admin privileges to use it. We’ve created a WPF program that uses this kind of enumeration and allows you to group the data in different ways, using the WPF resources. If you need to enumerate  your files very fast, you can consider this way to do it.

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

One thing that has been recently announced by Microsoft is the availability of .NET Core 3. With it, you will be able to create WPF and Winforms apps with .NET Core. And one extra bonus is that both WPF and Winforms are being open sourced. You can check these in https://github.com/dotnet/wpf and https://github.com/dotnet/winforms.

The first step to create a .NET Core WPF program is to download the .NET Core 3.0 preview from https://dotnet.microsoft.com/download/dotnet-core/3.0. Once you have it installed, you can check that it was installed correctly by open a Command Line window and typing dotnet –info and seeing the installed version:

:

With that in place, you can change the current folder to a new folder and type

dotnet new wpf
dotnet run

This will create a new .NET Core 3.0 WPF project and will compile and run it. You should get something like this:

If you click on the Exit button, the application exits. If you take a look at the folder, you will see that it generated the WPF project file, App.xaml and App.xaml.cs, MainWindow.xaml and MainWindow.xaml.cs. The easiest way to edit these files is to use Visual Studio Code. Just open Visual Studio Code and go to menu File/Open Folder and open the folder for the project. There you will see the project files and will be able to run and debug your code:

A big difference can be noted in the csproj file. If you open it, you will see something like this:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

That’s very simple and there’s nothing else in the project file. There are some differences between this project and other types of .NET Core, like the console one:

  • The output type is WinExe, and not Exe, in the console app
  • The UseWPF clause is there and it’s set to true

Now, you can modify and run the project inside VS Code. Modify MainWindow.xaml and put this code in it:

<Window x:Class="DotNetCoreWPF.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:DotNetCoreWPF" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="40"/>
        </Grid.RowDefinitions>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>

            <TextBlock Text="Id"      Grid.Column="0" Grid.Row="0" Margin="5" VerticalAlignment="Center"/>
            <TextBlock Text="Name"    Grid.Column="0" Grid.Row="1" Margin="5" VerticalAlignment="Center"/>
            <TextBlock Text="Address" Grid.Column="0" Grid.Row="2" Margin="5" VerticalAlignment="Center"/>
            <TextBlock Text="City"    Grid.Column="0" Grid.Row="3" Margin="5" VerticalAlignment="Center"/>
            <TextBlock Text="Email"   Grid.Column="0" Grid.Row="4" Margin="5" VerticalAlignment="Center"/>
            <TextBlock Text="Phone"   Grid.Column="0" Grid.Row="5" Margin="5" VerticalAlignment="Center"/>
            <TextBox Grid.Column="1" Grid.Row="0" Margin="5"/>
            <TextBox Grid.Column="1" Grid.Row="1" Margin="5"/>
            <TextBox Grid.Column="1" Grid.Row="2" Margin="5"/>
            <TextBox Grid.Column="1" Grid.Row="3" Margin="5"/>
            <TextBox Grid.Column="1" Grid.Row="4" Margin="5"/>
            <TextBox Grid.Column="1" Grid.Row="5" Margin="5"/>
        </Grid>
        <Button Content="Submit" Width="65" Height="35" Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="5,0"/>
    </Grid>
</Window>

Now, you can compile and run the app in VS Code with F5, and you will get something like this:

If you don’t want to use Visual Studio Code, you can edit your project in Visual Studio 2019. The first preview still doesn’t have a visual editor for the XAML file, but you can edit the XAML file in the editor, it will work fine.

Porting a WPF project to .NET Core

To port a WPF project to .NET Core, you should run the Portability Analyzer tool first, to see what problems you will find before porting it to .NET Core. This tool can be found here. You can download it and run on your current application, and check what APIs that are not portable.

I will be porting my DiskAnalisys project. This is a simple project, that uses the File.IO functions to enumerate the files in a folder and uses two NuGet packages to add a Folder Browser and Charts to WPF. The first step is to run the portability analysis on it. Run the PortabilityAnalizer app and point it to the folder where the executable is located:

When you click on the Analyze button, it will analyze the executable and generate an Excel spreadsheet with the results:

As you can see, all the code is compatible with .NET Core 3.0. So, let’s port it to .NET Core 3.0. I will show you three ways to do it: creating a new project, updating the .csproj file and using a tool.

Upgrading by Creating a new project

This way needs the most work, but it’s the simpler to fix. Just create a new folder and name it DiskAnalysisCorePrj. Then open a command line window and change the directory to the folder you’ve created. Then, type these commands:

dotnet new wpf
dotnet add package wpffolderbrowser
dotnet add package dotnetprojects.wpf.toolkit
dotnet run

These commands will create the WPF project, add the two required NuGet packages and run the default app. You may see a warning like this:

D:\Documentos\Artigos\Artigos\CSharp\WPFCore\DiskAnalysisCorePrj\DiskAnalysisCorePrj.csproj : warning NU1701: Package 'DotNetProjects.Wpf.Toolkit 5.0.43' was restored using '.NETFramework,Version=v4.6.1' instead of the project target framework '.NETCoreApp,Version=v3.0'. This package may not be fully compatible with your project.
D:\Documentos\Artigos\Artigos\CSharp\WPFCore\DiskAnalysisCorePrj\DiskAnalysisCorePrj.csproj : warning NU1701: Package 'WPFFolderBrowser 1.0.2' was restored using '.NETFramework,Version=v4.6.1' instead of the project target framework '.NETCoreApp,Version=v3.0'. This package may not be fully compatible with your project.
D:\Documentos\Artigos\Artigos\CSharp\WPFCore\DiskAnalysisCorePrj\DiskAnalysisCorePrj.csproj : warning NU1701: Package 'DotNetProjects.Wpf.Toolkit 5.0.43' was restored using '.NETFramework,Version=v4.6.1' instead of the project target framework '.NETCoreApp,Version=v3.0'. This package may not be fully compatible with your project.
D:\Documentos\Artigos\Artigos\CSharp\WPFCore\DiskAnalysisCorePrj\DiskAnalysisCorePrj.csproj : warning NU1701: Package 'WPFFolderBrowser 1.0.2' was restored using '.NETFramework,Version=v4.6.1' instead of the project target framework '.NETCoreApp,Version=v3.0'. This package may not be fully compatible with your project.

This means that the NuGet packages weren’t converted to .NET Core 3.0, but they are still usable (remember, the compatibility report showed 100% compatibility). Then, copy MainWindow.xaml and MainWindow.xaml.cs from the original folder to the new one. We don’t need to copy any other files, as no other files were changed. Then, type

dotnet run

and the program is executed:

Converting by Changing the .csproj file

This way is very simple, just changing the project file, but can be challenging, especially for very large projects. Just create a new folder and name it DiskAnalysisCoreCsp. Copy all files from the main folder of the original project (there’s no need of copying the Properties folder) and edit the .csproj file, changing it to:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="dotnetprojects.wpf.toolkit" Version="5.0.43" />
    <PackageReference Include="wpffolderbrowser" Version="1.0.2" />
  </ItemGroup>
</Project>

Then, type

dotnet run

and the program is executed.

Converting using a tool

The third way is to use a tool to convert the project. You must install the conversion extension created by Brian Lagunas, from here. Then, open your WPF project in Visual Studio, right-click in the project and select “Convert Project to .NET Core 3”.

That’s all. You now have a NET Core 3 app. If you did that in Visual Studio 2017, you won’t be able to open the project, you will need to compile it with dotnet run, or open it in Visual Studio code.

Conclusions

As you can see, although this is the first preview of WPF .NET Core, it has a lot of work done, and you will be able to port most of your WPF projects to .NET Core.