n00b pro

09. Bestanden en mappen

Dit hoofdstuk is onderdeel van de cursus C#. Andere cursussen in dezelfde reeks: HTML, CSS, Javascript, Ontwikkelomgeving.

Youtube: bestanden & excepties

https://www.youtube.com/watch?v=j-ECPszC8x8

Namespaces en klassen

In .NET zit de functionaliteit voor het werken met bestanden en mappen verspreid over heel wat namespaces en klassen.

Voor paden

Klasse Omschrijving
System.IO.Path werken met paden; methodes Combine(), GetFileNameWithoutExtension(), GetDirectoryName()...
System.Environment omgevingsspecifieke paden; methodes GetFolderPath(), properties SpecialFolder.Desktop, SpecialFolder.MyDocuments...

Voor bestanden en mappen

De meeste klassen voor het werken met bestanden en mappen vind je in de System.IO namespace:

Klasse Omschrijving
Directory werken met mappen; methodes GetDirectories(), GetFiles(), Delete(), CreateDirectory()...
DirectoryInfo map informatie opvragen; properties FullName, Name, Parent, Exists...
File werken met bestanden; methodes ReadAllText(), WriteAllText(), Copy(), Delete()...
FileInfo bestand informatie opvragen; properties Name, Extension, Directory, Exists, Length...
StreamReader bestanden streamend lezen (geheugenvriendelijk, enkel nodig bij zeer grote bestanden)
StreamWriter bestanden streamend schrijven (geheugenvriendelijk, enkel nodig bij zeer grote bestanden)
System.IO.Path werken met paden; methodes Combine(), GetFileNameWithoutExtension(), GetDirectoryName()...

Voor dialoogvensters

Allerhande dialoogvensters vind je in Microsoft.Win32, System.Windows en System.Windows.Forms:

Namespace Klasse Omschrijving
Microsoft.Win32 OpenFileDialog dialoogvenster om bestanden te openen
Microsoft.Win32 OpenFolderDialog dialoogvenster om mappen te openen — pas beschikbaar vanaf .NET8!
Microsoft.Win32 SaveFileDialog dialoogvenster om bestanden op te slaan
System.Windows MessageBox foutmeldings-, waarschuwings- en berichtvensters
System.Windows.Forms FolderBrowserDialog ouderwets dialoogvenster om een map te kiezen

Paden

Systeemmappen vinden

Op elke computer is de fysieke locatie voor systeemmappen als Desktop, My Documents... verschillend. Gebruik daarom altijd de Environment.SpecialFolder enumeratie:

string desktopFolder = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);

Andere typische voorbeelden zijn My Documents, My Pictures enz...:

Paden combineren

Gebruik best altijd System.IO.Path.Combine() om paden te combineren:

string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "textfiles\\myfile.txt");
Console.WriteLine(filePath);
C:\Users\rogie\Documents\textfiles\myfile.txt
resultaat in de console

Bestanden

Overzicht

Alle methodes om bestanden te lezen, schrijven, bewerken enz... vind je in de File klasse:

Methode Omschrijving
AppendAllLines() array of lijst toevoegen aan een bestand
AppendAllText() tekst toevoegen aan een bestand
Copy() kopieer een bestand
Delete() verwijder een bestand
Exists() controleert of een bestand bestaat
Move() verplaats een bestand
ReadAllLines() bestand inlezen in een string[] array
ReadAllText() bestand inlezen in een string
WriteAllLines() array of lijst keer schrijven naar een bestand
WriteAllText() tekst schrijven naar een bestand

Properties om bestandsinformatie op te vragen vind je in de FileInfo klasse:

Property Omschrijving
Directory map van het bestand (volledig pad)
DirectoryName mapnaam van het bestand
Exists bestaat het of niet (true of false)
Extension extensie (met punt)
FullName volledig pad
Length bestandsgrootte in bytes
Name bestandsnaam

Code voorbeelden

Bestand inlezen in een string

string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "myfile.txt");

string inhoud = File.ReadAllText(filePath); // lees tekstinhoud bestand in

Bestand inlezen in een array

string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "myfile.txt");

string[] regels = File.ReadAllLines(filePath); // lees regels bestand in

String schrijven naar een bestand

string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "myfile.txt");
string inhoud = @"Dit is regel 1
Dit is regel 2";

File.WriteAllText(filePath, inhoud); // nieuw bestand als het nog niet bestaat

Array of lijst schrijven naar een bestand

string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "myfile.txt");
string[] regels = new string[] { "Dit is lijn 1", "Dit is lijn 2" };

File.WriteAllLines(filePath, regels); // nieuw bestand als het nog niet bestaat

Tekst toevoegen aan een bestaand bestand

Naast File.WriteAllText() en File.WriteAllLines() bestaan er ook methodes File.AppendAllText() en File.AppendAllLines():

string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "myfile.txt");

File.AppendAllText(filePath, $"Am no an listening depending up believing{Environment.NewLine}");
string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "myfile.txt");
string[] regels = new string[] { "Dit is lijn 3", "Dit is lijn 4" };

File.AppendAllLines(filePath, regels);

Controleren of een bestand bestaat

string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "mycontacts.txt");

if (!File.Exists(filePath))
{
   // ...
}

Informatie opvragen

Twee klassen bieden gedeeltelijk overlappende functionaliteit voor het opvragen van informatie: FileInfo en Path.

Codevoorbeeld met FileInfo (bestand “test.txt” op de Desktop):

string filePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test.txt");
FileInfo fi = new FileInfo(filePath);
Console.WriteLine($@"
bestandsnaam: {fi.Name}
extensie: {fi.Extension}
gemaakt op: {fi.CreationTime.ToString()}
mapnaam: {fi.Directory.Name}
");
bestandsnaam: test.txt
extensie: .txt
gemaakt op: 24/10/2023 19:45:16
mapnaam: Desktop
resultaat in de console

Codevoorbeeld met Path: (bestand “test.txt” op de Desktop):

string filePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test.txt");
Console.WriteLine($@"
bestandsnaam: {Path.GetFileName(filePath)}
bestandsnaam zonder extensie: {Path.GetFileNameWithoutExtension(filePath)}
extensie: {Path.GetExtension(filePath)}
map: {Path.GetDirectoryName(filePath)}
");
bestandsnaam: test.txt
bestandsnaam zonder extensie: test
extensie: .txt
map: C:\Users\rogier\Desktop
resultaat in de console

Streamend lezen en schrijven

Alle vorige methodes laden de hele bestandsinhoud in één keer in het geheugen in. Voor heel grote bestanden is dat niet efficiënt. In die (uitzonderlijke) gevallen kan je gebruik maken van StreamReader en StreamWriter.

StreamReader voorbeeld (zoeken naar een tekst):

List<string> foundLines = new List<string>();
string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "mycontacts.txt");

// open stream and start reading
using (StreamReader reader = File.OpenText(filePath)) {
    string line;
    while ((line = reader.ReadLine()) != null) {
        if (line.Contains("jennifer")) foundLines.Add(line);
    }
} // stream closes automatically

StreamWriter voorbeeld:

// prepare
string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "myfile.txt");

// open stream and start writing
using (StreamWriter writer = File.CreateText(filePath)) {
    writer.WriteLine("Dit is lijn 1");
    writer.WriteLine("Dit is lijn 2");
} // stream closes automatically

In praktijk zul je streamend lezen of schrijven in deze cursus nooit nodig hebben.

Mappen

Overzicht

Alle methodes om te werken met mappen vind je in de Directory klasse:

Methode Omschrijving
CreateDirectory() maak een map aan
Delete() verwijder een map
Exists() controleert of een map bestaat
GetDirectories() vraag een lijst submappen in de map op
GetFiles() vraag een lijst bestanden in de map op

Properties om mapinformatie op te vragen vind je in de DirectoryInfo klasse:

Property Omschrijving
Exists bestaat het of niet (true of false)
FullName volledig pad
Name bestandsnaam
Parent basismap (volledig pad)

Code voorbeelden

Map inhoud lezen

Gebruik de methodes GetDirectories en GetFiles. Voorbeeld voor het lezen van de bestanden op de desktop:

string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);

string[] files = Directory.GetFiles(desktop); // lijst van bestanden (volledig pad)
foreach (string file in files)
{
    Console.WriteLine(new FileInfo(file).Name); // geef enkel de namen weer
}

Controleren of een map bestaat

string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string startfolder = System.IO.Path.Combine(folderPath, "myfolder");

if (!Directory.Exists(startfolder))
{
   // ...
}

Informatie opvragen

Gebruik de DirectoryInfo klasse om informatie op te vragen. Schematisch codevoorbeeld (map “Odisee” op de desktop):

string folderPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Odisee");
DirectoryInfo di = new DirectoryInfo(folderPath);
Console.WriteLine($@"
mapnaam: {di.Name}
basismap: {di.Parent.Name}
gemaakt op: {di.CreationTime}
");
mapnaam: Odisee
basismap: Desktop
gemaakt op: 29/01/2023 15:46:31
resultaat in de console

Dialoogvensters

OpenFileDialog

Schematisch:

OpenFileDialog dialog = new OpenFileDialog();
dialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
dialog.Filter = "Tekstbestanden|*.TXT;*.TEXT";
string chosenFileName;
bool? dialogResult = dialog.ShowDialog();
if (dialogResult == true) {
   // user picked a file and pressed OK
   chosenFileName = dialog.FileName;
} else {
   // user cancelled or escaped dialog window
}

OpenFolderDialog

Let op! Tot voor kort bestond (zeer vreemd genoeg) dit dialoogvenster niet. Het is pas toegevoegd sinds .NET8, dus werkt niet in oudere projecttypes als WPF .NET Framework! Daar werd je verondersteld third party oplossingen te gebruiken. Schematisch:

OpenFolderDialog dialog = new OpenFolderDialog();
dialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string chosenFolderName;
bool? dialogResult = dialog.ShowDialog();
if (dialogResult == true)
{
   // user picked a folder and pressed OK
   chosenFolderName = dialog.FolderName;
}
else
{
   // user cancelled or escaped dialog window
}

SaveFileDialog

Schematisch:

SaveFileDialog dialog = new SaveFileDialog();
dialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
dialog.Filter = "Tekstbestanden|*.TXT;*.TEXT";
dialog.FileName = "savedfile.txt";
if (dialog.ShowDialog() == true) {
   File.WriteAllText(dialog.FileName, "tekstinhoud hier");
} else {
   // user pressed Cancel or escaped dialog window
}

MessageBox

Zie .NET classes - MessageBox

Exception handling

Wanneer gebruiken

De regel wanneer wel en wanneer niet te gebruiken is heel eenvoudig:

Gebruik Exception handling enkel voor externe fouten waar je geen controle over hebt en die op geen andere manier op te vangen zijn, niet om mogelijke fouten in je eigen code weg te moffelen!

Gebruik het dus enkel in situaties waar je zelf echt niks anders aan kan doen:

Gebruik het niet wanneer er alternatieven bestaan:

Hoe gebruiken

Enkel indien echt nodig

Als er een evenwaardig alternatief bestaat, mag je het niet gebruiken. Twee voorbeelden:

try
{
   int[] numbers = new int[3];
   Console.WriteLine(numbers[5]); // Index out of bounds
}
catch (IndexOutOfRangeException ex)
{
   Console.WriteLine("Handled out-of-bounds access");
}
int[] numbers = new int[3];
if (numbers.Length > 5)
{
   Console.WriteLine(numbers[5]);
}
else
{
   Console.WriteLine("Index out of range");
}
try
{
   string content = File.ReadAllText("somefile.txt");
}
catch (FileNotFoundException)
{
   Console.WriteLine("File not found");
}
if (File.Exists("somefile.txt"))
{
   string content = File.ReadAllText("somefile.txt");
}
else
{
   Console.WriteLine("File not found");
}
int num;
try
{
   int num = int.Parse(Console.ReadLine());
}
catch (FormatException)
{
   Console.WriteLine("Invalid format");
}
int num;
if (!int.TryParse(Console.ReadLine(), out num))
{
   Console.WriteLine("Invalid format");
}

Zinvolle catch

Een try zonder zinvolle catch verbergt gewoon de fout, en dat is niet de bedoeling:

try
{
   File.Delete("somefile.txt");
}
catch
{
   // geen logging, geen actie ondernomen
}
try
{
   File.Delete("somefile.txt");
}
catch (IOException e)
{
   Console.WriteLine($"Er is een fout opgetreden: {e.Message}")
}

Specifieke exceptietypes

try
{
   string content = File.ReadAllText("somefile.txt");
}
catch (Exception ex) // te breed
{
   Console.WriteLine("An error occurred");
}
try
{
   string content = File.ReadAllText("somefile.txt");
}
catch (IOException) // ok, specifiek genoeg
{
   Console.WriteLine("Fout bij het lezen van het bestand");
}

Meerdere catch-blokken

Overweeg meerdere catch-blokken, met exceptietypes van specifiek naar algemeen:

string content;
string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "myfile.txt");
try {
   content = File.ReadAllText(filePath);
} catch (FileNotFoundException e) { // file not found
   lblMessage.Content = $"File {filePath} not found";
} catch (IOException e) { // unable to open for reading
   lblMessage.Content = $"Unable to open {filePath}";
} catch (Exception e) { // use general Exception as fallback
   lblMessage.Content = $"Unknown error reading {filePath}";
}
// process content
// ...

Merk op dat hoewel de FileNotFoundException in principe kan vervangen worden door een if (!File.Exists(filePath)) ..., het gebruik hier te verdedigen is omdat er al een try-catch blok voorkomt.

Beperkte try-blok

Zet niet meer code in het try-blok dan strikt nodig:

try {
   string content;
   string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
   string filePath = System.IO.Path.Combine(folderPath, "myfile.txt");
   content = File.ReadAllText(filePath);
   // process content
   // ...
} catch (FileNotFoundException e) { // file not found
   lblMessage.Content = $"File {filePath} not found";
} catch (IOException e) { // unable to open for reading
   lblMessage.Content = $"Unable to open {filePath}";
} catch (Exception e) { // use general Exception as fallback
   lblMessage.Content = $"Unknown error reading {filePath}";
}
string content;
string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = System.IO.Path.Combine(folderPath, "myfile.txt");
try {
   content = File.ReadAllText(filePath);
} catch (FileNotFoundException e) { // file not found
   lblMessage.Content = $"File {filePath} not found";
} catch (IOException e) { // unable to open for reading
   lblMessage.Content = $"Unable to open {filePath}";
} catch (Exception e) { // use general Exception as fallback
   lblMessage.Content = $"Unknown error reading {filePath}";
}
// process content
// ...

Zo laat mogelijk (meestal)

Een vuistregel is dat het beter is de

Finally

Het finally-blok wordt altijd uitgevoerd op het einde van het try-catch blok, wat er ook gebeurt. Dit verzekert b.v. dat bronnen correct afgesloten worden:

string content;
try
{
   reader = new StreamReader("somefile.txt");
   content = reader.ReadToEnd();
}
catch (FileNotFoundException)
{
   Console.WriteLine($"Bestand niet gevonden");
}
catch (IOException ex)
{
   Console.WriteLine($"Fout bij lezen: {ex.Message}");
}
finally
{
   reader.Close(); // bestand wordt gesloten, zelfs als een exceptie optreedt
   Console.WriteLine("File closed.");
}

Throw

Je kan ook zelf excepties opgooien. Wil je b.v. een methodes al declareren, maar de body pas later uitwerken, dan kan je tijdelijk NotImplementedException gebruiken

public static User TryLogin(string login, string pw) {
   throw new NotImplementedException("Deze feature is nog niet geïmplementeerd");
}
public static User FindUser(int id) {
   throw new NotImplementedException("Deze feature is nog niet geïmplementeerd");
}
public static void Delete() {
   throw new NotImplementedException("Deze feature is nog niet geïmplementeerd");
}
// ...

In een tweede voorbeeld worden in een methode FindKeywordInFile() niet alleen excepties opgevangen, maar ook opgegooid, zodat ze hoger in de Main() weer kunnen opgevangen en afgehandeld worden:

static void Main() {
   try {
      int lineNumber = FindKeywordInFile("sample.txt", "keyword");
   }
   catch (FileNotFoundException ex) {
      Console.WriteLine($"Fout: {ex.Message}");
   }
   catch (IOException ex) {
      Console.WriteLine($"Fout: {ex.Message}");
   }
   catch (Exception ex) {
      Console.WriteLine($"Onverwachte fout: {ex.Message}");
   }
}

static int FindKeywordInFile(string filePath, string keyword) {
   if (!File.Exists(filePath)) {
      throw new FileNotFoundException($"Bestand '{filePath}' niet gevonden.");
   }
   if (string.IsNullOrWhiteSpace(keyword)) {
      throw new ArgumentException("De zoekterm mag niet null of leeg zijn.");
   }
   try {
      string[] lines = File.ReadAllLines(filePath);
      for (int i = 0; i < lines.Length; i++) {
         if (lines[i].Contains(keyword)) {
               return i + 1;  // gevonden op regel i + 1
         }
      }
      return -1; // keyword niet gevonden
   }
   catch (IOException) {
      throw new IOException($"Kan het bestand '{filePath}' niet lezen.");
   }
   catch (Exception ex) {
      throw new Exception("Een onbekende fout is opgetreden.", ex);
   }
}

Je kan je de vraag stellen wat voor zin het heeft een exceptie te catchen, en dezelfde exceptie weer te throwen:

static int FindKeywordInFile(string filePath, string keyword) {
   ...
   catch (IOException) {
      throw new IOException($"Kan het bestand '{filePath}' niet lezen.");
   }
   ...
}

Op zich kan je dit weglaten; dit werkt evengoed:

static void Main() {
   try {
      int lineNumber = FindKeywordInFile("sample.txt", "keyword");
   }
   catch (FileNotFoundException ex) {
      Console.WriteLine($"Fout: {ex.Message}");
   }
   catch (IOException ex) {
      Console.WriteLine($"Fout: {ex.Message}");
   }
   catch (Exception ex) {
      Console.WriteLine($"Onverwachte fout: {ex.Message}");
   }
}

static int FindKeywordInFile(string filePath, string keyword) {
   if (!File.Exists(filePath)) {
      throw new FileNotFoundException($"Bestand '{filePath}' niet gevonden.");
   }
   if (string.IsNullOrWhiteSpace(keyword)) {
      throw new ArgumentException("De zoekterm mag niet null of leeg zijn.");
   }
   string[] lines = File.ReadAllLines(filePath);
   for (int i = 0; i < lines.Length; i++) {
      if (lines[i].Contains(keyword)) {
            return i + 1;  // gevonden op regel i + 1
      }
   }
   return -1; // keyword niet gevonden
}

Excepties catchen en weer opgooien is alleen nuttig in volgende situaties:

Maar als je niks toevoegt, heeft een exceptie opvangen en weer opgooien geen zin natuurlijk:

...
catch (IOException ex) {
   throw ex; // pretty pointless
}
...