After some time, I needed to analyze the disk space of my C: drive and used the program I have developed in my previous post and I got an “UnauthorizedAccessException”. After some thinking, I realized that was happening because of some paths that I have not access unless I am an admin.

Running as admin is not a good thing to do and, so, I thought of other ways to get the disk space. Soon, I realized that using GetFiles for all directories, although it was the easiest way to get the information, wasn’t the way to do it: at some point, there would be some path with no access and that exception would be thrown.

If I was using Powershell, I could use the -ErrorAction “SilentlyContinue” to continue without an error message, but there is no such thing in the GetFiles method (here is a suggestion for the .NET Framework designers Smile).

So, I had to enumerate all files, directory by directory and process any exception thrown. I came up to a recursive function like this one:

[sourcecode language=”csharp” padlinenumbers=”true”]
private List<FileInfo> GetFilesInDirectory(string directory)
{
    var files = new List<FileInfo>();
    try
    {
        var directories = Directory.GetDirectories(directory);
        try
        {
            var di = new DirectoryInfo(directory);
            files.AddRange(di.GetFiles("*"));
        }
        catch
        {
        }
        foreach (var dir in directories)
        {
            files.AddRange(GetFilesInDirectory(Path.Combine(directory, dir)));
        }
    }
    catch
    {
    }
 
    return files;
}
[/sourcecode]

Initially, I get all directories in the selected path, then I get all files for that directory and process the subdirectories recursively. All exceptions are caught and swallowed, so the processing doesn’t stop if I don’t have access to the folder. As I made this change, I made another one: the previous code was blocking the program while I was enumerating the files, so I used a Task to enumerate the files in the background:

[sourcecode language=”csharp”]
List<FileInfo> files = null;
await Task.Factory.StartNew( () =>
  files = GetFilesInDirectory(selectedPath)
    .Where(f => f.Length >= minSize)
    .OrderByDescending(f => f.Length)
    .ToList());
[/sourcecode]

That way, the enumeration doesn’t block the UI and the program remains responsible all times.

With these changes, everything was working fine and I had a program that could analyze my disk space, even if I had no access to all folder in the disk.

The full source code for the project is in https://github.com/bsonnino/DiskAnalysis

Some time ago, I’ve written an article about analyzing disk space using Excel and Powershell. While these tools are very easy to use and very flexible, that requires some organization: you must run a Powershell script, open the resulting file in Excel and run the analysis. To do that, I prefer to run a single command and have the data on my hands with no extra operations. So, I decided to create a WPF program to get the disk data and show it.

Getting Disk Information

The way to get disk information on .NET is to use the method GetFiles of the DirectoryInfo class, like this:

[sourcecode language=”csharp” padlinenumbers=”true”]
var di = new DirectoryInfo(selectedPath);
var files = di.GetFiles("*",SearchOption.AllDirectories);
[/sourcecode]

Then, we can add the files to a DataGrid and show them, but I’d like to get only the largest files and just some data. The best option for that is to use Linq. So, I decided to use Linq to get the same info and show it on the window.

The first step is to get the initial folder and start the enumeration. WPF doesn’t have a native folder selection , but that can be solved with the brave open source developers out there. I installed WPFFolderBrowser (http://wpffolderbrowser.codeplex.com/) using the Nuget Package Manager (just right click on the References node in the solution explorer and select Manage NuGet packages and type WPFFolderBrowser on the search box).

Then, I added the basic UI for the window. I used a TabControl with three tabs and a button at the top to select the folder and start the search:

[sourcecode language=”text” padlinenumbers=”true”]
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="40"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="5">
        <TextBlock Text="Minimum size:" VerticalAlignment="Center" Margin="0,0,5,0"/>
        <TextBox Width="150" x:Name="MinSizeBox" Text="1048576" VerticalContentAlignment="Center"/>
    </StackPanel>
    <Button Width="85" Height="30" Content="Start" Click="StartClick"/>
    <TabControl Grid.Row="1">
        <TabItem Header="File Data">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="30"/>
                </Grid.RowDefinitions>
                <ListBox x:Name="FilesList">

                </ListBox>
                <StackPanel Grid.Row="1" Orientation="Horizontal">
                    <TextBlock x:Name="TotalFilesText" Margin="5,0" VerticalAlignment="Center"/>
                    <TextBlock x:Name="LengthFilesText" Margin="5,0" VerticalAlignment="Center"/>
                </StackPanel>
            </Grid>
        </TabItem>
        <TabItem Header="Extensions">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <ListBox x:Name="ExtList">

                </ListBox>
            </Grid>
        </TabItem>
        <TabItem Header="ABC Curve">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <ListBox x:Name="AbcList">

                </ListBox>
            </Grid>
        </TabItem>
    </TabControl>
</Grid>
[/sourcecode]

image

The code for the start button is:

[sourcecode language=”csharp”]
private void StartClick(object sender, RoutedEventArgs e)
{
    var fbd = new WPFFolderBrowserDialog();
    if (fbd.ShowDialog() != true)
        return;
    var selectedPath = fbd.FileName;
    var di = new DirectoryInfo(selectedPath);
    Int64 minSize;
    if (!Int64.TryParse(MinSizeBox.Text, out minSize))
        return;
    var files = di.GetFiles("*", SearchOption.AllDirectories)
        .Where(f => f.Length >= minSize)
        .Select(f => new {f.Name, f.Length, f.DirectoryName, f.FullName, f.Extension})
        .OrderByDescending(f => f.Length)
        .ToList();
    TotalFilesText.Text = $"# Files: {files.Count}";
    LengthFilesText.Text = $"({totalSize:N0} bytes)";
    FilesList.ItemsSource = files;
}
[/sourcecode]

This code shows the folder selection dialog. If the user selects a file, it creates a DirectoryInfo and then uses GetFiles to enumerate the files in the folder and its subfolders. I am using Linq to select only the files with size greater than the minimum size, select only the data I want and sort the resulting files by length. At the end, I am converting the enumeration to a list. I am doing that because this list will be traversed some times to get the data by extension or the ABC curve, and I wanted to avoid multiple enumeration.

If you run this code and click the start button, you will see that, after some time, the data is shown in the main window:

image

But that’s not what we want. We want a better display for the data, so we need to create an ItemTemplate for the list:

[sourcecode language=”text”]
<ListBox x:Name="FilesList">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding FullName}"/>
                <TextBlock Text="{Binding Length, StringFormat=N0}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
[/sourcecode]

And now we have the data shown in a better way:

image

We can use Linq to fill the other tabs:

[sourcecode language=”csharp”]
private void StartClick(object sender, RoutedEventArgs e)
{
    var fbd = new WPFFolderBrowserDialog();
    if (fbd.ShowDialog() != true)
        return;
    var selectedPath = fbd.FileName;
    var di = new DirectoryInfo(selectedPath);
    Int64 minSize;
    if (!Int64.TryParse(MinSizeBox.Text, out minSize))
        return;
    var files = di.GetFiles("*", SearchOption.AllDirectories)
        .Where(f => f.Length >= minSize)
        .Select(f => new {f.Name, f.Length, f.DirectoryName, f.FullName, f.Extension})
        .OrderByDescending(f => f.Length)
        .ToList();
    var totalSize = files.Sum(f => f.Length);
    TotalFilesText.Text = $"# Files: {files.Count}";
    LengthFilesText.Text = $"({totalSize:N0} bytes)";
    FilesList.ItemsSource = files;
    ExtList.ItemsSource = files.GroupBy(f => f.Extension)
        .Select(g => new {Extension = g.Key, Quantity = g.Count(), Size = g.Sum(f => f.Length)})
        .OrderByDescending(t => t.Size);
    var tmp = 0.0;
    AbcList.ItemsSource = files.Select(f =>
        {
            tmp += f.Length;
            return new {f.Name, Percent = tmp/totalSize*100};
        });
}
[/sourcecode]

For the extension list, we made a grouping by extension and selected the data we want, getting the quantity and total length for each group. For the ABC curve, we used a temporary value to get the total length for the files larger than the selected one. If you run the program, you will see that the data is not formatted. We can format the data by using the ItemTemplate for the lists:

[sourcecode language=”text”]
<TabItem Header="Extensions">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ListBox x:Name="ExtList">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Extension}" Width="100" Margin="5"/>
                        <TextBlock Text="{Binding Quantity, StringFormat=N0}" Width="100" Margin="5" TextAlignment="Right"/>
                        <TextBlock Text="{Binding Size,StringFormat=N0}" Width="150" Margin="5" TextAlignment="Right"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</TabItem>
<TabItem Header="ABC Curve">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ListBox x:Name="AbcList">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Name}" Width="250" Margin="5"/>
                        <TextBlock Text="{Binding Percent, StringFormat=N2}" Width="100" TextAlignment="Right"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</TabItem>
[/sourcecode]

When we run the program, we can see the data for the extensions and the ABC Curve:

image

image

Now we need only to add the charts to the window.

Adding Charts

We will add the WPF Toolkit (https://wpf.codeplex.com/releases/view/40535) to get the charts, but, as it doesn’t seem to be maintained anymore, I’ll use a fork of this project that has a NuGet package and is at https://github.com/dotnetprojects/WpfToolkit.

To add the Pie Chart on the Extensions page, we must add this XAML code:

[sourcecode language=”text”]
<chartingToolkit:Chart Title="Extensions"
               Grid.Row="0"
               Grid.Column="1"
               HorizontalAlignment="Stretch"
               VerticalAlignment="Stretch">
    <chartingToolkit:PieSeries DependentValuePath="Size"
                       IndependentValuePath="Extension"
                       x:Name="ExtSeries" />
</chartingToolkit:Chart>
[/sourcecode]

We must add the namespace at the top of the file:

[sourcecode language=”text”]
xmlns:chartingToolkit="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=DotNetProjects.DataVisualization.Toolkit"
[/sourcecode]

When we run the application, we see the data and the chart, side by side:

image

For the ABC Curve, we must add this XAML:

[sourcecode language=”text”]
<chartingToolkit:Chart Title="Abc Curve"
               Grid.Row="0"
               Grid.Column="1"
               HorizontalAlignment="Stretch"
               VerticalAlignment="Stretch">
    <chartingToolkit:Chart.Axes>
        <chartingToolkit:CategoryAxis Orientation="X" ShowGridLines="False"  />
        <chartingToolkit:LinearAxis Title="% Size"
                                 Orientation="Y"
                                 ShowGridLines="True" />
    </chartingToolkit:Chart.Axes>
    <chartingToolkit:LineSeries DependentValuePath="Percent"
                        IndependentValuePath="Item"
                        x:Name="AbcSeries" />
</chartingToolkit:Chart>
[/sourcecode]

Then, we change the source code to assign the data for the series:

[sourcecode language=”csharp”]
private void StartClick(object sender, RoutedEventArgs e)
{
    var fbd = new WPFFolderBrowserDialog();
    if (fbd.ShowDialog() != true)
        return;
    var selectedPath = fbd.FileName;
    var di = new DirectoryInfo(selectedPath);
    Int64 minSize;
    if (!Int64.TryParse(MinSizeBox.Text, out minSize))
        return;
    var files = di.GetFiles("*", SearchOption.AllDirectories)
        .Where(f => f.Length >= minSize)
        .Select(f => new {f.Name, f.Length, f.DirectoryName, f.FullName, f.Extension})
        .OrderByDescending(f => f.Length)
        .ToList();
    var totalSize = files.Sum(f => f.Length);
    TotalFilesText.Text = $"# Files: {files.Count}";
    LengthFilesText.Text = $"({totalSize:N0} bytes)";
    FilesList.ItemsSource = files;
    var extensions = files.GroupBy(f => f.Extension)
        .Select(g => new {Extension = g.Key, Quantity = g.Count(), Size = g.Sum(f => f.Length)})
        .OrderByDescending(t => t.Size).ToList();
    ExtList.ItemsSource = extensions;
    ExtSeries.ItemsSource = extensions;
    var tmp = 0.0;
    var abcData = files.Select(f =>
    {
        tmp += f.Length;
        return new {f.Name, Percent = tmp/totalSize*100};
    }).ToList();
    AbcList.ItemsSource = abcData;
    AbcSeries.ItemsSource = abcData.OrderBy(d => d.Percent).Select((d,i) => new {Item = i, d.Percent});
}
[/sourcecode]

When we run the code, we can see the ABC curve.

image

Conclusion

As you can see, it’s fairly easy to get the disk space in a WPF program. With some Linq queries, we can analyze the data the way we want, and then present it using the WPF visualization.

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