Up to now, BiblioTech has lived entirely in the console: every loan, every new member, every
catalog query has been shown with Console.WriteLine and received with Console.ReadLine. This
lesson takes the first step toward a real interface, with Windows Forms (often abbreviated
WinForms), the oldest graphical interface framework in .NET that's still in use. Even though
more modern alternatives exist today (WPF, Blazor, MAUI, covered in the coming lessons), Windows
Forms remains a legitimate and very productive choice for one specific type of application:
internal desktop tools on Windows, with no sophisticated visual ambitions, where development
speed matters more than aesthetics. You'll build a form that shows BiblioTech's catalog and lets
you lend a book with one click, reusing exactly the same domain logic from the previous modules.
Contents
- What Windows Forms is and when it still makes sense today
- Creating a project with
dotnet new winforms - The form designer and basic controls
- The control event model:
Clickand the delegates from Module 4 - Complete example: a lending form for BiblioTech
- Best practices when connecting the interface to the domain
- What Windows Forms is and when it still makes sense today
Windows Forms appeared in 2002, alongside the very first version of .NET, and for years was the
standard way to build desktop applications in C#. Its model is simple: a form (Form) is a
window, on which controls (buttons, text boxes, lists...) are placed via a visual
drag-and-drop designer. Each control exposes properties (position, text, color) and events
(Click, TextChanged) that the programmer hooks code onto.
Does it still make sense in 2026? Yes, for one very specific application profile:
| Characteristic | Windows Forms |
|---|---|
| Platform | Windows only (not cross-platform) |
| Learning curve | Very low: the visual designer generates almost all of the interface code |
| Visual appearance | Functional, with the native Windows look; not meant for sophisticated designs |
| UI/logic separation | Weak by default (event-handling code tends to get mixed in with logic) |
| Typical use case | Internal company tools, administrative utilities, simple management applications |
| Recommended modern alternative for a more polished UI | WPF (next lesson) |
For BiblioTech, Windows Forms fits perfectly into one scenario: an internal application used by library staff at the front-desk computers, requiring nothing more to be installed than .NET, and where visual appearance isn't a priority over the speed of having something working.
- Creating a project with
dotnet new winforms
dotnet new winformsJust like you created console projects with dotnet new console in previous modules, Windows
Forms has its own template:
This generates a minimal structure:
Program.cs: the entry point, which starts the application and shows the first form.Form1.cs: the form code you write yourself (events, logic).Form1.Designer.cs: code generated automatically by the visual designer; not edited by hand under normal circumstances.Form1.resx: the form's resources (icons, embedded text).
// Program.cs generated by the template
ApplicationConfiguration.Initialize();
Application.Run(new Form1());Application.Run(new Form1()) creates an instance of the main form and enters Windows's
message loop: an internal loop, managed by the framework, that waits for operating system
events (mouse clicks, key presses, screen redraws) and dispatches them to the corresponding form.
This loop is exactly what makes a click on a button end up running your code: it isn't magic,
it's the very same event model you already know from Module 4, now applied to the user's
interaction with the mouse and keyboard.
- The form designer and basic controls
The form designer (available in Visual Studio on Windows) lets you drag controls from a toolbox onto the form, and adjust their properties from a side panel, with no need to write code by hand. The most common controls:
| Control | What it's for | Main property |
|---|---|---|
Label |
Displaying non-editable text (titles, labels) | Text |
TextBox |
Single-line text input | Text |
Button |
Triggering an action on click | Text (the button's text) |
ListBox |
A simple list of selectable items | Items, SelectedItem |
DataGridView |
A table with rows and columns, ideal for collections of objects | DataSource |
When you drag a Button onto the form and change its Text property to "Lend", the designer
automatically writes something like this into Form1.Designer.cs (generated code, not manual):
// Form1.Designer.cs (fragment generated by the designer)
private Button lendButton;
private void InitializeComponent()
{
lendButton = new Button();
lendButton.Location = new Point(120, 200);
lendButton.Size = new Size(100, 30);
lendButton.Text = "Lend";
lendButton.Click += LendButton_Click; // event wiring, see section 4
// ...
Controls.Add(lendButton);
}Controls.Add(lendButton) adds the button to the form's Controls collection — the set of
child controls drawn on top of it —; without that line, the button would exist as an object in
memory but would never appear on screen.
- The control event model:
Click and the delegates from Module 4
Click and the delegates from Module 4The Delegates and Events lesson (Module 4) explained that an event is, at heart, a list of
methods (delegates) that run when something happens — there, the example was
Library.LoanRegistered. Windows Forms controls use exactly the same mechanism: Button.Click is
an event of type EventHandler, which you subscribe to with +=, just as you did with
LoanRegistered:
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent(); // generated by the designer: creates and places the controls
lendButton.Click += LendButton_Click; // subscription to the event, same as in Module 4
}
private void LendButton_Click(object? sender, EventArgs e)
{
// code that runs every time the user clicks "Lend"
}
}The signature (object? sender, EventArgs e) is .NET's standard event pattern: sender is the
object that raised the event (here, lendButton) and e carries additional information (empty
for Click, but not for other events like KeyPress, which includes the key that was pressed).
Recognizing this signature is key: it's the same object, EventArgs structure used by practically
every event in the .NET standard library, not just those in Windows Forms.
- Complete example: a lending form for BiblioTech
The domain already built — Library, LibraryItem, Book, Member — is reused without
modifying a single class. The form only adds a visual layer on top:
// MainForm.cs
public partial class MainForm : Form
{
private readonly Library _library;
public MainForm(Library library)
{
InitializeComponent();
_library = library;
LoadCatalogIntoListBox();
lendButton.Click += LendButton_Click;
}
private void LoadCatalogIntoListBox()
{
catalogListBox.Items.Clear();
foreach (LibraryItem item in _library.Catalog)
{
// ToString() isn't overridden on LibraryItem, so the text is composed here instead
catalogListBox.Items.Add($"{item.Title} ({item.Author})");
}
}
private void LendButton_Click(object? sender, EventArgs e)
{
int selectedIndex = catalogListBox.SelectedIndex;
if (selectedIndex < 0)
{
MessageBox.Show("Select a book from the list before lending it.", "Notice");
return;
}
LibraryItem selectedItem = _library.Catalog[selectedIndex];
if (!selectedItem.Available)
{
MessageBox.Show($"'{selectedItem.Title}' is already on loan.", "Notice");
return;
}
selectedItem.Lend(); // domain logic that already exists, Module 2
MessageBox.Show($"Loan registered: {selectedItem.Title}", "Loan completed");
LoadCatalogIntoListBox(); // refresh the list to reflect the new state
}
}// Program.cs
Library library = new Library();
library.AddItem(new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"));
library.AddItem(new Book("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"));
ApplicationConfiguration.Initialize();
Application.Run(new MainForm(library));Nothing about Library, LibraryItem, or Book has changed: the form just reads
_library.Catalog to fill the ListBox, and calls selectedItem.Lend() when the user clicks —
exactly the same method you already used from the console back in Module 2. MessageBox.Show(...)
displays a pop-up dialog box, the simplest way to communicate a message to the user in Windows
Forms, the visual equivalent of the Console.WriteLine from earlier modules.
For a larger catalog, a DataGridView with catalogListBox.DataSource = _library.Catalog (instead
of a manually filled ListBox) would automatically show a table with one column per public
property, though it requires adjusting which columns are shown so as not to expose Available as
unformatted plain text.
- Best practices when connecting the interface to the domain
- The form shouldn't contain business logic:
LendButton_ClickcallsselectedItem.Lend(), it doesn't reimplement the availability check inside the form — that logic already lives, correctly, inLibraryItemsince Module 2. - Passing
Libraryinto the form's constructor, as done here, instead of creating it inside the form itself: this keeps the form decoupled from howLibraryis built or configured (with text, JSON, SQLite persistence... as covered in Module 5), a principle that will be revisited in more depth in Module 8 when discussing dependency injection. - Refreshing the interface after every state change: as with
LoadCatalogIntoListBox()afterLend(), so that what the user sees always matches_library's real state.
Common Mistakes and Tips
- Editing
Form1.Designer.csby hand: it's code generated by the visual designer; any manual change can be lost the next time the form is edited from the designer. Behavior changes go in the "normal".csfile (Form1.cs), not in the.Designer.csone. - Forgetting to check
SelectedIndex < 0: if the user clicks "Lend" without having selected anything in theListBox,SelectedIndexis-1; accessing the catalog with that index would throw a runtime exception. - Mixing business logic into the event method: duplicating the
Availablecheck here instead of delegating it toLend()(which already does it, Module 2) would create two sources of truth that could drift out of sync over time. - Tip: Windows Forms only compiles and runs on Windows; if your development team uses different operating systems, keep this in mind before choosing it for a new project.
Exercises
-
Add a
TextBoxnamedtitleTextBoxand a secondButtonnamedsearchButtontoMainForm. InsearchButton'sClickevent, filter_library.Catalogby title (usingContains, already covered with strings in Module 1) and show only the matching results incatalogListBox. -
Modify
LendButton_Clickso that, instead ofMessageBox.Show, it shows the confirmation message in aLabelnamedstatusLabelplaced at the bottom of the form.
Solutions
private void SearchButton_Click(object? sender, EventArgs e)
{
string searchText = titleTextBox.Text;
catalogListBox.Items.Clear();
IEnumerable<LibraryItem> matches = _library.Catalog
.Where(item => item.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase));
foreach (LibraryItem item in matches)
{
catalogListBox.Items.Add($"{item.Title} ({item.Author})");
}
}
private void LendButton_Click(object? sender, EventArgs e)
{
int selectedIndex = catalogListBox.SelectedIndex;
if (selectedIndex < 0)
{
statusLabel.Text = "Select a book from the list before lending it.";
return;
}
LibraryItem selectedItem = _library.Catalog[selectedIndex];
if (!selectedItem.Available)
{
statusLabel.Text = $"'{selectedItem.Title}' is already on loan.";
return;
}
selectedItem.Lend();
statusLabel.Text = $"Loan registered: {selectedItem.Title}";
LoadCatalogIntoListBox();
}
Conclusion
In this lesson you've built BiblioTech's first real graphical interface with Windows Forms: a
dotnet new winforms project, basic controls (ListBox, Button, Label) placed with the
visual designer, and the control event model (Click), which turns out to be exactly the same
delegate-and-event mechanism from Module 4 applied to user interaction. All of this without
touching a single line of Library, LibraryItem, or Book: the interface is a new layer on top
of the domain already built.
The next lesson introduces WPF, a more modern alternative to Windows Forms, with an important underlying difference: instead of placing controls imperatively from a designer, WPF describes the interface declaratively with XAML, and separates the interface from the logic with more discipline through the MVVM pattern and data binding — you'll see the same BiblioTech catalog, but connected to the interface in a rather different way.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
