Руководство для начинающих. Часть 4

Я программист .NET. Я много программирую на VB.NET и C#, ASP.NET/Winforms/WPF/WCF Flash Silverlight. Но когда начал писать данные статьи, я, естественно, выбрал мой любимый язык — C#. Через некоторое время я получил сообщение от одного человека с просьбой публиковать исходный код на VB.NET и C# в статьях этой серии. Я ответил, что у меня нет времени. И тогда этот человек, Роберт Рэнк (Robert Ranck), вызвался помочь с преобразованием моих исходных проектов на C# в VB.NET.

За этот и последующие проекты VB.NET следует благодарить Роберта Рэнка. Спасибо, Роберт! Ваше участие, несомненно, сделает эту серию более доступной для всех разработчиков .NET.

Также я хотел бы выразить благодарность Карлу Шифлету (Karl Shifflett) (плодовитый автор статей и блогов, также известный под псевдонимом Molenator) за его ответы на мои глупые вопросы по VB.NET. Замечу, что Карл недавно приступил к написанию серии более продвинутых статей по WPF (примеры в которых будут пока на VB.NET, но, надеюсь, появятся и на C#). У Карла должна получиться отличная серия статей, и я прошу всех поддержать его работу. Непросто заставить себя писать всю серию на одном языке, не говоря уже о двух. Первую статью Карла можно прочитать здесь. Лично мне она нравится.

Введение

Это моя четвертая статья из серии статей о WPF для начинающих. В этой статье мы поговорим о свойствах зависимости. А вот предполагаемое содержание этой серии:

В этой статье я планирую кратко остановиться на следующих вопросах:

  • Отличие свойств CLR от свойств зависимости
  • Свойства зависимости
  • Приоритет значений свойств зависимости
  • Наследование значений свойств зависимости
  • Присоединенные свойства
  • Метаданные значений свойств зависимости
  • Проверка и обратные вызовы свойства зависимости и приведенные значения

Отличие свойств CLR от свойств зависимости

Примечание. Этим рисунком я хотел сказать, что свойство CLR довольно хорошее, но существенно проигрывает мощи Халка. Глянув на рисунок, мой босс сказал, что «человек-паук выглядит что надо, а Халк тормозной, не умеет говорить и все ломает. Не очень-то хорошая аналогия». Но она мне нравится, и я оставил ее. Надеюсь, вы понимаете, о чем я. В любом случае мне нравится Халк, он крутой парень. Я хочу сказать, что способность уничтожить танк голыми руками — вот это класс. Мне так кажется.

Честно говоря, картинка не так плоха. Отличия обычного свойства CLR, которое позволяет возвращать и задавать закрытый член, от возможностей свойства зависимости вполне очевидны.

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

Для тех, кто не знаком со свойствами CLR, сообщаю, что они определяются следующим образом.

private int x;

public int X

{

    get { return x; }

    set { x = value; }

}

И эквивалентный код на VB.NET.

Private x As Integer

Public Property X() As Integer

Get

    Return x

End Get

Set(ByVal Value As Integer)

    x = value

End Set

End Property

Достаточно просто. Со свойствами зависимости все по-другому (совсем по-другому). Свойства зависимости — это не просто оболочка, которая возвращает и задает значения.

В следующей таблице перечислено несколько возможностей, реализуемых с помощью свойств зависимости.

Возможности, доступные с помощью свойств зависимости
Уведомление об изменении
Обратные вызовы
Проверка значений свойств
Наследование значений свойств*
Участие в анимации*
Участие в стилях*
Участие в шаблонах*
Привязка данных
Изменения макета*
Переопределение значений данных по умолчанию*

Как видите, свойства зависимости — это не простые свойства CLR. Как однажды заметил Джош Смит (Josh Smith), это «свойства на стероидах».

Примечание. Возможности, отмеченные звездочкой, недоступны без свойств зависимости. Свойство CLR не подходит для них во всех отношениях, а кроме того, Visual Studio или другая интегрированная среда разработки, вероятно, вызовет исключение при попытке использования свойства CLR вместо свойства зависимости.

Свойства зависимости

Как я уже сказал, свойства зависимости — это усиленная версия их более слабых «родственников», свойств CLR. Но зачем они нужны? В процессе разработки платформы WPF майкрософтовская команда WPF решила, что систему свойств нужно изменить, чтобы обеспечить такие возможности, как уведомления об изменении, проверка значений, наследование значений, участие в привязках, анимации, стилях и шаблонах.

Многое в подсистеме свойств зависимости напоминает подсистему перенаправленных событий, и аналогично этой подсистеме для определения собственных свойств зависимости необходимо выполнить несколько действий. А именно:

  • объявить свойство зависимости (всегда public static readonly);
  • инициализировать свойство зависимости с помощью одного из следующих методов: DependencyProperty.RegisterAttached, DependencyProperty.Register, DependencyProperty.RegisterReadOnly или DependencyPropertyRegisterAttachedReadOnly;
  • объявить оболочку свойства для методов get/set (см. примечание ниже).

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

Очень важное примечание… Внимание!

«Компилятор XAML учитывает оболочку при компиляции, однако во время выполнения платформа WPF вызывает непосредственно базовые методы GetValue и SetValue! Поэтому для обеспечения эквивалентности между заданием свойства в XAML и процедурном коде необходимо, чтобы оболочки свойств НЕ содержали никакой логики, кроме вызовов методов GetValue/SetValue. Если нужно добавить собственную логику, следует использовать зарегистрированные обратные вызовы. Вся платформа WPF построена на принципе соблюдения этого правила в оболочках свойств, так что это предупреждение касается каждого, кто создает собственный класс с собственными свойствами зависимости».

Выпуск платформы Windows Presentation Foundation. Адам Натан (Adam Nathan), Sams. 2007 г.

Приоритет значений свойств зависимости

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

Применяется следующий порядок очередности, начиная с самого старшего:

  1. Приведение системы свойств. Например, из активной анимации или приведения базового типа (где свойство зависимости может быть определено с помощью CoerceValueCallback — этот вопрос будет рассмотрен в разделе Обратные вызовы свойства зависимостей), что даже приводит к перезаписи выполняющейся анимации.
  2. Активные анимации или анимации с поведением Hold (в этом случае у них должно быть последнее анимированное значение).
  3. Локальное значение. Локальное значение может быть задано с помощью свойства «оболочки», что эквивалентно заданию в качестве атрибута или элемента свойства в XAML либо вызову метода SetValue.
  4. Свойства шаблона TemplatedParent. Элемент имеет TemplatedParent, если он был создан как часть шаблона (ControlTemplate или DataTemplate). В шаблоне применяется следующий приоритет:
    • триггеры из шаблона TemplatedParent;
    • наборы свойств (как правило, с помощью атрибутов XAML) в шаблоне TemplatedParent.
  5. Неявный стиль.
  6. Триггеры стиля.
  7. Триггеры шаблона. Любой триггер из шаблона внутри стиля или непосредственно применяемого шаблона.
  8. Методы присвоения стиля.
  9. Стиль по умолчанию (из темы). В стиле по умолчанию применяется следующий порядок приоритетов:
    • активные триггеры в стиле темы;
    • методы присвоения в стиле темы.
  10. Наследование. Некоторые свойства зависимости наследуют значения от родительского элемента к дочерним, поэтому их не нужно специально задавать для каждого элемента внутри приложения. См. раздел Наследование значений свойств зависимости.
  11. Значение по умолчанию из метаданных свойства зависимости. У любого заданного свойства зависимости может быть значение по умолчанию, задаваемое при регистрации этого свойства в системе обработки свойств. См. раздел Метаданные свойства зависимости.

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

Наследование значений свойств зависимости

Еще один вариант — наследовать значения из свойства зависимости, объявленного для совершенно другого элемента. Чтобы стало понятнее, я добавил в прилагаемое демонстрационное приложение (вверху статьи) проект Using_Inhertied_DPs, который при выполнении выглядит так:

На рисунке ниже показан код XAML (это весь код данного проекта).

Как видите, если свойство TextElement.FontSize объявлено на уровне объекта Window, любой элемент управления, для которого не объявлено собственное значение TextElement.FontSize, будет наследовать значение свойства TextElement.FontSize, объявленного для объекта Window. Но как такое может быть? Элемент TextElement — не Window или Label. Как же все это работает? Подсистема свойств WPF очень хитрая, фактически она позволяет объявлять свойства зависимости так, что они могут использоваться другими классами, отличными от тех, в которых объявляется свойство зависимости. Они называются присоединенными свойствами. Мы остановимся на них более подробно в разделе Присоединенные свойства.

Итак, половина загадки решена. Класс Windowв этом примере использует присоединенное свойство. Прекрасно. А что с наследованием? Как оно работает? Это еще одна возможность, которую обеспечивает подсистема свойств WPF при объявлении свойства зависимости. Чтобы разобраться в этом, рассмотрим несколько снимков экрана превосходного декомпилятора .NET Итак, приступим. Мы также посмотрим на классы, используемые в этом примере, чтобы сохранить контекст.

Начнем с исходного текста свойства TextElement.FontSize, которое фактически относится к классу System.Windows.Documents.TextElement в библиотеке PresentationFramework.dll. Именно здесь первый раз объявляется свойство зависимости FontSize. Обратите внимание, свойство объявляется как присоединенное. Это значит, что другие классы могут ссылаться на это свойство зависимости с использованием синтаксиса TextElement.FontSize, даже если они не имеют ничего общего с классом TextElement.

На странице MSDN, посвященной свойству TextElement.FontSize, видно, что фактически оно объявляется с метаданными Inherits. Это позволяет элементам управления с этим присоединенным свойством наследовать значение от вышестоящего события в визуальном дереве, если они не объявляют новое значение этого свойства.

Чтобы лучше разобраться, продолжим наше расследование. Пока мы знаем, что на самом деле элемент TextElement не объявляет свойство зависимости FontSize, а само свойство — присоединенное и помечено метаданными Inherits. А что можно сказать об элементах управления, которые могут использовать это присоединенное свойство? Например, об элементе System.Windows.Controls.Label, как в нашем примере. Давайте посмотрим.

Оказывается, элемент System.Windows.Controls.Label в действительности не объявляет свойство зависимости FontSize. Нам придется двигаться по объектному дереву наследования, пока мы не дойдем до элемента System.Windows.Controls.Control, в котором находится объявление свойства FontSize. Мы увидим следующее.

Третий элемент System.Windows.Controls.Label на снимке экрана демонстрационного проекта обладает другим значением FontSize, потому что не наследует значение свойства (из свойства TextElement.FontSize, заданного в элементе Window1), а явно объявляет собственное, тем самым переопределяя наследуемое значение, получаемое от объекта Window1.

Так работает свойство TextElement.FontSize. А как быть с собственным наследуемым свойством? Я подготовил другой демонстрационный проект в рамках общего решения (доступного вверху страницы). Проект называется DP_Custom_Inherited_Properties и при выполнении выглядит следующим образом.

Не слишком впечатляет, да?

Но для новичка это именно то, что нужно, — сложный и большой проект тут не подойдет. А вот мое демонстрационное приложение — в самый раз. Итак, начнем вскрытие. Это приложение состоит из двух важных частей: код программной части, где объявляется наследуемое свойство зависимости, и интерфейс пользователя, в котором используется свойство зависимости. Начнем с кода программной части.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Navigation;

using System.Windows.Shapes;

namespace Custom_Inherited_DPs

{

    ///

    /// Это простой подкласс класса Button, наследующий свойство зависимости MinDate

    ///

    public class MyCustomButton : Button

    {

        ///

        /// Создает новый объект MyCustomButton и добавляет класс MyCustomButton

        /// в дочерние объекты свойства зависимости MyStackPanel.MinDate

        ///

        static MyCustomButton()

        {

            MinDateProperty = MyStackPanel.MinDateProperty.AddOwner(typeof(MyCustomButton),

            new FrameworkPropertyMetadata(DateTime.MinValue,

            FrameworkPropertyMetadataOptions.Inherits));

        }

        #region Inherited DP declaration

        ///

        /// Объявление свойства зависимости MinDate

        ///

        public static readonly DependencyProperty MinDateProperty;

        public DateTime MinDate

        {

            get { return (DateTime)GetValue(MinDateProperty); }

            set { SetValue(MinDateProperty, value); }

        }

        #endregion

    }

    ///

    /// Это простой подкласс класса StackPanel, являющийся источником для

    /// наследуемого свойства зависимости MinDate

    ///

    public class MyStackPanel : StackPanel

    {

        ///

        /// Создает новый объект MyStackPanelи регистрирует свойство зависимости MinDate

        ///

        static MyStackPanel()

        {

            MinDateProperty = DependencyProperty.Register("MinDate",

            typeof(DateTime),

            typeof(MyStackPanel),

            new FrameworkPropertyMetadata(DateTime.MinValue,

            FrameworkPropertyMetadataOptions.Inherits));

        }

        #region Source for Inherited MinDate DP declaration

        ///

        /// Объявление свойства зависимости MinDate

        ///

        public static readonly DependencyProperty MinDateProperty;

        public DateTime MinDate

        {

            get { return (DateTime)GetValue(MinDateProperty); }

            set { SetValue(MinDateProperty, value); }

        }

        #endregion

    }

}

И версия VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows

Imports System.Windows.Controls

Imports System.Windows.Data

Imports System.Windows.Documents

Imports System.Windows.Input

Imports System.Windows.Media

Imports System.Windows.Media.Imaging

Imports System.Windows.Navigation

Imports System.Windows.Shapes

    '''

    ''' Это простой подкласс класса Button, наследующий свойство зависимости MinDate

    '''

    Public Class MyCustomButton

        Inherits Button

        '''

        ''' Создает новый объект MyCustomButton и добавляет класс MyCustomButton

        ''' в дочерние объекты свойства зависимости MyStackPanel.MinDate

        '''

        Shared Sub New()

            MinDateProperty = MyStackPanel.MinDateProperty.AddOwner(GetType(MyCustomButton),

            New FrameworkPropertyMetadata(DateTime.MinValue,

            FrameworkPropertyMetadataOptions.[Inherits]))

        End Sub

#Region "Inherited DP declaration"

        '''

        ''' Объявление свойства зависимости MinDate

        '''

        Public Shared ReadOnly MinDateProperty As DependencyProperty

        Public Property MinDate() As DateTime

            Get

                Return DirectCast(GetValue(MinDateProperty), DateTime)

            End Get

            Set(ByVal value As DateTime)

                SetValue(MinDateProperty, value)

            End Set

        End Property

#End Region

    End Class

    '''

    ''' Это простой подкласс класса StackPanel, являющийся источником для

    ''' наследуемого свойства зависимости MinDate

    '''

    Public Class MyStackPanel

        Inherits StackPanel

        '''

        ''' Создает новый объект MyStackPanelи регистрирует свойство зависимости MinDate

        '''

        Shared Sub New()

            MinDateProperty = DependencyProperty.Register("MinDate", GetType(DateTime),

            GetType(MyStackPanel), New FrameworkPropertyMetadata(DateTime.MinValue,

            FrameworkPropertyMetadataOptions.[Inherits]))

        End Sub

#Region "Source for Inherited MinDate DP declaration"

        '''

        ''' Объявление свойства зависимости MinDate

        '''

        Public Shared ReadOnly MinDateProperty As DependencyProperty

        Public Property MinDate() As DateTime

            Get

                Return DirectCast(GetValue(MinDateProperty), DateTime)

            End Get

            Set(ByVal value As DateTime)

                SetValue(MinDateProperty, value)

            End Set

        End Property

#End Region

    End Class

Как видите, я определил два класса. Класс MyStackPanel (подкласс StackPanel) служит источником для наследуемого свойства зависимости MinDate. Другой класс, MyButton (подкласс Button), объявляет наследуемое свойство. При этом важно узнать, как объявляется свойство зависимости в классе MyButton. Посмотрим на следующий фрагмент кода.

MinDateProperty = MyStackPanel.MinDateProperty.AddOwner(typeof(MyCustomButton),

new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits));

И на VB.NET.

MinDateProperty = MyStackPanel.MinDateProperty.AddOwner(GetType(MyCustomButton),

New FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.[Inherits]))

Как видно из кода, собственное свойство зависимости MinDate элемента MyStackPanel используется для добавления владельца типа MyButton.

Это код программной части. Теперь давайте посмотрим на разметку XAML.

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:Custom_Inherited_DPs"

    xmlns:sys="clr-namespace:System;assembly=mscorlib"

    WindowStartupLocation="CenterScreen"

    Title="Using custom inherited DPs" Height="400" Width="400">

   

       

           

           

           

           

                    Content="{Binding RelativeSource={x:Static RelativeSource.Self},

                    Path=MinDate}"

                Height="20"/>

       

  

Как видно, свойство зависимости MinDateзадается только в экземпляре MyStackPanel в следующей строке.

А поскольку свойство зависимости MinDate элемента MyCustomButton наследует это значение (благодаря тому, как объявлено свойство зависимости MinDate элемента MyCustomButton), его совсем не требуется объявлять. Оно просто наследуется от свойства зависимости MinDate элемента MyStackPanel. И оно отображается с использованием привязки данных RelativeSource.

    Content="{Binding RelativeSource={x:Static RelativeSource.Self},

    Path=MinDate}"

    Height="20"/>

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

Присоединенные свойства

Присоединенные свойства — это просто еще один вид свойств зависимости. С помощью присоединенных свойств можно использовать свойства зависимости из классов, которые находятся вне текущего класса. Вспомните Canvas.Left из первой части — это и есть присоединенное свойство. Но зачем оно нужно? Такое свойство будет обеспечивать общее место для переноса чего-либо. Например, при использовании свойства Canvas.Left левая граница элемента будет задана в соответствии с указанным значением.

Прекрасно, но способов использования присоединенных свойств гораздо больше. Вот лишь несколько идей, которые предлагают другие авторы:

Я, конечно, тоже в стороне не останусь. Вот моя идея. Допустим, мы хотим, чтобы все окна в приложении выглядели одинаково: все были с верхним баннером и областью содержимого. Что-нибудь наподобие частично построенных форм или главных страниц в ASP.NET. Чтобы сделать это, необходимо следующее:

  1. Создать присоединенное свойство зависимости.
  2. Задать его для всех окон, которые должны выглядеть одинаково.

Вот так. Рассмотрим код. Он взят из проекта Attached_Properties_DPs (входящего в решение вверху страницы).

Вот как выглядит окно с неактивным присоединенным свойством зависимости (свойству зависимости присвоено значение false).

И с активным присоединенным свойством зависимости (свойству зависимости присвоено значение true).

Ниже приведена разметка XAML.

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:Attached_Properties_DPs"

    local:AttachedPropertyChildAdder.IsMasterHeaderApplied="true"

    WindowStartupLocation="CenterScreen"

    Title="Attached_Properties_DPs" Height="400" Width="600">

       

       

Самая важная строка — local:AttachedPropertyChildAdder.IsMasterHeaderApplied="true". Она определяет, активно ли присоединенное свойство зависимости. И код программной части.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Shapes;

using System.Windows.Media;

using System.Windows.Media.Imaging;

namespace Attached_Properties_DPs

{

    ///

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

    /// В этом примере в содержимое окна по умолчанию добавляется заголовок с

    /// новым содержимым. Аналогично использованию главных страниц в ASP.NET

    ///

    public class AttachedPropertyChildAdder

    {

        #region Register IsMasterHeaderApplied DP

        public static readonly DependencyProperty IsMasterHeaderAppliedProperty =

            DependencyProperty.RegisterAttached("IsMasterHeaderApplied",

                typeof(Boolean),

                typeof(AttachedPropertyChildAdder),

                new FrameworkPropertyMetadata(IsMasterHeaderAppliedChanged));

        public static void SetIsMasterHeaderApplied(DependencyObject element, Boolean value)

        {

            element.SetValue(IsMasterHeaderAppliedProperty, value);

        }

        public static Boolean GetIsMasterHeaderApplied(DependencyObject element)

        {

            return (Boolean)element.GetValue(IsMasterHeaderAppliedProperty);

        }

        #endregion

        #region PropertyChanged callback

        ///

        /// Вызывается, когда пользователь присоединенного свойства зависимости IsMasterHeaderApplied Attached изменяет

        /// значение свойства зависимости IsMasterHeaderApplied.

        ///

        ///

        ///

        public static void IsMasterHeaderAppliedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)

        {

            if ((bool)args.NewValue)

            {

                if (obj is Window)

                {

                    Window wnd = (Window)obj;

                    wnd.Loaded += new RoutedEventHandler(wnd_Loaded);

                }

            }

        }

        ///

        /// Присоединение к событию загрузки окна для замены содержимого окна

        /// пользовательским содержимым для демонстрации возможностей свойств зависимости.

        ///

        /// В этом примере создается заголовок окна.

        ///

        /// Задание свойства IsMasterHeaderApplied обеспечит

        /// использование заголовка.

        ///

        /// Аналогично главным страницам в ASP.NET

        ///

        public static void wnd_Loaded(object sender, RoutedEventArgs e)

        {

            try

            {

                DockPanel dp = new DockPanel();

                dp.LastChildFill = true;

                StackPanel sp = new StackPanel();

                dp.Children.Add(sp);

                sp.Background = new SolidColorBrush(Colors.CornflowerBlue);

                sp.Orientation = Orientation.Vertical;

                sp.SetValue(DockPanel.DockProperty, Dock.Top);

                BitmapImage bitmap = new BitmapImage(new Uri("Images/Header.png", UriKind.Relative));

                Image image = new Image();

                image.Source = bitmap;

                sp.Children.Add(image);

                UIElement el = ((DependencyObject)sender as Window).Content as UIElement;

                el.SetValue(DockPanel.DockProperty, Dock.Bottom);

                ((DependencyObject)sender as Window).Content = null;

                dp.Children.Add(el);

               ((DependencyObject)sender as Window).Content = dp;

            }

            catch (Exception ex)

            {

                System.Diagnostics.Debug.WriteLine(string.Format("Exception : {}",ex.Message));

            }

        }

        #endregion

    }

}

И на VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows

Imports System.Windows.Controls

Imports System.Windows.Shapes

Imports System.Windows.Media

Imports System.Windows.Media.Imaging

    '''

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

    ''' В этом примере в содержимое окна по умолчанию добавляется заголовок с

    ''' новым содержимым. Аналогично использованию главных страниц в ASP.NET

    '''

    Public Class AttachedPropertyChildAdder

#Region "Register IsMasterHeaderApplied DP"

    Public Shared ReadOnly IsMasterHeaderAppliedProperty As DependencyProperty =

    DependencyProperty.RegisterAttached("IsMasterHeaderApplied", GetType(Boolean),

    GetType(AttachedPropertyChildAdder),

    New FrameworkPropertyMetadata((AddressOf IsMasterHeaderAppliedChanged)))

    Public Shared Sub SetIsMasterHeaderApplied(ByVal element As DependencyObject,

        ByVal value As Boolean)

        element.SetValue(IsMasterHeaderAppliedProperty, value)

    End Sub

    Public Shared Function GetIsMasterHeaderApplied(ByVal element As DependencyObject) As Boolean

        Return CBool(element.GetValue(IsMasterHeaderAppliedProperty))

    End Function

#End Region

#Region "PropertyChanged callback"

        '''

        ''' Вызывается, когда пользователь присоединенного свойства зависимости IsMasterHeaderApplied Attachedизменяет

        ''' значение свойства зависимости IsMasterHeaderApplied.

        '''

        '''

        '''

        Public Shared Sub IsMasterHeaderAppliedChanged(ByVal obj As DependencyObject,

            ByVal args As DependencyPropertyChangedEventArgs)

            If CBool(args.NewValue) Then

                If TypeOf obj Is Window Then

                    Dim wnd As Window = DirectCast(obj, Window)

                    AddHandler wnd.Loaded, AddressOf wnd_Loaded

                End If

            End If

        End Sub

        '''

        ''' Присоединение к событию загрузки окна для замены содержимого окна

        ''' пользовательским содержимым для демонстрации возможностей свойств зависимости.

        '''

        ''' В этом примере создается заголовок окна.

        '''

        ''' Задание свойства IsMasterHeaderApplied обеспечит

        ''' использование заголовка.

        '''

        ''' Аналогично главным страницам в ASP.NET

        '''

        Public Shared Sub wnd_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)

            Try

                Dim dp As New DockPanel()

                dp.LastChildFill = True

                Dim sp As New StackPanel()

                dp.Children.Add(sp)

                sp.Background = New SolidColorBrush(Colors.CornflowerBlue)

                sp.Orientation = Orientation.Vertical

                sp.SetValue(DockPanel.DockProperty, Dock.Top)

                Dim bitmap As New BitmapImage(New Uri("Images/Header.png", UriKind.Relative))

                Dim image As New Image()

                image.Source = bitmap

                sp.Children.Add(image)

                Dim el As UIElement = TryCast(TryCast(

                    DirectCast(sender, DependencyObject), Window).Content, UIElement)

                el.SetValue(DockPanel.DockProperty, Dock.Bottom)

                TryCast(DirectCast(sender, DependencyObject), Window).Content = Nothing

                dp.Children.Add(el)

                TryCast(DirectCast(sender, DependencyObject), Window).Content = dp

            Catch ex As Exception

                System.Diagnostics.Debug.WriteLine(String.Format("Exception : {}", ex.Message))

            End Try

        End Sub

#End Region

    End Class

Все очень четко. В одной строке кода в любом элементе Window мы можем задать, использовать ли в нем заголовок. Естественно, это очень простой пример, но, воспользовавшись некоторыми знаниями о перенаправленных командах (см. часть 3), можно легко представить, что такой заголовок содержит все меню приложения, и сделать так, чтобы все окна использовали одинаковую систему меню путем задания всего одного свойства.

Метаданные свойств зависимости

Класс FrameworkPropertyMetadata наследуется от класса PropertyMetadata, и для большинства целей разработки приложений уровня платформы WPF в качестве метаданных свойств используется тип FrameworkPropertyMetadata, а не базовые типы метаданных PropertyMetadata или UIPropertyMetadata. Это касается сценариев как с имеющимися, так и с пользовательскими свойствами зависимости.

Но какая польза от этого класса? При каждом определении регистрации и добавления или присоединения свойства зависимости необходимо предоставить экземпляр класса FrameworkPropertyMetadata. Так мы информируем систему свойств WPF о необходимости особым образом обращаться со свойством зависимости, использующим этот экземпляр FrameworkPropertyMetadata. Например, если посмотреть на конструкторы класса FrameworkPropertyMetadata, цель такой системы свойств может проясниться (прошу прощения за размер изображения, полную информацию можно найти в документации MSDN).

Итак, с помощью класса FrameworkPropertyMetadata можно предоставить системе свойств следующую информацию:

  • значения по умолчанию;
  • одно из значений FrameworkPropertyMetadataOptions, например AffectsMeasure/AffectsArrange/AffectsRender/Inherits и т. д.;
  • делегаты обратного вызова измененных свойств;
  • приведенные значения;
  • отмена поддержки анимации для свойства;
  • предоставление одного из событий UpdateSourceTrigger, например PropertyChanged, LostFocus, Explicit и т. д.

Всего один экземпляр FrameworkPropertyMetadata дает нам жесткий контроль над множеством метаданных свойства зависимости. На практике мы увидим это ниже, при обсуждении обратных вызовов и проверки.

Проверка и обратные вызовы свойства зависимости и приведенные значения

У вас, наверное, могло сложиться мнение, что свойства зависимости довольно сложные и мощные (Халк, одним словом). Но не следует забывать еще о нескольких принципах. Вот они:

  • обратные вызовы для тех случаев, когда изменяется свойство зависимости;
  • приведенные значения для изменения неприемлемого значения свойства зависимости;
  • проверка допустимости значения.

Использование делегатов при первой регистрации свойства зависимости решает большинство из перечисленных задач. В состав прилагаемого решения (вверху статьи) входит проект Callback_Validation_DPs, демонстрирующий эти принципы. По сути, есть класс Gauge, наследуемый от класса Control, с тремя свойствами зависимости:

  • CurrentReading
  • MinReading
  • MaxReading

Каждое из них предназначено для проверки допустимости. Значение CurrentReading сравнивается со значениями свойств зависимости MinReading и MaxReading и при необходимости приводится. Рассмотрим пример применения этих свойств.

При работе демонстрационное приложение выглядит так.

Обратные вызовы и приведенные значения для случаев, когда изменяется свойство зависимости

Свойство зависимости CurrentReading объявляется так.

public static readonly DependencyProperty CurrentReadingProperty =

    DependencyProperty.Register(

    "CurrentReading",

    typeof(double),

    typeof(Gauge),

    new FrameworkPropertyMetadata(

        Double.NaN,

        FrameworkPropertyMetadataOptions.None,

        new PropertyChangedCallback(OnCurrentReadingChanged),

        new CoerceValueCallback(CoerceCurrentReading)

    ),

    new ValidateValueCallback(IsValidReading)

);

И на VB.NET.

Public Shared ReadOnly CurrentReadingProperty As DependencyProperty =

    DependencyProperty.Register("CurrentReading",

    GetType(Double),

    GetType(Gauge),

    New FrameworkPropertyMetadata([Double].NaN,

    FrameworkPropertyMetadataOptions.None,

    New PropertyChangedCallback(AddressOf OnCurrentReadingChanged),

    New CoerceValueCallback(AddressOf CoerceCurrentReading)),

    New ValidateValueCallback(AddressOf IsValidReading))

Посмотрите, как объявляется объект CoerceValueCallbackс делегатом, указывающим на метод CoerceCurrentReading, при объявлении которого свойство CurrentReadingсравнивается со свойствами зависимости Min/Maxи приводится при необходимости. Также обратите внимание на объявление объекта PropertyChangedCallbackс делегатом, указывающим на метод OnCurrentReadingChanged, который, в свою очередь, обеспечивает при необходимости приведение свойств зависимости Min/Max. В основном это делается для того, чтобы свойство зависимости Minбыло меньше Max и аналогично для свойства зависимости Max. В моем примере свойства зависимости Min/Maxна самом деле не изменяются, но я хотел показать, как это делается на случай, если это вам понадобится.

private static void OnCurrentReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

    d.CoerceValue(MinReadingProperty);  //вызов делегата CoerceValueCallback ("CoerceMinReading")

    d.CoerceValue(MaxReadingProperty);  //вызов делегата CoerceValueCallback ("CoerceMaxReading")

}

...

...

...

///

/// Приведение значения Coerce CurrentReading, если оно выходит за допустимые пределы

///

private static object CoerceCurrentReading(DependencyObject d, object value)

{

    Gauge g = (Gauge)d;

    double current = (double)value;

    if (current < g.MinReading) current = g.MinReading;

    if (current > g.MaxReading) current = g.MaxReading;

    return current;

}

И на VB.NET.

Private Shared Function CoerceCurrentReading(ByVal d As DependencyObject,

    ByVal value As Object) As Object

    Dim g As Gauge = DirectCast(d, Gauge)

    Dim current As Double = CDbl(value)

    If current < g.MinReading Then

        current = g.MinReading

    End If

    If current > g.MaxReading Then

        current = g.MaxReading

    End If

    Return current

End Function

'''

''' Приведение значения Coerce CurrentReading, если оно выходит за допустимые пределы

'''

Private Shared Sub OnCurrentReadingChanged(ByVal d As DependencyObject,

    ByVal e As DependencyPropertyChangedEventArgs)

    d.CoerceValue(MinReadingProperty)

    'invokes the CoerceValueCallback delegate ("CoerceMinReading")

    d.CoerceValue(MaxReadingProperty)

    'invokes the CoerceValueCallback delegate ("CoerceMaxReading")

End Sub

Здесь происходит приведение значения свойства зависимости CurrentReading в диапазоне от MinReadingи MaxReading, при этом выполняется условие Min < Max и Max > Min. Так что мы никогда не выйдем за привязанные значения для любого из этих трех свойств зависимости.

Проверка допустимости значения

Вспомним, что свойство зависимости CurrentReading объявляется следующим образом.

public static readonly DependencyProperty CurrentReadingProperty = DependencyProperty.Register(

    "CurrentReading",

    typeof(double),

    typeof(Gauge),

    new FrameworkPropertyMetadata(

        Double.NaN,

        FrameworkPropertyMetadataOptions.None,

        new PropertyChangedCallback(OnCurrentReadingChanged),

        new CoerceValueCallback(CoerceCurrentReading)

    ),

    new ValidateValueCallback(IsValidReading)

);

И на VB.NET.

Public Shared ReadOnly CurrentReadingProperty As DependencyProperty =

    DependencyProperty.Register("CurrentReading",

    GetType(Double),

    GetType(Gauge),

    New FrameworkPropertyMetadata([Double].NaN,

    FrameworkPropertyMetadataOptions.None,

    New PropertyChangedCallback(AddressOf OnCurrentReadingChanged),

    New CoerceValueCallback(AddressOf CoerceCurrentReading)),

    New ValidateValueCallback(AddressOf IsValidReading))

Одно из значений позволяет определить, содержит ли свойство зависимости допустимое значение. Для этого используется делегат ValidateValueCallback (в данном случае IsValidReading(object value)), что гарантирует применение к свойству зависимости только допустимых значений. Посмотрим на этот метод.

public static bool IsValidReading(object value)

{

    Double v = (Double)value;

    return (!v.Equals(Double.NegativeInfinity) && !v.Equals(Double.PositiveInfinity));

}

И на VB.NET.

Public Shared Function IsValidReading(ByVal value As Object) As Boolean

    Dim v As Double = CDbl(value)

    Return (Not v.Equals([Double].NegativeInfinity)

    AndAlso Not v.Equals([Double].PositiveInfinity))

End Function

На этом закончим

Можно много говорить о свойствах зависимости, но мы затронули самые основы — для этой статьи вполне достаточно. Если она вам понравилась, проголосуйте и оставьте комментарии. Тогда, может, прочитаете и следующую статью этой серии. Спасибо!

С уважением,

Саша Барбер.