Главная страница   /   7.4. Построение WPF приложений (Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Марк Симан

7.4. Построение WPF приложений

Если вы думали, что создавать WCF сервис было сложно (как думал и я), то вы оцените, что создавать Windows Presentation Foundation (WPF) приложение почти настолько же просто, как и консольное приложение.

Точка входа в WPF приложение является довольно очевидной и не сложной, и, несмотря на то, что она не предоставляет швов, которые явно предназначены для разрешения DI, мы можем с легкостью сформировать приложение любым предпочитаемым нами способом.

WPF композиция

Точка входа в WPF приложение определяется в его классе App. Как и большинство других классов WPF, этот класс разбит на два файла: App.xaml и App.xaml.cs. Мы можем определить, что происходит на стадии начальной загрузки в обоих файлах в зависимости от наших потребностей.

При создании нового WPF проекта в Visual Studio файл App.xaml определяет атрибут StartupUri, который устанавливает, какое окно демонстрируется при запуске приложения – в данном примере Window1:

<Application x:Class="MyWpfApplication.App"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	StartupUri="Window1.xaml">
</Application>

Смысл данного стиля объявления заключается в том, что объект Window1 создается и демонстрируется без какого-либо дополнительного контекста. Когда вы хотите добавить зависимости к окну, наиболее подходящим может стать более явный подход. Вы можете удалить атрибут StartupUri и присоединить окно с помощью переопределения метода OnStartup. Это позволяет вам полностью присоединить первое окно до того, как оно будет продемонстрировано, но вам придется за это заплатить: вы должны не забыть явно вызвать метод Show для окна.

Метод OnStartup, таким образом, становится Composition Root приложения. Вы можете использовать DI-контейнер или Poor Man's DI для создания окна. В следующем примере используется Poor Man's DI для того, чтобы проиллюстрировать, что вам не приходится полагаться на возможности какого-либо конкретного DI-контейнера.

Пример: присоединение ценного клиента управления товарами

В предыдущем примере разрабатывался веб-сервис, который мы можем использовать для управления каталогом товаров в шаблонном приложении Commerce. В данном примере мы создадим WPF приложение, которое использует этот веб-сервис для управления товарами. Рисунок 7-10 демонстрирует скриншот этого приложения.

Рисунок 7-10: Главное окно приложения Product Management – это список товаров. Вы можете добавлять новые товары, редактировать существующие или удалять их. При добавлении или редактировании товаров используется модальное окно редактирования. Все операции реализуются посредством вызова соответствующей операции для веб-сервиса управления товарами из раздела 7.3.2.

Приложение реализует Model View ViewModel (MVVM) подход и содержит три уровня, которые продемонстрированы на рисунке 7-11. Обычно мы держим ту составляющую, в которой находится большая часть логики, изолированно от других модулей – в данном примере PresentationLogic.ProductManagementClient – это humble-исполнитель (humble executable), который выполняет несколько большую работу, нежели просто определяет пользовательский интерфейс и делегирует реализацию другим модулям.

Рисунок 7-11: Приложение состоит из трех отдельных блоков. Блок ProductManagementClient – исполняемый, и включает в себя пользовательский интерфейс, реализованный в XAML без выделенного кода (code-behind). Библиотека PresentationLogic содержит ViewModels и опорные классы, а библиотека ProductWcfAgent включает в себя Adapter между пользовательской абстракцией IProductManagementAgent и конкретным WCF прокси, который используется для взаимодействия с веб-сервисом управления товарами. Стрелки-указатели зависимостей означают, что ProductManagementClient выступает в роли Composition Root, поскольку он соединяет вместе остальные модули.

Благодаря подходу MVVM мы передаем ViewModel в свойство DataContext главного окна, а механизм связывания данных и движок шаблонизации данных заботятся о корректном представлении данных, как только мы вплетаем новые ViewModels или изменяем данные существующих ViewModels.

MVVM

Model View ViewModel (MVVM) – это паттерн проектирования, для которого отлично подходит WPF. Он разделяет код пользовательского интерфейса на три отдельных ответственности.

Model – это основополагающая модель приложения. Ею часто, но не всегда, является доменная модель. Она часто состоит из POCO-объектов. В текущем примере доменная модель реализуется в веб-сервисе, поэтому на данном уровне у вас нет истинной доменной модели. Тем не менее, приложение функционирует с абстракциями, находящимися поверх прокси веб-сервиса, и это и есть ваша Model. Обратите внимание на то, что Model обычно выражается UI-нейтральным способом. Это не предполагает, что Model будет раскрыта напрямую пользовательским интерфейсом, поэтому она не раскрывает никакой WPF-специфичной функциональности.

View – это рассматриваемый нами пользовательский интерфейс. В WPF мы можем официально выразить View в XAML и использовать механизм связывания данных и движок шаблонизации данных для представления данных. Можно выразить Views без использования выделенного кода.

ViewModel – мост между View и Model. Каждый ViewModel – это класс, который преобразовывает и раскрывает Model конкретным специфическим способом. В WPF это означает, что ViewModel может раскрывать списки как ObservableCollections, и тому подобное.

Внедрение зависимостей в главный ViewModel

MainWindow содержит только XAML разметку и не содержит никакого пользовательского выделенного кода. Зато он использует механизм связывания данных для отображения данных на экране и управления пользовательскими командами. Для того чтобы это позволить мы должны передать MainWindowViewModel в его свойство DataContext.

MainWindowViewModel раскрывает такие данные, как список товаров, а также команды создания, обновления или удаления товара. Возможность такой функциональности зависит от сервиса, который обеспечивает доступ к каталогу товаров: абстракция IProductManagementAgent.

Помимо IProductManagementAgent для MainWindowViewModel также необходим сервис, который он может использовать для того, чтобы контролировать его оконную среду (windowing environment), например, демонстрацию модальных диалоговых окон. Эта зависимость называется IWindow.

MainWindowViewModel использует паттерн Constructor Injection со следующей сигнатурой конструктора:

public MainWindowViewModel(IProductManagementAgent agent, IWindow window)

Для соединения приложения мы должны создать MainWindowViewModel и передать его в свойство DataContext экземпляра MainWindow.

Соединение MainWindow и MainWindowViewModel

Данному примеру придает остроты тот факт, что для корректной реализации IWindow, вам нужен указатель на реальное WPF окно (MainWindow); но для ViewModel необходим IWindow, а свойство DataContext экземпляра MainWindow должно быть ViewModel. Другими словами, вы получаете циклическую зависимость.

В главе 6 мы имели дело с циклическими зависимостями и прошлись по соответствующей части этого конкретного примера, поэтому я не буду повторять это в данной главе. Достаточно сказать, что вводится MainWindowViewModelFactory, который является ответственным за создание экземпляров MainWindowViewModel.

Вы используете эту фабрику в рамках реализации IWindow под названием MainWindowAdapter для того, чтобы создать MainWindowViewModel и передать его в свойство DataContext экземпляра MainWindow:

var vm = this.vmFactory.Create(this);
this.WpfWindow.DataContext = vm;

Переменная члена vmFactory – это экземпляр IMainWindowViewModelFactory, и вы передаете в его метод Create экземпляр содержащегося класса, который реализует IWindow. Итоговый экземпляр ViewModel затем передается в DataContext WpfWindow, который является экземпляром MainWindow.

Примечание

Я не рассматриваю детали, поскольку мы рассматривали их в главе 6. Вернитесь к этой главе и прочтите раздел о циклических зависимостях, если вам нужно освежить в памяти то, что сейчас происходит.

Подсказка

Для механизма связывания WPF данных необходимо, чтобы мы передавали зависимость (ViewModel) в свойство DataContext. По моему мнению, это неправильное использование Property Injection, поскольку это сигнализирует о том, что зависимость является необязательной, а это абсолютно не так. Тем не менее, WPF 4 вводит нечто, имеющее название XamlSchemaContext, который может использоваться в качестве шва, который, в свою очередь, дает нам большую гибкость в тех ситуациях, когда дело доходит до создания экземпляров Views на основании разметки.

Рисунок 7-12 демонстрирует окончательную диаграмму зависимостей приложения.

Рисунок 7-12: Диаграмма зависимостей MainWindowAdapter, который является основным объектом приложения. MainWindowAdapter использует MainWindowViewModelFactory для создания соответствующего ViewModel и передачи его в MainWindow. Для того чтобы создать MainWindowViewModel фабрике нужно передать WcfProductManagementAgent во ViewModel. Этот "посредник" является адаптером между IProductManagementAgent и WCF прокси. Он требует, чтобы ProductChannelFactory создал экземпляры WCF прокси, а также IClientContractMapper, который может выполнять преобразование между ViewModels и WCF контрактами данных.

Теперь, когда вы идентифицировали все строительные блоки приложения, вы можете скомпоновать их. Для того чтобы сохранить Poor Man's DI код симметричным, используя при этом DI-контейнер, я реализовал это в виде Resolve метода специализированного класса контейнера. В следующем листинге продемонстрирована реализация.

Листинг 7-10: Композиция главного окна
public IWindow ResolveWindow()
{
	IProductChannelFactory channelFactory =
		new ProductChannelFactory();
	IClientContractMapper mapper =
		new ClientContractMapper();
	IProductManagementAgent agent =
		new WcfProductManagementAgent(
			channelFactory, mapper);
	IMainWindowViewModelFactory vmFactory =
		new MainWindowViewModelFactory(agent);
	Window mainWindow = new MainWindow();
	IWindow w =
		new MainWindowAdapter(mainWindow, vmFactory);
	return w;
}

В конечном итоге вы возвращаете экземпляр IWindow, реализованный MainWindowAdapter, а для этого вам нужны WPF Window и IMainWindowViewModelFactory. Первым окном, которое вы должны продемонстрировать пользователям, должно быть MainWindow, поэтому именно его вы и передаете в MainWindowAdapter.

MainWindowViewModelFactory использует паттерн Constructor Injection для запроса IProductManagementAgent, поэтому вы должны скомпоновать WcfProductManagementAgent с двумя его зависимостями.

Окончательный MainWindowAdapter, возвращаемый из метода, обертывает MainWindow, поэтому, когда мы вызываем метод Show, он делегирует полномочия методу Show MainWindow. Это именно то, что вы и будете делать в Composition Root.

Реализация Composition Root

Теперь, когда вы знаете, как подключить приложение, вам нужно всего лишь сделать это в правильном месте. Как описывалось в предыдущем разделе, вам для начала нужно открыть App.xaml и удалить атрибут StartupUri, поскольку вы хотите самостоятельно явным образом компоновать начальное окно запуска.

После того, как вы это сделали, вам нужно только переопределить метод OnStartup в App.xaml.cs и вызвать контейнер.

Листинг 7-11: Реализация WPF Composition Root
protected override void OnStartup(StartupEventArgs e)
{
	base.OnStartup(e);
	var container =
		new ProductManagementClientContainer();
	container.ResolveWindow().Show();
}

В данном примере вы используете специализированный ProductManagementClientContainer, но вы также могли использовать и универсальный DI-контейнер, например, Unity или StructureMap. Вы просите контейнер преобразовать экземпляр IWindow, а затем вызвать его метод Show. Возвращаемый экземпляр IWindow – это MainWindowAdapter, когда вы вызываете его метод Show, он вызывает метод Show инкапсулированного MainWindow, который становится причиной того, что желаемое окно демонстрируется пользователю.

WPF предлагает простое место для Composition Root. Все, что вам нужно сделать – удалить StartupUri из App.xaml, переопределить OnStartup в App.xaml.cs и скомпоновать там приложение.

До настоящего момента вы встречались с примерами, в которых фреймворки предоставляют швы, позволяющие нам перенять жизненный цикл экземпляров ключевых объектов (веб-страниц, экземпляров сервисов, окна и других). Во многих случаях это довольно просто; но даже когда это настолько усложняется, как например, в WCF, мы все еще можем достичь цели и реализовать подлинный механизм внедрения зависимостей, не подвергая при этом риску наши принципы.

Некоторые фреймворки, тем не менее, не предоставляют нам такой роскоши.