Most of the Windows Forms applications I encounter do not exist or have extremely low unit test coverage. And they are often difficult to maintain, with hundreds or even thousands of lines of code behind the various Form classes in the project, but it doesn't have to be. Just because Windows Forms is a "legacy" technology doesn't mean you're destined to cause unmaintainable chaos. Here are ten tips for creating maintainable and testable Windows Forms applications.
1. Isolate your user interface with user controls
First, avoid putting too many controls on a form. Generally, the main form of your application can be broken down into logical areas (we can call them "views"). Your own life will be easier if you put the controls for each of these areas into their own containers, and in Windows Forms, the easiest way is to use user controls. So if you have an Resource-style application with a tree view on the left and a details view on the right, put TreeView into its own UserControl and create a UserControl for each possible right-hand view. Similarly, if you have tab controls, create a separate UserControl for each page in the tab control.
Not only does this prevent your classes from becoming difficult to manage, but it also makes tasks easier by resizing and setting tab order. It also allows you to easily disable entire parts of the user interface at once, if necessary. You will also find that it becomes easier to redesign the UI layout of your application when you break down the user interface into smaller UserControls that contain logically grouped controls.
2. Exclusion of non-UI code from subsequent code
In Windows Forms applications, you will always find the code to access the network, database, or file system in the code behind the form. This seriously violates the "single responsibility principle". The focus of your Form or UserControl class should be just the user interface. So when you detect code that is not UI-related in the code behind it, refactor it into a class with a single responsibility. Therefore, you can create a PreferencesManager class, or a class responsible for calling specific Web services. You can then inject these classes as dependencies into your UI components (although this is only the first step-we can expand the idea further, and we'll see soon).
3. Create passive views with interfaces
A particularly useful technique is to implement a view interface for each form and user control you create. This interface should contain properties that allow you to set and retrieve the state and content of controls in the view. It may also include events that report user interaction, such as clicking a button or moving a slider. The goal is that the implementation of these view interfaces is completely passive. Ideally, there should be no conditional logic in the code behind your Forms and User Controls.
The following is an example view interface for a new user entry view. The implementation of this view should be trivial. No business logic belongs in the code that follows (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 implementation is as simple as possible, you will be able to maximize the migration to alternative UI frameworks (such as WPF) because the only thing you need to do is recreate the views in new technology. All other code can be reused.
4. Use presenters to control views
So if you have made all views passive and implemented interfaces, you need something that implements the application's business logic and controls the views. We can call these "presenter" classes. This is a pattern called a "Model View Presenter" or MVP.
In the Model View Display, your view is completely passive, and the display instructs the view what data to display. It also allows the view to communicate with the presenter. In my example above, it is done by raising events, but usually with this pattern, your view can call the presenter directly.
绝对不允许视图开始直接操作模型(包括你的业务实体、数据库层等)。如果你遵循 MVP 模式,你的应用程序中的所有业务逻辑都可以轻松测试,因为它位于 Presenter 或其他非 UI 类中。
5. Create services for error reporting
Normally, your presenter class needs to display an error message. But don't just put MessageBox.Show into a non-UI class. You will make the method impossible for unit testing. Instead, create a service (such as IErrorDisplayService) that your presenter can call when they need to report problems. This keeps your presenter unit testable and also provides the flexibility to change the way errors are presented to users in the future.
6. Use Command Mode
If your application includes a toolbar with a large number of buttons for users to click, command mode may be very suitable. The command pattern requires you to create a class for each command. This has the great benefit of dividing your code into small classes, each with a responsibility. It also allows you to centralize everything related to a specific command. Should this command be enabled? Should it be visible? What are its tooltips and shortcuts? Does it require specific privileges or permissions to execute? How should exceptions thrown when a command is running be handled?
Command patterns allow you to standardize how to handle each issue common to all commands in your application. Your command object will have an Execute method that actually contains code to perform the required behavior for the command. In many cases, this will involve calling other objects and business services, so you need to inject them as dependencies into the command object. Your command object itself should be (and directly) unit testable.
7. Manage dependencies using IoC containers
If you are using the Presenter class and Command class, you may find that the number of classes they rely on grows over time. This is where control inversion containers like Unity or StructureMap can really help you. No matter what level of dependency they have, they allow you to easily build views and presenters.
8. Use 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 in which the initiator of the event and the handler of the event do not need to be coupled to each other at all. When an "event" occurs in your code that needs to be handled elsewhere, just post a message to the event aggregator. The code that needs to respond to the message can then subscribe to and process it without worrying about who initiated it.
For example, you send a "Request Help" message that contains details of the user's current location in the UI. Another service then processes the message and ensures that the correct page in the help document is launched in the Web browser. Another example is navigation. If your application has multiple screens, you can publish a "navigation" message to the event aggregator, and subscribers can then respond to the message by ensuring that the new screen appears in the user interface.
In addition to fundamentally separating publishers and subscribers of events, event aggregators have the huge benefit of creating code that is easy to unit test.
9. Threading using Async and Await
If you are targeting. NET 4 and later and are using Visual Studio 12 or later, don't forget that you can use the new async and await keywords, which will greatly simplify any threaded code in your application and automatically handle the return of background tasks into the UI thread after completion. They also greatly simplify exception handling across multiple chained background tasks. They are great for Windows Forms applications and are well worth a try if you don't already have one.
10. not too late
可以将我上面描述的所有模式和技术改造为现有的 Windows 窗体应用程序,但我可以从痛苦的经验告诉你,这可能需要大量工作,尤其是当窗体背后的代码达到数千行时。如果你开始使用 MVP、事件聚合器和命令模式等模式构建应用程序,你会发现随着它们变得越来越大,维护起来会少很多痛苦。你还可以对所有业务逻辑进行单元测试,这对于持续的可维护性至关重要。
Author: Mark Heath
Original link: https://markheath.net/post/maintainable-winforms
Reprinted from Weixin Official Accounts: OneByOneDotNet
Link to public account article: mp.weixin.qq.com/s/ks_ghCRxMmOQPYFib0cb3g