Построение специального свойства зависимости

26

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

Начните с создания приложения WPF. Выберите пункт меню Project --> Add New Item (Проект --> Добавить новый элемент), укажите в качестве шаблона User Control (WPF) (Пользовательский элемент управления (WPF)), и назначьте ему имя ShowNumberControl.xaml:

Вставка элемента UserControl

Как и окно, типы WPF UserControl имеют ассоциированный с ними файл XAML и связанный файл кода. Обновим XAML-разметку пользовательского элемента управления, добавив элемент управления Label в Grid:

<UserControl x:Class="WpfApplication1.ShowNumberControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Label Name="numberDisplay" Height="50" Width="200" Background="LightBlue"></Label>
    </Grid>
</UserControl>

В файле кода этого специального элемента управления создайте обычное свойство .NET, которое упаковывает int и устанавливает новое значение для свойства Content элемента Label:

// Обычное свойство .NET
private int currNumber = 0;
public int CurrentNumber
{
      get { return currNumber; }
      set
      {
           currNumber = value;
           // Передаем в метку текущее значение
           numberDisplay.Content = CurrentNumber.ToString();
      }
}

Теперь обновите определение XAML окна, чтобы объявить экземпляр специального элемента управления внутри диспетчера компоновки StackPanel. Поскольку специальный элемент управления — не часть основного стека сборок WPF, потребуется определить специальное пространство имен XML, которое отобразится на этот элемент. Ниже показана необходимая разметка:

<Window x:Class="MyDependencyProperty.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:myCtrls="clr-namespace:MyDependencyProperty"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <myCtrls:ShowNumberControl x:Name="mSNC" CurrentNumber="100"></myCtrls:ShowNumberControl>
    </Grid>
</Window>

Как видите, визуальный конструктор Visual Studio 2010 вроде бы корректно отображает значение, установленное в свойстве CurrentNumber. А что если необходимо применить объект анимации к свойству CurrentNumber, чтобы значение изменялось от 100 до 200 за период в 10 секунд? Желая сделать это в разметке, можно было бы обновить раздел <myCtrls:ShowNumberControl> следующим образом:

<myCtrls:ShowNumberControl x:Name="mSNC" CurrentNumber="100">
            <myCtrls:ShowNumberControl.Triggers>
                <EventTrigger RoutedEvent="myCtrls:ShowNumberControl.Loaded">
                    <BeginStoryboard>
                        <Storyboard TargetProperty="CurrentNumber">
                            <Int32Animation From="100" To="200" Duration="0:0:10"></Int32Animation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </myCtrls:ShowNumberControl.Triggers>
</myCtrls:ShowNumberControl>

Если попробовать запустить это приложение, то объект анимации не сможет найти правильную цель, и потому будет проигнорирован. Причина в том, что свойство CurrentNumber не зарегистрировано как свойство зависимости! Чтобы исправить это, вернемся к файлу кода специального элемента управления и полностью закомментируем текущую логику свойства (включая приватное поле заднего плана). Теперь поместите курсор внутрь области класса и введите фрагмент кода propdp. После ввода propdp нажмите два раза клавишу <Tab>. Фрагмент кода развернется, предоставив базовый скелет свойства зависимости:

public int CurrentNumber
        {
            get { return (int)GetValue(CurrentNumberProperty); }
            set { SetValue(CurrentNumberProperty, value); }
        }

        // Using a DependencyProperty as the backing store for CurrentNumber.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CurrentNumberProperty =
            DependencyProperty.Register("CurrentNumber", typeof(int), 
            typeof(ShowNumberControl), new UIPropertyMetadata(0));     

Это подобно тому, что вы видели в реализации свойства Height; однако фрагмент кода регистрирует свойство встроенным способом, а не в статическом конструкторе (что хорошо). Также обратите внимание, что объект UIPropertyMetadata используется для определения целого значения по умолчанию (0) вместо более сложного объекта FrameworkPropertyMetadata.

Добавление процедуры проверки достоверности данных

Хотя теперь есть свойство зависимости по имени CurrentNumber, все же анимация в действии не видна. Следующая необходимая поправка, которая может понадобиться, состоит в указании функции для выполнения некоторой логики проверки достоверности. Для целей данного примера предположим, что нужно обеспечить, чтобы значение свойства CurrentNumber находилось в пределах от 0 до 500.

Для этого добавьте финальный аргумент к методу DependencyProperty.Register() типа ValidateValueCallback, указывающий на метод по имени ValidateCurrentNumber. ValidateValueCallback — это делегат, который может только указывать на методы, возвращающие bool и принимающие object в качестве единственного аргумента. Этот object представляет новое значение, которое присваивается. Реализуйте ValidateCurrentNumber для возврата true, если входящее значение находится в заданном диапазоне, и false — в противном случае:

// Using a DependencyProperty as the backing store for CurrentNumber.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CurrentNumberProperty =
            DependencyProperty.Register("CurrentNumber", typeof(int), 
            typeof(ShowNumberControl), new UIPropertyMetadata(100),
            new ValidateValueCallback(ValidateCurrentNumber));

        public static bool ValidateCurrentNumber(object value)
        {
            if (Convert.ToInt32(value) >= 0 && Convert.ToInt32(value) <= 500)
                return true;
            else
                return false;
}

Реакция на изменение свойства

Итак, допустимое число уже имеется, но все еще нет анимации. Последнее изменение, которое потребуется внести — это задать второй аргумент конструктора UIPropertyMetadata, которым является PropertyChangedCallback. Этот делегат может указывать на любой метод, принимающий DependencyObject в качестве первого параметра и DependencyPropertyChangeEventArgs — в качестве второго.

Конечной целью внутри метода CurrentNumberChanged() является изменение свойства Content объекта Label на новое значение, присвоенное свойством CurrentNumber. Однако здесь присутствует серьезная проблема: метод CurrentNumberChanged() является статическим, поскольку он должен работать со статическим объектом DependencyProperty. Тогда как же получить доступ к Label для текущего экземпляра ShowNumberControl? Эта ссылка находится в первом параметре DependencyObject. Новое значение можно найти, используя входные аргументы события. Ниже показан необходимый код, который изменит свойство Content объекта Label:

public static readonly DependencyProperty CurrentNumberProperty =
            DependencyProperty.Register("CurrentNumber", typeof(int), 
            typeof(ShowNumberControl), 
            new UIPropertyMetadata(100,
                new PropertyChangedCallback(CurrentNumberChanged)),
            new ValidateValueCallback(ValidateCurrentNumber));

        private static void CurrentNumberChanged(DependencyObject depObj,
            DependencyPropertyChangedEventArgs args)
        {
            ShowNumberControl s = (ShowNumberControl)depObj;
            Label theLabel = s.numberDisplay;
            theLabel.Content = args.NewValue.ToString();
        }

Какой длинный путь пришлось пройти, чтобы всего лишь изменить содержимое метки! Однако преимущество в том, что свойство зависимости CurrentNumber теперь может быть целью для стиля WPF, объекта анимации, операций привязки данных и т.д. На рисунке показано завершенное приложение (теперь оно действительно меняет значение во время выполнения):

Работающая анимация

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

Если вы окажетесь в ситуации, когда нужно строить множество специальных элементов управления, поддерживающих специальные свойства, загляните в подраздел "Properties" ("Свойства") узла "WPF Fundamentals" ("Основы WPF") документации .NET Framework 4.0 SDK. Там вы найдете намного больше примеров построения свойств зависимости, присоединяемых свойств, различные способы конфигурирования метаданных и массу других деталей.

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