Skip to content
Theme
Accent
PhilosophyCase StudiesComponentsGitHub ↗
← All case studies.NET / WPF desktop

file-viewer

Opening and editing multi-GB files without the app falling over.

github.com/dhruvch1244/File-Viewer

A boundary fast enough that the tests actually get run

FileViewer.Core has zero reference to WPF. Its test suite, FileViewer.Core.Tests, runs as plain xUnit against a plain library — no WPF App, no window, no UI harness. That speed isn't incidental: it's the direct, measurable consequence of the boundary being real rather than a naming convention, and it's also what makes the tests something people actually run instead of skip under deadline pressure.

src/FileViewer.Core/FileViewer.Core.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

  <ItemGroup>
    <InternalsVisibleTo Include="FileViewer.Core.Tests" />
  </ItemGroup>

</Project>

Edits as an overlay, not a mutation

Cell edits, row add/duplicate/delete, and bulk delete are all represented as entries in an ordered overlay over the original file contents — the original stays untouched in memory for the whole session. The view renders original + overlay, never a mutated copy. Undo is "remove the last overlay entry," not "compute and apply an inverse" — which is why it's structurally guaranteed to work rather than best-effort: there's no edit that lacks an inverse, because the inverse is simply not applying it. The overlay only gets materialized into a new file at an explicit Save/Export — closing without saving is trivially correct, since it's just discarding the overlay.

src/FileViewer.Core/Overlay/EditOverlay.cs
public void Undo()
{
    lock (_gate)
    {
        if (_rowOps.Count == 0) return;
        RowOp op = _rowOps[^1];
        _rowOps.RemoveAt(_rowOps.Count - 1);
        ApplyReversalNoLock(op);
    }
}

Scale as a day-one constraint, not a later optimization

The app targets files up to 2 GB and stays responsive by indexing on load rather than holding the whole file in memory or re-scanning it per interaction — the kind of decision that has to be made in the data-access design from the start, because retrofitting indexing onto code written as if files were small is a rewrite, not a patch.

src/FileViewer.Core/Indexing/FileIndex.cs
/// Owns a read-only handle onto a DIF file plus the unmanaged row index and sort-key arrays built
/// by <see cref="FileIndexer"/>. The source file is never mutated — <see cref="GetRowBytes"/> reads
/// a row's bytes on demand via <see cref="RandomAccess"/> rather than through a persistent
/// memory-mapped view of the whole file: mapping the entire file into one contiguous view up front
/// is an all-or-nothing commitment that can fail on Windows (ERROR_NOT_ENOUGH_MEMORY) depending on
/// how much memory/page-file space happens to be free on the machine at that moment — a real failure
/// this project hit in practice, independent of the file's actual size (see git history). Reading
/// each row on demand instead means opening the file never requires reserving space for the whole
/// thing at once.