Introduction
As a .NET Solutions Architect with over 13 years of experience, I have worked with many legacy and modern .NET applications; VB.NET remains important for maintaining existing Windows apps and migrating parts of systems to newer .NET runtimes. This tutorial focuses on the essentials of VB.NET: variables, control structures, object-oriented programming, and building a practical Windows Forms application so you can apply these concepts immediately.
Throughout this guide you'll get hands-on experience building a basic task manager (GUI) using Visual Studio 2022 and .NET 8. You will also get concrete guidance on modern error handling, secure local persistence with System.Text.Json, async file I/O for responsive UIs, a lightweight logging helper, and troubleshooting steps for common problems.
Setting Up Your Development Environment
Install Visual Studio and .NET
For a beginner-friendly and fully integrated experience, use Visual Studio 2022 (Community or later) with the .NET desktop development workload selected. Target the .NET 8 runtime for new projects to get the latest performance and security improvements.
- Download Visual Studio: https://visualstudio.microsoft.com/
- Get .NET SDKs and documentation: https://dotnet.microsoft.com/
- When creating projects, choose the Visual Basic language and the Windows Forms App (.NET) template for GUI applications.
Recommended tool versions used in this tutorial:
- Visual Studio 2022 (17.x) with .NET desktop workload
- .NET SDK 8.x (target framework net8.0)
- System.Text.Json (built into .NET 8)
Understanding VB.NET Basics: Syntax and Data Types
Core Syntax and Data Types
VB.NET syntax is explicit and readable. Variables are declared with Dim and typed for clarity and performance.
Dim count As Integer = 0
Dim title As String = "Task Manager"
Dim isActive As Boolean = True
Dim createdAt As DateTime = DateTime.UtcNow
Common data types you'll use:
- Integer — whole numbers
- String — text
- Boolean — true/false
- DateTime — dates and times
Use Option Strict On at the project level to enforce explicit conversions and catch type-related issues early. Benefits of Option Strict On include preventing implicit narrowing conversions, avoiding late-bound calls (which are slower and error-prone), and surfacing issues at compile time rather than runtime — improving code reliability and preventing subtle bugs.
Creating a Windows Forms Application (step-by-step)
This section walks through creating a minimal task manager (Add/Remove tasks + persistence to JSON) using the Windows Forms template and VB.NET. The example uses .NET 8 and System.Text.Json for serialization.
1) Create the Project
- Open Visual Studio > Create a new project.
- Filter by Visual Basic and select Windows Forms App (.NET). Click Next.
- Name the project
TaskManagerVB, target.NET 8, and create the project.
2) Design the Form
From the Toolbox drag these controls onto Form1 (set their Name properties as shown):
- TextBox — Name:
txtNewTask - Button — Name:
btnAdd, Text:Add - ListBox — Name:
lstTasks - Button — Name:
btnRemove, Text:Remove Selected - Button — Name:
btnSave, Text:Save
Beginner tip — add new class files for the model and repository:
- In Solution Explorer, right-click the project (TaskManagerVB) > Add > Class...
- Name the new files
TaskItem.vbandTaskRepository.vbrespectively and paste the code from the Task Manager: Code Examples section into each file. - Keep
Form1.vbfocused on UI logic; place domain and persistence code in separate files for maintainability.
Quick run instructions:
- Press F5 or click the Start button in Visual Studio to build and run the app with the debugger attached.
- Use Ctrl+F5 to run without the debugger after initial testing.
Visual reference: simple UI mockup
The SVG below is a lightweight screenshot-style mockup of the Form layout (TextBox, Add button, ListBox, Remove, Save). Use it as a visual guide when arranging controls in the Designer.
3) Add Imports
System.Text.Json is included with .NET 8 and provides high-performance JSON serialization. We'll use it with asynchronous File I/O APIs to keep the UI responsive. See Async/Await for responsive I/O for patterns and guidance.
Task Manager: Code Examples
Below is a compact, complete example you can paste into your project. Place model and repository classes in their own files, and add the Form1 code to the form's code-behind. The example uses safe storage in the user's LocalApplicationData folder, async I/O, and a simple atomic-save pattern.
Model: TaskItem.vb
Public Class TaskItem
Public Property Id As Guid
Public Property Title As String
Public Property CreatedAt As DateTime
Public Sub New()
Id = Guid.NewGuid()
CreatedAt = DateTime.UtcNow
End Sub
Public Overrides Function ToString() As String
Return Title
End Function
End Class
Repository: TaskRepository.vb
Imports System.IO
Imports System.Text.Json
Imports System.Threading.Tasks
Public Class TaskRepository
Private ReadOnly _filePath As String
Private ReadOnly _options As JsonSerializerOptions
Public Sub New(filePath As String)
_filePath = filePath
_options = New JsonSerializerOptions With {
.WriteIndented = True,
.PropertyNameCaseInsensitive = True
}
End Sub
Public Async Function LoadAsync() As Task(Of List(Of TaskItem))
If Not File.Exists(_filePath) Then
Return New List(Of TaskItem)()
End If
Using stream = File.OpenRead(_filePath)
Dim result = Await JsonSerializer.DeserializeAsync(Of List(Of TaskItem))(stream, _options)
Return If(result, New List(Of TaskItem)())
End Using
End Function
Public Async Function SaveAsync(tasks As List(Of TaskItem)) As Task
' Atomic save: write to temp file then replace
Dim dir = Path.GetDirectoryName(_filePath)
If Not Directory.Exists(dir) Then
Directory.CreateDirectory(dir)
End If
Dim tempFile = _filePath & ".tmp"
Using stream = File.Create(tempFile)
Await JsonSerializer.SerializeAsync(stream, tasks, _options)
End Using
' Replace (overwrite) the original file
File.Copy(tempFile, _filePath, True)
File.Delete(tempFile)
End Function
End Class
Form1 code-behind: Form1.vb
Option Strict On
Imports System.IO
Imports System.Threading.Tasks
Public Class Form1
Private ReadOnly _tasks As New List(Of TaskItem)()
Private _repo As TaskRepository
Private Async Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' Store app data in LocalApplicationData to avoid permission issues
Dim appFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TaskManagerVB")
Dim filePath = Path.Combine(appFolder, "tasks.json")
_repo = New TaskRepository(filePath)
Try
Dim loaded = Await _repo.LoadAsync()
_tasks.Clear()
_tasks.AddRange(loaded)
RefreshList()
Catch ex As Exception
' Handle corrupt JSON or IO issues
MessageBox.Show($"Failed to load tasks: {ex.Message}", "Load Error", MessageBoxButtons.OK, MessageBoxIcon.Warning)
End Try
End Sub
Private Sub RefreshList()
lstTasks.Items.Clear()
For Each t In _tasks
lstTasks.Items.Add(t)
Next
End Sub
Private Sub btnAdd_Click(sender As Object, e As EventArgs) Handles btnAdd.Click
Dim text = txtNewTask.Text.Trim()
If String.IsNullOrEmpty(text) Then
MessageBox.Show("Enter a task title.", "Validation", MessageBoxButtons.OK, MessageBoxIcon.Information)
Return
End If
Dim item = New TaskItem With {.Title = text}
_tasks.Add(item)
lstTasks.Items.Add(item)
txtNewTask.Clear()
End Sub
Private Sub btnRemove_Click(sender As Object, e As EventArgs) Handles btnRemove.Click
Dim idx = lstTasks.SelectedIndex
If idx >= 0 AndAlso idx < _tasks.Count Then
_tasks.RemoveAt(idx)
RefreshList()
Else
MessageBox.Show("Select a task to remove.", "Validation", MessageBoxButtons.OK, MessageBoxIcon.Information)
End If
End Sub
Private Async Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click
Try
Await _repo.SaveAsync(_tasks)
MessageBox.Show("Tasks saved.", "Save", MessageBoxButtons.OK, MessageBoxIcon.Information)
Catch ex As Exception
MessageBox.Show($"Failed to save tasks: {ex.Message}", "Save Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
End Class
Notes:
- Event handlers use
Asyncwhere appropriate to avoid blocking the UI thread. - We store the file under
LocalApplicationDatato reduce permission errors and avoid writing to Program Files or user root directories. - Atomic save (write to temp then replace) reduces the chance of corrupting the file if the app crashes during write.
Annotated Code (comments for beginners)
The blocks below repeat the same code as above but include inline comments to explain what each important line does. These are safe to copy into separate files in your project.
Annotated Model: TaskItem.vb
' Simple domain model for a task
Public Class TaskItem
' Unique identifier used to distinguish tasks
Public Property Id As Guid
' The visible title shown in the UI
Public Property Title As String
' When the task was created (UTC for consistency)
Public Property CreatedAt As DateTime
Public Sub New()
' Generate a new GUID on construction
Id = Guid.NewGuid()
' Use UTC to avoid timezone surprises when shared
CreatedAt = DateTime.UtcNow
End Sub
' Override ToString so ListBox displays the Title by default
Public Overrides Function ToString() As String
Return Title
End Function
End Class
Annotated Repository: TaskRepository.vb
Imports System.IO
Imports System.Text.Json
Imports System.Threading.Tasks
Public Class TaskRepository
Private ReadOnly _filePath As String
Private ReadOnly _options As JsonSerializerOptions
' filePath: full path to tasks.json (store in LocalApplicationData)
Public Sub New(filePath As String)
_filePath = filePath
' JsonSerializerOptions can be reused to reduce allocations
_options = New JsonSerializerOptions With {
.WriteIndented = True, ' make the JSON readable for debugging
.PropertyNameCaseInsensitive = True ' tolerate case differences
}
End Sub
' LoadAsync: returns an empty list if file not found or JSON is null
Public Async Function LoadAsync() As Task(Of List(Of TaskItem))
If Not File.Exists(_filePath) Then
Return New List(Of TaskItem)()
End If
Using stream = File.OpenRead(_filePath)
Dim result = Await JsonSerializer.DeserializeAsync(Of List(Of TaskItem))(stream, _options)
Return If(result, New List(Of TaskItem)())
End Using
End Function
' SaveAsync: atomic write pattern to reduce corruption risk
Public Async Function SaveAsync(tasks As List(Of TaskItem)) As Task
Dim dir = Path.GetDirectoryName(_filePath)
If Not Directory.Exists(dir) Then
Directory.CreateDirectory(dir)
End If
Dim tempFile = _filePath & ".tmp"
Using stream = File.Create(tempFile)
' SerializeAsync writes JSON directly to the stream
Await JsonSerializer.SerializeAsync(stream, tasks, _options)
End Using
' Copy temp over target to atomically replace
File.Copy(tempFile, _filePath, True)
File.Delete(tempFile)
End Function
End Class
Annotated Form1.vb (key event handlers)
Option Strict On
Imports System.IO
Imports System.Threading.Tasks
Public Class Form1
Private ReadOnly _tasks As New List(Of TaskItem)()
Private _repo As TaskRepository
' Called when the form loads; we initialize the repository and load saved tasks
Private Async Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim appFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TaskManagerVB")
Dim filePath = Path.Combine(appFolder, "tasks.json")
_repo = New TaskRepository(filePath)
Try
Dim loaded = Await _repo.LoadAsync() ' Await keeps UI responsive
_tasks.Clear()
_tasks.AddRange(loaded)
RefreshList()
Catch ex As Exception
' Surface a friendly message but log details for support (see security/troubleshooting below)
MessageBox.Show($"Failed to load tasks: {ex.Message}", "Load Error", MessageBoxButtons.OK, MessageBoxIcon.Warning)
End Try
End Sub
' RefreshList repopulates the ListBox from the in-memory list
Private Sub RefreshList()
lstTasks.Items.Clear()
For Each t In _tasks
lstTasks.Items.Add(t)
Next
End Sub
' Add button: validate input then add a new TaskItem
Private Sub btnAdd_Click(sender As Object, e As EventArgs) Handles btnAdd.Click
Dim text = txtNewTask.Text.Trim()
If String.IsNullOrEmpty(text) Then
MessageBox.Show("Enter a task title.", "Validation", MessageBoxButtons.OK, MessageBoxIcon.Information)
Return
End If
Dim item = New TaskItem With {.Title = text}
_tasks.Add(item)
lstTasks.Items.Add(item)
txtNewTask.Clear()
End Sub
' Remove button: remove the selected task from the list
Private Sub btnRemove_Click(sender As Object, e As EventArgs) Handles btnRemove.Click
Dim idx = lstTasks.SelectedIndex
If idx >= 0 AndAlso idx < _tasks.Count Then
_tasks.RemoveAt(idx)
RefreshList()
Else
MessageBox.Show("Select a task to remove.", "Validation", MessageBoxButtons.OK, MessageBoxIcon.Information)
End If
End Sub
' Save button: persist changes asynchronously
Private Async Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click
Try
Await _repo.SaveAsync(_tasks)
MessageBox.Show("Tasks saved.", "Save", MessageBoxButtons.OK, MessageBoxIcon.Information)
Catch ex As Exception
MessageBox.Show($"Failed to save tasks: {ex.Message}", "Save Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
End ClassDownload & Run the Sample
To fulfill the "Try the code today" promise, this guide provides complete source files you can copy into a new Visual Basic Windows Forms project named TaskManagerVB and run locally. There is no official hosted sample in this article; the intended "download" flow is copying files into your project and running them in Visual Studio.
If you want to publish or share your copy of the sample, follow these steps:
git init
git add .
git commit -m "Initial TaskManagerVB sample"
# Create a repo on GitHub using the website, then add the remote and push
git remote add origin git@github.com:your-username/TaskManagerVB.git
git branch -M main
git push -u origin main
Helpful endpoints while you create or share the repository:
- GitHub home (create a repo via the website): https://github.com/
- Visual Studio downloads and guidance: https://visualstudio.microsoft.com/
- .NET SDK and docs: https://dotnet.microsoft.com/
If you prefer a ZIP file of your project, create it locally after confirming the project builds. In Windows Explorer, right-click the project folder > Send to > Compressed (zipped) folder. That ZIP can be distributed or attached to issues for debugging.
Note: The guidance above clarifies that copying the supplied source is the intended quick-start path. If you publish your project to GitHub, use a repository root URL (for example, https://github.com/your-username/TaskManagerVB) so others can clone it.
Architecture Diagram
This diagram shows the simple local architecture used by the Task Manager: the WinForms client (UI) reads and writes a JSON file in LocalApplicationData. An optional cloud sync or backup layer can be added separately.
Handling Errors and Debugging in VB.NET
Robust error handling and effective debugging make the difference between fragile samples and production-ready code. The sample uses friendly MessageBox notifications for user-facing errors and points where you should capture richer diagnostic information with logging.
Key patterns
- Catch exceptions close to the I/O boundary (file reads/writes) and provide clear user-facing messages while logging details for diagnostics.
- Never swallow exceptions silently — at minimum log them. For recoverable errors, implement retries with backoff where appropriate.
- Validate inputs before operations to reduce exceptions (e.g., verify non-empty strings, file path existence).
Using Visual Studio to debug
- Set breakpoints and step through code (F9 to toggle breakpoints; F10/F11 to step).
- Use Exception Settings (Debug > Windows > Exception Settings) to break on thrown exceptions (first-chance exceptions) for a tighter feedback loop.
- Inspect locals and watch windows to confirm values, and use the Immediate Window to evaluate expressions at runtime.
- Use "Attach to Process" to debug a running EXE if you launched the app outside the debugger.
Common failure modes and quick fixes
- Corrupt JSON: Rename the existing tasks.json and let the app recreate it. Log and report the corrupted payload for analysis.
- Permission errors: Ensure the app writes to LocalApplicationData rather than program files. Run Visual Studio as administrator only for debugging scenarios that require elevated access, not as a routine.
- Antivirus/quarantine: If writes fail intermittently, check AV logs; signing or whitelisting the app might be necessary in locked-down environments.
Security and Troubleshooting
This section provides a lightweight logging helper you can drop into the project and pragmatic security guidance for local desktop persistence.
Logging helper (Logger.vb)
Use a minimal, async file logger to capture exceptions and operational events. Do not log sensitive user data (PII) — sanitize inputs before logging.
Imports System.IO
Imports System.Text
Imports System.Threading
Imports System.Threading.Tasks
Public NotInheritable Class Logger
Private Shared ReadOnly _semaphore As New SemaphoreSlim(1, 1)
Private Shared ReadOnly _logDir As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TaskManagerVB", "logs")
Private Shared ReadOnly _logFile As String = Path.Combine(_logDir, "app.log")
Private Shared ReadOnly _maxBytes As Long = 5 * 1024 * 1024 ' 5 MB
Private Sub New()
End Sub
Public Shared Async Function LogInfoAsync(message As String) As Task
Await WriteAsync("INFO", message)
End Function
Public Shared Async Function LogErrorAsync(message As String) As Task
Await WriteAsync("ERROR", message)
End Function
Private Shared Async Function WriteAsync(level As String, message As String) As Task
Try
Await _semaphore.WaitAsync()
If Not Directory.Exists(_logDir) Then Directory.CreateDirectory(_logDir)
Dim timestamp = DateTime.UtcNow.ToString("o")
Dim line = $"{timestamp} [{level}] {message}" & Environment.NewLine
' Rotate if too large
If File.Exists(_logFile) Then
Dim fi = New FileInfo(_logFile)
If fi.Length > _maxBytes Then
Dim rotated = Path.Combine(_logDir, $"app-{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.log")
File.Move(_logFile, rotated)
End If
End If
Await File.AppendAllTextAsync(_logFile, line, Encoding.UTF8)
Catch
' Swallow logging errors to avoid crash loops; consider Windows Event Log for critical failures
Finally
_semaphore.Release()
End Try
End Function
End Class
Security best practices
- Do not place sensitive secrets (API keys, passwords) in plain text files. If you add cloud sync, use secure storage for credentials (Windows Credential Manager) or token-based auth.
- Avoid logging PII. If you must, encrypt logs or redact sensitive fields before writing.
- Restrict file ACLs on the application folder if the environment requires stricter access control.
- For stronger protection of persisted data consider using OS-level data protection APIs (for example, DPAPI) when storing personal data locally.
Troubleshooting checklist
- If tasks fail to load: check the log (LocalApplicationData/TaskManagerVB/logs) for stack traces and error messages.
- If saving fails intermittently: confirm no third-party process locks the file; consider retry with jitter for transient sharing violations.
- If the UI freezes: verify long-running work is awaited asynchronously (see Async/Await for responsive I/O).
Async/Await for responsive I/O
Use async methods for disk or network I/O to keep the WinForms UI thread responsive. The sample uses Async event handlers and JsonSerializer.SerializeAsync. Below are practical patterns and an improved SaveAsync variant that supports CancellationToken and uses asynchronous FileStream.
Why async matters
- Blocking file or network calls on the UI thread make the application unresponsive and can trigger "Not responding" windows.
- Async I/O frees the UI thread to handle painting and user input while I/O completes in the background.
Improved SaveAsync with cancellation (example)
This variant keeps the original atomic-save pattern but adds cancellation support and uses an async FileStream. Use this in repositories where users can cancel long-running operations.
Imports System.IO
Imports System.Text.Json
Imports System.Threading
Imports System.Threading.Tasks
Public Class TaskRepositoryAsync
Private ReadOnly _filePath As String
Private ReadOnly _options As JsonSerializerOptions
Public Sub New(filePath As String)
_filePath = filePath
_options = New JsonSerializerOptions With {.WriteIndented = True, .PropertyNameCaseInsensitive = True}
End Sub
Public Async Function SaveAsync(tasks As List(Of TaskItem), ct As CancellationToken) As Task
Dim dir = Path.GetDirectoryName(_filePath)
If Not Directory.Exists(dir) Then Directory.CreateDirectory(dir)
Dim tempFile = _filePath & ".tmp"
' Use FileOptions.Asynchronous for a true async file stream
Using stream = New FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous)
ct.ThrowIfCancellationRequested()
Await JsonSerializer.SerializeAsync(stream, tasks, _options, ct)
Await stream.FlushAsync(ct)
End Using
ct.ThrowIfCancellationRequested()
File.Copy(tempFile, _filePath, True)
File.Delete(tempFile)
End Function
End Class
Notes:
- In UI code, pass
CancellationToken.Noneif you don't support cancellation. For longer operations expose a Cancel button wired to a CancellationTokenSource. - In library code, prefer
ConfigureAwait(False)on awaited tasks to avoid capturing the synchronization context; do not callConfigureAwait(False)in UI event handler continuations where you need to update UI controls. - Use progress reporting (IProgress(Of T)) for long-running syncs so the UI can show progress.
Further Reading
- Visual Studio downloads and docs: https://visualstudio.microsoft.com/
- .NET docs and SDK: https://dotnet.microsoft.com/
- GitHub home (create a repository to share samples): https://github.com/
Key Takeaways
- VB.NET remains a practical choice for maintaining Windows desktop applications; use
Option Strict Onand typed models to reduce runtime errors. - Persist application state to LocalApplicationData and use atomic-save patterns to minimize corruption risk.
- Use async/await for file and network I/O to keep the UI responsive; support cancellation for long tasks.
- Implement minimal, non-blocking logging; never write PII to logs in plain text.
Conclusion
This tutorial walked through building a simple VB.NET WinForms task manager using Visual Studio 2022 and .NET 8. You now have runnable model, repository, and form code, an async I/O pattern, a lightweight logger, and guidance on security and troubleshooting.
Next steps: copy the code into a TaskManagerVB project, run it in Visual Studio, and extend features such as edit-in-place, cloud sync, or unit tests for repository logic.
