В предыдущей части мы познакомились с расширениями привязки и разобрались, как их применять на практике, например, для локализации. Сегодня же продолжим изучать особенности библиотеки 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), который предложен в статье. Отметим, у одной вью-модели может быть несколько представлений, однако обратная ситуация, когда представлене работает сразу с несколькими вью-моделями, на пракике редкость из-за описаных выше технических сложностей. Но с помощью прямых инжекций отношение вью-модель-представление запросто расширяется с однин ко многим до многие ко многим.
Спасибо за интерес!
Автор: Makeman