Skip to content

2 - Windows Forms

Let's start the apiary project!

Based on the specification, we would need a desktop application, a web application and a shared database. We will work on the code week by week and the structure will change, however, it seems we need 3 projects at the end:

  1. Desktop application
  2. Web application
  3. Shared code (database related code, common logic, etc.)

From the project specification, we would need several data types and common logic that are used from both the desktop application and the web counterpart. As we are developing the application, we are going to add the components to the shared project, however, it needs to be created first.

Create a new Class Library
Create a new Class Library.

On the next page, the configuration should look like as follows:

  • Project name: Common
  • Solution name: Apiary
  • Place solution and project in the same directory: unchecked

An empty project is created with an example class Class1.cs, which is also empty. The general rule should be that every project should contain code that is necessary for its work (e.g., GUI, event handling), and everything that is reusable across the projects should be in the shared code (Common project).

Windows Forms

Windows Forms is a UI framework for building Windows desktop apps1. The Windows Forms development platform supports a broad set of app development features, including controls, graphics, data binding, and user input. Windows Forms features a drag-and-drop visual designer in Visual Studio to easily create apps. Applications can access the local hardware and file system of the computer where the app is running.

There are two implementations of Windows Forms:

  1. The open-source implementation hosted on GitHub that runs on .NET.
  2. The .NET Framework 4 implementation that's supported by Visual Studio 2022, Visual Studio 2019, and Visual Studio 2017. .NET Framework 4 is a Windows-only version of .NET and is considered a Windows Operating System component. This version of Windows Forms is distributed with .NET Framework.

Let's create the Windows Forms application for our project! In the Solution Explorer, right click to the Solution 'Apiary' (1 of 1 project) and select Add -> New Project... We need the Windows Forms App template (with the C# language tag and without the .NET Framework postfix!).

Create a new Windows Forms project
Create a new Windows Forms project.

On the next page, the configuration should look like as follows:

  • Project name: Administration

After the project is created, our Solution contains 2 projects and both of them are loaded. A designer tab is opened when the project is created.

Form1.cs [Design]
Form1.cs [Design].

Form and Control

A form is a visual surface on which you display information to the user. You build apps by adding controls to forms and developing responses to user actions, such as mouse clicks or key presses. A control is a discrete UI element that displays data or accepts data input.

When a user does something to your form or one of its controls, the action generates an event. The app reacts to these events and processes the events when they occur. The forms are separated into two parts on the implementation level:

  • Generated code: Expand the Form1.cs in the Solution Explorer and open the Form1.Designer.cs file
  • User code: Right click on Form1.cs in the Solution Explorer and "View Code"
Form1.cs user code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace Administration
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponents();
        }
    }
}
Partial Classes and Members

It's possible to split the definition of a class, a struct, an interface, or a member over two or more source files4. Each source file contains a section of the type or member definition, and all parts are combined when the application is compiled.

There are several situations when splitting a class definition is desirable:

  • Declaring a class over separate files enables multiple programmers to work on it at the same time.
  • You can add code to the class without having to recreate the source file that includes automatically generated source. Visual Studio uses this approach when it creates Windows Forms, Web service wrapper code, etc. You can create code that uses these classes without having to modify the file created by Visual Studio.
  • Source generators can generate extra functionality in a class.

To split a class definition, use the partial keyword modifier. In practice, each partial class is typically defined in a separate file, making it easier to manage and expand the class over time.

Check the Form1.cs and the Form1.Designer.cs files!

Guidelines:

  • The partial keyword indicates that other parts of the class, struct, or interface can be defined in the namespace.
  • All the parts must use the partial keyword.
  • All the parts must be available at compile time to form the final type.
  • All the parts must have the same accessibility, such as public, private, and so on.
  • If any part is declared abstract, then the whole type is considered abstract.
  • If any part is declared sealed, then the whole type is considered sealed.
  • If any part declares a base type, then the whole type inherits that class.
  • All partial-type definitions meant to be parts of the same type must be defined in the same assembly and the same module (.exe or .dll file). Partial definitions can't span multiple modules.
namespace

Namespaces are heavily used in C# programming in two ways2. First, .NET uses namespaces to organize its many classes, as follows:

1
System.Console.WriteLine("Hello World!");

System is a namespace and Console is a class in that namespace. The using keyword can be used so that the complete name isn't required, as in the following example:

1
2
using System;
Console.WriteLine("Hello World!");

Second, declaring your own namespaces can help you control the scope of class and method names in larger programming projects. Use the namespace keyword to declare a namespace, as in the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace SampleNamespace
{
    class SampleClass
    {
        public void SampleMethod()
        {
            System.Console.WriteLine("SampleMethod inside SampleNamespace");
        }
    }
}

The name of the namespace must be a valid C# identifier name. You can declare a namespace for all types defined in that file, as shown in the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// This using is outside the namespace scope, so it applies globally
using System;

namespace SampleNamespace; // File-scoped namespace declaration

// This using is inside the namespace scope
using System.Text;

class SampleClass
{
    // ...
}

In the example3, System is globally accessible, while System.Text applies only within SampleNamespace. The advantage of this new syntax is that it's simpler, saving horizontal space and braces. That makes the code easier to read. File scoped namespaces can't include more namespace declarations.

Summary:

  • They organize large code projects.
  • They're delimited by using the . operator.
  • The using directive obviates the requirement to specify the name of the namespace for every class.
  • The global namespace is the "root" namespace: global::System always refers to the .NET System namespace.
Code Regions

The #region directive is used to mark a block of code that you can expand or collapse. This can, for example, be useful for larger files for better readability or for focusing on code that you're currently working on. The #endregion specifies the end of a #region block of code.

Form1.Designer.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#region Windows Form Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
    // ...
}

#endregion

Graphical User Interface

Right Click somewhere on the form and select Properties. A new window will open called Properties5 in the right hand side by default, containing the properties of the selected UI element. When a new UI element is selected, this view will automatically load its properties.

Properties View
Properties View.

A description is visible at the bottom of the view, helping understanding the selected property, e.g., the Text property is selected and the tooltip is The text associated with the control. Change its value to Apiary and it changes the title of the form.

The next important view is the Toolbox6 that can be opened from View > Toolbox menu item. It's default location is in the left-hand side and contains the controls that can be added to a form.

Toolbox
Toolbox View.

You can drag and drop different controls onto the surface of the designer you are using, and resize and position the controls. Toolbox appears in conjunction with designer views, such as the designer view of a Windows Forms App project. Toolbox displays only those controls that can be used in the current designer. You can search within Toolbox to further filter the items that appear.

The Administration project has two main responsibilities: managing products and orders. The GUI should handle these separately and the users should be able to navigate between them easily. A menu strip can come handy with the required options.

Menu Strip
Menu Strip.

In the Toolbox, expand the Menus & Toolbars and simply drag and drop a MenuStrip control to the form. Wherever we put it, the menu strip will be visible at the top of the form, and the menuStrip1 is visible at the bottom of the designer.

The effects of the double-click

It is important not to double-click to any items of the form blindly. The double click will create an event handler to the "click" event of that control where the cursor is. (See later.)

Click to the menu strip "Type here" placeholder and type "Products". When the menu item is created, we can add two more items to the Products, called "Add" and "List". Also a new item can be created for the Orders.

Menu Strip Created
Menu Strip Created.

When the user clicks to the "Products" menu item, it expands and shows the available options, and when the user clicks to the "Add" item from the list, we would like to open a new window (a.k.a. a new Form).

Event Handling

An event is an action that you can respond to, or "handle," in code7. Events are usually generated by a user action, such as clicking the mouse or pressing a key, but they can also be generated by program code or by the system. Event-driven applications run code in response to an event. Each form and control exposes a predefined set of events that you can respond to. If one of these events is raised and there's an associated event handler, the handler is invoked and code is run.

The types of events raised by an object vary, but many types are common to most controls. For example, most objects have a Click event that's raised when a user clicks on it.

Events in .NET are based on the delegate model8. The delegate model follows the observer design pattern, which enables a subscriber to register with and receive notifications from a provider. An event sender pushes a notification when an event occurs. An event receiver defines the response.

delegate

Delegates are classes commonly used within .NET to build event-handling mechanisms. Delegates roughly equate to function pointers, commonly used in Visual C++ and other object-oriented languages. Unlike function pointers, however, delegates are object-oriented, type-safe, and secure. Also, where a function pointer contains only a reference to a particular function, a delegate consists of a reference to an object, and references to one or more methods within the object.

Delegates are used to pass methods as arguments to other methods. Event handlers are essentially methods you invoke through delegates. When you create a custom method, a class such as a Windows control can call your method when a certain event occurs.

You can assign any method from any accessible class or struct that matches the delegate type to the delegate. The method can be either static or an instance method. The flexibility allows you to programmatically change method calls, or plug new code into existing classes.

In the context of method overloading, the signature of a method doesn't include the return value. However, in the context of delegates, the signature does include the return value. In other words, a method must have a compatible return type as the return type declared by the delegate.

The declaration of a delegate type is similar to a method signature9.

1
public delegate void Callback(string message);

A delegate object is normally constructed by providing the name of the method the delegate wraps, or with a lambda expression. A delegate can be invoked once instantiated in this manner. Invoking a delegate calls the method attached to the delegate instance. The parameters passed to the delegate by the caller are passed to the method. The delegate returns the return value, if any, from the method. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Create a method for a delegate.
public static void DelegateMethod(string message)
{
    Console.WriteLine(message);
}
// ...
// Instantiate the delegate.
Callback handler = DelegateMethod;

// Call the delegate.
handler("Hello World");

Because the instantiated delegate is an object, it can be passed as an argument, or assigned to a property. A method can accept a delegate as a parameter, and call the delegate at some later time. This is known as an asynchronous callback, and is a common method of notifying a caller when a long process completes. When a delegate is used in this fashion, the code using the delegate doesn't need any knowledge of the implementation of the method being used.

1
2
3
4
public static void MethodWithCallback(int param1, int param2, Callback callback)
{
    callback("The number is: " + (param1 + param2).ToString());
}

When a delegate is constructed to wrap an instance method, the delegate references both the instance and the method. A delegate has no knowledge of the instance type aside from the method it wraps. A delegate can refer to any type of object as long as there's a method on that object that matches the delegate signature. When a delegate is constructed to wrap a static method, it only references the method.

1
2
3
4
5
public class MethodClass
{
    public void Method1(string message) { }
    public void Method2(string message) { }
}
A delegate can call more than one method when invoked, referred to as multicasting. To add an extra method to the delegate's list of methods—the invocation list—simply requires adding two delegates using the addition or addition assignment operators ('+' or '+=').

1
2
3
4
5
6
7
8
var obj = new MethodClass();
Callback d1 = obj.Method1;
Callback d2 = obj.Method2;
Callback d3 = DelegateMethod;

//Both types of assignment are valid.
Callback allMethodsDelegate = d1 + d2;
allMethodsDelegate += d3;

The allMethodsDelegate contains three methods in its invocation list —Method1, Method2, and DelegateMethod. The original three delegates, d1, d2, and d3, remain unchanged. When allMethodsDelegate is invoked, all three methods are called in order. If the delegate uses reference parameters, the reference is passed sequentially to each of the three methods in turn, and any changes by one method are visible to the next method. If the delegate has a return value and/or out parameters, it returns the return value and parameters of the last method invoked.

To remove a method from the invocation list, use the subtraction or subtraction assignment operators (- or -=).

1
2
3
4
5
//remove Method1
allMethodsDelegate -= d1;

// copy AllMethodsDelegate while removing d2
Callback oneMethodDelegate = (allMethodsDelegate - d2)!;

Multicast delegates are used extensively in event handling. Event source objects send event notifications to recipient objects that registered to receive that event. To register for an event, the recipient creates a method designed to handle the event, then creates a delegate for that method and passes the delegate to the event source. The source calls the delegate when the event occurs. The delegate then calls the event handling method on the recipient, delivering the event data. The event source defines the delegate type for a given event.

Select the "Add" menu item and check that the "addToolStripMenuItem" is selected in the Properties window. Select the lightning icon (Events) in the Properties window and type "AddProduct" to the "Click" event, then press Enter.

Click Event Handler
Click Event Handler.

On the Enter press, the IDE generates a new function into the Form1.cs user code.

Form1.cs user code
1
2
3
4
private void AddProduct(object sender, EventArgs e)
{

}

And a tooltip above the function name reflects that this new function has 1 reference. Let's follow it! It brings us to the IDE generated code (Form1.Designer.cs)

Form1.Designer.cs
1
2
3
4
5
6
7
//
// addToolStripMenuItem
//
addToolStripMenuItem.Name = "addToolStripMenuItem";
addToolStripMenuItem.Size = new Size(120, 26);
addToolStripMenuItem.Text = "Add";
addToolStripMenuItem.Click += AddProduct;

The type of the addToolStripMenuItem.Click is EventHandler? ToolStripItem.Click. As discussed in the delegates part, our new event handler method is added via the addition assignment operator (+=).

We would like to open a new form if the menu item is clicked, so first, we need that new form. In the Solution Explorer, right click to the project "Administration" and (in the middle) Add -> Form (Windows Forms).... The template should be the Form (Windows Forms) and type ProductAdd as the name. A new, empty form is created, however, unreachable in runtime. Let's open it from our AddProduct event handler:

Form1.cs user code
1
2
3
4
5
6
7
8
private void AddProduct(object sender, EventArgs e)
{
    using var window = new ProductAdd();
    window.ShowDialog();

    // when a Form is shown via the "ShowDialog" method
    // we can check the `window.DialogResult` after its closed
}
using statement

The using statement ensures the correct use of an IDisposable instance10.

When control leaves the block of the using statement, the acquired IDisposable instance is disposed. In particular, the using statement ensures that a disposable instance is disposed even if an exception occurs within the block of the using statement.

The Form class implements the IDisposable interface, if we check the ProductAdd.Designer.cs source file, we'll see a Dispose method.

ProductAdd.Designer.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;

/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
    if (disposing && (components != null))
    {
        components.Dispose();
    }
    base.Dispose(disposing);
}

Press "F5" and check whether the new window pops up on the menu item click!

"Add Product" Form

We are going to create a not-so-complicated GUI based on the common Windows Forms controls that is enough to register a new product to the system. In our simplified world, a product has the following properties:

Property Type Notes
Name text required
Description text required, might be a longer text, even multiline
Price number required and will be handled as HUF
Packaging Date date required, when the product is ready for the store
Expiration Date date required, indicates the final date a manufacturer guarantees a product's safety, quality, or potency under recommended storage conditions
Image image required, an eye-catchy image of the product, shown in the webshop
All Windows Forms Add Product window
All Windows Forms Add Product Window.

Expand the All Windows Forms category in the Toolbox and in this example, we will use the following controls:

Control Description
Label Labels are used to display text that cannot be edited by the user. They're used to identify objects on a form and to provide a description of what a certain control represents or does11.
TextBox Text boxes are used to get input from the user or to display text. The TextBox control is generally used for editable text, although it can also be made read-only. Text boxes can display multiple lines, wrap text to the size of the control, and add basic formatting. The TextBox control provides a single format style for text displayed or entered into the control12.
DateTimePicker Allows the user to select a single item from a list of dates or times. When used to represent a date, it appears in two parts: a drop-down list with a date represented in text, and a grid that appears when you click on the down-arrow next to the list13.
Button Allows the user to click it to perform an action. The Button control can display both text and images. When the button is clicked, it looks as if it is being pushed in and released14.
PictureBox PictureBox control is used to display graphics in bitmap, GIF, JPEG, metafile, or icon format15.
ErrorProvider The component is used to show the user in a non-intrusive way that something is wrong. It is typically used in conjunction with validating user input on a form, or displaying errors within a dataset16.

Tip

When a control is double-clicked in the Toolbox, it's automatically added to the current open form with default settings. Also it can be placed to the form by a drag-and-drop method. (There are technologies that built upon the Windows Forms, where the designer support is not fully optimized. In those scenarios, controls can be created by adding the code manually.)

Success

Let's create the Add Product window as the image shows. Drag and drop the controls from the toolbox!

When the controls are on the form, their properties can be set to match the desired behavior. In the Design category of the properties, the name sets how the control is named in the code, it's common for every control. E.g., if the first label name is set to labelName, then the generated code looks like this:

ProductAdd.Designer.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//
// labelName
//
labelName.AutoSize = true;
labelName.Font = new Font("Segoe UI", 12F);
labelName.Location = new Point(101, 15);
labelName.Name = "labelName";
labelName.Size = new Size(68, 28);
labelName.TabIndex = 0;
labelName.Text = "Name:";

And if we would like to set the Text property of the control, the labelName is available as a variable from the user code. Usually the Text property sets the visible text what the user can see on the form. (In the example, the default font size is changed as well from 9pt to 12pt, the other properties have their default values.)

Input validation

Input validation is the critical security process of inspecting data from users or external systems to ensure it meets strict, predefined criteria (type, format, length, range) before processing. The ErrorProvider control can help givin feedback to the user if some of their inputs is not valid. Drag and drop an ErrorProvider control to the form, it will'be placed to the bottom of the screen. Let's see how it's working with the Price: we would like to have numerical input in that TextBox and the software will handle it as Hungarian Forint (HUF, Ft).

When the user starts typing in that TextBox, the TextChanged event is triggered, and we can handle that for validation. Click to TextBox for price and rename it to price in the Design category. Select the lightning icon (Events) in the Properties window and type ValidatePrice to the "TextChanged" event, then press Enter. A new event handler function is generated to the user code.

ProductAdd.cs
1
2
3
4
5
6
7
private void ValidatePrice(object sender, EventArgs e)
{
    if (price.Text.Length > 0 && !float.TryParse(price.Text, out var _))
        errorProvider.SetError(price, $"{price.Text} is not valid numerical value for HUF currency!");
    else
        errorProvider.SetError(price, string.Empty);
}

The user input can be accessed through price.Text and the string type has Length. If there is a text, then try to convert it to a float number: if there's an error during the conversion, then we can tell the errorProvider to show an error message next to the price (the TextBox) and if the user hovers the mouse above the error, the tooltip shows the given text. If the text is deleted from the control, or the float parsing succeeded, then the errorProvider can be cleared.

Error Provider
Error Provider with Name and Price TextBoxes.

The errorProvider can be set to multiple controls at the same time with different error messages. Add the SaveProduct Click event to the Save button and start the event handling with input validation.

ProductAdd.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
private void SaveProduct(object sender, EventArgs e)
{
    errorProvider.Clear(); // clears all the errors and hides the red cross

    string productName = name.Text;
    string productDescription = description.Text;

    if (productName.Length == 0)
        errorProvider.SetError(name, "Name is required");

    if (productDescription.Length == 0)
        errorProvider.SetError(description, "Description is required");

    if (price.Text.Length == 0)
        errorProvider.SetError(price, "Will this product be free?");
    else if (!float.TryParse(price.Text, out float productPrice))
        errorProvider.SetError(price, $"{price.Text} is not valid numerical value for HUF currency!");

    // ...
}

Date

We got two date information for a product: when the product is ready (packaging date) and and the final date when its still consumable (expiration date). The DateTimePicker control handles the date selection well, rename the two control to packagingDate and expirationDate, respectively. So far, we set the properties of the controls in the Properties window, however, they can also be set from the user code as well.

When a user creates a new product, it's assumed that the product is already packaged, so the packaging date cannot be in the future. Also, the expiration date should be at least that day, when the product is packaged.

ProductAdd.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public ProductAdd()
{
    InitializeComponent();

    packagingDate.MaxDate = DateTime.Today;

    expirationDate.MinDate = packagingDate.Value;
    expirationDate.Value = packagingDate.Value.AddDays(90);
    // ...
}

After the InitializeComponent() call, the controls are accessible and their properties can be set. After the packagingDate.MaxDate = DateTime.Today call, the control won't show dates after the current date and it won't accept future dates as input. After the expirationDate.MinDate = packagingDate.Value call, the control won't show dates before the packaging date and its dynamically updating based on packagingDate value. And last, after the expirationDate.Value = packagingDate.Value.AddDays(90) the default value for the expiration date will be the packaging day + 90 days.

In the SaveProduct event handler, the dates can be accessed via the Value property.

ProductAdd.cs
1
2
3
4
5
6
7
private void SaveProduct(object sender, EventArgs e)
{
    // ...
    DateTime productPackaged = packagingDate.Value;
    DateTime productExpires = expirationDate.Value;
    // ...
}

Images

We would like to include an image to the product, as usually easier to sell products with images. The PictureBox can display images15, now we should tell to it what to show, furthermore, how to resize it within the form. Rename the PictureBox control the pictureBox, resize it to the desired size and set the SizeMode to Zoom.

PictureBox Size Modes

The SizeMode property, which is set to values in the PictureBoxSizeMode enumeration, controls the clipping and positioning of the image in the display area.17

Name Value Description
Normal 0 The image is placed in the upper-left corner of the PictureBox. The image is clipped if it is larger than the PictureBox it is contained in.
StretchImage 1 The image within the PictureBox is stretched or shrunk to fit the size of the PictureBox.
AutoSize 2 The PictureBox is sized equal to the size of the image that it contains.
CenterImage 3 The image is displayed in the center if the PictureBox is larger than the image. If the image is larger than the PictureBox, the picture is placed in the center of the PictureBox and the outside edges are clipped.
Zoom 4 The size of the image is increased or decreased maintaining the size ratio.

With the help of an OpenFileDialog, the user can select an image file from the local storage and that image is being fed to the PictureBox. Drag and drop an OpenFileDialog control from the Toolbox and rename it to openFileDialog.

First, we need to specify the file types that the user can see and select in the OpenFileDialog.

ProductAdd.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public ProductAdd()
{
    // ...

    string separator = string.Empty;
    var codecs = ImageCodecInfo.GetImageEncoders();
    foreach (var c in codecs)
    {
        string codecName = c.CodecName[8..].Replace("Codec", "Files").Trim();
        openFileDialog.Filter = String.Format("{0}{1}{2} ({3})|{3}", openFileDialog.Filter, separator, codecName, c.FilenameExtension);
        separator = "|";
    }

    // openFileDialog.Filter:
    // "BMP Files (*.BMP;*.DIB;*.RLE)|*.BMP;*.DIB;*.RLE|JPEG Files (*.JPG;*.JPEG;*.JPE;*.JFIF)|*.JPG;*.JPEG;*.JPE;*.JFIF|GIF Files (*.GIF)|*.GIF|TIFF Files (*.TIF;*.TIFF)|*.TIF;*.TIFF|PNG Files (*.PNG)|*.PNG"
}

The code snippet gets the supported image codecs and set the openFileDialog.Filter property. The filter is separated by the | character, and the user can select one of them from a drop-down list (e.g., JPEG Files (.jpg, .jpeg)) By this the user won't be able to select a text file (or an exe?) and the program won't try to load it as an image file.

Then, create a click event handler to the "Select Image" button, called OpenFile.

ProductAdd.cs
1
2
3
4
5
6
7
private void OpenFile(object sender, EventArgs e)
{
    if (openFileDialog.ShowDialog() == DialogResult.OK)
    {
        pictureBox.Image = Image.FromFile(openFileDialog.FileName);
    }
}

Also querying the pictureBox.Image, we can get the image as a System.Drawing.Image object.

Created Form
Created Form.

References


Last update: 2026-02-16 07:04:54