.NET’te Klasörü ZIP’leme (Progress ile) – WPF Örneği
ZipArchive ve IProgress kullanarak bir klasörü ZIP’lerken ilerlemeyi WPF ProgressBar’a aktarın ve UI’ı akıcı tutun.
ZipFile.CreateFromDirectory pratik bir metot olsa da, size ilerleme (progress) bilgisi vermez.
Masaüstü uygulamalarında (özellikle WPF’de) büyük bir klasör ZIP’lenirken bir progress bar göstermek genellikle önemlidir.
Bu örnekte ZIP dosyasını ZipArchive ile oluşturup ilerlemeyi IProgress<T> üzerinden raporluyoruz.
UI tarafında ise basit bir WPF ViewModel, INotifyPropertyChanged sayesinde ProgressBar ve durum metnini güncelliyor.
Amaç
- Bir klasörü ZIP dosyasına dönüştürmek.
- İlerlemeyi göstermek (yüzde + işlenen dosya).
- UI’ı akıcı tutmak (async + progress callback).
- UI üzerinden kaynak klasör ve ZIP kaydetme yolunu seçebilmek.
Zip Helper (ZipArchive + Progress)
Helper, klasör altındaki tüm dosyaları gezer, her dosya için ZipArchive içinde bir entry oluşturur ve
dosya stream’ini ZIP’e kopyalar.
Her dosya eklendikten sonra ilerlemeyi IProgress<ZipProgressInfo> ile raporlar.
- Neden ZipArchive? Entry’lerin nasıl oluşturulduğunu ve progress’in ne zaman raporlanacağını tam kontrol etmenizi sağlar.
- Neden dosya bazlı progress? Basittir ve çoğu UI senaryosu için yeterince iyidir. (Byte bazlı progress mümkündür ama daha karmaşıktır.)
- Relative path: ZIP entry’leri seçilen klasöre göre relatif path ile oluşturulmalıdır.
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;
// Çıktı klasörü yoksa oluştur
var outDir = Path.GetDirectoryName(zipFilePath);
if (!string.IsNullOrWhiteSpace(outDir))
Directory.CreateDirectory(outDir);
// Varsa üzerine yaz
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 içinde '/' tercih edilir
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));
}
}
}
WPF ViewModel (INotifyPropertyChanged neden gerekli?)
WPF’de kontrol’ler ViewModel property’lerine bind edilebilir. Bir property değiştiğinde UI’ın otomatik güncellenmesi için
ViewModel’in PropertyChanged event’ini tetiklemesi gerekir.
INotifyPropertyChanged tam olarak bu amaçla kullanılır.
Bu ViewModel şunları tutar:
SourceFolderveZipPath(kullanıcının seçtiği değerler)ProgressValue(0-100) veStatusText(işlenen dosya bilgisi)- Klasör seçme, zip kaydetme yolu seçme ve işlemi başlatma/iptal etme komutları
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 = "Hazır.";
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 = "Klasör seçin",
Multiselect = false
};
bool? ok = dlg.ShowDialog();
if (ok == true)
{
SourceFolder = dlg.FolderName;
}
}
private void SelectZipPath()
{
var dlg = new SaveFileDialog
{
Title = "ZIP dosyasını kaydet",
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 = "Başlatılıyor...";
_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 = "Bitti.";
}
catch (OperationCanceledException)
{
StatusText = "İptal edildi.";
}
catch (Exception ex)
{
StatusText = $"Hata: {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));
}
Not: Progress<T>, UI thread üzerinde oluşturulduğunda callback’leri aynı context’e geri “post” eder.
Bu yüzden bind edilen property’leri güncellemek güvenlidir.
Not: Bu örnekte Microsoft.Win32.OpenFolderDialog kullanılıyor ve bu dialog WPF için .NET 8+ ile birlikte geliyor.
Daha eski bir .NET sürümü hedefliyorsanız, WPF’nin yerleşik bir folder picker’ı yoktur. Bu durumda:
-
WPF uygulaması içinde WinForms klasör seçicisini (
System.Windows.Forms.FolderBrowserDialog) kullanabilir veya - Klasör seçimi için ayrı bir kütüphane tercih edebilirsiniz (ör. Windows API Code Pack / Common File Dialog).
XAML (Butonlar + ProgressBar + Binding)
Bu ekran şunları sağlar:
- Kaynak klasör seçme butonu
- ZIP’in kaydedileceği yeri seçme butonu
- Başlat ve İptal butonları
- ProgressBar + durum metni
<Window x:Class="ZipDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Klasörü ZIP'le" 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="Kaynak klasör:" />
<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="Seç..."
Command="{Binding SelectFolderCommand}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,10,10"
VerticalAlignment="Center" Text="ZIP yolu:" />
<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="Kaydet..."
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="Başlat"
Command="{Binding StartZipCommand}" />
<Button Width="110"
Content="İptal"
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>
ViewModel’i Bağlamak (DataContext)
View (XAML) ile ViewModel’i bağlamak için iki yaygın yöntem vardır:
- Code-behind (demo için en basit):
MainWindowconstructor’ındaDataContextset edilir. - XAML (ViewModel parameterless constructor’a sahipse):
Window.DataContextiçinde tanımlanır.
Bu örnekte en basit yaklaşım code-behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ZipFolderViewModel();
}
}
XAML ile yapmak isterseniz (ViewModel parametresiz oluşturulabiliyorsa):
<Window ...
xmlns:local="clr-namespace:ZipDemo">
<Window.DataContext>
<local:ZipFolderViewModel />
</Window.DataContext>
<!-- ... -->
</Window>
Küçük Command Helper’ları (RelayCommand / AsyncRelayCommand)
WPF command’ları UI mantığını code-behind’den uzak tutar. Aşağıdaki implementasyon minimal bir örnektir:
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);
}
Sık Yapılan İyileştirmeler
- İlerlemeyi dosya yerine byte bazında raporlamak (dosya boyutları çok farklıysa daha doğru yüzde verir).
- Kilitli dosyaları atlayıp uyarı listesi toplamak (tüm işlemi fail etmek yerine).
- Birden fazla sıkıştırmayı paralel çalıştırırken dikkatli olmak (disk I/O çok hızlı bottleneck olur).
- ViewModel’i code-behind’de
newlemek yerine dependency injection ile oluşturmak.
TL;DR
ZipArchive, progress raporlamak için gereken kontrolü sağlar.IProgress<T>, WPF’de progress güncellemelerini basit ve UI-safe hale getirir.INotifyPropertyChanged, ViewModel property’leri değiştiğinde UI’ın otomatik güncellenmesini sağlar.- Küçük bir MVVM kurgu (command + binding) ile UI hem akıcı hem de temiz kalır.