101,000 Lines and Zero ViewModels
How 10,000 lines of copy-pasted code became 384.
For engineers and anyone who's wondered why software gets bloated.
The crash was fixed. The scheduler worked. The architecture had layers. But before I could build a new UI — the thing that would give us dark mode, Linux, macOS, and everything else — I needed to deal with the screen code. And the screen code had a problem of its own.
Eleven Kitchens, One Recipe
EVEMon had eleven list views showing different character data: Assets, Market Orders, Contracts, Industry Jobs, Wallet Journal, Wallet Transactions, Mail, Notifications, Kill Log, Planetary, and Research Points.
Each was a self-contained Windows Forms control between 400 and 1,500 lines long. Each contained its own filtering logic, its own sorting logic, its own grouping logic. The pattern was nearly identical across all eleven — but subtly different in each, because they were all hand-copied rather than derived from a shared design.
Imagine eleven different kitchens all following the same recipe, but each copying it by hand with slight variations. If you find a mistake in step three, you have to fix it in all eleven kitchens separately. And you can't taste-test any of them without turning on the entire restaurant.
In code terms: fix a sorting bug in assets, and the same bug exists in the other ten. Add a grouping option to contracts, and you modify eleven files. None of it testable because it's embedded inside Windows controls that need a running application to execute.
The Pattern
I counted the duplicated methods:
UpdateContent — 421 occurrences across 47 files
IsTextMatching — duplicated in every list control
UpdateSort — duplicated in every list control
UpdateContentByGroup — duplicated in every list controlEach control was roughly 800 lines doing the same thing: get items, filter them, sort them, group them, draw them. Repeated eleven times with minor variations.
One Base Class, 384 Lines
The fix was a generic base class: ListViewModel<TItem, TColumn, TGrouping>. One class that handles the entire filter/sort/group pipeline. Each concrete implementation provides four things:
- Where does the data come from? — return the list of items
- Does this item match the search text? — return true or false
- How do I compare two items? — return a sort order
- What group does this item belong to? — return a group key
Everything else — pipeline execution, change notifications, grouping, sort toggling, event subscriptions, new-item tracking — is handled by the base class. Set a filter, and it automatically refreshes. Change the sort column, and it re-sorts. Change the grouping, and it re-groups. The pipeline runs in the right order every time.
The Results
| ViewModel | Lines | Replaced |
|---|---|---|
| Assets | 108 | ~1,200 lines |
| Market Orders | 121 | ~800 lines |
| Contracts | 160 | ~750 lines |
| Industry Jobs | 111 | ~600 lines |
| Wallet Journal | 158 | ~500 lines |
| Wallet Transactions | 107 | ~500 lines |
| Mail Messages | 85 | ~400 lines |
| Notifications | 72 | ~400 lines |
| Kill Log | 153 | ~700 lines |
| Planetary | 107 | ~600 lines |
| Research Points | 96 | ~450 lines |
Eleven controls averaging 700 lines each became eleven ViewModels averaging 115 lines each. The largest is 160 lines. Most are under 120.
Plus two new ones that the original didn't have — Contacts and Standings — each 73 lines.
Why This Was the Hardest Part
The ViewModel layer doesn't sound exciting. There's no dramatic crash, no deadline, no user-facing feature. It's structural work that nobody sees.
But it was the single most important piece of the migration. Here's why:
With the business logic separated from the Windows screen code, I could replace the entire screen-drawing framework — and the ViewModels would work with both the old and new screens simultaneously. When I built the new Avalonia views, all thirteen list ViewModels carried over unchanged. Zero modifications. The logic that filters your market orders, groups your assets, and sorts your contracts works identically whether the screen is drawn by Windows Forms or Avalonia.
That's the payoff of architecture work. It doesn't feel like progress while you're doing it. But it makes everything after it possible.
The crash investigation started with a scheduler bug. Fixing the scheduler required splitting the architecture. Splitting the architecture required separating the events. Separating the events made the ViewModels possible. And the ViewModels made a new UI possible.
It was time to build that UI.