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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 isDictionary<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>, andMemory<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 | |
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 | |
| 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 | |
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 | |
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.8AddTransient: 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 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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 progress of the Apiary project can be accessed: apiary-practice-3.tar.gz