Cargando...

Crear un ZIP de una carpeta con progreso en .NET (ejemplo WPF)

Genera un ZIP desde una carpeta en .NET y actualiza una ProgressBar en WPF usando ZipArchive e IProgress para una UI fluida.

ZipFile.CreateFromDirectory es práctico, pero no ofrece actualizaciones de progreso. En aplicaciones de escritorio (especialmente WPF), suele ser importante mostrar una barra de progreso mientras se comprime una carpeta grande.

En este ejemplo, se crea el ZIP con ZipArchive y se reporta el progreso mediante IProgress<T>. Del lado de la UI, un ViewModel WPF simple actualiza una ProgressBar y un texto de estado gracias a INotifyPropertyChanged.


Objetivo


Zip Helper (ZipArchive + Progress)

El helper recorre todos los archivos de la carpeta, crea entradas en ZipArchive y copia los streams de los archivos dentro de la nueva estructura ZIP. Después de agregar cada archivo, reporta el progreso vía 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;

        // Asegurar que exista la carpeta de salida
        var outDir = Path.GetDirectoryName(zipFilePath);
        if (!string.IsNullOrWhiteSpace(outDir))
            Directory.CreateDirectory(outDir);

        // Sobrescribir si ya existe
        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 prefiere '/'

            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 (¿Por qué INotifyPropertyChanged?)

En WPF, los controles pueden enlazarse (binding) a propiedades del ViewModel. Cuando una propiedad cambia, la UI se actualiza automáticamente si el ViewModel dispara el evento PropertyChanged. Para eso se usa INotifyPropertyChanged.

Este ViewModel contiene:


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 = "Listo.";
    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 = "Seleccionar carpeta",
            Multiselect = false
        };

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

    private void SelectZipPath()
    {
        var dlg = new SaveFileDialog
        {
            Title = "Guardar archivo 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 = "Iniciando...";

        _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 = "Terminado.";
        }
        catch (OperationCanceledException)
        {
            StatusText = "Cancelado.";
        }
        catch (Exception ex)
        {
            StatusText = $"Error: {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));
}

Nota: Progress<T> envía las actualizaciones de vuelta al contexto UI capturado (si se crea en el hilo de UI). Por eso es seguro actualizar propiedades enlazadas.

Nota: Este ejemplo usa Microsoft.Win32.OpenFolderDialog, disponible para WPF a partir de .NET 8+. Si apuntas a una versión anterior, WPF no incluye un selector de carpetas integrado. En ese caso:

  • Puedes usar un selector WinForms (System.Windows.Forms.FolderBrowserDialog) dentro de una app WPF, o
  • Usar una librería dedicada (por ejemplo Windows API Code Pack / Common File Dialog).

XAML (Botones + ProgressBar + binding)

Esta vista ofrece:


<Window x:Class="ZipDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Comprimir carpeta" 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="Carpeta origen:" />

    <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="Seleccionar..."
            Command="{Binding SelectFolderCommand}" />

    <TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,10,10"
               VerticalAlignment="Center" Text="Ruta 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="Guardar..."
            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="Iniciar"
              Command="{Binding StartZipCommand}" />

      <Button Width="110"
              Content="Cancelar"
              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>

Conectar el ViewModel (DataContext)

Hay dos formas comunes de conectar la View (XAML) con el ViewModel:

En este ejemplo, lo más simple es code-behind:


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

Alternativa en XAML (si el ViewModel puede crearse sin parámetros):


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

Helpers pequeños para comandos (RelayCommand / AsyncRelayCommand)

Los comandos WPF evitan poner lógica de UI en code-behind. Aquí tienes una implementación mínima:


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);
}

Mejoras comunes


TL;DR

  • ZipArchive ofrece el control necesario para reportar progreso.
  • IProgress<T> hace simples y seguras las actualizaciones de progreso en WPF.
  • INotifyPropertyChanged actualiza la UI automáticamente cuando cambian propiedades del ViewModel.
  • Un setup MVVM pequeño (comandos + bindings) mantiene la UI fluida y limpia.