- Mastering Windows Presentation Foundation
- Sheridan Yuen
- 2271字
- 2021-06-24 16:49:07
Implementing Dependency Injection
Dependency injection is a well-known design pattern that aids in decoupling various components of an application. If one class uses another class to perform some functionality internally, then the class that is internally used becomes a dependency of the class that uses it. It cannot achieve its objectives without it. In some cases, this is not a problem, but in others, it can represent a huge problem.
For example, let's imagine that we have a FeedbackManager class that is responsible for providing operational feedback to the end users. In that class, we have a FeedbackCollection instance that holds the Feedback objects that are currently being displayed to the current user. Here, the Feedback objects are a dependency of the FeedbackCollection instance and that, in turn, is a dependency of the FeedbackManager class.
These objects are all tightly coupled, which is usually a bad thing in software development. However, they are also tightly related by necessity. A FeedbackCollection object would be useless without the Feedback objects, as would the FeedbackManager object.
In this particular case, these objects require this coupling to make them useful together. This is called composition, where the individual parts form a whole, but do little on their own, so it really is no problem for them to be connected in this way.
On the other hand, let's now contemplate the connection between our View Models and our DAL. Our View Models will definitely need access to some data, so it would at first seem to make sense to encapsulate a class in our View Models that provides the data that it requires.
While that would certainly work, it would unfortunately result in the DAL class becoming a dependent of the View Model class. Moreover, it would permanently couple our View Model component to the DAL and break the Separation of Concerns that MVVM provides. The kind of connection that we require in this situation is more like aggregation, where the individual parts are useful on their own.
In these cases, we want to be able to use the individual components separately and to avoid any tight coupling between them. Dependency Injection is a tool that we can use to provide this separation for us. In the absolute simplest terms, Dependency Injection is implemented through the use of interfaces. We've already seen some basic examples of this in the DataController class from the Separating the Data Access Layer section, and the EmailManager example from the previous section.
However, they were very basic examples and there are a variety of ways of improving them. Many application frameworks will provide the ability for developers to use Dependency Injection to inject the dependencies into their classes and we can do the same with ours. In its simplest form, our DependencyManager class will simply need to register the dependencies and provide a way to resolve them when required. Let's take a look:
using System; using System.Collections.Generic; namespace CompanyName.ApplicationName.Managers { public class DependencyManager { private static DependencyManager instance; private static Dictionary<Type, Type> registeredDependencies = new Dictionary<Type, Type>(); private DependencyManager() { } public static DependencyManager Instance { get { return instance ?? (instance = new DependencyManager()); } } public int Count { get { return registeredDependencies.Count; } } public void ClearRegistrations() { registeredDependencies.Clear(); } public void Register<S, T>() where S : class where T : class { if (!typeof(S).IsInterface) throw new ArgumentException("The S generic type parameter of the Register method must be an interface.", "S"); if (!typeof(S).IsAssignableFrom(typeof(T))) throw new ArgumentException("The T generic type parameter must be a class that implements the interface specified by the S generic type parameter.", "T"); if (!registeredDependencies.ContainsKey(typeof(S))) registeredDependencies.Add(typeof(S), typeof(T)); } public T Resolve<T>() where T : class { Type type = registeredDependencies[typeof(T)]; return Activator.CreateInstance(type) as T; } public T Resolve<T>(params object[] args) where T : class { Type type = registeredDependencies[typeof(T)]; if (args == null || args.Length == 0) return Activator.CreateInstance(type) as T; else return Activator.CreateInstance(type, args) as T; } } }
You may have noticed that we are using the Singleton pattern again for this class. In this case, it again fits our requirements exactly. We want one, and only one, instance of this class to be instantiated and we want it to stay alive for as long as the application is running. When testing, it is used to inject our mock dependencies into the View Models, so it is part of the framework that enables our Separation of Concerns.
The Count property and the ClearRegistrations method are more useful for testing than when running the application and the real action goes on in the Register and Resolve methods. The Register method registers the interface type represented by the S generic type parameter, with the concrete implementation of that interface represented by the T generic type parameter.
As the S generic type parameter must be an interface, an ArgumentException is thrown at runtime if the type parameter class supplied is not one. A further check is performed to ensure that the type specified by the T generic type parameter actually implements the interface specified by the S generic type parameter, and a further ArgumentException is thrown if the check fails.
The method then verifies the fact that the type parameter provided is not already in the Dictionary and adds it if it is unique in the collection. Therefore, in this particular implementation, we can only specify a single concrete implementation for each supplied interface. We could change this to either update the stored reference if an existing type was passed again, or even to store multiple concrete types for each interface. It all depends on the application requirements.
Note the generic type constraint declared on this method that ensures that the type parameters will at least be classes. Unfortunately, there is no such constraint that would allow us to specify that a particular generic type parameter should be an interface. However, this type of parameter validation should be used where possible, as it helps the users of our framework to avoid using these methods with inappropriate values.
The Resolve methods use some simple reflection to return the concrete implementations of the interface types represented by the generic type parameters used. Again, note the generic type constraints declared by these two methods, that specify that the type used for type T parameter must be a class. This is to prevent the Activator.CreateInstance methods from throwing an Exception at runtime, if a type that could not be instantiated were used.
The first overload can be used for classes without any constructor parameters, and the second has an additional params input parameter to pass the parameters to use when instantiating classes that require constructor parameters.
The DependencyManager class can be set up during application startup, using the App.xaml.cs file. To do this, we first need to find the following StartupUri property setting in the Application declaration at the top of the App.xaml file:
StartupUri="MainWindow.xaml"
We then need to replace this StartupUri property setting with the following Startup property setting:
Startup="App_Startup"
In this example, App_Startup is the name of the initialization method that we want to be called at startup. Note that as the WPF Framework is no longer starting the MainWindow class, it is now our responsibility to do so:
using System.Windows; using CompanyName.ApplicationName.Managers; using CompanyName.ApplicationName.ViewModels; using CompanyName.ApplicationName.ViewModels.Interfaces; namespace CompanyName.ApplicationName { public partial class App : Application { public void App_Startup(object sender, StartupEventArgs e) { RegisterDependencies(); new MainWindow().Show(); } private void RegisterDependencies() { DependencyManager.Instance.ClearRegistrations(); DependencyManager.Instance.Register<IDataProvider, ApplicationDataProvider>(); DependencyManager.Instance.Register<IEmailManager, EmailManager>(); DependencyManager.Instance.Register<IExcelManager, ExcelManager>(); DependencyManager.Instance.Register<IWindowManager, WindowManager>(); } } }
When we want to inject these dependencies into a View Model in the application at runtime, we could use the DependencyManager class like this:
UsersViewModel viewModel = new UsersViewModel(DependencyManager.Instance.Resolve<IEmailManager>(), DependencyManager.Instance.Resolve<IExcelManager>(), DependencyManager.Instance.Resolve<IWindowManager>());
The real beauty of this system is that when testing our View Models, we can register our mock manager classes instead. The same preceding code will then resolve the interfaces to their mock concrete implementations, thereby freeing our View Models from their actual dependencies:
private void RegisterMockDependencies() { DependencyManager.Instance.ClearRegistrations(); DependencyManager.Instance.Register<IDataProvider, MockDataProvider>(); DependencyManager.Instance.Register<IEmailManager, MockEmailManager>(); DependencyManager.Instance.Register<IExcelManager, MockExcelManager>(); DependencyManager.Instance.Register<IWindowManager, MockWindowManager>(); }
We've now seen the code that enables us to swap out our dependent classes with mock implementations when we are testing our application. However, we've also seen that not all of our manager classes will require this. So, what exactly represents a dependency? Let's take a look at a simple example involving a UI popup message box:
using CompanyName.ApplicationName.DataModels.Enums; namespace CompanyName.ApplicationName.Managers.Interfaces { public interface IWindowManager { MessageBoxButtonSelection ShowMessageBox(string message, string title, MessageBoxButton buttons, MessageBoxIcon icon); } }
Here, we have an interface that declares a single method. This is the method that the developers will call from the View Model classes when they need to display a message box in the UI. It will use a real MessageBox object during runtime, but that uses a number of enumerations from the System.Windows namespace.
We want to avoid interacting with these enumeration instances in our View Models, as that will require adding a reference to the PresentationFramework assembly and tie our View Models to part of our Views component.
We therefore need to abstract them from our interface method definition. In this case, we have simply replaced the enumerations from the PresentationFramework assembly with custom enumerations from our domain that merely replicate the original values. As such, there is little point in showing the code for these custom enumerations here.
While it's never a good idea to duplicate code, it's an even worse idea to add a UI assembly like the PresentationFramework assembly to our ViewModels project. By encapsulating this assembly within the Managers project and converting its enumerations, we can expose the functionality that we need from it without tying it to our View Models:
using System.Windows; using CompanyName.ApplicationName.Managers.Interfaces; using MessageBoxButton = CompanyName.ApplicationName.DataModels.Enums.MessageBoxButton; using MessageBoxButtonSelection = CompanyName.ApplicationName.DataModels.Enums.MessageBoxButtonSelection; using MessageBoxIcon = CompanyName.ApplicationName.DataModels.Enums.MessageBoxIcon; namespace CompanyName.ApplicationName.Managers { public class WindowManager : IWindowManager { public MessageBoxButtonSelection ShowMessageBox(string message, string title, MessageBoxButton buttons, MessageBoxIcon icon) { System.Windows.MessageBoxButton messageBoxButtons; switch (buttons) { case MessageBoxButton.Ok: messageBoxButtons = System.Windows.MessageBoxButton.OK; break; case MessageBoxButton.OkCancel: messageBoxButtons = System.Windows. MessageBoxButton.OkCancel; break; case MessageBoxButton.YesNo: messageBoxButtons = System.Windows.MessageBoxButton.YesNo; break; case MessageBoxButton.YesNoCancel: messageBoxButtons = System.Windows.MessageBoxButton.YesNoCancel; break; default: messageBoxButtons = System.Windows.MessageBoxButton.OKCancel; break; } MessageBoxImage messageBoxImage; switch (icon) { case MessageBoxIcon.Asterisk: messageBoxImage = MessageBoxImage.Asterisk; break; case MessageBoxIcon.Error: messageBoxImage = MessageBoxImage.Error; break; case MessageBoxIcon.Exclamation: messageBoxImage = MessageBoxImage.Exclamation; break; case MessageBoxIcon.Hand: messageBoxImage = MessageBoxImage.Hand; break; case MessageBoxIcon.Information: messageBoxImage = MessageBoxImage.Information; break; case MessageBoxIcon.None: messageBoxImage = MessageBoxImage.None; break; case MessageBoxIcon.Question: messageBoxImage = MessageBoxImage.Question; break; case MessageBoxIcon.Stop: messageBoxImage = MessageBoxImage.Stop; break; case MessageBoxIcon.Warning: messageBoxImage = MessageBoxImage.Warning; break; default: messageBoxImage = MessageBoxImage.Stop; break; } MessageBoxButtonSelection messageBoxButtonSelection = MessageBoxButtonSelection.None; switch (MessageBox.Show(message, title, messageBoxButtons, messageBoxImage)) { case MessageBoxResult.Cancel: messageBoxButtonSelection = MessageBoxButtonSelection.Cancel; break; case MessageBoxResult.No: messageBoxButtonSelection = MessageBoxButtonSelection.No; break; case MessageBoxResult.OK: messageBoxButtonSelection = MessageBoxButtonSelection.Ok; break; case MessageBoxResult.Yes: messageBoxButtonSelection = MessageBoxButtonSelection.Yes; break; } return messageBoxButtonSelection; } } }
We start with our using directives and see further examples of using alias directives. In this case, we created some enumeration classes with the same names as those from the System.Windows namespace. To avoid the conflicts that we would have caused by adding a standard using directive for our CompanyName.ApplicationName.DataModels.Enums namespace, we add aliases to enable us to work with just the types from our namespace that we require.
After this, our WindowManager class simply converts the UI-related enumeration values to and from our custom enumerations, so that we can use the functionality of the message box, but not be tied to its implementation. Imagine a situation where we need to use this to output an error message:
WindowManager.ShowMessageBox(errorMessage, "Error", MessageBoxButton.Ok, MessageBoxIcon.Error);
When execution reaches this point, a message box will pop up, displaying an error message with an error icon and heading. The application will freeze at this point while waiting for user feedback and, if the user does not click a button on the popup, it will remain frozen indefinitely. If execution reaches this point during a unit test and there is no user to click the button, then our test will freeze indefinitely and never complete.
In this example, the WindowManager class is dependent upon having a user present to interact with it. Therefore, if the View Models used this class directly, they would also have the same dependency. Other classes might have a dependency on an email server, database, or other type of resource, for example. These are the types of classes that View Models should only interact with via interfaces.
In doing so, we provide the ability to use our components independently from each other. Using our IWindowManager interface, we are able to use our ShowMessageBox method independently of the end users. In this way, we are able to break the user dependency and run our unit tests without them. Our mock implementation of the interface can simply return a positive response each time and the program execution can continue unheeded:
using CompanyName.ApplicationName.DataModels.Enums; using CompanyName.ApplicationName.Managers.Interfaces; namespace Test.CompanyName.ApplicationName.Mocks.Managers { public class MockWindowManager : IWindowManager { public MessageBoxButtonSelection ShowMessageBox(string message, string title, MessageBoxButton buttons, MessageBoxIcon icon) { switch (buttons) { case MessageBoxButton.Ok: case MessageBoxButton.OkCancel: return MessageBoxButtonSelection.Ok; case MessageBoxButton.YesNo: case MessageBoxButton.YesNoCancel: return MessageBoxButtonSelection.Yes; default: return MessageBoxButtonSelection.Ok; } } } }
This simple example shows another method of exposing functionality from a source to our View Models, but without it becoming a dependency. In this way, we can provide a whole host and variety of capabilities to our View Models, while still enabling them to function independently.
We now have the knowledge and tools to build functionality into our application framework in many different ways, yet our probe into application frameworks is still not quite complete. One other essential matter is that of connecting our Views with our View Models. We'll need to decide how the users of our framework should do this, so let's look at some choices.
- TypeScript入門與實(shí)戰(zhàn)
- Developing Mobile Web ArcGIS Applications
- Designing Hyper-V Solutions
- The Computer Vision Workshop
- Object-Oriented JavaScript(Second Edition)
- Quarkus實(shí)踐指南:構(gòu)建新一代的Kubernetes原生Java微服務(wù)
- Visual Basic程序設(shè)計(jì)實(shí)踐教程
- Illustrator CS6設(shè)計(jì)與應(yīng)用任務(wù)教程
- 單片機(jī)原理及應(yīng)用技術(shù)
- Android移動(dòng)應(yīng)用開發(fā)項(xiàng)目教程
- Flink技術(shù)內(nèi)幕:架構(gòu)設(shè)計(jì)與實(shí)現(xiàn)原理
- 超簡單:Photoshop+JavaScript+Python智能修圖與圖像自動(dòng)化處理
- Spring Data JPA從入門到精通
- 零基礎(chǔ)學(xué)SQL(升級(jí)版)
- Python數(shù)據(jù)科學(xué)實(shí)踐指南