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

  1. What Windows Forms is and when it still makes sense today
  2. Creating a project with dotnet new winforms
  3. The form designer and basic controls
  4. The control event model: Click and the delegates from Module 4
  5. Complete example: a lending form for BiblioTech
  6. Best practices when connecting the interface to the domain

  1. 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.

  1. Creating a project with dotnet new winforms

Just like you created console projects with dotnet new console in previous modules, Windows Forms has its own template:

dotnet new winforms -n BiblioTech.Desktop
cd BiblioTech.Desktop

This generates a minimal structure:

BiblioTech.Desktop/
├── Program.cs
├── Form1.cs
├── Form1.Designer.cs
└── Form1.resx
  • 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.

  1. 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.

  1. The control event model: Click and the delegates from Module 4

The 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.

  1. 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.

  1. Best practices when connecting the interface to the domain

  • The form shouldn't contain business logic: LendButton_Click calls selectedItem.Lend(), it doesn't reimplement the availability check inside the form — that logic already lives, correctly, in LibraryItem since Module 2.
  • Passing Library into the form's constructor, as done here, instead of creating it inside the form itself: this keeps the form decoupled from how Library is 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() after Lend(), so that what the user sees always matches _library's real state.

Common Mistakes and Tips

  • Editing Form1.Designer.cs by 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" .cs file (Form1.cs), not in the .Designer.cs one.
  • Forgetting to check SelectedIndex < 0: if the user clicks "Lend" without having selected anything in the ListBox, SelectedIndex is -1; accessing the catalog with that index would throw a runtime exception.
  • Mixing business logic into the event method: duplicating the Available check here instead of delegating it to Lend() (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

  1. Add a TextBox named titleTextBox and a second Button named searchButton to MainForm. In searchButton's Click event, filter _library.Catalog by title (using Contains, already covered with strings in Module 1) and show only the matching results in catalogListBox.

  2. Modify LendButton_Click so that, instead of MessageBox.Show, it shows the confirmation message in a Label named statusLabel placed 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.

© Copyright 2026. All rights reserved