Расширение, изменение и создание элементов управления на платформе UWP. Часть 1



    В 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 разработчик
    Mobile Dimension
    Mobile Dimension — разработчик мобильных решений
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 7

      0
      Интересная статься. Тут сам недавно писал валидацию для текст бокса в WPF. А почему вы отказались реализовывать валидацию через ValidationRule?
        0
        Спасибо за интересный вопрос! В свое время мы также были им озадачены. Ведь в WPF, как и в его предшественнике Windows Forms, представлены средства валидации. Но путь последующих платформ был тернист и в процессе они что-то теряли и приобретали. Так, в частности, были утрачены коробочные средства валидации: класс ValidationRule, свойства NotifyOnValidationError и ValidationRules у привязок. По этой причине в UWP разработчики вынуждены своими силами валидировать и форматировать введенные данные через события TextChanged или TextChanging
          0
          Буду знать, спасибо.
        0
        Спасибо за описание бихевиоров, всё хотел вникнуть, как их самому делать, да подходящего повода не было.
        А валидировать мне удобнее через стандартный механизм из Prism https://blogs.msdn.microsoft.com/francischeung/2013/05/07/prism-for-windows-runtime-validating-user-input/
        Жду следующую статью. Напишите, пожалуйста, про создание контролов, у которых есть дефолтный шаблон, который можно менять из бленда по Edit a copy.
          0
          Мы рады, что материал оказался полезным!
          Prism – хорошая библиотека. Просто валидация ввода через присоединенные свойства была по большей части примером, демонстрирующим функциональные возможности и способ применения данного механизма на задаче близкой к читателю.
          Следующая часть как раз и будет посвящена теме изменения существующих элементов управления и в частности интересующему вас механизму!
          0
          Спасибо большое за статью :)

          Хотел узнать, есть ли какая-то существенная разница, если в случае с Behaviors вместо написания своего, использовать EventTriggerBehavior + ControlStoryboardAction?

          На мой взгляд, в этом примере нагляднее будет прямо в XAML описать всю логику + Storyboard, чем смотреть в код класса.
            0
            Извините, что долго отвечали:) Честно говоря, в своей практике мы предпочитаем описывать собственные поведения внутри кода класса. Поправьте нас, если мы не правы, но в документации мы не обнаружили средств по расширению новых поведений сосбтвенными параметрами. В случае, если таких средств не предусмотрено, то считаем метод, описанный в данной статье более предпочтительным и гибким. Но если такие срадства предусмотены, то выбор способа зависит от индивидуальных предпочтений, либо от код-стайла принятого на проекте.

          Only users with full accounts can post comments. Log in, please.