Files, settings and persistence

OpenFileDialog and SaveFileDialog, JSON configuration, user settings, recent-file lists, and the file-handling rules that keep a desktop app trustworthy.

File dialogs

using var open = new OpenFileDialog
{
    Title = "Open data file",
    Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*",
    FilterIndex = 1,
    Multiselect = true,
    CheckFileExists = true,
    RestoreDirectory = true
};

if (open.ShowDialog(this) != DialogResult.OK) return;
foreach (var path in open.FileNames)
    queue.Enqueue(path);

using var save = new SaveFileDialog
{
    Filter = "JSON report (*.json)|*.json",
    FileName = $"report-{DateTime.Today:yyyy-MM-dd}",
    DefaultExt = "json",
    AddExtension = true,
    OverwritePrompt = true
};

if (save.ShowDialog(this) == DialogResult.OK)
    File.WriteAllText(save.FileName, JsonSerializer.Serialize(report, JsonOptions));
  • On modern Windows the common dialog offers a virtual location rather than a real path. Check File.Exists before opening, and handle the empty FileName that some shells return.
  • RestoreDirectory = true puts the working directory back afterwards; without it a relative path used later silently points somewhere else.
  • Filenames and paths are not sanitised for you. A file named CON.txt or a path with a trailing dot fails on real Windows.

Settings and configuration

public sealed class AppSettings
{
    public string Theme { get; set; } = "Light";
    public int RecentCount { get; set; } = 50;
    public List<string> RecentFiles { get; set; } = new();
}

static string SettingsPath => Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
    "OrderTool", "settings.json");

public static AppSettings Load()
{
    try
    {
        return File.Exists(SettingsPath)
            ? JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(SettingsPath)) ?? new AppSettings()
            : new AppSettings();
    }
    catch (JsonException)
    {
        File.Move(SettingsPath, SettingsPath + ".bad", overwrite: true);  // keep the evidence
        return new AppSettings();                                         // and start clean
    }
}

public static void Save(AppSettings s)
{
    Directory.CreateDirectory(Path.GetDirectoryName(SettingsPath)!);
    var tmp = SettingsPath + ".tmp";
    File.WriteAllText(tmp, JsonSerializer.Serialize(s, new JsonSerializerOptions { WriteIndented = true }));
    File.Move(tmp, SettingsPath, overwrite: true);   // write-then-rename, never a half file
}
What to storeWhereWhy
Machine configurationappsettings.json beside the executableShips with the app, read-only at runtime
Per-user preferences%APPDATA%Writable without administrator rights
Per-user cached data%LOCALAPPDATA%Not synced to a roaming profile
SecretsWindows Credential Manager or DPAPIA JSON file is readable by anyone with the profile
DocumentsDocuments or a user-chosen pathUsers expect to find their own files

Write settings atomically: serialise to a temporary file in the same directory, then rename over the target. A crash during a direct write leaves a truncated file behind, and the user's next launch is the one that breaks.

Recent files and safe handling

void RememberFile(string path)
{
    var recent = _settings.RecentFiles;
    recent.RemoveAll(p => string.Equals(p, path, StringComparison.OrdinalIgnoreCase));
    recent.Insert(0, path);
    if (recent.Count > _settings.RecentCount)
        recent.RemoveRange(_settings.RecentCount, recent.Count - _settings.RecentCount);
    AppSettings.Save(_settings);
    RebuildRecentMenu();
}
  1. Open with FileShare.Read when you only read, so another process is not blocked.
  2. Use Path.GetFullPath before comparing paths; the same file has many spellings.
  3. Skip entries in the recent list that no longer exist, but keep them until the user asks to clean up - a disconnected drive is not a deleted file.
  4. Never construct a path by concatenating user input: use Path.Combine and reject any result that escapes the intended directory.
  5. Log the exception type and the path, never the file contents, when a save fails.
⚠️
A desktop app that crashes on a corrupt settings file is a support ticket you will never close. Treat every file you read at startup as untrusted: wrap it, validate it, and fall back to defaults so the window always opens.

FAQ

Application settings or user settings?
Application settings are shared and read-only at runtime; user settings are per profile and writable. In .NET 8 and later, prefer a JSON file in %APPDATA% over the legacy settings designer - it is visible, diffable and testable.
How do I store a password?
Do not. Use CredentialManager or protect the value with DPAPI scoped to the current user, and only ever store a secret you cannot replace with a token.

Async work, threading and progress Deployment: ClickOnce, MSIX and single-file

Last refreshed 2026-09-18.