- PVSM.RU - https://www.pvsm.ru -

Совершенствуем xaml: Bindable Converters, Switch Converter, Sets

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

Нам понадобятся некоторые знания из прошлых статей [1], в частности, понимание принципа прямых инжекций [2].
Совершенствуем xaml: Bindable Converters, Switch Converter, Sets - 1

Bindable Converters

Рано или поздно многие xaml-разработчики сталкивается с вопросом, возможно ли создать конвертер, поддерживающий привязку каких-либо параметров? Но даже если помимо реализации интерфейса IValueConverter, произвести наследование от класса DependencyObject и объявить в конвертере DependencyProperty, то привязка работать в большинстве случаев не станет, поскольку конвертер не является элементом визуального дерева! Конечно, возможно пойти ещё дальше и создать гибрид контрола-конвертера, незаметно помещаемого на представление, но такое экзотическое решения вряд ли можно назвать красивым, да и спектр его применения ограничен.

Но на выручку приходит принцип прямых инжекций, ведь ничто не мешает применить StoreBinding к Dependency Converter.

<BooleanConverter 
    x:Key="BindableConverter" 
    OnTrue="Value1" 
    OnFalse="Value2" 
    OnNull="{StoreBinding StoreKey=viewModels: SettingsViewModel, Path=AnyValue3}"/>

Всё гениальное просто!

Отметим, что таким образом нельзя привязать конвертер к элементу визуального дерева, даже если они находятся на одном представлении. Но и такая проблема решаема, например, с помощью Attached Property у контрола и создания соответствующего расширения привязки.

<ToggleButton a:SourceKey="MyToogleButton">

<BooleanConverter 
    x:Key="BindableConverter" 
    OnTrue="Value1" 
    OnFalse="Value2" 
    OnNull="{RemoteBinding SourceKey=MyToogleButton, Path=IsChecked}"/>

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

Switch Converter

Часто в больших проектах приходится создавать много различных однотипных классов-конвертеров, логика которых очень напоминает поведение операторов if-else и switch, например, для различных перечислений (Enums). Но на самом деле, в таких случаях достаточно ограничиться применением универсального Switch Converter:

<SwitchConverter Default="ResultValue0" x:Key="ValueConverter1">
    <Case Key="KeyValue1" Value="ResultValue1"/>
    <Case Key="KeyValue2" Value="ResultValue2"/>
</SwitchConverter>

Более того, свойства этого конвертера (в том числе конструкции Case) являются Dependency, то есть доступными для привязки с помощью StoreBinding! Кроме того, поддерживается Type Mode, когда ключом является не само значение объекта, а его тип:

<SwitchConverter TypeMode="True" Default="{StaticResource DefaultDataTemplate}" x:Key="InfoConverter">
    <Case Type="local:Person" Value="{StaticResource PersonDataTemplate}"/>
    <Case Type="local:PersonGroup" Value="{StaticResource PersonGroupDataTemplate}"/>
</SwitchConverter>

Получается, что такой конвертер запросто применим в качестве DataTemplateSelector даже там, где последний не поддерживается! Универсальность Switch Converter позволяет покрыть огромное число случаев, стоит только применить к нему немного фантазии.

<c:SwitchConverter Default="{StaticResource ControlTemplate0}" x:Key="TemplateSelectorConverter">
	<m:Case Key='Value1' Value="{StaticResource ControlTemplate1}"/>
	<m:Case Key='Value2' Value="{StaticResource ControlTemplate2}"/>
</c:SwitchConverter>

<ListBox ItemsSource="{Binding Items}">
	<ListBox.ItemTemplate>
		<DataTemplate>
			<ContentControl Template="{Binding DataType, Converter={StaticResource TemplateSelectorConverter}}"/>
		</DataTemplate>
	</ListBox.ItemTemplate>
</ListBox>

Global Resources

Раз уж зашёл разговор о конвертерах, то стоит рассказать, как лучше всего организовать работу с ними. Прежде всего самые распространённые нужно вынести в отдельный словарь ресурсов:

<!--AppConverters .xaml-->
<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <BooleanConverter x:Key="NullToTrueConverter" OnNull="True" OnNotNull="False"/>
    <BooleanConverter x:Key="NullToFalseConverter" OnNull="False" OnNotNull="True"/>
    <BooleanConverter x:Key="NullToVisibleConverter" OnNull="Visible" OnNotNull="Collapsed"/>
    <BooleanConverter x:Key="NullToCollapsedConverter" OnNull="Collapsed" OnNotNull="Visible"/>
    <BooleanConverter x:Key="TrueToFalseConverter" OnTrue="False" OnFalse="True" OnNull="True"/>
    <BooleanConverter x:Key="FalseToTrueConverter" OnTrue="False" OnFalse="True" OnNull="False"/>
    <BooleanConverter x:Key="TrueToVisibleConverter" OnTrue="Visible" OnFalse="Collapsed" OnNull="Collapsed"/>
    <BooleanConverter x:Key="TrueToCollapsedConverter" OnTrue="Collapsed" OnFalse="Visible" OnNull="Visible"/>
    <BooleanConverter x:Key="FalseToVisibleConverter" OnTrue="Collapsed" OnFalse="Visible" OnNull="Collapsed"/>
    <BooleanConverter x:Key="FalseToCollapsedConverter" OnTrue="Visible" OnFalse="Collapsed" OnNull="Visible"/>
    <EqualsConverter x:Key="EqualsToCollapsedConverter" OnEqual="Collapsed" OnNotEqual="Visible"/>
    <EqualsConverter x:Key="EqualsToVisibleConverter" OnEqual="Visible" OnNotEqual="Collapsed"/>
    <EqualsConverter x:Key="EqualsToFalseConverter" OnEqual="False" OnNotEqual="True"/>
    <EqualsConverter x:Key="EqualsToTrueConverter" OnEqual="True" OnNotEqual="False"/>
    <AnyConverter x:Key="AnyToCollapsedConverter" OnAny="Collapsed" OnNotAny="Vsible"/>
    <AnyConverter x:Key="AnyToVisibleConverter" OnAny="Visible" OnNotAny="Collapsed"/>
    <AnyConverter x:Key="AnyToFalseConverter" OnAny="False" OnNotAny="True"/>
    <AnyConverter x:Key="AnyToTrueConverter" OnAny="True" OnNotAny="False"/>

</ResourceDictionary>

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

<Application 
    x:Class="Sparrow.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Views/AppView.xaml">
    
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <!--<ResourceDictionary Source="AppConverters.xaml"/>-->
                <ResourceDictionary Source="AppStore.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
    
</Application>

Sets

Очень полезным приёмом является использование в xaml универсальной коллекции Set, применимой во множестве случаев. Она позволяет избежать «многоэтажных конструкций», вынести общие моменты и сделать разметку намного более аккуратной, а также передавать несколько аргументов в команду.

public class Set : ObservableCollection<object>
{
}
<Set x:Key="EditMenuSet" x:Shared="False">
    <MenuItem
        Header="{Localizing Undo}"
        Command="Undo"/>
    <MenuItem
        Header="{Localizing Redo}"
        Command="Redo"/>
    <Separator/>
    <MenuItem
        Header="{Localizing Cut}"
        Command="Cut"/>
    <MenuItem
        Header="{Localizing Copy}"
        Command="Copy"/>
    <MenuItem
        Header="{Localizing Paste}"
        Command="Paste"/>
    <MenuItem
        Header="{Localizing Delete}"
        Command="Delete"/>
    <Separator/>
    <MenuItem
        Header="{Localizing SelectAll}"
        Command="SelectAll"/>
</Set>

<MenuItem Header="{Localizing Edit}" ItemsSource="{StaticResource EditMenuSet}"/>
<Set x:Key="ParameterSet">
    <system:String>/Views/AnyView.xaml</system:String>
    <system:String>SecondParaneter</system:String>
</Set>

<Button 
    Content="{Localizing GoToAnyView}"
    Command="{Context Key=GoTo}"
    CommandParameter="{StaticResource ParameterSet}">

Благодарю за внимание!

Обращение автора

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

Глупо скрывать эти знания или брать за них плату, поскольку даже воспринять и осознать их оказывается не так просто. Возможно, через некоторое время кто-то поймёт, как много ресурсов помогли сохранить эти простые по своей форме советы, и захочет поблагодарить создателя библиотеки Aero Framework, потому реквизиты для пожертвований доступны на этой странице [3]. Быть может, некоторые даже пожелают ещё более углубиться в изучение и приобрести исходные коды реальных проектов [4] (конечно, для индивидуальных разработчиков их стоимость может показаться ощутимой, но для компаний это символическая плата).

Поэтому, даже если вы пользуетесь библиотекой бесплатно, то, пожалуйста, рекомендуйте её своим коллегам, друзья и всем тем, кто в теме! Спасибо!

Автор: Makeman

Источник [5]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-2/87747

Ссылки в тексте:

[1] прошлых статей: http://habrahabr.ru/users/makeman/topics/

[2] принципа прямых инжекций: http://habrahabr.ru/post/254373/

[3] этой странице: http://makeloft.by/ru/tools

[4] исходные коды реальных проектов: http://makeloft.by/ru/works

[5] Источник: http://habrahabr.ru/post/254731/