Most of the Windows Forms applications I encounter have either no unit tests or very low test coverage. They also tend to be difficult to maintain, with hundreds or even thousands of lines of code behind various Form classes in the project. But it doesn't have to be this way. Just because Windows Forms is a "legacy" technology doesn't mean you're doomed to create an unmaintainable mess. Here are ten tips for creating maintainable and testable Windows Forms applications.
1. Isolate your UI with User Controls
First, avoid placing too many controls on a single form. Often, the main form of your application can be broken down into logical areas (we can call them "views"). If you put the controls for each of these areas into their own container, your life becomes much easier. In Windows Forms, the simplest way to do this is using User Controls. So, if you have an Explorer-style application with a TreeView on the left and a details view on the right, put the TreeView into its own UserControl and create a UserControl for each possible right-side view. Similarly, if you have a TabControl, create a separate UserControl for each page inside the TabControl.
This not only prevents your classes from becoming unmanageable, but also makes tasks like resizing and setting the tab order much simpler. It also allows you to easily disable an entire section of the UI at once when necessary. You'll also find that redesigning the UI layout of your application becomes much easier when your UI is broken down into smaller UserControls containing logically grouped controls.
2. Keep non-UI code out of the code-behind
In Windows Forms applications, you often find code that accesses the network, database, or file system in the code-behind of forms. This is a serious violation of the "Single Responsibility Principle." The focus of your Form or UserControl class should be solely the user interface. So, when you detect non-UI-related code in the code-behind, refactor it into classes with a single responsibility. For example, you could create a PreferencesManager class or a class responsible for calling a specific web service. These classes can then be injected as dependencies into your UI components (though this is only the first step—we can extend this idea further, as we'll see soon).
3. Create passive views with interfaces
A particularly useful technique is to make every Form and UserControl you create implement a view interface. This interface should contain properties that allow you to set and retrieve the state and content of the controls in the view. It may also include events that report user interactions, such as button clicks or slider movements. The goal is that the implementations of these view interfaces are completely passive. Ideally, there should be no conditional logic in the code-behind of your Forms and UserControls.
Here is an example of a view interface for a new user entry view. The implementation of this view should be trivial. No business logic belongs in the code-behind (we'll discuss where it belongs next).
interface INewUserView
{
string FirstName { get; set; }
string LastName { get; set; }
event EventHandler SaveClicked;
}
By ensuring that your view implementations are as simple as possible, you'll maximize your ability to migrate to alternative UI frameworks (like WPF), because the only thing you'll need to do is recreate the view in the new technology. All other code can be reused.
4. Use presenters to control views
So, if you've made all your views passive and implemented interfaces, you need something that can execute your application's business logic and control the views. We can call these "presenter" classes. This is the pattern known as Model-View-Presenter (MVP).
In MVP, your views are completely passive, and the presenter instructs the view on what data to display. Views are also allowed to communicate with the presenter. In my example above, this is done by raising events, but often with this pattern, the view can call the presenter directly.
Views must never be allowed to directly manipulate the model (including your business entities, database layer, etc.). If you follow the MVP pattern, all business logic in your application becomes easily testable because it resides in presenters or other non-UI classes.
5. Create a service for error reporting
Often, your presenter classes need to display error messages. But don't just put MessageBox.Show into a non-UI class. You'll make that method untestable. Instead, create a service (e.g., IErrorDisplayService) that your presenter can call when it needs to report a problem. This keeps your presenter units testable and also provides flexibility to change how errors are presented to the user in the future.
6. Use the Command pattern
If your application includes a toolbar with many buttons for the user to click, the Command pattern may be a great fit. The Command pattern dictates that you create a class for each command. This has the great benefit of breaking your code into small classes, each with a single responsibility. It also allows you to centralize everything related to a specific command. Should the command be enabled? Should it be visible? What are its tooltip and shortcut keys? Does it require specific privileges or permissions to execute? How should exceptions thrown when the command runs be handled?
The Command pattern allows you to standardize the way you handle every issue common to all commands in your application. Your command object will have an Execute method that actually contains the code to perform the desired behavior for that command. In many cases, this will involve calling other objects and business services, so you'll need to inject them as dependencies into the command object. Your command objects themselves should be (and directly) unit testable.
7. Manage dependencies with an IoC container
If you're using presenter classes and command classes, you may find that the number of classes they depend on grows over time. This is where an Inversion of Control container like Unity or StructureMap can really help. They allow you to easily construct views and presenters regardless of how many levels of dependencies they have.
8. Use the Event Aggregator pattern
Another design pattern that is very useful in Windows Forms applications is the Event Aggregator pattern (sometimes called "Messenger" or "Event Bus"). This is a pattern where the event raiser and the event handler do not need to be coupled to each other at all. When something happens in your code that needs to be handled elsewhere, simply publish a message to the event aggregator. Then, code that needs to respond to that message can subscribe and handle it without worrying about who raised it.
For example, you send a "Request Help" message containing details about the user's current location in the UI. Another service handles that message and ensures the correct page in the help documentation is launched in a web browser. Another example is navigation. If your application has multiple screens, you can publish a "Navigation" message to the event aggregator, and a subscriber can respond by ensuring the new screen is displayed in the user interface.
In addition to fundamentally decoupling the publisher and subscriber of events, the Event Aggregator pattern also has the great benefit of creating code that is extremely easy to unit test.
9. Use async and await for threading
If you're targeting .NET 4 and later and using Visual Studio 12 or later, don't forget that you can use the new async and await keywords. This will greatly simplify any threading code in your application and automatically handle marshaling back to the UI thread when background tasks complete. They also greatly simplify exception handling across multiple chained background tasks. They are perfect for Windows Forms applications, and if you haven't tried them yet, they are well worth exploring.
10. It's never too late
All the patterns and techniques I've described above can be retrofitted into existing Windows Forms applications, but I can tell you from painful experience that it can be a lot of work, especially when the code-behind of a form reaches thousands of lines. If you start building your applications using patterns like MVP, Event Aggregator, and Command, you'll find that as they grow larger, they become much less painful to maintain. You can also unit test all business logic, which is essential for long-term maintainability.
Original author: Mark Heath
Original link: https://markheath.net/post/maintainable-winforms
Reproduced from WeChat public account: OneByOneDotNet
WeChat article link: https://mp.weixin.qq.com/s/ks_ghCRxMmOQPYFib0cb3g