AuthorJonathan AllenTranslatorYao Qilin
After the term "View-model" emerged, many developers have many questions. View models need to deal with the intersection of views, models, and external services. This is clear, but accurate practices are often taken over. What should it contain, what should it not contain, and there is no clear list. They will eventually become a hodgedge of everything. This article does not intend to give a clear answer, but explores several of the many roles that the view model assumes.
When you read different roles and modes in this article, remember the following three points:
- These examples are from real projects.
- Most view models have multiple roles.
- The importance of strictly following a pattern is lower than that of a runable program.
Model role
Providing data for a view is a vital role of the view model. However, even if you still use the XAML data binding technology, there are many ways to provide data.
Use the view model to replace the data context
Replacing the data context with the view model is the simplest model end (model-side) mode and may be the most common. Real data is exposed by one or more simple attributes of the view model. To some extent, this is not a pattern. It only associates the view model with the real data context attributes of the view and injects other functions, such as encapsulated navigation or icommand called by the Service. This topic will be discussed later in this article.
View model as activity record
Unfortunately, "using a model as an active record" is a common error pattern. In this mode, there is no real model in the application. On the contrary, all fields are directly provided by the view model itself. For example, customerviewmodel may contain the firstname, lastname, and customerid fields. As this is a view model, external services may be attached. You can expose them through icommands such as loadcustomercommand and savecustomercommand, which converts the view model into activity records.
Note that the activity record mode is effective in some scenarios. The problem is that the use of activity records is exaggerated to a certain extent. If they are used as other roles of the view model, they will almost become a "omnipotent object )".
Unit test has no security. You can create a virtual unit test by using an integrated test with a simulated (Mock) service. However, this method is often time-consuming and error-prone.
If there is no model, it is not mvvm.
Use the view model as an adapter or decorator
A view model can be used as an adapter or decorator to temporarily wrap a model and provide additional information or a new format. However, this is a very dangerous practice and should be avoided if possible.
We use the classic fullname as an example, which brings two risks.
Push-based package
In the push-based wrapper, we assume that only the view model can push data updates to the view.
public class PersonViewModel : INotifyPropertyChanged{ private readonly Person m_Model; public PersonViewModel(Person model) { m_Model = model; } public string FirstName { get{ return m_Model.FirstName} set { m_Model.FirstName = value; OnPropertyChanged(new PropertyChangedEventArgs("FirstName")); OnPropertyChanged(new PropertyChangedEventArgs("FullName")); }}
This means that this operation fails if you change the model directly without using the personviewmodel package. These changes are not propagated to the view, resulting in synchronization problems.
Event-based wrappers
An alternative solution based on the push package is to rely on the events triggered by the model and forwarded by the view model. The following code is used:
public class PersonViewModel : INotifyPropertyChanged{ private readonly Person m_Model; public PersonViewModel(Person model) { m_Model = model; m_Model.PropertyChanged += Model_PropertyChanged; } public string FirstName { get{ return m_Model.FirstName} set { m_Model.FirstName = value; } } void Model_PropertyChanged(object sender, PropertyChangedEventArgs e) { OnPropertyChanged(e); switch (e.PropertyName) { case "FirstName": case "LastName:": OnPropertyChanged(new PropertyChangedEventArgs("FullName")); break; } }
In this solution, it is risky to attach the attribute change notification from the model to the view model. When a model is accessed by multiple view models, memory leakage may occur.
(Click to enlarge the image)
XAML-based controls can be used to handle model events, because data binding infrastructure can prevent memory leakage. In winform, you can use the component's infrastructure to release (dispose) event handlers. However, the life cycle of the view model does not depend on controls or controls, because controls can be reloaded, so there is no proper place for us to release these event handlers.
This is not necessarily a problem. If you are sure that nothing else holds a reference to the model, you can use the view model wrapper. However, a safer approach is to enhance the model itself (such as placing the fullname attribute in the model) or use a value converter. These two methods also allow you to write unit tests without involving the view model.
If the view model listens to events in the model, check whether memory leakage occurs.
View as the aggregation Root
According to Wikipedia, aggregation is defined:
A collection of objects bound to a root object. This root object is also called an aggregate root. The aggregation root prevents external objects from holding references to their members to ensure consistency of changes in the aggregation.
The main difference between an aggregate root and an adapter or decorator is that its model will never be exposed. You cannot access any model or model attributes or events at all. Only the aggregate root holds a reference to the model, so the memory leakage seen in the previous section can be completely prevented.
If the view model completely hides the details of complex object graphs, it may be an aggregation root.
Use the view model as the Web MVC model
This is a relatively new phenomenon I have seen in the ASP. NET MVC community. Many developers call the class created or loaded by the Controller and passed to the view "view model ". This corresponds to the "Domain Model" and "input model. Dino Esposito's article "three models in ASP. net mvc Applications" explains this well.
This type of view model is easy to recognize because it does not have any roles or responsibilities except data and business rules that are completely dependent on data. Therefore, it has all the advantages of other pure models, such as easy unit testing.
There is no need to blame this naming method. Just like the difference between traditional MVC and web MVC, this is one of the habits we have to accept.
Be sure not to confuse "view model" and "view model". In particular, mvvm applications may include these two models.
Role at the view end
Different degrees of coupling exist between views and view models. Let's take a look at the most closely coupled, and then discuss more idealized view models.
Hide the view model as code
For beginners of XAML, there is often an anti-pattern error. It is often said that too much code should not be put in "XAML. cs" or "XAML. VB. Good beginners often misunderstand that they cannot put any code into it. Therefore, they threw everything into the "view model" and turned it into a code hidden file with the wrong location.
At this time, if you threw everything into the code hidden file, the problem is the same. In addition, throwing everything into the view model makes static analysis more difficult and may cause memory leakage.
If you move all the code from XXX. XAML. CS to xxxviewmodel. CS, the view model becomes a "code hidden" file.
Using the view model as a traditional MVC Controller
A key difference between traditional MVC and web MVC lies in the relationship between controllers and views. In Web MVC, the controller can create and return any view it wants. In addition to providing data models for these views, there is no interaction with them.
The traditional MVC view and controller are always closely coupled. Each controller is specially built to provide services for events generated by users in a special view. This mode also appears in mvvm. The view model assumes the Controller role, while the icommand replaces the event.
The differences between using a view model as a controller and using it as a code hidden file are very subtle, so there are some general principles:
View model as controller flag
- Use icommand to process external resources
- The visual state change is triggered by exposing additional properties.
- Exposes the attributes and events that the control can respond.
- Only listen for General events on the view, such as loaded and unloaded
Hide a view as a sign of a code file
- Use eventtocommand to process all events in the view Model
- Call visual State Manager to change the visual state.
- Directly interact with controls
- View-dependent Layout
- In order to make the view model work correctly, the layer-relationship diagram below the events that the control must respond to shows the differences between the Controller type and the hidden code view model.
(Click to enlarge the image)
The difference between controller and code hiding has little impact on the application itself. However, if you want to test the view model without relying on the view, this difference is apparent. Although I do not agree to perform unit tests on integrated components such as view models, it is very helpful to execute integration tests to ensure that they work properly with external resources (databases and web services. This bidirectional coupling between views and view models makes integration testing more difficult.
If the view model and view are mapped one to one, the view model becomes a controller.
Use the view model as the shared Controller
Many people, including me, advocate that if the view model cannot be shared by multiple views, it is not a real view model. Although I am not so doggy about this theory, I still regard it as a useful design model.
If you want to synchronize multiple views that use the same data, you can use the shared Controller method. For example, a form contains a data grid and a chart that displays the data. In this case, you can directly share the model, but share the view model will not make mistakes. In this way, if you switch to a completely different data, the two views will be notified at the same time.
Note that shared view models are much harder to write than controller view models. They are not flexible. You must add more code in the hidden code file than usual. The advantage is that shared view models are easier to test because they eliminate a lot of complexity.
A feasible alternative is to share the model (rather than the Controller) in the traditional MVC mode. XAML Data Binding can well support these two designs, So it depends on which scheme is less destructive to the overall design.
If view models are designed to be shared, they are easier to test.
Use the view model as the Navigator
The four main XAML-based UI frameworks all contain navigation-style applications, but Windows Phone or Windows 8 Metro applications have a special liking for them, because in these applications, navigation frameworks are all at the core.
Fortunately, if you use other things (such as the top-level view model) to wrap the navigation framework, it is also very suitable for unit testing and integration testing. When page transfer is triggered, the view model does not send the URI to the navigation framework, but sends the URI to a verifiable simulation class. See the navigatorviewmodel and simplenavigator classes of granite. XAML.
An abstract navigation framework is crucial for building a testable view model.
Use the view model as the MVP Display Device
The main difference between the Model-View-display mode and the traditional MVC mode is that the interaction mode between the model and view is different. In MVC mode, the model directly triggers view listening events. In MVP mode, the dashboard listens to events and updates the view itself.
Given that XAML is very powerful, we hardly notice the existence of this pattern in the technology we are talking about. But in fact it plays an important role in multi-threaded applications.
This mode is often used when the model is updated by background threads without any user interaction. In this case, the view model is responsible for sending the notification to the UI thread. This may involve copying data to the data-Bound Model, or directly updating the control itself.
It is worth noting that this is not the only way to process multithreading. If the computing complexity is not high, it is best to simply mail all asynchronous messages to the UI thread and process them there. Some libraries (such as responsive extensions) can simplify these operations.
Consider using the display mode to ensure that asynchronous messages are not processed in the UI thread.
Service/resource role
So far, we have used "External Services" as a black box, which indicates the file systems, databases, web services, and other external resources that an application may need to access. Now it's time to consider how to actually access these services. There are two basic ways to achieve this.
Use the view model as the data access layer
For small applications and well-defined external services, direct calling through the view model is very simple. This model is ideal for simple applications, and WCF focuses on interface design, making it easier to implement dependency injection.
The disadvantage is that the scalability is not good enough. As applications become more complex, you will find more and more logic in the view model. This is especially true before. Net 4.5 introduces the async/await keyword.
If you find that your view model is overwhelmed by the callback logic, you can consider adding a separate client service layer.
Note that when using this mode, many developers choose not to use external services as unique components for testing. Theoretically, if only the view model can access the service layer, the test on the view model can be used to test the service layer at the same time.
The WCF interface is the perfect choice for dependency injection and simulation, especially when the application does not contain a separate data access layer.
View model and data access layer
In either case, you may need a separate data access layer:
- The view model becomes too large and difficult to maintain.
- Duplicate the same external service logic among multiple view models.
This mode is hard to write correctly. If you do not pay attention to the specific roles of the dal and Vm in the application, all logic may end up in the Dal or VM. This not only violates the intention of this pattern, but also is very confusing when the maintainer sees components that are almost empty as unnecessary sample code.
You have two options when writing an integration test. You can test the two components at the same time for the view model as before. Alternatively, the data access layer can be used.
The latter provides us with a clean approach to introduce interface-based dependency injection. In turn, it allows us to use a simulated unit test to test the view model.
Conclusion: master the view model.
The only way to really grasp the view model and avoid it from becoming a hodgedge is to first determine what the term actually means to you. List all concerns and see which component it belongs.
The following example is from one of my applications. Your list may be very similar and may be completely different.
Focus
Components
Navigate between pages
Basic View model (Silverlight navigation Framework)
Format
View (value converter)
Show dialog box in view
View code hiding
Load dependency
Basic View Model
Create a controller
Basic View Model
Triggers a WCF call to load or save the Model
View-specific Controller
Verify user input
Model (inotifydataerrorinfo)
Error Report
Basic View Model
Of course, this table only contains client code. For real projects, you also need to create two such tables, one for the server code, and the other for which data table corresponds to which database architecture. As the project develops and the design evolves, you need to update these tables to reflect new code, or refactor the code until they match again. In this way, we can always know the components they belong to before adding new features.
If you want to learn from this article, I hope:
The design model is just a suggestion. It is up to you to decide how to convert it into a practical design to meet your needs.