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