
В 2006 году вместе с .NET 3.0 разработчикам были предоставлены программные платформы WPF и Silverlight. На протяжении следующих десяти лет Microsoft выпускала новые версии своей операционной системы и соответствующие им платформы. И вот, в 2016 году вместе с Windows 10 была выпущена Universal Windows Platform.
Все платформы отличались в той или степени возможностями API, но общий для них всех язык разметки XAML оставался практически неизменным. Поэтому все разработчики, вне зависимости от того, на какой платформе они работают, сталкиваются с одними и теми же задачами: расширение или изменение существующих, а также разработка новых элементов управления. Это очень важные навыки, необходимые для разработки приложений, удовлетворяющих требованиям дизайна и функциональности.
Эти задачи обусловлены тем, что на любой платформе разработчик располагает ограниченным набором элементов управления необходимых для разработки приложений. Его инструментарий составляют элементы из поставки Microsoft (в случае с UWP — Windows Universal Platform SDK) и от сторонних поставщиков или разработчиков. Даже все вместе они не могут покрыть всех требований, которые появляются при разработке приложений. Имеющиеся элементы управления могут не устраивать по ряду причин: внешний вид, поведение или функционирование. К сожалению, по сей день нет единого источника информации, который подробно и доступно освещал бы решения данных задач. Все, что остается разработчикам на протяжении длительного времени — собирать информацию в интернете крупица за крупицей.
Целью данной серии из трех статей является систематизация способов изменения, расширения и создания новых элементов управления.
Часть 1. Расширение существующих элементов управления
В первой части пойдет речь о расширении существующих элементов управления без вмешательства в их внутреннее устройство.
Предположим, что общее поведение и функционирование элемента управления устраивает разработчика, но его необходимо расширить. Так, например, элемент управления TextBox предоставляет возможность ввода данных, но лишен функционала валидации. Самый простой способ получить требуемый результат заключается в добавлении логики в code-behind представления (View) содержащей этот TextBox.
public sealed partial class MainPage : Page { public MainPage () { this.InitializeComponent (); textbox.TextChanged += Textbox_TextChanged; } private void Textbox_TextChanged (object sender, TextChangedEventArgs e) { // Some validation logic } }
Однако разработка на UWP предполагает использование архитектурного паттерна MVVM, одна из главных целей которого – отделение логики от представления. Следовательно, она должна быть инкапсулирована либо во ViewModel представления, либо в новый элемент управления взаимодействие, с которым будет осуществляться как с черным ящиком без нарушения принципов MVVM.
Таким образом, решение, представленное в листинге выше, подходит только в случае великой лени разработчика, уверенного в том, что на последующих стадиях разработки оно ему не аукнется. В случае же, если подобная валидация в рамках приложения потребуется более чем в одном месте, то это является прямым признаком того, что необходимо вынести данную логику в отдельную сущность.
Существует два способа расширения элементов управления без вмешательства в их внутренее строение и функционирование, реализацию которых можно сравнить с паразитизмом – присоединенные свойства и поведения.
Присоединенные Свойства (Attached Properties)
Присоединенное свойство – разновидность свойств зависимости, определяемое в отдельном классе и присоединяемое к целевому объекту на уровне XAML.
Рассмотрим механизм работы присоединенных свойств на вышеуказанном примере валидации TextBox для страницы регистрации.

Невалидная и валидная формы регистрации
Определим класс TextBoxExtensions, содержащий следующие присоединенные свойства:
1. RegexPattern – свойство, принимающее на вход строку шаблона валидации RegEx. В случае, если строка пустая считаем, что валидация поля ввода не требуется.
2. IsValid – свойство, содержащее значение текущего статуса валидации поля ввода на основании заданного в свойстве RegexPattern шаблона.
Также этот класс содержит метод OnRegexPatternChanged, срабатывающий при изменении значения свойства RegexPattern. Если его значение не пустое, то подписываемся на событие TextChanged элемента управления TextBox, в контексте которого работают свойства RegexPattern и IsValid.
В обработчике события Textbox_TextChanged вызываем метод ValidateText, валидирующий строку по переданному шаблону. Его результат присваиваем свойству IsValid.
public class TextBoxExtensions { public static string GetRegexPattern (DependencyObject obj) { return (string) obj.GetValue (RegexPatternProperty); } public static void SetRegexPattern (DependencyObject obj, string value) { obj.SetValue (RegexPatternProperty, value); } public static readonly DependencyProperty RegexPatternProperty = DependencyProperty.RegisterAttached ("RegexPattern", typeof (string), typeof (TextBoxExtensions), new PropertyMetadata (string.Empty, OnRegexPatternChanged)); public static bool GetIsValid (DependencyObject obj) { return (bool) obj.GetValue (IsValidProperty); } public static void SetIsValid (DependencyObject obj, bool value) { obj.SetValue (IsValidProperty, value); } public static readonly DependencyProperty IsValidProperty = DependencyProperty.RegisterAttached ("IsValid", typeof (bool), typeof (TextBoxExtensions), new PropertyMetadata (true)); private static void OnRegexPatternChanged (DependencyObject d, DependencyPropertyChangedEventArgs e) { var textbox = d as TextBox; if (textbox == null) { return; } textbox.TextChanged -= Textbox_TextChanged; var regexPattern = (string) e.NewValue; if (string.IsNullOrEmpty (regexPattern)) { return; } textbox.TextChanged += Textbox_TextChanged; SetIsValid (textbox, ValidateText (textbox.Text, regexPattern)); } private static void Textbox_TextChanged (object sender, TextChangedEventArgs e) { var textbox = sender as TextBox; if (textbox == null) { return; } if (ValidateText (textbox.Text, GetRegexPattern (textbox))) { SetIsValid (textbox, true); } else { SetIsValid (textbox, false); } } private static bool ValidateText (string text, string regexPattern) { if (Regex.IsMatch (text, regexPattern)) { return true; } return false; } }
Далее привязываем эти свойства к полям ввода в разметке и задаем значения свойства RegexPattern.
<TextBox Grid.Row="1" Grid.Column="1" ap:TextBoxExtensions.RegexPattern="." ap:TextBoxExtensions.IsValid="{x:Bind ViewModel.IsUserNameValid, Mode=TwoWay}" IsSpellCheckEnabled="False"/> <TextBox Grid.Row="2" Grid.Column="1" ap:TextBoxExtensions.RegexPattern="^\d{2}\.\d{2}\.\d{4}$" ap:TextBoxExtensions.IsValid="{x:Bind ViewModel.IsBirthdateValid, Mode=TwoWay}"/> <TextBox Grid.Row="3" Grid.Column="1" ap:TextBoxExtensions.RegexPattern="^([\w\.\-]+)@([\w\-]+)((\.(\w){2,4})+)$" ap:TextBoxExtensions.IsValid="{x:Bind ViewModel.IsEmailValid, Mode=TwoWay}" IsSpellCheckEnabled="False"/> <PasswordBox Grid.Row="4" Grid.Column="1" ap:PasswordBoxExtensions.RegexPattern="." ap:PasswordBoxExtensions.IsValid="{x:Bind ViewModel.IsPasswordValid, Mode=TwoWay}" />
Имеем чистый сode-behind.
public sealed partial class RegistrationView : UserControl { public RegistrationViewModel ViewModel { get; private set; } public RegistrationView () { this.InitializeComponent (); this.DataContext = ViewModel = new RegistrationViewModel (); } }
И логику доступности кнопки регистрации на уровне ViewModel.
public class RegistrationViewModel : BindableBase { private bool isUserNameValid = false; public bool IsUserNameValid { get { return isUserNameValid; } set { Set (ref isUserNameValid, value); RaisePropertyChanged (nameof (IsRegisterButtonEnabled)); } } private bool isBirthdateValid = false; public bool IsBirthdateValid { get { return isBirthdateValid; } set { Set (ref isBirthdateValid, value); RaisePropertyChanged (nameof (IsRegisterButtonEnabled)); } } private bool isEmailValid = false; public bool IsEmailValid { get { return isEmailValid; } set { Set (ref isEmailValid, value); RaisePropertyChanged (nameof (IsRegisterButtonEnabled)); } } private bool isPasswordValid = false; public bool IsPasswordValid { get { return isPasswordValid; } set { Set (ref isPasswordValid, value); RaisePropertyChanged (nameof (IsRegisterButtonEnabled)); } } public bool IsRegisterButtonEnabled { get { return IsUserNameValid && IsBirthdateValid && IsEmailValid && IsPasswordValid; } } }
Листинг класса PasswordBoxExtensions опущен, т.к. повторяет класс TextBoxExtensions чуть менее, чем полностью и существует только лишь по той причине, что оба элемента управления наследуются не от некоего абстрактного класса TextInput, от которого они могли бы получить общие поля и события, а от слишком общего класса Control.
Благодаря присоединенным свойствам, нам удалось расширить функционал существующих классов TextBox и PasswordBox без вмешательства в их внутренее строение. И нам даже не потребовалось порождать новый класс-потомок от них, что не всегда возможно.
Поведения (Behaviors)
Поведения появились в Expression Blend 3 с целью предоставить разработчикам механизм решения таких задач, возникающих на стороне пользовательского интерфейса, как: анимации, визуальные эффекты, drag-and-drop и т.п.
UWP не поставляет с собой библиотеку для работы с поведениями. Будучи частью Expression Blend SDK, её необходимо устанавливать отдельно, например, через Nuget.
Предположим, что мы работаем с элементом управления FlipView и требуется, чтобы при его пролистывании новый элемент воспроизводил анимацию появления.

Анимация поведения
Определим класс FlipViewItemFadeInBehavior, наследуемый от класса BehaviorT, где T – имя класса, к которому или потомкам которого можно добавлять требуемое поведение.
В нем переопределяем метод OnAttached, в котором подписываемся на событие SelectionChanged ассоциируемого объекта типа FlipView.
В обработчике события FlipView_SelectionChanged привязываем требуемую анимацию к новому элементу и запускаем её. Время воспроизведения анимации можем параметризовать определив свойство Duration.
public class FlipViewItemFadeInBehavior : Behavior<FlipView> { public double Duration { get; set; } protected override void OnAttached () { base.OnAttached (); AssociatedObject.SelectionChanged += FlipView_SelectionChanged; } protected override void OnDetaching () { base.OnDetaching (); AssociatedObject.SelectionChanged -= FlipView_SelectionChanged; } private void FlipView_SelectionChanged (object sender, SelectionChangedEventArgs e) { var flipView = sender as FlipView; var selectedItem = flipView.SelectedItem as UIElement; Storyboard sb = new Storyboard (); DoubleAnimation da = new DoubleAnimation { Duration = new Duration (TimeSpan.FromSeconds (Duration)), From = 0d, To = 1d }; Storyboard.SetTargetProperty (da, "(UIElement.Opacity)"); Storyboard.SetTarget (da, selectedItem); sb.Children.Add (da); sb.Begin (); } }
Теперь мы готовы добавить данное поведение к требуемым элементам управления в разметке.
xmlns:b="using:ArticleSandbox.Controls.Behaviors" xmlns:i="using:Microsoft.Xaml.Interactivity" <FlipView HorizontalAlignment="Center" VerticalAlignment="Center"> <FlipView.Items> <Rectangle Fill="Red" Width="200" Height="100"/> <Rectangle Fill="Green" Width="200" Height="100"/> <Rectangle Fill="Blue" Width="200" Height="100"/> </FlipView.Items> <i:Interaction.Behaviors> <b:FlipViewItemFadeInBehavior Duration="2"/> </i:Interaction.Behaviors> </FlipView>
Таким образом, нам удалось вынести логику анимации в отдельную сущность с последующей возможностью использования через определение в разметке.
Оба рассмотренных механизма расширяют существующие элементы управления и важно четко понимать, когда и какой механизм использовать.
Если необходимо расширить элемент управления какой-то логикой, то это признак того, что результата можно достичь посредством присоединяемых свойств. Если же элементу управления нужно предоставить какой-то визуальный эффект или анимацию, то стоит обратить внимание на механизм поведений.
Продолжение читайте во второй части: "Изменение существующих элементов управления"
Ян Мороз, старший .NET разработчик