Создание Attached Property для BusyIndicator шаг за шагом

в 5:54, , рубрики: .net, attached properties, binding, silverlight, wpf, XAML, разработка, метки: , , , , ,

Данная статья является продолжением статьи Автоматический BusyIndicator для асинхронных операций и не только.


В случае использования attached property всё что нам нужно будет написать в XAML-е это:

<BusyIndicator AsyncIndicator.Attached="true" >
    <ListBox ItemsSource="{Binding DataList, IsAsync=true}" ...>
        ...
    </ListBox>
<BusyIndicator>

Как по мне — меньше уже и не придумаешь!

Начнём со стандартного прототипа attached property:

public static class AsyncIndicator
{
    static AsyncIndicator() { }

    public static readonly DependencyProperty AttachedProperty =
       DependencyProperty.RegisterAttached("Attached", 
           typeof (bool), typeof (ContentControl), 
           new FrameworkPropertyMetadata(false, AttachedChanged));

    public static Boolean GetAttached(UIElement element)
    {
        return (Boolean) element.GetValue(AttachedProperty);
    }

    public static void SetAttached(UIElement element, Boolean value)
    {
        element.SetValue(AttachedProperty, value);
    }

    private static void AttachedChanged(DependencyObject busyIndicator,
        DependencyPropertyChangedEventArgs e)
    {

    }
}

Что интересно, методы GetAttached и SetAttached теоретически не нужны и никогда не вызываются в нашем сценарии, но без них attached property не будет доступна.

Нам интересен метод события AttachedChanged. Общая идея такова: ищем у контента BusyIndicator'а свойство ItemsSource (точнее dependency property ItemsSourceProperty), eсли таковое свойство найдено — перехватываем момент изменения этого свойства. Если его значение равно null — включаем индикатор, иначе — выключаем.

Сразу же возникает небольшое затруднение: в момент вызова AttachedChanged контент у BusyIndicator'а ещё не установлен, что вообщем-то не удивительно.
Стандартного события типа ContentChanged у ContentControl'а я не нашёл, поэтому пришлось пойти обходым путём:

private static void AttachedChanged(DependencyObject busyIndicator,
    DependencyPropertyChangedEventArgs e)
{
    SetPropertyChangedCallback(ContentControl.ContentProperty, busyIndicator, 
        ContentChangedCallback);
}

private static void SetPropertyChangedCallback(DependencyProperty dp, 
    DependencyObject d, PropertyChangedCallback callback, bool reset = false)
{
    if (dp == null || d == null) return;
    var typ = d.GetType();
    var metadata = dp.GetMetadata(typ);
    var oldValue = metadata.SetPropValue("Sealed", false);
    metadata.PropertyChangedCallback -= callback;
    if (!reset)
        metadata.PropertyChangedCallback += callback;
    metadata.SetPropValue("Sealed", oldValue);
}

private static void ContentChangedCallback(DependencyObject busyIndicator,
    DependencyPropertyChangedEventArgs e)
{
    if (!(bool) busyIndicator.GetValue(AttachedProperty)) return;
    SetBusyIndicator(e.OldValue as DependencyObject, null);
    SetBusyIndicator(e.NewValue as DependencyObject, busyIndicator);
}

Прокомментирую немного данный код.
Метод SetPropertyChangedCallback получает метаданные dependency property ContentProperty и добавляет (или убирает) обработчик события по изменению значения этого свойства.
Тут есть один небольшой хак: дело в том, что просто так поменять метаданные после инициализации нельзя, о чём прямым текстом говорится в возникающем исключении. Однако анализ исходного кода модуля PropertyMetadata.cs показал, что признаком этой инициализации является internal свойство Sealed. Дабы не загромождать код примера, я не привожу реализацию метода SetPropValuе, но думаю написать изменение свойства объекта через отражение (reflection) ни у кого не составит труда.

Если кто-то из хабразнатоков WPF подскажет, как эту задачу решить более красиво — напишите в комментариях, плз.

Теперь метод ContentChangedCallback вызовется у нас в момент установки нового контента у BusyIndicator'a. Первая строчка этого метода очень важна — т.к. этот метод будет вызываться для всех BusyIndicator'ов, а не только того, у которого мы установили свойство AsyncIndicator.Attached="true". Поэтому мы проверяем чтобы значение этого свойства равнялось именно true.

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

Рассмотрим метод SetBusyIndicator:

private static readonly DependencyProperty _busyIndicatorProperty =
    DependencyProperty.RegisterAttached("%BusyIndicatorProperty%",
    typeof (ContentControl), typeof (DependencyObject));

private static void SetBusyIndicator(DependencyObject contentObject, 
    DependencyObject busyIndicator)
{
    if (contentObject != null)
    {
        SetPropertyChangedCallback(GetItemsSourceValue(contentObject), 
            contentObject, ItemsSourceChangedCallback, 
            busyIndicator == null);
        contentObject.SetValue(_busyIndicatorProperty, busyIndicator);
    }
    UpdateBusyIndicator(busyIndicator, contentObject);
}

private static object GetItemsSourceValue(DependencyObject contentObject)
{
    var itemsSourceProperty = contentObject.GetFieldValue("ItemsSourceProperty");
    return contentObject == null 
        ? null 
        : contentObject.GetValue(itemsSourceProperty);
}

private static void UpdateBusyIndicator(DependencyObject busyIndicator, 
    DependencyObject contentObject)
{
    if (busyIndicator == null) return;
    if (contentObject == null)
        busyIndicator.SetPropValue("IsBusy", false);
    else
    {
        var itemsSource = contentObject == null 
                ? null 
                : contentObject.GetValue(GetItemsSourceValue(contentObject));
        busyIndicator.SetPropValue("IsBusy", itemsSource == null);
    }
}

private static void ItemsSourceChangedCallback(DependencyObject contentObject,
    DependencyPropertyChangedEventArgs e)
{
    var busyIndicator = contentObject == null 
            ? null 
            : contentObject.GetValue(_busyIndicatorProperty) as DependencyObject;
    UpdateBusyIndicator(busyIndicator, contentObject);
}

Этот код тоже требует некоторых пояснений:
Во-первых, метод SetBusyIndicator устанавливает обработчик события на изменение dependency property ItemsSource уже знакомым нам способом.
Во-вторых, нужно каким-то образом сохранить ссылку на экземпляр busyIndicator в элементе управления contentObject для того, чтобы впоследствии contentObject знал какому BusyIndicator'y менять признак IsBusy. Самым простым и очевидным решением для этого, как мне показалось, является использование ещё одной приватной dependency property _busyIndicatorProperty.
В-третьих, в методе UpdateBusyIndicator мы через отражение устанавливаем значение свойства IsBusy у нашего BusyIndicator'а, сохранённого в _busyIndicatorProperty.

Полный текст примера

Спасибо за внимание.

Автор: LionSoft


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js