Pull to refresh

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

Reading time4 min
Views11K


Краеугольным камнем разработки приложений для 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
Tags:
Hubs:
+10
Comments14

Articles