Определение свойств зависимости в WinRT

170

Допустим, вы пишете приложение, в котором все элементы управления Button должны выводить текст с применением градиентной кисти. Конечно, можно просто задать свойству Foreground каждого элемента управления Button объект LinearGradientBrush, но разметка станет довольно громоздкой. Можно попробовать использовать стиль со свойством Foreground, содержащим LinearGradientBrush, но тогда для всех Button будет использоваться один объект LinearGradientBrush с одинаковыми градиентными цветами; вероятно, вам такое решение покажется недостаточно гибким.

По сути нам нужен элемент управления Button с двумя свойствами Color1 и Color2, которым задаются цвета градиентной заливки. Похоже, нужно создать нестандартный элемент управления: класс, производный от Button, который создает LinearGradientBrush в своем конструкторе и определяет свойства Color1 и Color2 для управления градиентом.

Могут ли свойства Color1 и Color2 быть простыми свойствами .NET с методами доступа set и get? Да, могут, однако такое определение свойств создаст ряд серьезных ограничений. Такие свойства не могут быть приемниками для применения привязки, стилей или анимации; на это способны только свойства зависимости.

Свойства зависимости (dependency proprties) несут чуть больше «балласта», чем обычные свойства, но каждый разработчик должен уметь определять их в своих классах. Начните с добавления в проект нового компонента Class. Присвойте ему имя GradientButton, а в файле объявите класс открытым и производным от Button:

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

namespace WinRTTestApp
{
    public class GradientButton : Button
    {
    }
}

Перейдем к заполнению этого класса. Два новых свойства с именами Color1 и Color2 относятся к типу Color. Для этих двух свойств необходимо создать два свойства зависимости типа DependencyProperty с именами Color1Property и Color2Property. Эти свойства должны быть открытыми и статическими, но их значения могут задаваться только в самом классе:

public static DependencyProperty Color1Property { private set; get; }
public static DependencyProperty Color2Property { private set; get; }

Объекты DependencyProperty могут создаваться в статическом конструкторе. Класс DependencyProperty определяет для создания этих объектов DependencyProperty статический метод с именем Register():

static GradientButton()
{
    Color1Property =
        DependencyProperty.Register("Color1",
            typeof(Color),
            typeof(GradientButton),
            new PropertyMetadata(Colors.White, OnColorChanged));
            
    Color2Property =
        DependencyProperty.Register("Color2",
            typeof(Color),
            typeof(GradientButton),
            new PropertyMetadata(Colors.Black, OnColorChanged));
}

Для создания вложенных свойств используется другой статический метод с именем DependencyProperty.RegisterAttached().

В первом аргументе Register() передается имя свойства, иногда оно используется парсерами XAML. Второй аргумент определяет тип свойства. Третий аргумент определяет тип класса, регистрирующего свойство зависимости.

Четвертый аргумент содержит объект типа PropertyMetadata. Конструктор существует в двух вариантах: в одном достаточно указать значение по умолчанию для свойства, а во втором также указывается метод, который должен вызываться при изменении свойства. Метод не будет вызываться, если новое значение совпадает с текущим.

Значение по умолчанию, передаваемое в первом аргументе конструктора PropertyMetadata, должно соответствовать типу, указанному во втором аргументе; в противном случае будет выдано исключение времени выполнения. Обеспечить выполнение этого требования не так просто, как может показаться. Например, программисты очень часто задают для свойств типа double значение но умолчанию 0. Во время компиляции 0 интерпретируется как целочисленное значение, поэтому во время выполнения обнаруживается несовпадение типов, и выдается исключение. Если вы определяете свойство зависимости типа double, укажите для него значение но умолчанию 0.0, чтобы компилятор знал правильный тип данных этого аргумента.

Другое возможное решение - определение объектов DependencyProperty в виде приватных статических полей и их возвращение открытыми статическими свойствами:

static readonly DependencyProperty color1Property =
        DependencyProperty.Register("Color1",
            typeof(Color),
            typeof(GradientButton),
            new PropertyMetadata(Colors.White, OnColorChanged));

static readonly DependencyProperty color2Property =
        DependencyProperty.Register("Color2",
            typeof(Color),
            typeof(GradientButton),
            new PropertyMetadata(Colors.Black, OnColorChanged));

public static DependencyProperty Color1Property
{
    get
    {
        return color1Property;
    }
}

public static DependencyProperty Color2Property
{
    get
    {
        return color2Property;
    }
}

Присутствие явного статического конструктора не является обязательным. Также возможно решение в духе WPF или Silverlight, где открытые статические свойства вообще отсутствуют, а статические поля просто определяются как открытые. Такое решение работает в Windows 8, но я обычно им не пользуюсь, потому что все открытые статические объекты DependencyProperty, определяемые стандартными элементами управления Windows Runtime, представляют собой свойства, а не поля.

Независимо от способа определения открытых статических объектов DependencyProperty классу GradientButton также необходимы определения «обычных» свойств .NET Color1 и Color2. Эти свойства всегда определяются в конкретной форме:

public Color Color1
{
    set { SetValue(Color1Property, value); }
    get { return (Color)GetValue(Color1Property); }
}

public Color Color2
{
    set { SetValue(Color2Property, value); }
    get { return (Color)GetValue(Color2Property); }
}

Метод доступа set всегда вызывает метод SetValue() (унаследованный от класса DependencyObject) со ссылкой на объект свойства зависимости, а метод get всегда вызывает GetValue() и преобразует возвращаемое значение к типу свойства. Если вы не хотите, чтобы свойство могло задаваться за пределами класса, объявите метод set защищенным или закрытым.

В нашем элементе управления GradientButton свойство Foreground должно содержать LinearGradientBrush, а свойства Color1 и Color2 должны задавать цвета двух объектов GradientStop, которые определяются в форме полей. Добавьте следующие два поля в класс:

GradientStop gradientStop1, gradientStop2;

Обычный конструктор экземпляров класса создает эти объекты, а также объект LinearGradientBrush, чтобы задать его свойству Foreground:

public GradientButton()
{
    gradientStop1 = new GradientStop
    {
        Offset = 0,
        Color = this.Color1
    };

    gradientStop2 = new GradientStop
    {
        Offset = 1,
        Color = this.Color2
    };

    LinearGradientBrush brush = new LinearGradientBrush();
    brush.GradientStops.Add(gradientStop1);
    brush.GradientStops.Add(gradientStop2);

    this.Foreground = brush;
}

Обратите внимание на то, как инициализаторы свойств двух объектов GradientStop обращаются к свойствам Color1 и Color2. Так цветам LinearGradientBrush задаются цвета по умолчанию, определяемые двумя свойствами зависимости.

Вспомните, что в определении двух свойств зависимости метод OnColorChanged был задан как метод, который должен вызываться при изменении значения свойства Color1 или Color2. Так как ссылка на этот метод используется в статическом конструкторе, сам метод тоже должен быть статическим:

private static void OnColorChanged(DependencyObject obj,
                                   DependencyPropertyChangedEventArgs e)
{
     // ...
}

А это уже выглядит немного странно. В конце концов, класс GradientButton создавался для многократного использования в приложении, а мы определяем статическое свойство, которое должно вызываться при изменении свойства Color1 или Color2 в экземпляре этого класса. Как узнать, к какому экземпляру относится метод?

Очень просто: у нас есть первый аргумент. В первом аргументе метода OnColorChanged всегда передается объект GradientButton, поэтому его можно безопасно преобразовать в GradientButton, после чего обращаться к полям и свойствам конкретного экземпляра GradientButton.

В статических методах изменения свойств я часто вызываю одноименный экземплярный метод, передавая ему второй аргумент:

private static void OnColorChanged(DependencyObject obj,
                                   DependencyPropertyChangedEventArgs e)
{
     (obj as GradientButton).OnColorChanged(e);
}

private void OnColorChanged(DependencyPropertyChangedEventArgs e)
{
     // ...
}

Второй метод выполняет всю работу по обращению к полям экземпляров и свойствам класса.

В объекте DependencyPropertyChangedEventArgs содержится полезная информация. Свойство Property относится к типу DependencyProperty и обозначает изменившееся свойство. В нашем примере свойство Property будет содержать либо Color1Property, либо Color2Property. DependencyPropertyChangedEventArgs также содержит свойства с именами OldValue и NewValue типа object.

В GradientButton обработчик изменения свойства задает свойству Color соответствующего объекта GradientStop значение из NewValue:

private void OnColorChanged(DependencyPropertyChangedEventArgs e)
{
    if (e.Property == Color1Property)
        gradientStop1.Color = (Color)e.NewValue;

    gradientStop1.Color = this.Color1;

    if (e.Property == Color2Property)
        gradientStop2.Color = (Color)e.NewValue;
}

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

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

namespace WinRTTestApp
{
    public class GradientButton : Button
    {
        public static DependencyProperty Color1Property { private set; get; }
        public static DependencyProperty Color2Property { private set; get; }

        public Color Color1
        {
            set { SetValue(Color1Property, value); }
            get { return (Color)GetValue(Color1Property); }
        }

        public Color Color2
        {
            set { SetValue(Color2Property, value); }
            get { return (Color)GetValue(Color2Property); }
        }

        GradientStop gradientStop1, gradientStop2;

        static GradientButton()
        {
            Color1Property =
                DependencyProperty.Register("Color1",
                    typeof(Color),
                    typeof(GradientButton),
                    new PropertyMetadata(Colors.White, OnColorChanged));

            Color2Property =
                DependencyProperty.Register("Color2",
                    typeof(Color),
                    typeof(GradientButton),
                    new PropertyMetadata(Colors.Black, OnColorChanged));
        }

        public GradientButton()
        {
            gradientStop1 = new GradientStop
            {
                Offset = 0,
                Color = this.Color1
            };

            gradientStop2 = new GradientStop
            {
                Offset = 1,
                Color = this.Color2
            };

            LinearGradientBrush brush = new LinearGradientBrush();
            brush.GradientStops.Add(gradientStop1);
            brush.GradientStops.Add(gradientStop2);

            this.Foreground = brush;
        }

        private static void OnColorChanged(DependencyObject obj,
                                   DependencyPropertyChangedEventArgs e)
        {
            (obj as GradientButton).OnColorChanged(e);
        }

        private void OnColorChanged(DependencyPropertyChangedEventArgs e)
        {
            if (e.Property == Color1Property)
                gradientStop1.Color = (Color)e.NewValue;

            gradientStop1.Color = this.Color1;

            if (e.Property == Color2Property)
                gradientStop2.Color = (Color)e.NewValue;
        }
    }
}

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

Еще один вариант: вместо обращения к свойству NewValue можно использовать значение свойства из класса, например:

gradientStop1.Color = this.Color1;

К моменту вызова обработчика изменения свойства Color уже задано новое значение.

Где хранятся фактические значения свойств Color1 и Color2? Скорее всего в некоем словаре, вероятно оптимизированном (как можно надеяться), но недоступном из API. Состоянием свойств управляет операционная система, а доступ к их значениям осуществляется только через вызовы SetValue и GetValue.

Файл XAML этого проекта определяет пару стилей, один из которых содержит элементы Setter для Color1 и Color2, и применяет эти стили к двум экземплярам GradientButton. Во всех ссылках на GradientButton в этом файле XAML должен использоваться префикс local пространства имен XAML, связанного с пространством имен DependencyProperties, в котором определяется GradientButton. Обратите внимание на префикс local в атрибуте TargetType элемента Style и при создании экземпляров кнопок:

<Page ...
    xmlns:local="using:WinRTTestApp">

    <Page.Resources>
        <Style x:Key="baseButtonStyle" TargetType="local:GradientButton">
            <Setter Property="FontSize" Value="48" />
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="Margin" Value="0 12" />
        </Style>

        <Style x:Key="blueRedButtonStyle" 
               TargetType="local:GradientButton"
               BasedOn="{StaticResource baseButtonStyle}">
            <Setter Property="Color1" Value="Blue" />
            <Setter Property="Color2" Value="Red" />
        </Style>
    </Page.Resources>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel>
            <local:GradientButton Content="GradientButton #1"
                                  Style="{StaticResource baseButtonStyle}" />

            <local:GradientButton Content="GradientButton #2"
                                  Style="{StaticResource blueRedButtonStyle}" />

            <local:GradientButton Content="GradientButton #3"
                                  Style="{StaticResource baseButtonStyle}"
                                  Color1="Aqua"
                                  Color2="Lime" />
        </StackPanel>
    </Grid>
</Page>

Первая кнопка использует для Color1 и Color2 значения по умолчанию, вторая получает значения, определяемые в Style, а третья использует локальные значения. Результат выглядит так:

Кнопки с градиентом, настроенные с помощью свойств зависимости

А сейчас я продемонстрирую альтернативный способ создания класса GradientButton, который позволяет определить LinearGradientBrush в XAML и избавит от необходимости в обработчиках изменения свойств. Заинтригованы?

Создайте отдельный проект. Чтобы создать класс GradientButton, выберите команду добавления нового компонента, но далее вместо пункта Class выберите пункт User Control и введите имя GradientButton. Как обычно, в проект включается пара файлов: GradientButton.xaml и GradientButton.xaml.cs.

Класс GradientButton является производным от UserControl. Определение класса в файле GradientButton.xaml.cs выглядит так:

public sealed partial class GradientButton : UserControl
{
    public GradientButton()
    {
        this.InitializeComponent();
    }
}

Замените базовый класс UserControl на Button:

public sealed partial class GradientButton : Button
{
    public GradientButton()
    {
        this.InitializeComponent();
    }
}

Тело класса выглядит почти так же, как в первом классе GradientButton, если не считать того, что конструктор экземпляров не делает ничего, кроме вызова InitializeComponent(). Обработчики изменения свойств отсутствуют. Вот как выглядит класс в проекте:

public sealed partial class GradientButton : Button
{
        public static DependencyProperty Color1Property { private set; get; }
        public static DependencyProperty Color2Property { private set; get; }

        public Color Color1
        {
            set { SetValue(Color1Property, value); }
            get { return (Color)GetValue(Color1Property); }
        }

        public Color Color2
        {
            set { SetValue(Color2Property, value); }
            get { return (Color)GetValue(Color2Property); }
        }

        static GradientButton()
        {
            Color1Property =
                DependencyProperty.Register("Color1",
                    typeof(Color),
                    typeof(GradientButton),
                    new PropertyMetadata(Colors.White));

            Color2Property =
                DependencyProperty.Register("Color2",
                    typeof(Color),
                    typeof(GradientButton),
                    new PropertyMetadata(Colors.Black));
        }

        public GradientButton()
        {
            this.InitializeComponent();
        }
}

При создании файл GradientButton.xaml содержит корневой элемент, указывающий, что класс является производным от класса UserControl:

<UserControl ...>
    ...
</UserControl>

Его также следует заменить классом Button:

<Button ...>
    ...
</Button>

Обычно размещение содержимого между корневыми тегами в файле XAML означает неявное задание свойства Content. Однако в данном случае мы не собираемся задавать свойство Content элемента Button - мы хотим задать свойству Foreground объекта GradientButton значение LinearGradientBrush, а для этого необходимы теги свойств элементов Button.Foreground. Полный файл XAML выглядит так:

<Button ...
    x:Name="root">

    <Button.Foreground>
        <LinearGradientBrush>
            <GradientStop Offset="0" 
                          Color="{Binding ElementName=root, 
                                          Path=Color1}" />
            <GradientStop Offset="1" 
                          Color="{Binding ElementName=root, 
                                          Path=Color2}" />
        </LinearGradientBrush>
    </Button.Foreground>
</Button>

Обратите внимание на элегантный способ задания свойств Color объектов GradientStop: корневому элементу присваивается имя «root», чтобы он мог быть источником двух привязок данных, ссылающихся на нестандартные свойства зависимости.

Файл MainPage.xaml этого проекта выглядит так же, как в предыдущем проекте, и результат тоже остается неизменным.

Пройди тесты
Лучший чат для C# программистов