Write asynchronous Pause/Resume for file enumeration processing in a drive
C# Advent Calendar 2025 に参加しています。1日目の記事。
前置き
今回はファイルの列挙処理をテーマにしました。
ついでにPrism.WPFで実装しました。
大した処理ではないですが、コードビハインドとの責務分離には良いと感じました。
非同期処理のPause/Resumeを書く場合、いくつ化のデメリットが存在しますが、このような比較的単純な処理であれば問題はありません。
次回、詳しく解説することになるでしょう。
実行デモ動画
用意しておくといいねが付いていいね。ツイートにもいいねするともっといいね。
Github
今回はタダ。
実装コード
Prism.Wpfをいれてくだちい。
- ViewModel
partialにして分割するか、ファイル選択メソッドでViewModel作るなどして責務分離を図るのも良いです。
using Microsoft.Win32;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
namespace EnumerateAllFile_PauseResume.ViewModel
{
public class FileEnumeratorViewModel : BindableBase
{
// 操作全体のキャンセルを制御するためのトークン発行元。
private CancellationTokenSource? _cts;
// 一時停止・再開制御用のシグナル。
private TaskCompletionSource<bool>? _pauseSignal;
private bool _isPaused;
public ObservableCollection<string> Files { get; } = new();
public DelegateCommand StartCommand { get; }
public DelegateCommand PauseCommand { get; }
public DelegateCommand ResumeCommand { get; }
public DelegateCommand FileSelectCommand { get; }
public FileEnumeratorViewModel()
{
StartCommand = new DelegateCommand(async () => await StartAsync());
PauseCommand = new DelegateCommand(Pause);
ResumeCommand = new DelegateCommand(Resume);
FileSelectCommand = new DelegateCommand(() => FilePatheSelect());
}
private string _filePath = "C\\";
public string FilePath
{
get => _filePath;
set => SetProperty(ref _filePath, value);
}
private string _filesText = string.Empty;
public string FilesText
{
get => _filesText;
set => SetProperty(ref _filesText, value);
}
private async Task StartAsync()
{
+ _cts?.Cancel(); // 二重起動防止
_cts = new();
Files.Clear();
if (string.IsNullOrEmpty(_filePath))
return;
try
{
await foreach (var file in EnumerateFilesAsync(_filePath, _cts.Token))
{
// UIスレッドでコレクション更新
Application.Current.Dispatcher.Invoke(() =>
{
Files.Add(file);
FilesText += file + Environment.NewLine;
});
}
}
catch (TaskCanceledException)
{
Files.Add("キャンセルされました。");
}
catch (Exception ex)
{
Files.Add($"エラー: {ex.Message}");
}
}
/// <summary>
/// 指定フォルダ以下のファイルを再帰的に非同期列挙する。
/// UnauthorizedAccessException 等が発生したフォルダはスキップする。
/// </summary>
private async IAsyncEnumerable<string> EnumerateFilesAsync(
string rootPath, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken token)
//C# 8.0 以降で導入された [EnumeratorCancellation] 属性をつけると、
//呼び出し側で渡した CancellationToken が 非同期列挙のキャンセルに自動で使われる ようになります。
{
IEnumerable<string> files;
IEnumerable<string> directories;
try
{
files = Directory.EnumerateFiles(rootPath);
}
catch (UnauthorizedAccessException)
{
yield break;
}
catch (IOException)
{
yield break;
}
foreach (var file in files)
{
// キャンセル要求が出ていないか確認。
// 要求があれば OperationCanceledException をスローして処理を中断。
token.ThrowIfCancellationRequested();
// 一時停止状態の場合は、TaskCompletionSource が再開されるまで待機。
// Resume() から _pauseSignal.TrySetResult(true) が呼ばれるとここが解除される。
if (_isPaused)
{
_pauseSignal = new TaskCompletionSource<bool>();
await _pauseSignal.Task;
}
// 呼び出し元(await foreach 側)に現在のファイルパスを1件返す。
// yield return により「中間結果を逐次返す」ことができ、全体完了を待たずにUI更新が可能になる。
yield return file;
// UIの過負荷を防ぐため、少し待機。
// tokenを渡すことで、この待機もキャンセル可能。
await Task.Delay(30, token);
}
try
{
directories = Directory.EnumerateDirectories(rootPath);
}
catch (UnauthorizedAccessException)
{
yield break;
}
catch (IOException)
{
yield break;
}
foreach (var dir in directories)
{
await foreach (var f in EnumerateFilesAsync(dir, token))
yield return f;
}
}
private void Pause() => _isPaused = true;
private void Resume()
{
_isPaused = false;
_pauseSignal?.TrySetResult(true);
}
private void FilePatheSelect()
{
// ダイアログのインスタンスを生成
var dialog = new OpenFolderDialog();
// ファイルの種類を設定
dialog.DefaultDirectory = "C\\";
// ダイアログを表示する
if (dialog.ShowDialog() == true)
{
// 選択されたファイル名 (ファイルパス) をメッセージボックスに表示
FilePath = dialog.FolderName;
}
}
}
}
- CodeBehind
TextBoxのScrolltoEnd()で。
こういう責務分離も良いと思います。
using EnumerateAllFile_PauseResume.ViewModel;
using System.Windows;
using System.Windows.Controls;
namespace EnumerateAllFile_PauseResume
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly FileEnumeratorViewModel _viewModel = new();
public MainWindow()
{
InitializeComponent();
DataContext = _viewModel;
}
private void FileDisplayer_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
var tb = sender as TextBox;
tb?.ScrollToEnd();
}
}
}
- XAML
<Window x:Class="EnumerateAllFile_PauseResume.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:EnumerateAllFile_PauseResume"
mc:Ignorable="d"
Title="MainWindow" Height="300" Width="600">
<Grid >
<StackPanel Orientation="Vertical" Background="AliceBlue" Grid.Row = "0"
HorizontalAlignment="Left" >
<StackPanel Orientation="Horizontal"
VerticalAlignment="Top" Grid.Column = "1" >
<Button Content="Start" Command="{Binding StartCommand}" Margin="0,0,5,0"
Width ="60" Height="50"/>
<Button Content="Pause" Command="{Binding PauseCommand}" Margin="0,0,5,0"
Width ="60" Height="50"
/>
<Button Content="Resume" Command="{Binding ResumeCommand}" Margin="0,0,5,0"
Width ="60" Height="50"
/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox x:Name="FilePatthBpx" Width="300" Height="26" Text="{Binding FilePath, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="Open"
x:Name="FileOpenButton" Width="80" Height="26"
Command= "{ Binding FileSelectCommand}"/>
</StackPanel>
<TextBox x:Name="FileDisplayer" Height="200" Width="600" Background="WhiteSmoke"
Text="{Binding FilesText}"
TextWrapping="Wrap"
TextChanged="FileDisplayer_TextChanged"
/>
</StackPanel>
</Grid>
</Window>
Code解説
- IAsyncEnumerable
IAsyncEnumerable は、C# 8.0 以降で導入された非同期ストリームを扱うためのインターフェースです。非同期的にデータを逐次生成・取得するシナリオ(例: データベースクエリ、APIからのデータストリーミング、ファイルの逐次読み込みなど)で使用されます。
- [EnumeratorCancellation]属性
IAsyncEnumerableインターフェイスのGetAsyncEnumeratorメソッドにはCancellationTokenを渡せるようになっていて、これを使って非同期処理の途中キャンセルをする想定になっています。
非同期イテレーターでは、以下のように、引数にEnumeratorCancellation属性(System.Runtime.CompilerServices名前空間)を付けることでこのCancellationTokenを受け取れるようになります。
参考URL
なかなか良いので追記。
IAsyncEnumerable を理解する
次回予告(次回更新予定)
書きかけ。
一応、一度AI出力させたものは覚え込むようにしている。
