Skip to content
Bruno Sonnino
Menu
  • Home
  • About
Menu

OpenFolderDialog in WPF

Posted on 9 October 2023

One feature that was missing in WPF was the OpenFolderDialog. To circumvent the lack of something to select a folder, there were some methods, but none of them were optimal:

  • Use Windows Forms, which required to add the System.Windows.Forms package
  • Use Win32 API, using P/Invoke or COM interfaces
  • Use a third party component

Version after version, we were still waiting for a native OpenFolderDialog for WPF, and now, in .NET 8.0, finally, it's here!

To implement it, a new base class was introduced, CommonItemDialog, which is the parent of OpenFolderDialog:

Source: https://devblogs.microsoft.com/dotnet/wpf-file-dialog-improvements-in-dotnet-8/

To used it, you must install .NET 8.0 preview 7 or later (you can get it at https://dotnet.microsoft.com/en-us/download/dotnet/8.0). Once you have it installed, we can create a new WPF app to use the OpenFolderDialog:

dotnet new wpf -o OpenFolderDialogWPF
cd OpenFolderDialogWPF
code .
Dos

Then, we can use OpenFolderDialog to select a folder an enumerate all files in it. In MainWindow.xaml, add this code:

<Window x:Class="OpenFolderDialogWPF.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:OpenFolderDialogWPF"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="450"
        Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="40" />
            <RowDefinition Height="*" />
            <RowDefinition Height="30" />
        </Grid.RowDefinitions>
        <Button Width="85"
                Height="30"
                Content="Select Folder"
                Click="SelectFolderClick"
                HorizontalAlignment="Right"
                Margin="5"
                Grid.Row="0" />
        <ListBox x:Name="FilesList"
                 HorizontalContentAlignment="Stretch"
                 Grid.Row="1" />
        <StackPanel Grid.Row="2"
                    Orientation="Horizontal">
            <TextBlock x:Name="TotalFilesText"
                       Margin="5,0"
                       VerticalAlignment="Center" />
            <TextBlock x:Name="LengthFilesText"
                       Margin="5,0"
                       VerticalAlignment="Center" />
        </StackPanel>
    </Grid>
</Window>
XML

We are adding a button to open the dialog to select the folder. Below it, we add a listbox that will contain the file list and a status bar to show the file count and the total size.

The code for the SelectFolderClick event handler for the button click is:

private void SelectFolderClick(object sender, RoutedEventArgs e)
{
    FilesList.Items.Clear();
    var ofd = new OpenFolderDialog();
    if (ofd.ShowDialog() != true)
    {
        FilesList.Items.Add("No folder selected");
        return;
    }
    var selectedPath = ofd.FolderName;
    var files = new DirectoryInfo(selectedPath)
        .GetFiles("*.*", SearchOption.AllDirectories)
        .Select(f => new 
        { 
            Name = Path.GetRelativePath(selectedPath,f.FullName), 
            Size = f.Length 
        });
    TotalFilesText.Text = $"File count: {files.Count()}";
    LengthFilesText.Text = $"Total size: {files.Sum(f => f.Size).ToString("N0")}";
    FilesList.ItemsSource = files
        .OrderByDescending(f => f.Size)
        .Select(f => $"{f.Name} ({f.Size.ToString("N0")})");
}
C#

We are using the new OpenFolderDialog to select a folder. If the user selects a folder, we get an array of files (we are using an anonymous class with the relative path and size of the file), which we use to get the count, total size and fill the listbox. The GetFiles method with the SearchOption.AllDirectories parameter will scan the folder and all subdirectories and get all files. We are ordering the files by descending size (the larger ones will come first).

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

Now we have a way to select folders and we don't need anything extra to manage the dialog. We can now port the project described here to .NET 8.0, and we will use the new component.

For that, clone the repository at https://github.com/bsonnino/FindDuplicates and open it in Visual Studio. This project targets .NET 4.7.2. It uses two Nuget packages, WPFFolderBrowserDialog (which we will replace by the native one) and CRC32C (which is no longer maintained). We'll replace both Nuget packages and we'll see if there are performance improvements.

When I run the code in the original file, I get something like this:

To start the conversion, we will use Dotnet convert assistant, shown in this article.

Once I used the latest version to upgrade the project (if you have an older version installed you can update it with dotnet tool update -g upgrade-assistant), I got a .NET 8 project:

This project still doesn't work, as CRC32C is not available for .NET 8. We will change the OpenFolderDialog and change CRC32.

For the OpenFolderDialog, the change is very simple: remove the line that references the Nuget package in the csproj file and make these changes in StartClick:

private async void StartClick(object sender, RoutedEventArgs e)
{
    var ofd = new OpenFolderDialog();
    if (ofd.ShowDialog() != true)
        return;
    var sw = new Stopwatch();
    sw.Start();
    FilesList.ItemsSource = null;
    var selectedPath = ofd.FolderName;
    var files = await GetPossibleDuplicatesAsync(selectedPath);
    FilesList.ItemsSource = await GetRealDuplicatesAsync(files);
    sw.Stop();
    var allFiles = files.SelectMany(f => f).ToList();
    TotalFilesText.Text = $"{allFiles.Count} files found " +
        $"({allFiles.Sum(f => f.Length):N0} total duplicate bytes) {sw.ElapsedMilliseconds} ms";
}
C#

For the CRC, we can use the Crc32 class in System.IO.Hashing. We will do the same procedure as we did with the CRC32C class: read the file, 10000 bytes at a time and call the Append method to add the bytes to the calculation. At the end, we call the GetCurrentHash method and return the result.

This class will return an array of bytes, and we must convert it to an UInt using the BitConverter class. The code for GetCrc32FromFile is:

public byte[] GetCrc32FromFile(string fileName)
{
    try
    {
        using (var file = new FileStream(fileName, FileMode.Open,FileAccess.Read))
        {
            const int NumBytes = 1000;
            var bytes = new byte[NumBytes];
            var numRead = file.Read(bytes, 0, NumBytes);
            if (numRead == 0)
                return [];
            var crc = new Crc32();
            while (numRead > 0)
            {
                crc.Append(bytes);
                numRead = file.Read(bytes, 0, NumBytes);
            }
            return crc.GetCurrentHash();
        }
    }
    catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException)
    {
        return [];
    }
}
C#

With this code, you have control on the buffer size, but if that's not an issue, you can use another overload to calculate the CRC, using the file stream:

public byte[] GetCrc32FromFile(string fileName)
{
    try
    {
        using (FileStream file = new FileStream(fileName, FileMode.Open))
        {
            var crc = new Crc32();
            crc.Append(file);
            return crc.GetCurrentHash();
        }
    }
    catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException)
    {
        return [];
    }
}
C#

We use this function in GetRealDuplicatesAsync to check if the files with the same size are really duplicates, by comparing their hash codes:

private async Task<Dictionary<uint, List<FileInfo>>> GetRealDuplicatesAsync(
    List<IGrouping<long, FileInfo>> files)
{
    var dictFiles = new ConcurrentDictionary<uint, ConcurrentBag<FileInfo>>();
    await Task.Factory.StartNew(() =>
    {
        Parallel.ForEach(files.SelectMany(f => f), file => 
        {
            var crc = GetCrc32FromFile(file.FullName);
            if (crc.Length > 0)
            {
                var hash = BitConverter.ToUInt32(GetCrc32FromFile(file.FullName),0);
                if (dictFiles.ContainsKey(hash))
                    dictFiles[hash].Add(file);
                else
                    dictFiles.TryAdd(hash, new ConcurrentBag<FileInfo>(new[] { file }));
            }
        });
    });
    return dictFiles.Where(p => p.Value.Count > 1)
        .OrderByDescending(p => p.Value.First().Length)
        .ToDictionary(p => p.Key, p => p.Value.ToList());
}
C#

When you run this code, you will have the same results as before:

The program is converted to .NET 8, it runs exactly the same and we are not using any external components. We are using the OpenFolderDialog and the Crc32 class from .NET.

All the source code for the projects are in https://github.com/bsonnino/OpenFolderDlgWPF

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • May 2025
  • December 2024
  • October 2024
  • August 2024
  • July 2024
  • June 2024
  • November 2023
  • October 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • June 2022
  • April 2022
  • March 2022
  • February 2022
  • January 2022
  • July 2021
  • June 2021
  • May 2021
  • April 2021
  • March 2021
  • February 2021
  • January 2021
  • December 2020
  • October 2020
  • September 2020
  • April 2020
  • March 2020
  • January 2020
  • November 2019
  • September 2019
  • August 2019
  • July 2019
  • June 2019
  • April 2019
  • March 2019
  • February 2019
  • January 2019
  • December 2018
  • November 2018
  • October 2018
  • September 2018
  • August 2018
  • July 2018
  • June 2018
  • May 2018
  • November 2017
  • October 2017
  • September 2017
  • August 2017
  • June 2017
  • May 2017
  • March 2017
  • February 2017
  • January 2017
  • December 2016
  • November 2016
  • October 2016
  • September 2016
  • August 2016
  • July 2016
  • June 2016
  • May 2016
  • April 2016
  • March 2016
  • February 2016
  • October 2015
  • August 2013
  • May 2013
  • February 2012
  • January 2012
  • April 2011
  • March 2011
  • December 2010
  • November 2009
  • June 2009
  • April 2009
  • March 2009
  • February 2009
  • January 2009
  • December 2008
  • November 2008
  • October 2008
  • July 2008
  • March 2008
  • February 2008
  • January 2008
  • December 2007
  • November 2007
  • October 2007
  • September 2007
  • August 2007
  • July 2007
  • Development
  • English
  • Português
  • Uncategorized
  • Windows

.NET AI Algorithms asp.NET Backup C# Debugging Delphi Dependency Injection Desktop Bridge Desktop icons Entity Framework JSON Linq Mef Minimal API MVVM NTFS Open Source OpenXML OzCode PowerShell Sensors Silverlight Source Code Generators sql server Surface Dial Testing Tools TypeScript UI Unit Testing UWP Visual Studio VS Code WCF WebView2 WinAppSDK Windows Windows 10 Windows Forms Windows Phone WPF XAML Zip

  • Entries RSS
  • Comments RSS
©2025 Bruno Sonnino | Design: Newspaperly WordPress Theme
Menu
  • Home
  • About