Chargement...

Créer un ZIP d’un dossier avec progression en .NET (exemple WPF)

Créez un ZIP depuis un dossier en .NET et mettez à jour une ProgressBar WPF via ZipArchive et IProgress pour une UI fluide.

ZipFile.CreateFromDirectory est pratique, mais ne fournit pas de mises à jour de progression. Dans les applications desktop (surtout en WPF), il est souvent important d’afficher une barre de progression lorsqu’on zippe un gros dossier.

Dans cet exemple, le ZIP est créé avec ZipArchive et la progression est reportée via IProgress<T>. Côté UI, un ViewModel WPF simple met à jour une ProgressBar et un texte de statut grâce à INotifyPropertyChanged.


Objectif


Zip Helper (ZipArchive + Progress)

Le helper parcourt tous les fichiers du dossier, crée des entrées dans ZipArchive et copie les streams des fichiers dans l’archive. Après chaque fichier ajouté, il reporte la progression via IProgress<ZipProgressInfo>.


using System.IO.Compression;

public sealed record ZipProgressInfo(
    int FilesProcessed,
    int TotalFiles,
    string CurrentFile)
{
    public double Percent => TotalFiles == 0 ? 0 : (FilesProcessed * 100.0 / TotalFiles);
}

public static class ZipHelper
{
    public static async Task ZipDirectoryAsync(
        string sourceDirectory,
        string zipFilePath,
        IProgress<ZipProgressInfo>? progress,
        CancellationToken ct)
    {
        if (!Directory.Exists(sourceDirectory))
            throw new DirectoryNotFoundException(sourceDirectory);

        var files = Directory.EnumerateFiles(sourceDirectory, "*", SearchOption.AllDirectories).ToList();
        var total = files.Count;

        // S'assurer que le dossier de sortie existe
        var outDir = Path.GetDirectoryName(zipFilePath);
        if (!string.IsNullOrWhiteSpace(outDir))
            Directory.CreateDirectory(outDir);

        // Écraser si le fichier existe déjà
        if (File.Exists(zipFilePath))
            File.Delete(zipFilePath);

        await using var zipStream = new FileStream(zipFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
        using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, leaveOpen: false);

        var processed = 0;

        foreach (var filePath in files)
        {
            ct.ThrowIfCancellationRequested();

            var entryName = Path.GetRelativePath(sourceDirectory, filePath)
                .Replace('\\', '/'); // ZIP préfère '/'

            var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);

            await using (var entryStream = entry.Open())
            await using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                await fileStream.CopyToAsync(entryStream, ct);
            }

            processed++;

            progress?.Report(new ZipProgressInfo(
                FilesProcessed: processed,
                TotalFiles: total,
                CurrentFile: entryName));
        }
    }
}

ViewModel WPF (Pourquoi INotifyPropertyChanged ?)

En WPF, les contrôles peuvent se binder aux propriétés du ViewModel. Lorsque la valeur d’une propriété change, l’UI se met à jour automatiquement si le ViewModel déclenche l’évènement PropertyChanged. C’est précisément le rôle de INotifyPropertyChanged.

Ce ViewModel contient :


using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using Microsoft.Win32;

public sealed class ZipFolderViewModel : INotifyPropertyChanged
{
    private string _sourceFolder = "";
    private string _zipPath = "";
    private double _progressValue;
    private string _statusText = "Prêt.";
    private bool _isBusy;

    private CancellationTokenSource? _cts;

    public string SourceFolder
    {
        get => _sourceFolder;
        set { _sourceFolder = value; OnPropertyChanged(); StartZipCommand.RaiseCanExecuteChanged(); }
    }

    public string ZipPath
    {
        get => _zipPath;
        set { _zipPath = value; OnPropertyChanged(); StartZipCommand.RaiseCanExecuteChanged(); }
    }

    public double ProgressValue
    {
        get => _progressValue;
        set { _progressValue = value; OnPropertyChanged(); }
    }

    public string StatusText
    {
        get => _statusText;
        set { _statusText = value; OnPropertyChanged(); }
    }

    public bool IsBusy
    {
        get => _isBusy;
        set
        {
            _isBusy = value;
            OnPropertyChanged();
            SelectFolderCommand.RaiseCanExecuteChanged();
            SelectZipPathCommand.RaiseCanExecuteChanged();
            StartZipCommand.RaiseCanExecuteChanged();
            CancelCommand.RaiseCanExecuteChanged();
        }
    }

    public RelayCommand SelectFolderCommand { get; }
    public RelayCommand SelectZipPathCommand { get; }
    public AsyncRelayCommand StartZipCommand { get; }
    public RelayCommand CancelCommand { get; }

    public ZipFolderViewModel()
    {
        SelectFolderCommand = new RelayCommand(SelectFolder, () => !IsBusy);
        SelectZipPathCommand = new RelayCommand(SelectZipPath, () => !IsBusy);

        StartZipCommand = new AsyncRelayCommand(StartZipAsync, CanStart);
        CancelCommand = new RelayCommand(Cancel, () => IsBusy);
    }

    private bool CanStart()
        => !IsBusy
           && !string.IsNullOrWhiteSpace(SourceFolder)
           && !string.IsNullOrWhiteSpace(ZipPath);

    private void SelectFolder()
    {
        var dlg = new Microsoft.Win32.OpenFolderDialog
        {
            Title = "Sélectionner un dossier",
            Multiselect = false
        };

        bool? ok = dlg.ShowDialog();
        if (ok == true)
        {
            SourceFolder = dlg.FolderName;
        }
    }

    private void SelectZipPath()
    {
        var dlg = new SaveFileDialog
        {
            Title = "Enregistrer le ZIP",
            Filter = "ZIP files (*.zip)|*.zip",
            DefaultExt = "zip",
            AddExtension = true
        };

        if (dlg.ShowDialog() == true)
            ZipPath = dlg.FileName;
    }

    private async Task StartZipAsync()
    {
        IsBusy = true;
        ProgressValue = 0;
        StatusText = "Démarrage...";

        _cts = new CancellationTokenSource();

        try
        {
            var progress = new Progress<ZipProgressInfo>(p =>
            {
                ProgressValue = p.Percent;
                StatusText = $"{p.FilesProcessed}/{p.TotalFiles} - {p.CurrentFile}";
            });

            await ZipHelper.ZipDirectoryAsync(SourceFolder, ZipPath, progress, _cts.Token);

            ProgressValue = 100;
            StatusText = "Terminé.";
        }
        catch (OperationCanceledException)
        {
            StatusText = "Annulé.";
        }
        catch (Exception ex)
        {
            StatusText = $"Erreur : {ex.Message}";
        }
        finally
        {
            _cts?.Dispose();
            _cts = null;
            IsBusy = false;
        }
    }

    private void Cancel()
    {
        _cts?.Cancel();
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    private void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

Remarque : Progress<T> renvoie les mises à jour vers le contexte UI capturé (s’il est créé sur le thread UI). Mettre à jour des propriétés bindées est donc sûr.

Remarque : Cet exemple utilise Microsoft.Win32.OpenFolderDialog, disponible en WPF à partir de .NET 8+. Si vous ciblez une version plus ancienne, WPF ne fournit pas de sélecteur de dossier intégré. Dans ce cas :

  • Utiliser un sélecteur WinForms (System.Windows.Forms.FolderBrowserDialog) dans une app WPF, ou
  • Utiliser une bibliothèque dédiée (par ex. Windows API Code Pack / Common File Dialog).

XAML (Boutons + ProgressBar + binding)

Cette vue propose :


<Window x:Class="ZipDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Zipper un dossier" Height="260" Width="560">

  <Grid Margin="16">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>

    <TextBlock Grid.Row="0" Grid.Column="0" Margin="0,0,10,10"
               VerticalAlignment="Center" Text="Dossier source :" />

    <TextBox Grid.Row="0" Grid.Column="1" Margin="0,0,10,10"
             Text="{Binding SourceFolder}" IsReadOnly="True" />

    <Button Grid.Row="0" Grid.Column="2" Margin="0,0,0,10"
            Content="Choisir..."
            Command="{Binding SelectFolderCommand}" />

    <TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,10,10"
               VerticalAlignment="Center" Text="Chemin du ZIP :" />

    <TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,10,10"
             Text="{Binding ZipPath}" IsReadOnly="True" />

    <Button Grid.Row="1" Grid.Column="2" Margin="0,0,0,10"
            Content="Enregistrer..."
            Command="{Binding SelectZipPathCommand}" />

    <StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3"
                Orientation="Horizontal" Margin="0,0,0,10">
      <Button Width="110" Margin="0,0,10,0"
              Content="Démarrer"
              Command="{Binding StartZipCommand}" />

      <Button Width="110"
              Content="Annuler"
              Command="{Binding CancelCommand}" />
    </StackPanel>

    <ProgressBar Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3"
                 Height="16" Minimum="0" Maximum="100"
                 Value="{Binding ProgressValue}" />

    <TextBlock Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="3" Margin="0,8,0,0"
               Text="{Binding StatusText}" TextTrimming="CharacterEllipsis" />
  </Grid>
</Window>

Connecter le ViewModel (DataContext)

Il existe deux façons courantes de relier la View (XAML) au ViewModel :

Pour cet exemple, l’approche la plus simple est le code-behind :


public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new ZipFolderViewModel();
    }
}

Variante XAML (si le ViewModel peut être créé sans paramètres) :


<Window ...
        xmlns:local="clr-namespace:ZipDemo">
  <Window.DataContext>
    <local:ZipFolderViewModel />
  </Window.DataContext>
  <!-- ... -->
</Window>

Petits helpers de commandes (RelayCommand / AsyncRelayCommand)

Les commandes WPF évitent de mettre de la logique UI dans le code-behind. Voici une implémentation minimale :


using System.Windows.Input;

public sealed class RelayCommand(Action execute, Func<bool>? canExecute = null) : ICommand
{
    private readonly Action _execute = execute;
    private readonly Func<bool> _canExecute = canExecute ?? (() => true);

    public bool CanExecute(object? parameter) => _canExecute();
    public void Execute(object? parameter) => _execute();

    public event EventHandler? CanExecuteChanged;

    public void RaiseCanExecuteChanged()
        => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

public sealed class AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null) : ICommand
{
    private readonly Func<Task> _execute = execute;
    private readonly Func<bool> _canExecute = canExecute ?? (() => true);
    private bool _isRunning;

    public bool CanExecute(object? parameter) => !_isRunning && _canExecute();

    public async void Execute(object? parameter)
    {
        if (!CanExecute(parameter)) return;

        try
        {
            _isRunning = true;
            RaiseCanExecuteChanged();
            await _execute();
        }
        finally
        {
            _isRunning = false;
            RaiseCanExecuteChanged();
        }
    }

    public event EventHandler? CanExecuteChanged;

    public void RaiseCanExecuteChanged()
        => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

Améliorations courantes


TL;DR

  • ZipArchive offre le contrôle nécessaire pour reporter la progression.
  • IProgress<T> rend les mises à jour de progression simples et sûres côté UI en WPF.
  • INotifyPropertyChanged met à jour l’UI automatiquement lorsque les propriétés du ViewModel changent.
  • Un petit setup MVVM (commands + bindings) garde l’UI fluide et propre.