Pull to refresh

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

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

Нам понадобятся некоторые знания из прошлых статей, в частности, понимание принципа прямых инжекций.


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

Поэтому, даже если вы пользуетесь библиотекой бесплатно, то, пожалуйста, рекомендуйте её своим коллегам, друзья и всем тем, кто в теме! Спасибо!
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+14
Comments 0
Comments Leave a comment

Articles