Свойство Tag в WinRT

63

Группа элементов управления RadioButton позволяет выбрать один из нескольких взаимоисключающих вариантов. С точки зрения программиста часто бывает удобно, чтобы каждый элемент RadioButton в конкретной группе соответствовал элементу перечисления, а значение перечислимого типа идентифицировалось объектом RadioButton. В этом случае все кнопки в группе могут совместно использовать один обработчик события.

Свойство Tag идеально подходит для этой цели. В нем можно сохранить любую информацию, предназначенную для идентификации элемента. Допустим, вы хотите написать программу для экспериментов со свойствами StrokeStartLineCap, StrokeEndLineCap и StrokeLineJoin, определяемыми классом Shape. При рисовании толстых линий эти свойства управляют формой концов и соединений сегментов. Свойствам StrokeStartLineCap и StrokeEndLineCap задаются значения из перечислимого типа PenLineCap, а свойству StrokeLineJoin - значения из перечислимого типа PenLineJoin.

Например, в перечисление PenLineJoin входит значение Bevel. Для представления этого варианта можно определить следующий элемент RadioButton:

<RadioButton Content="Коническая линия"
             Tag="Bevel" />

Недостаток такого решения заключается в том, что парсер XAML интерпретирует "Bevel" как строку, поэтому в обработчике события в файле отделенного кода приходится использовать конструкцию switch/case, чтобы различать разные строки, или преобразовывать строки в фактическое значение PenLineJoin.Bevel методом Enum.TryParse.

Свойство Tag лучше определить иначе: выделив его в элемент свойства и явно указав, что ему задается значение типа PenLineJoin:

<RadioButton Content="Коническая линия">
    <RadioButton.Tag>
        <PenLineJoin>Bevel</PenLineJoin>
    </RadioButton.Tag>
</RadioButton>

Конечно, решение получается немного громоздким, однако я использовал его в тестовом проекте. В файле XAML определяются три группы элементов управления RadioButton для трех свойств Shape. Каждая группа содержит три или четыре элемента управления, соответствующих членам перечисления:

<Page ...>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.Resources>
            <Style TargetType="RadioButton">
                <Setter Property="FontSize" Value="18" />
            </Style>
        </Grid.Resources>
        
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <StackPanel Name="startLineCapPanel"
                    Grid.Row="0" Grid.Column="0"
                    Margin="24">

            <RadioButton Content="Обрезанное начало прямой"
                         Checked="OnStartLineCapRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineCap>Flat</PenLineCap>
                </RadioButton.Tag>
            </RadioButton>

            <RadioButton Content="Округлое начало прямой"
                         Checked="OnStartLineCapRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineCap>Round</PenLineCap>
                </RadioButton.Tag>
            </RadioButton>

            <RadioButton Content="Квадратное начало прямой"
                         Checked="OnStartLineCapRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineCap>Square</PenLineCap>
                </RadioButton.Tag>
            </RadioButton>

            <RadioButton Content="Треугольное начало прямой"
                         Checked="OnStartLineCapRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineCap>Triangle</PenLineCap>
                </RadioButton.Tag>
            </RadioButton>
        </StackPanel>

        <StackPanel Name="endLineCapPanel"
                    Grid.Row="0" Grid.Column="2"
                    Margin="24">

            <RadioButton Content="Обрезанный конец прямой"
                         Checked="OnEndLineCapRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineCap>Flat</PenLineCap>
                </RadioButton.Tag>
            </RadioButton>

            <RadioButton Content="Округлый конец прямой"
                         Checked="OnEndLineCapRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineCap>Round</PenLineCap>
                </RadioButton.Tag>
            </RadioButton>

            <RadioButton Content="Квадратный конец прямой"
                         Checked="OnEndLineCapRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineCap>Square</PenLineCap>
                </RadioButton.Tag>
            </RadioButton>

            <RadioButton Content="Треугольный конец прямой"
                         Checked="OnEndLineCapRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineCap>Triangle</PenLineCap>
                </RadioButton.Tag>
            </RadioButton>
        </StackPanel>

        <StackPanel Name="lineJoinPanel"
                    Grid.Row="1" Grid.Column="1"
                    HorizontalAlignment="Center"
                    Margin="24">

            <RadioButton Content="Конический стык"
                         Checked="OnLineJoinRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineJoin>Bevel</PenLineJoin>
                </RadioButton.Tag>
            </RadioButton>

            <RadioButton Content="Прямой стык"
                         Checked="OnLineJoinRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineJoin>Miter</PenLineJoin>
                </RadioButton.Tag>
            </RadioButton>

            <RadioButton Content="Округлый стык"
                         Checked="OnLineJoinRadioButtonChecked">
                <RadioButton.Tag>
                    <PenLineJoin>Round</PenLineJoin>
                </RadioButton.Tag>
            </RadioButton>
        </StackPanel>

        <Polyline Name="polyline"
                  Grid.Row="0"
                  Grid.Column="1"
                  Points="0 0, 500 1000, 1000 0"
                  Stroke="{StaticResource ApplicationForegroundThemeBrush}"
                  StrokeThickness="100"
                  Stretch="Fill"
                  Margin="24" />
    </Grid>
</Page>

Каждая из трех групп RadioButton находится в собственной панели StackPanel, а все элементы управления в одной панели совместно используют один обработчик события Checked.

Разметка не устанавливает ни один элемент RadioButton в помеченном состоянии - за это отвечает обработчик события Loaded, определенный в конструкторе в файле отделенного кода. (Почему-то при выполнении инициализации в конструкторе вместо обработчика Loaded элемент RadioButton для способа соединения инициализируется, а два других - нет.)

В конце разметки определяется объект Polyline, ожидающий задания своих свойств StrokeStartLineCap, StrokeEndLineCap и StrokeLineJoin. Это происходит в следующих трех обработчиках событий Checked, которые также определяются в файле отделенного кода:

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            Loaded += (sender, args) =>
            {
                foreach (UIElement child in startLineCapPanel.Children)
                    (child as RadioButton).IsChecked =
                        (PenLineCap)(child as RadioButton).Tag == polyline.StrokeStartLineCap;

                foreach (UIElement child in endLineCapPanel.Children)
                    (child as RadioButton).IsChecked =
                        (PenLineCap)(child as RadioButton).Tag == polyline.StrokeEndLineCap;

                foreach (UIElement child in lineJoinPanel.Children)
                    (child as RadioButton).IsChecked =
                        (PenLineJoin)(child as RadioButton).Tag == polyline.StrokeLineJoin;
            };
        }

        private void OnStartLineCapRadioButtonChecked(object sender, RoutedEventArgs args)
        {
            polyline.StrokeStartLineCap = (PenLineCap)(sender as RadioButton).Tag;
        }

        private void OnEndLineCapRadioButtonChecked(object sender, RoutedEventArgs args)
        {
            polyline.StrokeEndLineCap = (PenLineCap)(sender as RadioButton).Tag;
        }

        private void OnLineJoinRadioButtonChecked(object sender, RoutedEventArgs args)
        {
            polyline.StrokeLineJoin = (PenLineJoin)(sender as RadioButton).Tag;
        }
    }
}

Обработчик Loaded перебирает элементы управления RadioButton во всех группах, задавая свойству IsChecked значение true, если значение Tag совпадает с соответствующим свойством Polyline. Вся дополнительная проверка RadioButton происходит под контролем пользователя. Обработчику события достаточно задать свойство объекта Polyline на основании свойства Tag установленного переключателя RadioButton. Результат выглядит так:

Использование свойства Tag для создания перечислений

Хотя разметка однозначно показывает, что свойству Tag задается элемент перечисления PenLineCap или PenLineJoin, парсер XAML фактически задает соответствующее ему целое число. Это целое число легко преобразуется в правильный элемент перечисления, но определенно не является им.

Чтобы устранить большую часть громоздкой разметки, достаточно определить пару простых пользовательских элементов управления. Наличие свойств зависимости у этих элементов управления не обязательно; достаточно обычного свойства .NET для тега, соответствующего конкретному типу.

Ниже показано, как работает эта схема. Следующие классы, производные от RadioButton, определяются специально для представления значений PenLineCap и PenLineJoin:

using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace WinRTTestApp
{
    public class LineCapRadioButton : RadioButton
    {
        public PenLineCap LineCapTag { set; get; }
    }

    public class LineJoinRadioButton : RadioButton
    {
        public PenLineJoin LineJoinTag { set; get; }
    }
}

Приведу маленький фрагмент XAML (группа трех элементов управления RadioButton), демонстрирующий, как обойтись без синтаксиса элементов свойств:

...

<StackPanel Name="startLineCapPanel"
            Grid.Row="0" Grid.Column="0"
            Margin="24">

     <local:LineCapRadioButton Content="Обрезанное начало прямой"
            LineCapTag="Flat"
            Checked="OnStartLineCapRadioButtonChecked" />

     <local:LineCapRadioButton Content="Округлое начало прямой"
            LineCapTag="Round"
            Checked="OnStartLineCapRadioButtonChecked" />

     <local:LineCapRadioButton Content="Квадратное начало прямой"
            LineCapTag="Square"
            Checked="OnStartLineCapRadioButtonChecked" />

     <local:LineCapRadioButton Content="Треугольное начало прямой"
            LineCapTag="Triangle"
            Checked="OnStartLineCapRadioButtonChecked" />
</StackPanel>

...

Обратите внимание: при вводе этой разметки IntelliSense правильно распознает свойства LineCapTag и LineJoinTag как относящиеся к перечислимому типу и дает возможность ввести один из элементов перечисления. Удобно, ничего не скажешь!

Переход на классы, производные от RadioButton, в основном влияет на файл XAML. Файл отделенного кода остается практически неизменным, если не считать сокращения преобразования типов:

public sealed partial class MainPage : Page
{
        public MainPage()
        {
            this.InitializeComponent();

            Loaded += (sender, args) =>
            {
                foreach (UIElement child in startLineCapPanel.Children)
                    (child as LineCapRadioButton).IsChecked =
                        (child as LineCapRadioButton).LineCapTag == polyline.StrokeStartLineCap;

                foreach (UIElement child in endLineCapPanel.Children)
                    (child as LineCapRadioButton).IsChecked =
                        (child as LineCapRadioButton).LineCapTag == polyline.StrokeEndLineCap;

                foreach (UIElement child in lineJoinPanel.Children)
                    (child as LineJoinRadioButton).IsChecked =
                        (child as LineJoinRadioButton).LineJoinTag == polyline.StrokeLineJoin;
            };
        }

        private void OnStartLineCapRadioButtonChecked(object sender, RoutedEventArgs args)
        {
            polyline.StrokeStartLineCap = (sender as LineCapRadioButton).LineCapTag;
        }

        private void OnEndLineCapRadioButtonChecked(object sender, RoutedEventArgs args)
        {
            polyline.StrokeEndLineCap = (sender as LineCapRadioButton).LineCapTag;
        }

        private void OnLineJoinRadioButtonChecked(object sender, RoutedEventArgs args)
        {
            polyline.StrokeLineJoin = (sender as LineJoinRadioButton).LineJoinTag;
        }
}
Пройди тесты
Лучший чат для C# программистов