Skip to content

3 - Windows Forms

At the end of the previous practice, the GUI for adding a new product has been created. To progress further, we need a class that represents a product and we need some kind of storage that can store the created products. The first, and the simplest solution is an in-memory "database" that stores products during the application lifecycle, however, it looses everything when the application shuts down.

At the beginning of the previous practice, we have create a separated project called Common in the Solution that contains the common code that will be used from the Windows Forms project and the web application as well.

It is a good idea to separate the code within that project as well, therefore, create a new folder called Models in the Common project. Right click on the Common and Add -> New folder and name it as Models. Then, right click on the Models folder and Add -> Class and name it as Product.cs.

Classes

class

A type that is defined as a class is a reference type.1 At run time, when you declare a variable of a reference type, the variable contains the value null until you explicitly create an instance of the class by using the new operator, or assign it an object of a compatible type created elsewhere, as shown in the following example:

1
2
MyClass mc = new MyClass();
MyClass mc2 = mc;

When the object is created, enough memory is allocated on the managed heap for that specific object, and the variable holds only a reference to the location of said object. The memory used by an object is reclaimed by the automatic memory management functionality of the Common Language Runtime (CLR), which is known as garbage collection.

Classes are declared by using the class keyword followed by a unique identifier:

1
2
3
4
5
// [access modifier] - [class] - [identifier]
public class Product
{
    // Fields, properties, methods and events go here...
}

An optional access modifier precedes the class keyword. The default access for a class type is internal. Because public is used in this case, anyone can create instances of this class. The name of the class follows the class keyword. The name of the class must be a valid C# identifier name. The remainder of the definition is the class body, where the behavior and data are defined. Fields, properties, methods, and events on a class are collectively referred to as class members.

Although they're sometimes used interchangeably, a class and an object are different things. A class defines a type of object, but it isn't an object itself. An object is a concrete entity based on a class, and is sometimes referred to as an instance of a class.

1
Product object1 = new();

When an instance of a class is created, a reference to the object is passed back to the programmer. In the previous example, object1 is a reference to an object that is based on Product.

1
2
Product object2 = new();
Product object3 = object2;

This code creates two object references that both refer to the same object. Therefore, any changes to the object made through object2 are reflected in subsequent uses of object3. Because objects that are based on classes are referred to by reference, classes are known as reference types.

Constructors and initialization

When you create an instance of a type, you want to ensure that its fields and properties are initialized to useful values. There are several ways to initialize values:

  • Accept default values
  • Field initializers
  • Constructor parameters
  • Object initializers

Every .NET type has a default value. Typically, that value is 0 for number types, and null for all reference types. You can rely on that default value when it's reasonable in your app.

When the .NET default isn't the right value, you can set an initial value using a field initializer:

1
2
3
4
5
public class Container
{
    // Initialize capacity field to a default value of 10:
    private int _capacity = 10;
}

You can require callers to provide an initial value by defining a constructor that's responsible for setting that initial value:

1
2
3
4
5
6
7
8
9
public class Container
{
    private int _capacity;

    public Container(int capacity)
    {
        _capacity = capacity;
    }
}

You can also use the required modifier on a property (see later) and allow callers to use an object initializer to set the initial value of the property. The addition of the required keyword mandates that callers must set those properties as part of a new expression:

1
2
3
4
5
6
7
8
public class Person
{
    public required string LastName { get; set; }
    public required string FirstName { get; set; }
}

var p1 = new Person(); // Error! Required properties not set
var p2 = new Person() { FirstName = "Grace", LastName = "Hopper" };
Access Modifiers

By using these access modifiers, you can specify the following six accessibility levels3:

  • public: No access restrictions.
  • protected: Access is limited to the containing class or types derived from the containing class.
  • internal: Access is limited to the current assembly.
  • protected internal: Access is limited to the current assembly or types derived from the containing class.
  • private: Access is limited to the containing type.
  • private protected: Access is limited to the containing class or types derived from the containing class within the current assembly.

Inheritance

Inheritance

Classes support inheritance, a fundamental characteristic of object-oriented programming. When you create a class, you can inherit from any other class that isn't defined as sealed. Other classes can inherit from your class and override class virtual methods. Furthermore, you can implement one or more interfaces.

Inheritance enables you to create new classes that reuse, extend, and modify the behavior defined in other classes.2 The class whose members are inherited is called the base class, and the class that inherits those members is called the derived class. A derived class can have only one direct base class. However, inheritance is transitive.

Inheritance is accomplished by using a derivation, which means a class is declared by using a base class from which it inherits data and behavior.

1
2
3
4
5
public class Manager : Employee
{
    // Employee fields, properties, methods and events are inherited
    // New Manager fields, properties, methods and events go here...
}

When a class declaration includes a base class, it inherits all the members of the base class except the constructors. A class in C# can only directly inherit from one base class.

A class can be declared as abstract. An abstract class contains abstract methods that have a signature definition but no implementation. Abstract classes can't be instantiated. They can only be used through derived classes that implement the abstract methods. By contrast, a sealed class doesn't allow other classes to derive from it.

In contrast to the classes, structs don't support inheritance, but they can implement interfaces.

Properties

Properties

A property is a member that provides a flexible mechanism to read, write, or compute the value of a data field. Properties appear as public data members, but they're implemented as special methods called accessors. This feature enables callers to access data easily and still helps promote data safety and flexibility.4

A property definition contains declarations for a get and set accessor that retrieves and assigns the value of that property:

1
2
3
4
public class Person
{
    public string? FirstName { get; set; }
}

The example shows an automatically implemented property. The compiler generates a hidden backing field for the property. The compiler also implements the body of the get and set accessors. Any attributes are applied to the automatically implemented property.

You can initialize a property to a value other than the default by setting a value after the closing brace for the property. You might prefer the initial value for the FirstName property to be the empty string:

1
2
3
4
public class Person
{
    public string FirstName { get; set; } = string.Empty;
}

You can also create read-only properties, or give different accessibility to the set and get accessors. Suppose that your Person class should only enable changing the value of the FirstName property from other methods in the class. You could give the set accessor private accessibility instead of internal or public:

1
2
3
4
public class Person
{
    public string? FirstName { get; private set; }
}

The FirstName property can be read from any code, but it can be assigned only from code in the Person class. An access modifier on an individual accessor must be more restrictive than the access of the property. The preceding code is legal because the FirstName property is public, but the set accessor is private. You couldn't declare a private property with a public accessor.

Summary:

  • Simple properties that require no custom accessor code can be implemented either as expression body definitions or as automatically implemented properties.
  • Properties enable a class to expose a public way of getting and setting values, while hiding implementation or verification code.
  • A get property accessor is used to return the property value, and a set property accessor is used to assign a new value. - An init property accessor is used to assign a new value only during object construction. These accessors can have different access levels.
  • The value keyword is used to define the value the set or init accessor is assigning.
  • Properties can be read-write, read-only, or write-only.

Let's complete the Product.cs based on what we have created last week.

Product.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace Common.Model
{
    public class Product
    {
        public int ID { get; set; }
        public string? Name { get; set; }
        public string? Description { get; set; }
        public float Price { get; set; }
        public System.Drawing.Image? Image { get; set; }
        public DateTime PackagingDate { get; set; }
        public DateTime ExpirationDate { get; set; }
    }
}

In-Memory Data Storing

We are able to create a Product on a Form and we are able to create an object from the user inputs. The next step should be to store the created product somehow: the first option is an in-memory solution. While the application is running, the created products are stored in a collection (we will replace the solution with a database in the future).

Interfaces

interface

An interface contains definitions for a group of related functionalities that a non-abstract class or a struct must implement.5 An interface can define static methods. An interface can define a default implementation for members. An interface can't declare instance data such as fields, automatically implemented properties, or property-like events.

By using interfaces, you can, for example, include behavior from multiple sources in a class. That capability is important in C# because the language doesn't support multiple inheritance of classes.

You define an interface by using the interface keyword. The name of an interface must be a valid C# identifier name. By convention, interface names begin with a capital I.

Interfaces can contain instance methods, properties, events, indexers, or any combination of those four member types. Interfaces can contain static constructors, fields, constants, or operators. Interface members that aren't fields can be static abstract. An interface can't contain instance fields, instance constructors, or finalizers. Interface members are public by default.

A class or struct that implements an interface must provide an implementation for all declared members without a default implementation provided by the interface. However, if a base class implements an interface, any class derived from the base class inherits that implementation.

Create a new folder called Database in the Common project. Right click on the Common and Add -> New folder and name it as Database. Then, right click on the Database folder and Add -> New Item (show all templates if needed) and name it as IDatabase.cs.

IDatabase.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace Common.Database
{
    public interface IDatabase
    {
        public void Store(Product product);
        public void Delete(int productID);
        public void Update(Product product);
        public IEnumerable<Product> List();
    }
}

With this interface, we are defining what we are expecting from an implementation: the basic CRUD actions for now. When a class implements this interface, the syntax is the same as for the inheritance: class Implementation : IDatabase. C# does not support multiple inheritance, but it does support implementing multiple interfaces: class Implementation : IDatabase, ISomethingElse, .... If a class inherits from an another and also implements an interface, then the base class should be the first in the list: class Implementation : BaseClass, IDatabase, ....

Create a new class in the same Database folder called InMemoryDatabase.cs, and add the : IDatabase to the end of the class definition. The IDE can generate all of the required functions.

InMemoryDatabase.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace Common.Database
{
    public class InMemoryDatabase : IDatabase
    {
        public void Delete(int productID) { throw new NotImplementedException(); }
        public IEnumerable<Product> List() { throw new NotImplementedException(); }
        public void Store(Product product) { throw new NotImplementedException(); }
        public void Update(Product product) { throw new NotImplementedException(); }
    }
}

An object can be created from this class (although everything throws an exception).

Collections

In this first, in-memory database, the data will be stored in a collection.

Collections - List

The .NET runtime provides many collection types that store and manage groups of related objects6. Some of the collection types, such as System.Array, System.Span<T>, and System.Memory<T>, are recognized in the C# language. In addition, interfaces like System.Collections.Generic.IEnumerable<T> are recognized in the language for enumerating the elements of a collection.

Collections provide a flexible way to work with groups of objects. You can classify different collections by these characteristics:

  • Element access: Every collection can be enumerated to access each element in order. Some collections access elements by index, the element's position in an ordered collection. The most common example is List<T>. Other collections access elements by key, where a value is associated with a single key. The most common example is Dictionary<TKey,TValue>.
  • Performance profile: Every collection has different performance profiles for actions like adding an element, finding an element, or removing an element.
  • Grow and shrink dynamically: Most collections support adding or removing elements dynamically. Notably, Array, Span<T>, and Memory<T> don't.

An indexable collection is a collection where you can access each element by using its index. The List<T> is the most common indexable collection.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
List<string> salmons = ["chinook", "coho", "pink", "sockeye"];

foreach (var salmon in salmons)
{
    Console.Write(salmon + " ");
}
// Output: chinook coho pink sockeye

// Remove an element from the list by specifying the object.
salmons.Remove("coho");

// Iterate using the index:
for (var index = 0; index < salmons.Count; index++)
{
    Console.Write(salmons[index] + " ");
}
// Output: chinook pink sockeye

// Add the removed element
salmons.Add("coho");

foreach (var salmon in salmons)
{
    Console.Write(salmon + " ");
}
// Output: chinook pink sockeye coho

The following example removes elements from a list by index. Instead of a foreach statement, it uses a for statement that iterates in descending order. The RemoveAt method causes elements after a removed element to have a lower index value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
List<int> numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

for (var index = numbers.Count - 1; index >= 0; index--)
{
    if (numbers[index] % 2 == 1)
    {
        numbers.RemoveAt(index);
    }
}

numbers.ForEach(number => Console.Write(number + " "));
// Output: 0 2 4 6 8
InMemoryDatabase.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
namespace Common.Database
{
    public class InMemoryDatabase : IDatabase
    {
        private int lastID = 0;
        private List<Product> Products { get; set; }

        public InMemoryDatabase()
        {
            Products = new List<Product>();
        }

        public void Delete(int productID)
        {
            var storedProduct = SearchByID(productID);
            Products.Remove(storedProduct);
        }

        public IEnumerable<Product> List()
        {
            return Products;
        }

        public void Store(Product product)
        {
            if (product.ID != 0)
                throw new Exception("Something went wrong: The given product is already in the database");

            product.ID = lastID++;
            Products.Add(product);
        }

        public void Update(Product product)
        {
            var storedProduct = SearchByID(product.ID);

            storedProduct.Name = product.Name;
            storedProduct.Description = product.Description;
            storedProduct.Price = product.Price;
            storedProduct.PackagingDate = product.PackagingDate;
            storedProduct.ExpirationDate = product.ExpirationDate;
            storedProduct.Image = product.Image;
        }

        private Product SearchByID(int productID)
        {
            var storedProduct = Products.Where(x => x.ID == productID).SingleOrDefault();

            if (storedProduct == null)
                throw new KeyNotFoundException($"Product with ID = {productID} is not found");

            return storedProduct;
        }
    }
}

We would like to use this implementation in the Administration application somehow. In the following practical classes, we will change the in-memory database to a real one, therefore, a replaceable solution might be the best approach.

Dependency Injection

Dependency Injection

.NET supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.7

Dependency injection addresses hard-coded dependency problems through:

  • The use of an interface or base class to abstract the dependency implementation.
  • Registration of the dependency in a service container.
  • Injection of the service into the constructor of the class where it's used. The framework takes on the responsibility of creating an instance of the dependency and disposing of it when it's no longer needed.

When designing services for dependency injection8:

  • Avoid stateful, static classes and members. Avoid creating global state by designing apps to use singleton services instead.
  • Avoid direct instantiation of dependent classes within services. Direct instantiation couples the code to a particular implementation.
  • Make services small, well-factored, and easily tested.
  • If a class has many injected dependencies, it might be a sign that the class has too many responsibilities and violates the Single Responsibility Principle (SRP). Attempt to refactor the class by moving some of its responsibilities into new classes.

To use this feature in the Administration application, the following NuGet packages are required:

  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.Hosting
  • Microsoft.Extensions.Hosting.Abstractions

Right click to the Administration and click the Manage NuGet Packages... item. Then Browse and install the packages. After the installation, the namespaces and types provided by the packages are available in the project.

Program.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static void Main()
{
    ApplicationConfiguration.Initialize();
    Application.SetHighDpiMode(HighDpiMode.SystemAware);

    var host = Host.CreateDefaultBuilder()
        .ConfigureServices((context, services) =>
        {
            services.AddSingleton<Common.Database.IDatabase, Common.Database.InMemoryDatabase>();
            services.AddTransient<Start>();
            services.AddTransient<ProductAdd>();
        }).Build();

    using var serviceScope = host.Services.CreateScope();
    var startForm = serviceScope.ServiceProvider.GetRequiredService<Start>();

    Application.Run(startForm);
}

We need to create a host app builder instance, then register every service we would like to use.

The container is responsible for cleanup of types it creates, and calls Dispose on IDisposable instances. Services resolved from the container should never be disposed by the developer. The container disposes services automatically based on their lifetime:

  • AddSingleton: Singleton services are disposed when the service container is disposed, usually at application shutdown.8
  • AddTransient: Transient and scoped services are disposed at the end of the scope in which they were resolved. In apps that process requests, this is typically at the end of the request.

List and Modify Orders

First, we need a DataGridView component to the main form from the Toolbox, and rename it to dataGridView.

List View
List Products

A Label (called title) is also placed to the top-left corner in order to show that Products or Orders are listed (future feature). It's Visibility property is set to false in the Properties window.

The constructor of the form should await an IServiceProvider parameter, and the dependency injection will take care of it.

Start.cs
1
2
3
4
5
6
private readonly IServiceProvider serviceProvider;
public Start(IServiceProvider _serviceProvider)
{
    serviceProvider = _serviceProvider;
    InitializeComponent();
}

To implement the listing, the Products -> List click event handler should be implemented:

Start.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void ListProducts(object sender, EventArgs e)
{
    title.Visible = true;
    title.Text = "Products";

    UpdateGridView();
}

private void UpdateGridView()
{
    // Requests the (singleton) IDatabase implementation
    // The "real" object is constructed in the Program.cs when registered
    // which implementation should be used.
    var database = serviceProvider.GetRequiredService<IDatabase>();

    dataGridView.DataSource = null;
    dataGridView.DataSource = database.List();

    // Hide the "ID" and the "Image" properties.
    dataGridView.Columns["ID"].Visible = false;
    dataGridView.Columns["Image"].Visible = false;
}

Also, since the Forms are handled by the DI, the AddProduct method should be changed:

Start.cs
1
2
3
4
5
6
7
8
9
private void AddProduct(object sender, EventArgs e)
{
    using var window = serviceProvider.GetRequiredService<ProductAdd>();
    window.ShowDialog();

    // If the user successfully added a record, refresh the list.
    if (window.DialogResult == DialogResult.OK)
        UpdateGridView();
}

The DataGridView allows users to click in the cells and edit the values, and of course, we -- developers -- do not want invalidated data to be stored. Therefore, the ReadOnly property should be set to true, and a validated method should be implemented for editing products. Add EditProduct event handler to the CellMouseClick event of the DataGridView.

Start.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private void EditProduct(object sender, DataGridViewCellMouseEventArgs e)
{
    // If the user clicked on an actual row and not on an empty space.
    if (dataGridView.CurrentRow?.DataBoundItem is not Product product)
        return;

    using var window = serviceProvider.GetRequiredService<ProductAdd>();
    window.Product = product; // "property injection"
    window.ShowDialog();

    if (window.DialogResult == DialogResult.OK)
        UpdateGridView();
}

Also the ProductAdd class should be prepared to handle the new IDatabase interface, the Product property, the storing or updating products cases.

ProductAdd.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public partial class ProductAdd : Form
{
    private readonly IDatabase database;

    // Don't want to serialize this property.
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public Product? Product { get; set; } = null;

    public ProductAdd(IDatabase db)
    {
        this.database = db;
        // ...
    }
    // ...
}

We can subscribe to the Load event that occurs when the user loads the form (after the ShowDialog call). Therefore, if any properties are set before the ShowDialog can be used in the event handler. Add a new button called Delete and set it's Visible property to false. In editing cases the button should be visible, but for new product, it should not. Assign the DeleteProduct event handler to the Click event.

ProductAdd.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void Loaded(object sender, EventArgs e)
{
    if (Product != null)
    {
        name.Text = Product.Name;
        description.Text = Product.Description;
        price.Text = Product.Price.ToString();

        packagingDate.Value = Product.PackagingDate;
        expirationDate.Value = Product.ExpirationDate;
        pictureBox.Image = Product.Image;

        buttonDelete.Visible = true;
    }
}

private void DeleteProduct(object sender, EventArgs e)
{
    // Assuming that the product is not null here, since the button is
    // only visible if the stage is editing.
    database.Delete(Product.ID);

    DialogResult = DialogResult.OK;
}

And one piece is missing: if we edit a product, it should be edited in the database, and a new product should be created (different function calls).

ProductAdd.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// the end of the `SaveProduct` event handler.

// Action is a specialized delegate that refers to methods with void return type
// and exactly one input parameter. E.g., our API functions.
Action<Product> action = (Product == null) ? database.Store : database.Update;
Product ??= new Product(); // compound assignment

Product.Name = productName;
Product.Description = productDescription;
Product.Price = productPrice;
Product.PackagingDate = productPackaged;
Product.ExpirationDate = productExpires;
Product.Image = productImage;

action(Product);

DialogResult = DialogResult.OK;

The progress of the Apiary project can be accessed: apiary-practice-3.tar.gz

References


Last update: 2026-03-04 06:30:36