В 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>

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

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

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

Продолжение читайте во второй части: «Изменение существующих элементов управления»
Поделиться с друзьями
-->

Комментарии (5)


  1. Smiz001
    08.06.2017 14:10

    Интересная статься. Тут сам недавно писал валидацию для текст бокса в WPF. А почему вы отказались реализовывать валидацию через ValidationRule?


    1. MobileDimension
      08.06.2017 14:50

      Спасибо за интересный вопрос! В свое время мы также были им озадачены. Ведь в WPF, как и в его предшественнике Windows Forms, представлены средства валидации. Но путь последующих платформ был тернист и в процессе они что-то теряли и приобретали. Так, в частности, были утрачены коробочные средства валидации: класс ValidationRule, свойства NotifyOnValidationError и ValidationRules у привязок. По этой причине в UWP разработчики вынуждены своими силами валидировать и форматировать введенные данные через события TextChanged или TextChanging


      1. Smiz001
        08.06.2017 16:24

        Буду знать, спасибо.


  1. dmitry_dvm
    08.06.2017 16:29

    Спасибо за описание бихевиоров, всё хотел вникнуть, как их самому делать, да подходящего повода не было.
    А валидировать мне удобнее через стандартный механизм из Prism https://blogs.msdn.microsoft.com/francischeung/2013/05/07/prism-for-windows-runtime-validating-user-input/
    Жду следующую статью. Напишите, пожалуйста, про создание контролов, у которых есть дефолтный шаблон, который можно менять из бленда по Edit a copy.


    1. MobileDimension
      08.06.2017 18:22

      Мы рады, что материал оказался полезным!
      Prism – хорошая библиотека. Просто валидация ввода через присоединенные свойства была по большей части примером, демонстрирующим функциональные возможности и способ применения данного механизма на задаче близкой к читателю.
      Следующая часть как раз и будет посвящена теме изменения существующих элементов управления и в частности интересующему вас механизму!