Руководство для начинающих. Часть 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, сообщаю, что они определяются следующим образом. 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. Но зачем они нужны? В процессе разработки платформы WPF майкрософтовская команда WPF решила, что систему свойств нужно изменить, чтобы обеспечить такие возможности, как уведомления об изменении, проверка значений, наследование значений, участие в привязках, анимации, стилях и шаблонах. Многое в подсистеме свойств зависимости напоминает подсистему перенаправленных событий, и аналогично этой подсистеме для определения собственных свойств зависимости необходимо выполнить несколько действий. А именно:
Это самая общая схема объявления свойств зависимости. Однако для реализации всех возможностей нужно хорошо владеть непростым синтаксисом свойств зависимости. Каждый этап будет рассмотрен отдельно в следующих подразделах. Но прежде необходимо понять, что такое приоритет значений. Так что сначала займемся этим. Очень важное примечание… Внимание! «Компилятор XAML учитывает оболочку при компиляции, однако во время выполнения платформа WPF вызывает непосредственно базовые методы GetValue и SetValue! Поэтому для обеспечения эквивалентности между заданием свойства в XAML и процедурном коде необходимо, чтобы оболочки свойств НЕ содержали никакой логики, кроме вызовов методов GetValue/SetValue. Если нужно добавить собственную логику, следует использовать зарегистрированные обратные вызовы. Вся платформа WPF построена на принципе соблюдения этого правила в оболочках свойств, так что это предупреждение касается каждого, кто создает собственный класс с собственными свойствами зависимости». Выпуск платформы Windows Presentation Foundation. Адам Натан (Adam Nathan), Sams. 2007 г. Приоритет значений свойств зависимости Одним из побочных эффектов новой системы свойств WPF, созданной для работы со свойствами зависимости, стала возможность получать значение этих свойств из различных мест. Например, для текущего свойства зависимости может быть задано значение по умолчанию и применена анимация. Какое значение примет свойство? Ответ на этот вопрос определяется приоритетом значений. Применяется следующий порядок очередности, начиная с самого старшего:
Не волнуйтесь, если вы не все поняли. Частично мы поговорим об этом в данной статье, а что-то разберем в последующих статьях этой серии о 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. Чтобы сделать это, необходимо следующее:
Вот так. Рассмотрим код. Он взят из проекта 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 можно предоставить системе свойств следующую информацию:
Всего один экземпляр FrameworkPropertyMetadata дает нам жесткий контроль над множеством метаданных свойства зависимости. На практике мы увидим это ниже, при обсуждении обратных вызовов и проверки. Проверка и обратные вызовы свойства зависимости и приведенные значения У вас, наверное, могло сложиться мнение, что свойства зависимости довольно сложные и мощные (Халк, одним словом). Но не следует забывать еще о нескольких принципах. Вот они:
Использование делегатов при первой регистрации свойства зависимости решает большинство из перечисленных задач. В состав прилагаемого решения (вверху статьи) входит проект Callback_Validation_DPs, демонстрирующий эти принципы. По сути, есть класс Gauge, наследуемый от класса Control, с тремя свойствами зависимости:
Каждое из них предназначено для проверки допустимости. Значение 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 На этом закончим Можно много говорить о свойствах зависимости, но мы затронули самые основы — для этой статьи вполне достаточно. Если она вам понравилась, проголосуйте и оставьте комментарии. Тогда, может, прочитаете и следующую статью этой серии. Спасибо! С уважением, Саша Барбер. |