Blue Flower

В предыдущей части мы познакомились с расширениями привязки и разобрались, как их применять на практике, например, для локализации. Сегодня же продолжим изучать особенности библиотеки Aero Framework и рассмотрим довольно интересную тему об инжекции контекста данных в xaml-разметку представлений, а заодно применим познания из прошлой статьи.

На практике часто встречается следующая задача: связать вью-модель, которая хранится в unity-контейнере, с одним или несколькими её представлениями (экранами). Обычно такое связывание происходит в бехаинд-коде, в результате чего у представления устанавливается нужное значение в свойство DataContext.

Во многих случаях это работает хорошо, но с таким подходом сопряжены определённые нюансы и трудности. Например, они касаются контекстных меню и другой всплывающей анимации, поскольку она не входит в визуальное дерево, а следовательно, для неё становится недоступным основной контекст данных. Другой случай связан с работой списковых элементов, когда контекстом уже является элемент списка, но есть необходимость в использовании другого источника привязки. Третий вариант возникает, когда одно представление работает сразу с несколькими вью-моделями.

Все эти трудности так или иначе решаемы, но существует универсальный и очень простой способ их красиво разрешить. О нём и пойдет речь.


Для начала определимся с терминологией. Каждый элементарный визуальный контрол — это маленькое атомарное представление. Сложные комплексные представления строятся на основе простых, образуя древовидную структуру (визуальное дерево), где каждый узел также является представлением вплоть до корня. Будем различать особый род представлений — экраны, которые в том или ином виде поддерживают навигацию и зачастую являются корневыми.

Пускай имеется единое хранилище Store, из которого по ключу извлекается нужный экземпляр объекта. Идея состоит в том, чтобы с помощью расширения xaml-разметки обеспечить возможность извлечения произвольного экземпляра объекта и дальнейшее его инжектирование в качестве контекста данных в любой узел визуального дерева.

Выглядит всё очень просто:

<Control DataContext="{Store Key=viewModels:AppViewModel}"/>

<Control>
	<Control.DataContext>
		<Store Key=viewModels:AppViewModel>
	</Control.DataContext>
</Control>

Получить доступ к вью-моделям из C#-кода также крайне легко:

var appViewModel = Store.Get<AppViewModel>();
var userViewModel = Store.Get<IUserViewModel>();

Более того, WPF позволяет выполнить инжектирование даже в привязку!

<Slider
	DataContext="{Store Key=viewModels:MapViewModel}"
	Minimum="{Bindind MinimumZoomValue}"
	Maximum="{Binding MaximumZoomValue}"
	Value="{Binding ZoomValue, Mode=TwoWay}"
	Visibility="{Binding ShowSlider, Source="{Store Key=viewModels:SettingsViewModel}", Converter={StaticResource TrueToVisibleConverter}}"/>

Обратите внимание на строку Visibility="{Binding ShowSlider, Source="{Store Key=viewModels:SettingsViewModel}"..., такой гибкости тяжело достичь даже с помощью бехаин-кода, а наша запись получилась очень лаконичной.

Справедливости ради нужно сказать, что парсеры разметки на многих других платформах требуют префиксы, а также не поддерживают вложенных друг в друга расширений, но эта проблема решается элементарно с помощью расширения привязки (Binding Extension):

<Slider
	DataContext="{m:Store Key=viewModels:MapViewModel}"
	Minimum="{Bindind MinimumZoomValue}"
	Maximum="{Binding MaximumZoomValue}"
	Value="{Binding ZoomValue, Mode=TwoWay}"
	Visibility="{m:StoreBinding Path=ShowSlider, StoreKey=viewModels:SettingsViewModel, Converter={StaticResource TrueToVisibleConverter}}"/>

За деталями реализации отсылаю к исходным кодам библиотеки Aero Framework, там всё очень прозрачно и понятно. Ключом же обычно является тип вью-модели или тип интерфейса, который она реализует, но ничто не запрещает использовать любые другие.

То есть, чтобы связать экран приложения (страницу или окно) достаточно лишь нескольких строк:

<!--WP7, WP8, WPF-->
<Page
	xmlns:viewModels="clr-namespace:AeroPlayer.ViewModels"
	DataContext="{m:Store Key=viewModels:SongViewModel}">
	...
</Page>

<!--WPF-->
<Window
	xmlns:viewModels="clr-namespace:AeroPlayer.ViewModels"
	DataContext="{Store viewModels:SongViewModel}">
	...
</Window>

<!--Windows Store, WP8.1-->
<Page xmlns:viewModels="using:AeroPlayer.ViewModels">  
    <Page.DataContext>
		<Store Key=viewModels:AppViewModel>
	</Page.DataContext>
	...
</Page>

И никакого бехаинд-кода! С контекстными меню теперь всё очень изящно:

<ContextMenu DataContext="{Store viewModels:AppViewModel}">
	...
</ContextMenu>

Но как красиво решаются подобные ситуации:

<ListBox DataContext={Store viewModels:AppViewModel} ItemsSource={Binding Persons}>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding FirstName}"/>
                <TextBlock Text="{Binding LastName}"/>
                <TextBlock 
                	Text="{Binding Age}"
                	Visibility="{StoreBinding Path=ShowDetails, StoreKey=viewModels:SettingsViewModel, Converter={StaticResource TrueToVisibleConverter}}"/>
            </StackPanel>
        </DataTemplate>
    <ListBox.ItemTemplate>
</ListBox>

Надеюсь, что вам уже захотелось применить на деле рассмотренный поход. Это и есть реализация принципа прямых инжекций (Direct Injections Principle), который предложен в статье. Отметим, у одной вью-модели может быть несколько представлений, однако обратная ситуация, когда представлене работает сразу с несколькими вью-моделями, на пракике редкость из-за описаных выше технических сложностей. Но с помощью прямых инжекций отношение вью-модель-представление запросто расширяется с однин ко многим до многие ко многим.

Спасибо за интерес!