Если вы уже работали с приложениями Windows Phone и Windows Store и, особенно, с шаблоном MVVM, вы должны быть знакомы с концепцией внедрения зависимостей . В типичном приложении, когда вам нужно использовать класс, вы просто создаете новый экземпляр, как в следующем примере:
private void OnButtonClicked(object sender, EventArgs e) { PopupService popupService = new PopupService(); popupService.ShowPopup("Sample title", "Sample message"); }
Таким образом, объекты создаются во время компиляции. Однако, когда вы работаете с шаблоном MVVM, этот подход имеет обратную сторону. Допустим, вы используете PopupService в нескольких классах (например, ViewModels), и внезапно вам нужно изменить его реализацию на новый. При предыдущем подходе вы вынуждены заходить в каждый класс, в котором вы используете PopupService, и менять реализацию на новую.
В подходе внедрения зависимостей вместо этого объекты регистрируются внутри контейнера , который является специальным классом, который выполняет диспетчеризацию объектов, когда они требуются. Как правило, при таком подходе каждый класс описывается интерфейсом. Например, класс PopupService может быть описан с помощью интерфейса под названием IPopupService , как в следующем примере:
public interface IPopupService { void ShowPopup(string title, string message); }
Затем, когда приложение запускается, мы указываем для каждого интерфейса, который является конкретной реализацией, которую мы хотим использовать в приложении, как в следующем коде (имейте в виду, что это всего лишь пример, есть несколько библиотек для реализации внедрения зависимости, каждая из них с его API и методами):
public App() { IUnityContainer container = new UnityContainer(); container.RegisterType<IPopupService, PopupService>(); }
В конце концов, когда классу нужно использовать объект PopupService , вместо простого создания нового экземпляра, он просит контейнер вернуть зарегистрированный экземпляр, как в следующем примере:
private void OnButtonClicked(object sender, EventArgs e) { IPopupService popupService = container.Resolve<IPopupService>(); popupService.ShowPopup("Sample title", "Sample message"); }
Преимущество этого подхода должно быть очевидным: всякий раз, когда нам нужно изменить реализацию интерфейса IPopupService , достаточно изменить конкретную реализацию интерфейса, который зарегистрирован в контейнере, например:
public App() { IUnityContainer container = new UnityContainer(); container.RegisterType<IPopupService, FakePopupService>(); }
Автоматически все классы, использующие класс PopupService , немедленно начнут использовать новую реализацию, называемую FakePopupService , просто изменив одну строку кода.
Инъекция зависимости и формы Xamarin
Инъекция зависимостей становится очень полезной, когда вы работаете с Xamarin Forms: цель этой технологии — дать разработчикам возможность делиться как можно большим количеством кода между тремя различными мобильными платформами (iOS, Android и Windows Phone). Технология Xamarin была создана с этой целью, однако Xamarin Forms выводит этот подход на новый уровень, позволяя разработчикам делиться не только бизнес-логикой, но и пользовательским интерфейсом. На самом деле Xamarin Forms использует подход, основанный на XAML: пользовательский интерфейс определяется с использованием XML, где каждый элемент управления идентифицируется определенным тегом XML. Самое большое отличие от стандартного XAML (который поддерживается только технологиями Microsoft, такими как Windows Phone или WPF) заключается в том, что элементы управления автоматически переводятся в элементы управления собственной платформы. Сюда,в отличие от кросс-платформенных приложений, основанных на веб-технологиях (которые предлагают одинаковый интерфейс на всех платформах), мы сможем поддерживать интерфейс в соответствии с рекомендациями и пользовательским интерфейсом платформы.
However, there are some scenarios which simply don’t fit the shared code approach. We can’t forget, in fact, that Xamarin, unlike web technologies, doesn’t provide a way to create the application just once and run it everywhere: one of the biggest pros of Xamarin, in fact, is that it allows developers to make use of every platform specific feature, unlike web applications that typically support only the features that are in common between every platform. Consequently, you still need to learn how Android and iOS development work if you want to create a real application. However, thanks to Xamarin, you won’t have to learn also a new language in the process: Xamarin, in fact, offers a way to use the native APIs with the familiar C# syntax.
The same applies for Xamarin Forms: it offers a way to share not just business logic but also user interface code but, in the end, we still need to deal with the specific platform features and implementations. Let’s say, for example, that we want to add the PopupService class we’ve previously seen in our Xamarin Forms project, which offers a ShowPopup() method that displays an alert to the user. Each platform has a different way to display a popup message: for example, in Windows Phone you use the MessageBox class; on Android, you have the AlertDialog class; on iOS, instead, you use the UIAlertView class. However, we would love to have a way to use the ShowPopup() method in our shared page and, automatically, see it rendered on each platform with its specific code.
Thanks to the dependency injection approach, we can: in the shared page we’re going to get a reference to the IPopupService class and to use the ShowPopup() method. At runtime, the dependency container will inject into the IPopupService object the specific implementation for the platform. However, compared to a regular dependency injection approach (like the one we’ve previously seen), there are some differences when we need to use it in Xamarin Forms.
Please note: the sample we’re going to see has been created just for demonstration purposes. Xamarin Forms, in fact, already offers a way to display popups to the user in a shared page, without having to deal with the different implementations for each platform.
One interface, multiple implementations
The first step is to create a common interface in the shared project, since it will be unique for each platform:
public interface IPopupService { void ShowPopup(string title, string message); }
Then we need a concrete implementation of this interface, one for each platform: we’re going to create in every specific platform project this time. For example, here is how it looks like the implementation in the Windows Phone project:
public class PopupService: IPopupService { public void ShowPopup(string title, string message) { MessageBox.Show(message, title, MessageBoxButton.OK); } }
Here is, instead, how it looks like in the Android project:
public class PopupService: IPopupService { public void ShowPopup(string title, string message) { AlertDialog.Builder alert = new AlertDialog.Builder(Forms.Context); alert.SetTitle(title) .SetMessage(message) .SetPositiveButton("Ok", (sender, args) => { Debug.WriteLine("Ok clicked"); }) .Show(); } }
The next step is to understand how to use the proper implementation for each platform. In our shared code, we’re going to simply use the interface, like in the following sample:
private void OnButtonClicked(object sender, EventArgs e) { IPopupService popupService = new PopupService(); popupService.ShowPopup("Sample title", "Sample message"); }
However, this code won’t simply work: we don’t have a single implementation of thePopupService class, but three different implementations, each of them with its namespace. Also the previous dependency injection approach we’ve seen doesn’t solve our problem: when we register the implementation in the container, we still need to specify which is the concrete class to use and, in our scenario, we have three of them.
Luckily, Xamarin Forms offers a smart approach to solve this situation: instead of manually registering the implementations into a container, we decorated the classes with an attribute. At runtime, automatically, Xamarin Forms will detect which is the interface connected to the implementation and will return to the application the proper object. To make it working, it’s enough to add the following attribute each concrete implementation of the class:
using System.Windows; using DependencySample.Services; using DependencySample.WinPhone.Services; [assembly: Xamarin.Forms.Dependency(typeof(PopupService))] namespace DependencySample.WinPhone.Services { public class PopupService: IPopupService { public void ShowPopup(string title, string message) { MessageBox.Show(message, title, MessageBoxButton.OK); } } }
The only variable part of the attribute is the parameter of the Dependency class: we need to specify the type of the current class (in our sample, it’s PopupService). Then, in our shared project, when we need to use the PopupService, we’re going to retrieve it using a Xamarin Forms class called DependencyService, like in the following sample:
private void OnButtonClicked(object sender, EventArgs e) { IPopupService popupService = DependencyService.Get<IPopupService>(); popupService.ShowPopup("Sample title", "Sample message"); }
We use the Get<T> method, where T is the interface of the class we want to use. Automatically, Xamarin Forms will analyze the registered DLLs in the project and will return the concrete implementation that is available in the platform’s specific project.
Wrapping up
In this post we’ve seen how to use the native dependency container that Xamarin Forms offers to developers. In the next post, we’ll see how to combine it with a traditional dependency injection approach, which comes useful when you’re developing a Xamarin Forms app using MVVM. You can download the sample project used in this post on