Reading Legacy Code
Before you start
You need: Articles 01–06. This one puts them together on an unfamiliar codebase.
Time: about 50 minutes, plus the practice.
Learning objective
Take an unfamiliar VB.NET application and, without changing it, explain what one screen does and where its logic lives.
Topics
- A survey order for an unfamiliar codebase
- Code-behind and the event model
Handles,WithEvents, andAddHandler- Tracing a button click to the database
- VB.NET to C# translation reference
- Reading before changing
Terminology
| Term | Meaning |
|---|---|
| Legacy application | Software still in production that predates current practice and has no active feature roadmap |
| Code-behind | The .vb file holding the logic for a form or page, paired with a designer file |
| Maintenance | Fixing defects and making small changes, without redesigning |
| Regression | Something that used to work and no longer does, caused by a change |
| Connection string | The configuration value telling ADO.NET which server, database, and credentials to use |
The distinction that matters to your job is between maintenance and redesign. On a maintenance task, the smallest change that fixes the problem is the correct one. Improvements you noticed belong in a list you hand to your lead, not in the same commit.
Survey order
Do not start reading at line 1 of the file you were pointed at. Build a map first, in this order.
1. Solution structure. Open the .sln and list the projects. One project is a small tool. Twelve projects with a .Data, .Business, and .Web split tell you where to expect what.
2. Configuration. Read Web.config or App.config. Connection strings tell you how many databases are involved. appSettings keys are usually the feature switches nobody documented.
3. Compiler options. Check Option Strict in project properties. Off means run-time type failures are possible everywhere and you should trust the compiler less.
4. Entry points. For Web Forms, the .aspx pages. For WinForms, the startup form. For a service, Sub Main. This is the list of things a user can actually do.
5. Data layer. Find where SqlConnection appears. That tells you whether data access is centralised in a few classes or scattered through the UI.
6. Only then, the screen you were asked about.
Use Find in Files — Ctrl+Shift+F. Set Look in to Entire Solution and, for these, File types to *.vb.
| Search for | Tells you |
|---|---|
New SqlConnection | How scattered data access is |
On Error | Where unstructured error handling survives |
Option Strict | Which files override the project setting |
TODO, HACK, FIXME | Known debt the last developer left you |
Ctrl+Shift+F, not Ctrl+F. Ctrl+F searches the current file only, and on an unfamiliar codebase that is the wrong question. Results open in the Find Results window; double-click a line to jump to it, and press F8 to walk through the hits.
Use Find in Files rather than Find All References for this survey. Find All References needs the symbol to resolve, and in a project with Option Strict Off a late-bound call has no symbol to resolve — a plain text search finds it, and the smarter tool does not.
A high SqlConnection count spread across UI files means business logic and data access are mixed, and a change in one screen will not be reusable in another.
Code-behind and the event model
A Web Forms page is three files:
StudentList.aspx markup and control declarations
StudentList.aspx.vb code-behind — the logic
StudentList.aspx.designer.vb generated control fields — do not edit
WinForms follows the same shape with .vb and .Designer.vb.
The .designer.vb file is regenerated by Visual Studio. Editing it by hand loses your changes the next time someone opens the designer. When a control field seems to be missing, it is a designer-file problem, not a code problem.
Handles, WithEvents, and AddHandler
VB.NET wires events three ways, and a legacy application usually contains all three.
' 1. Handles clause — the common form. The field must be declared WithEvents.
Protected WithEvents btnSearch As Button
Protected Sub btnSearch_Click(sender As Object, e As EventArgs) Handles btnSearch.Click
LoadStudents()
End Sub
' 2. One handler, several events
Protected Sub Filter_Changed(sender As Object, e As EventArgs) _
Handles ddlClass.SelectedIndexChanged, ddlSection.SelectedIndexChanged
LoadStudents()
End Sub
' 3. AddHandler — wired at run time, invisible to a name search
AddHandler grdStudents.RowCommand, AddressOf Students_RowCommand
The third form is the one that costs you time. A handler attached with AddHandler has no Handles clause, so searching for the control name will not find it. When you cannot find what responds to a control, search for AddHandler and for AddressOf.
Handles binds by the field name, not the method name. Renaming the method is safe; renaming the control field breaks every Handles clause referring to it.
In Web Forms, also check AutoEventWireup in the .aspx page directive. When True, methods named Page_Load are wired by convention with no Handles clause at all.
Tracing a button click
The exercise: a user clicks Search on the student list and the wrong students appear. Find where the logic is, without changing anything.
1. Find the control in the markup.
<asp:Button ID="btnSearch" runat="server" Text="Search" />
<asp:GridView ID="grdStudents" runat="server" AutoGenerateColumns="False">
2. Find the handler. Search the code-behind for btnSearch. Look for Handles btnSearch.Click; if there is none, search for AddHandler btnSearch.
3. Read the page lifecycle. In Web Forms, Page_Load runs before the click handler on every postback. A Page_Load that reloads the grid without an IsPostBack check will overwrite whatever the handler did:
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
If Not IsPostBack Then
LoadStudents() ' correct — only on first load
End If
End Sub
A missing IsPostBack check is the single most common Web Forms defect. The symptom is exactly "my filter does nothing" or "the grid resets itself".
4. Follow the call chain down.
btnSearch_Click
└─ LoadStudents()
└─ StudentService.Search(schoolId, term)
└─ StudentRepository.Search(...)
└─ SqlCommand → Student table
Use Go to definition (F12) at each step rather than searching by name — a Module function with no receiver is otherwise very hard to place.
5. Note where the filter value actually comes from. txtSearch.Text, a Session variable, a ViewState entry, and a query-string value are four different sources, and legacy screens mix them.
6. Write down the chain before you touch anything. That written chain is what you attach to the ticket, and it is what the capstone assignment asks for.
VB.NET to C# reference
Useful in both directions: for translating an example you found in C#, and for explaining VB.NET code to a colleague who only reads C#.
| VB.NET | C# |
|---|---|
Dim x As Integer = 5 | int x = 5; |
Nothing | null (and default for value types) |
Me / MyBase | this / base |
& | + for strings |
AndAlso / OrElse | && / || |
And / Or | & / | |
Not | ! |
<> | != |
Is / IsNot | == / != on references |
Mod / \ | % / integer / |
Sub / Function | void / typed method |
ByVal / ByRef | (default) / ref |
Optional p As Integer = 0 | int p = 0 |
ParamArray | params |
Shared | static |
Friend | internal |
MustInherit / MustOverride | abstract |
NotInheritable / NotOverridable | sealed |
Overridable / Overrides | virtual / override |
Shadows | new (member hiding) |
Inherits / Implements | : Base / : IInterface |
Property X As Integer | public int X { get; set; } |
Select Case | switch |
For i = 0 To 4 | for (i = 0; i <= 4; i++) |
Dim a(4) As Integer | int[] a = new int[5]; |
List(Of T) | List<T> |
Function(x) x.Name | x => x.Name |
CType(o, Student) | (Student)o |
TryCast(o, Student) | o as Student |
Try/Catch/Finally | same |
Catch ex As E When cond | catch (E ex) when (cond) |
Using | using |
AddressOf M | M (method group) |
NameOf(x) | nameof(x) |
' comment | // comment |
| line break | ; |
Three rows deserve care when translating:
For ... Tois inclusive.For i = 0 To 4isi <= 4, five iterations.Dim a(4)allocates five elements, not four.CTypeversusTryCast.CTypethrows on failure and also performs conversions (CType("5", Integer)works);TryCastreturnsNothingand only works on reference types.
Reading before changing
Before you edit anything in an unfamiliar module, be able to answer:
- What calls this? Use Find all references, not a text search.
- What does it call? Follow each call one level down.
- What state does it read that is not a parameter —
Session,ViewState, aModulevariable, a static cache? - What does it write besides its return value — a database row, a file, a global?
- Is it wired to an event, and does anything else handle the same event?
- Is there a test? If not, what manual steps prove it works today?
If you cannot answer the last one, establish it before changing anything. A change you cannot verify is a change you cannot defend.
When something looks wrong, use the version history before deleting it. A strange-looking condition is often a deliberate fix for a real incident, and the commit message usually says so.
Errors you will hit
| What you see | Cause | What to do |
|---|---|---|
| A handler that never runs | Missing Handles clause, or the control was renamed | Check the Handles and the designer file |
| A control the code-behind cannot see | The .designer.vb is out of sync | Open the page in the designer to regenerate it |
| Two handlers for one button | Handles plus an AddHandler somewhere else | Search for AddHandler |
| Search finds nothing you expect | Searched the wrong scope | Set Find in Files to Entire Solution |
| A method that seems unused | Called by name through late binding | Search the whole solution for the string |
Late binding defeats Find All References. With Option Strict Off, a method can be invoked by a name built at run time, so the only reliable search is a plain text search for the name.
Common mistakes
- Reading the reported file first and never building a map of the application
- Searching for a control name and concluding nothing handles it, when
AddHandlerwas used - Editing a
.designer.vbfile by hand - Missing that
Page_Loadruns before the click handler on every postback - Assuming a call with no receiver is local, when it is a
Modulemember - Translating
For i = 0 To nasi < n - Using
CTypewhereTryCastwas meant, turning a null check into an exception - Deleting odd-looking code before reading its history
- Fixing unrelated things you noticed, in the same change
Practice
Pick one screen in an existing VB.NET application. Without editing anything, produce a written trace: the markup control, the handler and how it is wired, every method called down to the SQL statement, and every piece of state read that is not a parameter. Note whether Page_Load guards with IsPostBack.
Then take a C# method of about twenty lines and translate it to VB.NET by hand, without a converter tool. Compile it. The errors you get are the parts of the mapping you have not internalised yet — most people discover the inclusive For ... To here.
Finally, run the AI drill from the course: ask an assistant to explain an unfamiliar legacy procedure, then verify every claim it makes against the code. Note anything it stated confidently that was wrong. That verification habit is the point of the exercise, not the explanation.
You can now
- Survey an unfamiliar VB.NET application in a defined order
- Use Find in Files across the whole solution to map data access and error handling
- Trace any screen from a control back to the database
- Translate between VB.NET and C# without a converter
- Say what to read before changing anything
Review questions
- Why can searching for a control's name fail to find its event handler?
- What breaks when you rename a control field that has
Handlesclauses pointing at it? - Why does a missing
IsPostBackcheck make a search filter appear to do nothing? - What is the difference between
CTypeandTryCast?
Next: Maintenance project