Инжекторы контекста xaml

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

    Спасибо за интерес!
    Метки:
    Поделиться публикацией
    Комментарии 23
    • +1
      Имхо, глобально доступные экземпляры вьюмоделей — это неудачная архитектура. Основные проблемы — подходит только для постоянно живущих вьюмоделей, увеличивает связность кода, усложняет тестирование. По-хорошему, DataContext для окна должна задавать вызывающая сторона, а для проброса контекста вне визуального дерева есть Binding Proxy.
      • +1
        Ничто не запрещает контролировать время жизни вью-моделей, если это нужно. Более того, применияя подход с сериализацией, который описан в этой статье, можно выгружать и загружать вью-модели, сохраняя их состояние в произвольный момент (смотрите пример проекта к библиотеке).

        Увеличения связности кода на практике не происходит никакого, скорее даже наоборот — она уменьшается, ведь не нужно использовать никаких дополнительных регистрации и код-бехаин логики. Насчёт тестирования то же самое.

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

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

            Ну, например, у меня в приложении открыто 3 окна: 2 с одним инстансом вьюмодели, и одно с другим. Как мне этим управлять?
            • 0
              Думаю, если вы хорошо подумаете над этим или другими каверзными вопросами, то и сами сможете на них ответить :)

              Нет никакой проблемы в том, что вью-модель используется на нескольких представлениях сразу. Просто удалите её из unity-контейнера в нужный вам момент, например, при закрытии окон. Или опишите подробнее тот сценарий и трудности, которые не позволяют этого сделать…
              • +1
                > Нет никакой проблемы в том, что вью-модель используется на нескольких представлениях сразу.

                Конечно нет, есть проблема, что две вьюмодели используются на нескольких представлениях сразу.

                > Но как красиво решаются подобные ситуации:
                > <ListBox DataContext={Store viewModels:AppViewModel} ItemsSource={Binding Persons}>

                Ну вот откройте 2 окна с этим кодом, сделайте 2 viewModels:SettingsViewModel (с ShowDetails = true и false) и каждую секунду добавляйте по элементу в Persons.
                Ничего хорошего не получится

                > Думаю, если вы хорошо подумаете над этим или другими каверзными вопросами, то и сами сможете на них ответить :)

                Конечно смогу, мой ответ — вы переизобрели ServiceLocator со всеми его недостатками, как то «The registry must be unique, which can make it a bottleneck for concurrent applications.»

                И мой ответ на «как мне этим управлять» — без ServiceLocator, статического Storage и, видимо, без Aero Framework.

                Но мне то ваш ответ любопытен.
                • 0
                  Начнём с того, что код библиотеки полностью открыт и ничто не мешает вместо Service Locator использовать любой другой шаблон проектирования на ваш выбор.

                  Большинство современных unity-контейнеров реализуют Service Locator, но значимость состоит не в том, что Aero Framework предлагает его использовать по умолчанию, а в том, что в библиотеке введён принцип прямых инжекций, когда инжектировать вью-модель можно точечно в любое место визуального дерева.

                  Что касается вашего примера, то в статье предпологается, что в SettingsViewModel хранятся глобальные параметры для пользователя, как обычно и бывает в приложениях, но запросто можно использовать и динамически создаваемые вью-модели. Взгляните на пример к библиотеке: AppViweModel статическая, а TextViewModel нет, — и всё прекрасно работает.

                  Довелось написать немало завершённых приложений и никаких ощутимых проблем со всем этим не было ни разу. Решайте сами, использовать библиотеку или нет в вашей работе. На мой взгляд, основная её ценность в простоте и лаконичности получаемого кода, чего не хватало многим проектам, которые попадались автору на практике и использовали другие MVVM-фреймворки.
                  • +1
                    > Большинство современных unity-контейнеров

                    Хорошее начало

                    > принцип прямых инжекций

                    Называйте это лучше «реверсия контроля». Слово «инжекций» у меня красненьким подчеркивает.

                    > инжектировать вью-модель можно точечно в любое место визуального дерева.

                    Ну вот осталось понять как вы это без сервис локатора(у вас это Storage, напомню) предлагаете делать?

                    > Что касается вашего примера, то в статье предпологается, что в SettingsViewModel хранятся глобальные параметры для пользователя

                    Что касается моего примера, то он отрицает то, что предполагается в вашей статье.

                    > Взгляните на пример к библиотеке: AppViweModel статическая, а TextViewModel нет, — и всё прекрасно работает.

                    Я бы посмотрел, но у вас ссылка куда-то пропала. Подозреваю, что TextViewModel создаётся при каждом обращении новый, тысячами и миллионами. Мне столько не надо, мне надо 2.

                    > Довелось написать немало завершённых приложений и никаких ощутимых проблем со всем этим не было ни разу.

                    Я тут ваш блокнотик запустил. Он на голом скролинге тысячи строчек выжрал 50 мб памяти за минуту.
                    Нечем тут хвастаться.
                    • 0
                      Спасибо, что сказали о нерабочей ссылке, иногда с One Drive бывают какие-то проблемы.
                      Попробуйте ещё раз (Aero Framework).

                      Ну вот осталось понять как вы это без сервис локатора(у вас это Storage, напомню) предлагаете делать?


                      Элементарно, в расширении разметки вы указываете произвольный ключ, а затем по нему достаёте вью-модель из контейнера или создаёте новую по определённым правилам, как вам нужно.

                      Я тут ваш блокнотик запустил. Он на голом скролинге тысячи строчек выжрал 50 мб памяти за минуту.
                      Нечем тут хвастаться.


                      Для современных систем это адекватная цифра, тем более для .NET приложения, где сборщик мусора уже изначально резервирует некоторое количество памяти у системы. Более того, совершенно пустое WPF приложение уже занимает в памяти около 7-10 Мб, поэтому 50 Мб для полноценного редактора хороший результат, и, стоит признать, ваша критика здесь всё-таки не конструктивна. Да и применение библиотеки далеко не ограничивается одним лишь редактором.
                      • 0
                        > Элементарно, в расширении разметки вы указываете произвольный ключ

                        И как мне в 2-х инстансах одной вьюмодели указать произвольный ключ?

                        > Более того, совершенно пустое WPF приложение уже занимает в памяти около 7-10 Мб, поэтому 50 Мб

                        50 при запуске, ещё 50 через минуту.

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

                          Во-вторых, если вы так смело называете приложение студенческой поделкой, то откройте ваш файлик в редакторе Visual Studio и сравните результаты. Это будет более объективно. Или редактор Visual Studio, по-вашему, тоже относится к разряду студенческих поделок?

                          В-третьих, очевидно, что в качестве уникального ключа можно использовать не только типы вью-моделей, но и строковые значения, например.
                          • 0
                            > то откройте ваш файлик в редакторе Visual Studio и сравните результаты.Это будет более объективно.

                            Я могу открыть файлик в notepad и «более объективно» сравнить результаты. Или вы хотите сказать, что у вас полный функционал студии реализован?

                            если интересно, notepad++ при скроле отъел 30 килобайт. С проверкой орфографии.
                            • 0
                              Вы чувствуете вообще разницу между native и managed кодом?

                              Да, в техническом плане функционал редактирования и просмотра текста аналогичен тому, что в Visual Studio и реализован грамотно, поэтому сравнение будет справедливым. Извините, но ваши суждения обнажают ваше дилетанство в этой области, ничего личного, просто как .NET-разработчик, имеющий немалый опыт, знаю, что говорю.

                              Пользоваться редактором или нет вы решаете сами. Это же касается и фреймворка. Не знаю, что вас так зацепило, и откуда столько негатива в адрес автора статьи, когда в ней он делится действительно полезной информацией с другими людьми.
                              • 0
                                > Вы чувствуете вообще разницу между native и managed кодом?

                                Если вы думаете, что я не видел как native выжирает сотню-другую мегабайт — вы заблуждаетесь.

                                > в техническом плане функционал редактирования и просмотра текста аналогичен

                                ох… AvalonDock не достаточно взять, что бы называться аналогом студии.
                                Ну раз уж назвались груздем… И где же у вас пипка над скролом, которая делит экран на 2?

                                > когда в ней он делится действительно полезной информацией с другими людьми.

                                С каких пор антипаттерны стали «действительно полезной информацией»?
                                Да и вообще, раз уж мы в третьем лице говорим, автору стоит оставить другим людям судить на сколько информация полезна или вредна.
                                • 0
                                  ох… AvalonDock не достаточно взять, что бы называться аналогом студии.
                                  Ну раз уж назвались груздем… И где же у вас пипка над скролом, которая делит экран на 2?

                                  Поищите её под скроллом… И делит она экран на 2, на 3 и так далее, чего даже нет в VS. Съели?))

                                  Да и вообще, раз уж мы в третьем лице говорим, автору стоит оставить другим людям судить на сколько информация полезна или вредна.

                                  Так каждый за себя и решит. А вы не отвечайте за всех.
                                  • 0
                                    > Съели?))

                                    То есть вам мало убогого AvalonDock, вы решили за достижение выдать, что вы drag&drop не осилили? Не мне вам вассказывать, что сделать 3

                                    Если бы я всерьёз хотел поговорить, то я бы, например, с хоткеев начал. Но ваша детская радость, что оно со студией сравнимо, умиляет.

                                    Впрочем, памяти оно теперь выжрало больше студии. Так что я пойду…
                                    • 0
                                      Вы поражаете своей невнимаельностью и поверхностностью. Drag&Drop превосходно работает во всех отношениях, да и все базовые хоткеи есть.

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

                                      Впрочем, памяти оно теперь выжрало больше студии. Так что я пойду…

                                      Боюсь только, что студия заняла далеко не 30 Кб. Но вы смело можете отправляться гордиться вашей «победой»:) Успехов!

                                      P.S. Извиняйте, если где-то был резок с вами, просто вы так упорно всё критиковали, что это нельзя было оставить без внимания.
                                      • 0
                                        > Drag&Drop превосходно работает во всех отношениях

                                        На той кнопке под скролом — нет.

                                        > да и все базовые хоткеи есть.

                                        Ctrl-С/Ctrl-V? Понимаю.

                                        Я не знаю что для вас «обработка текста», от VS только функцию обработки текста я запустить не могу. А вы едва ли на десятую(а то и сотую) часть этого функционала тратите ресурсов компа больше и называете это «реализован грамотно».

                                        Справедливости ради, я не «всё критиковал», я другие статьи полистал, там, кроме выдумывания собственных велосипедов (Selfish Bike, ага), ничего особенно плохого.
                                        • 0
                                          На той кнопке под скролом — нет.
                                          Ох, вы так придирчивы, но не учитываете даже, что логика-то работы разная… И драг-н-дроп, где это нужно, работает там на ура.

                                          Ctrl-С/Ctrl-V? Понимаю.
                                          Ну, если это все комбинации, что вы знаете, то рекомендую изучить и другие.

                                          И если на вашем компьютере не хватает ресурсов, чтобы комфортно работать с редактором, то советую задуматься над модернизацией машины. У множества других людей всё превосходно работает.

                                          Справедливости ради, я не «всё критиковал», я другие статьи полистал, там, кроме выдумывания собственных велосипедов (Selfish Bike, ага), ничего особенно плохого.
                                          Хорошо, что полистали. Пользуйтесь велосипедами на здоровье. Или не пользуйтесь из принципа, ваше личное дело.
                                          • 0
                                            > не учитываете даже, что логика-то работы разная…

                                            Конечно разная, ваша без d&d.

                                            > Ну, если это все комбинации, что вы знаете, то рекомендую изучить и другие.

                                            Спасибо. А вы их реализуете? Списочек из студии сами найдёте?

                                            > И если на вашем компьютере не хватает ресурсов, чтобы комфортно работать с редактором, то советую задуматься над модернизацией машины.

                                            //удаляя остатки «редактора» с компа
                                            Да-да, обязательно подумаю.
                                            • 0
                                              Всё, что нужно, давно реализовано. Не нравится — не пользуйтесь.
                          • 0
                            Для чистоты эксперимента отключите также в редакторе функции проверки орфографии и автодополнения слов, поскольку они требовательны к памяти, после чего уже можно конструктивно критиковать приложение, сравненивая с аналогичными решениями в техническом плане.
          • 0
            Стоит также упомянуть о том, что зачастую после загрузки представления возникает необходимость в обновлении каких-либо данных. Чтобы обойтись без бехаинд-кода в библиотеке Aero Framework предусмотрен механизм контекстных триггеров команд.

            <View DataContext="{Store Key=viewModels:ProductsViewModel}">
                <Attacher.ContextTriggers>
                    <ContextTrigger 
                        EventName="Loaded" 
                        UseEventArgsAsCommandParameter="False"
                        Command="{Context Key=Refresh, StoreKey=viewModels:ProductsViewModel}"/>
                </Attacher.ContextTriggers>
            ...
            </View>
            

            public class ProductsViewModel : ContextObject, IExposable
            {
                public Product CurrentProduct
                {
                    get { return Get(() => CurrentProduct); }
                    set { Set(() => CurrentProduct, value); }
                }
            
                public ContextSet<Product> Products { get; set; }
            
                public virtual void Expose()
                {
                    Products = new ContextSet<Product>();
                    
                    this[() => CurrentProduct].PropertyChanged += (sender, args) => Context.Get("GoToProduct").RaiseCanExecuteChanged();
                    
                    this[Context.Get("GoToProduct")].CanExecute += (sender, args) => args.CanExecute = CurrentProduct != null;
                    this[Context.Get("GoToProduct")].Executed += (sender, args) => Navigator.GoTo(args.Parameter);
                    this[Context.Refresh].Executed += async (sender, args) =>
                    {
                        try
                        {
                            var products = await Bank.Current.GetProducts();
                            CurrentProduct = null;
                            Products.Clean();
                            products.ForEach(p => Products.Add);
                        }
                        catch (Exception exception)
                        {
                            Error = Unity.App.Localize(exception.Message);
                        }
                    };
                }
            }
            


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

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.