Расширение функциональности элементов управления Windows с помощью AttachedProperty

  • Tutorial


Краеугольным камнем разработки приложений для Windows (WPF, SilverLight, WP, WinRT) является паттерн MVVM. Который основан на концепции связывания данных модели представления и пользовательского интерфейса, что позволяет, используя декларативное описание UI посредством XAML избавится от codebehind (так я и не придумал/нашел русского перевода) и перенести всю логику работы с пользовательским интерфейсом в модель представления.

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

Написать данную статью меня побудила статья «Автоматическое выделение ссылок в универсальных приложениях Windows». В статье найдено решение конкретной проблемы и предложено работающее решение. Однако для его использования необходимо в codebehind для каждого текстового блока вызывать код. Более того если данные предполагают изменение в процессе работы необходимо следить за их изменением. В процессе своей работы такие решения встречаю довольно часто, они отличаются реализацией, но их все отличает одно неизменное свойство, сложность поддержки и сопровождения кода.

Для решения подобных задач, необходимо использовать присоединяемые свойства (AttachedProperty), данная технология предоставляет три необходимые для решения задачи возможности:

1) Хранить любое значение в контексте элемента управления для которого оно было задано
2) Уведомлять об изменении данных свойства
3) Использоваться для декларативного связывания в XAML

Решим задачу из приведенного выше примера с помощью присоединяемого свойства, для этого создадим новый статический класс с именем RtbEx и добавим в него описание нового AttachedProperty:

public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached("Text", typeof(string), typeof(RtbEx), new PropertyMetadata(default(string)));
        
public static void SetText(DependencyObject element, string value)
{
    element.SetValue(TextProperty, value);
}

public static string GetText(DependencyObject element)
{
    return (string) element.GetValue(TextProperty);
}

Мы указали для свойства имя Text и тип значения string. Отдельно обращу внимание на методы [Set|Get]Text они добавлены для следования рекомендованному шаблону объявления свойств и предназначены для упрощения доступа к значению свойства. Теперь мы можем использовать это свойство для хранения связанных с элементом управления данных.

var someText = “Some Text”;
RtbEx.SetText(richTextBlock, someText);
someText = RtbEx.GetText(richTextBlock);

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

public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached("Text", typeof(string), typeof(RtbEx), new PropertyMetadata(default(string), OnTextChanged));

Обработчик события изменения свойства должен иметь тип PropertyChangedCallback

private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
     var richTextBlock = d as RichTextBlock;
     if (richTextBlock == null)
     {
         return;
     }

     richTextBlock.Blocks.Clear();

     var text = e.NewValue as string;
     if (string.IsNullOrWhiteSpace(text))
     {
         return;
     }

     richTextBlock.Blocks.Add(CreateParagraph(text));
}

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

private static Paragraph CreateParagraph(string text)
{
    var paragraph = new Paragraph();

    var splitResult = Regex.Split(text, @"(https?://\S+)");
    foreach (var part in splitResult)
    {
        if (part.StartsWith("http", StringComparison.OrdinalIgnoreCase))
        {
            var hyperLink = new Hyperlink {NavigateUri = new Uri(part)};
            hyperLink.Inlines.Add(new Run {Text = part});

            paragraph.Inlines.Add(hyperLink);
            continue;
        }
                    
        paragraph.Inlines.Add(new Run {Text = part});
    }

    return paragraph;
}

Код формирования наполнения RTB не важен в контексте задачи и далек от идеала, он просто делит строку на блоки с ссылками и простым текстом, после чего строит иерархию представления документа RichTextBlock.

Оформленное таким образом расширение можно использовать из XAML с использованием обычных привязок данных, без использования дополнительного кода.

Для этого в документ XAML добавим пространство имен содержащее расширение

xmlns:ex="using:RtbEx.Extensions"

И зададим связывание с помощью обычного {binding}

<RichTextBlock ex:RtbEx.Text="{Binding SomeText}" FontSize="20"/>


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

Код тестового приложения доступен на Github: https://github.com/Viacheslav01/RtbEx
Share post

Similar posts

Comments 14

    +6
    Возможно, вы имели в виду паттерн MVVM (model-view-viewmodel)? MVVP выглядит, как MVP (model-view-presenter), который несколько отличается от MVVM.
      +1
      Да все верно, закралось пару опечаток, спасибо всем кто указал на ошибки!
        0
        На мой вкус вполне приемлемым переводом «codebehind» является «застраничный файл» или «застраничный код». Кто-то может не согласиться.
          0
          Мне понравился предложенный вариант «закулисный код»
            0
            А мне не нравится. Как-то по-театральному звучит :-D
              0
              Вот поэтому я и не смог определиться с переводом и использую codebehind :)
          • UFO just landed and posted this here
              0
              Да для каждого свой, статическая конструкция здесь, только для объявления метаданных свойства, вся остальная магия скрыта за реализацией DependencyProperty
        –1
        Использование присоединяемых свойств ограничено только вашим воображением,
        И производительностью компьютера, т.к. Boxing/Unboxing на каждый чих.
          0
          К сожалению, я не знаком с реализацией этого механизма, вы можете описать проблему детальнее?
            –1
            Вы ведь сами пишете
            public static void SetText(DependencyObject element, string value)
            {
                element.SetValue(TextProperty, value);
            }
            
            public static string GetText(DependencyObject element)
            {
                return (string) element.GetValue(TextProperty);
            }
            
            Разве не видно, где здесь упаковка?
              0
              Нет не вижу здесь упаковки. Но могу предположить, что она тут появится если типом свойства будет value тип. Однако использование этого кода не подразумевает множественных вызовов, а при малом количестве вызовов, упаковкой/распаковкой можно пренебречь.
            0
            В общем-то, все dependency properties — это сплошной boxing/unboxing. Волков бояться — в лес не ходить. Конечно, это вам не сишный код, где компилятор оптимизирует каждый чих, но за удобства нужно платить.
            0
            Спасибо, так намного удобнее!

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