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

    Данная статья является продолжением статьи Автоматический 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.

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

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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0
      > Тут есть один небольшой хак… internal свойство… но думаю написать изменение свойства объекта через отражение (reflection)…
      Еще одна скверная идея. Раз в тэгах SL, то вот к чему это может привести, не говоря уже о производительности.
      Будет смешана тяжелая операция актуализации ItemsSource с достаточно тяжелой рефлексией. А ВЕДЬ, кажется, НИЧЕГО ЭТОГО НЕ НУЖНО.

      Объясните мне, пожалуйста зачем такие сложности?
        0
        Я же написал, что мне это тоже не очень нравится, хотя это работает. Если есть реальные предложения, как сделать это без хаков — напишите, ради продуктивной дискуссии я, в общем-то, и написал эту статью.

        >> А ВЕДЬ, кажется, НИЧЕГО ЭТОГО НЕ НУЖНО.
        А что нужно? Вы знаете более простой способ перехватить изменение контента у ContentControl'а и ItemsSource у Selector'а?

        >> Объясните мне, пожалуйста зачем такие сложности?
        Как бы в статье объясняется зачем. Что именно непонятно?
          0
          > Вы знаете более простой способ перехватить изменение контента у ContentControl'а и ItemsSource у Selector'а?
          Как раз это и не нужно при использовании паттерна MVVM. На вьюхе лежит ListBox, ItemsSource которого привязан к коллекции во ViewModel. Именно во ViewModel и должна лежать логика «что-когда-откуда». Вы же перетаскиваете логику во вьюху.
          ViewModel рулит всеми транзакциями, она же переключает флаг IsBusy. К этому флагу привязывайте что хотите на вьюхе.
          Я вижу как минимум два варианта:
          1) Использование ControlTemplate/Style с триггерами, по которым показывать/скрывать IsBusy адорнер
          2) Создание отдельного BusyAdorner, который подкладывается контролу в его AdornerLayer

          Можно отдельный пост на это написать… было б время.
            0
            Всё это требует отдельной разработки для каждой View и ViewMode, и если форм много — это займёт много времени на копипаст. Я же хотел свести время добавления BusyIndicator'ов к минимуму, затрагивая по возможности только один слой — View.
              0
              > Всё это требует отдельной разработки для каждой View и ViewMode, и если форм много — это займёт много времени на копипаст.
              1) ООП, бро! Вспоминаем о наследовании
              или
              2.1) Если у форм совершенно разная логика — то несколько ViewModel'ей + DataTemplate + на ContentControl навешивать индикатор занятости
              2.2) Логика схожая — одна ViewModel + DataTemplate +…

              Если не лениться и сделать как Best Practices советую, то добавочное время имплементации очень быстро окупится. А изложенная в статье минимизация очень легко превратится в «Pain in Da S» для самого автора и для членов команды.

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

      Самое читаемое