В 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>
Таким образом, нам удалось вынести логику анимации в отдельную сущность с последующей возможностью использования через определение в разметке.
Оба рассмотренных механизма расширяют существующие элементы управления и важно четко понимать, когда и какой механизм использовать.
Если необходимо расширить элемент управления какой-то логикой, то это признак того, что результата можно достичь посредством присоединяемых свойств. Если же элементу управления нужно предоставить какой-то визуальный эффект или анимацию, то стоит обратить внимание на механизм поведений.
Продолжение читайте во второй части: «Изменение существующих элементов управления»
Автор: MobileDimension