📜 ⬆️ ⬇️

Creating Attached Property for BusyIndicator step by step

This article is a continuation of the Automatic BusyIndicator article for asynchronous operations and more .


In the case of attached property all we need to write in XAML is:
 <BusyIndicator AsyncIndicator.Attached="true" > <ListBox ItemsSource="{Binding DataList, IsAsync=true}" ...> ... </ListBox> <BusyIndicator> 

As for me, you can’t think less!

Let's start with the standard 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) { } } 

Interestingly, the GetAttached and SetAttached theoretically not needed and are never called in our script, but the attached property will not be available without them.

We are interested in the AttachedChanged event method. The general idea is this: look for the content BusyIndicator 'and the ItemsSource property (more precisely, the dependency property ItemsSourceProperty ), if such a property is found, we intercept the moment of this property change. If its value is null - turn on the indicator, otherwise turn it off.

Immediately there is a slight difficulty: at the time of calling AttachedChanged content of BusyIndicator is not yet established, which is not surprising.
I did not find a standard event like ContentChanged in ContentControl , so I had to go around it:
 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); } 

I will comment on this code a little.
The SetPropertyChangedCallback method receives metadata of dependency property ContentProperty and adds (or removes) an event handler for changing the value of this property.
There is one small hack: the fact is that it is impossible to simply change the metadata after initialization, which is directly indicated in the exception. However, an analysis of the source code of the PropertyMetadata.cs module showed that the indication of this initialization is the internal Sealed property. In order not to overload the code of the example, I do not give an implementation of the SetPropValu method, but I think it would be easy for anyone to write a change in the property of an object through reflection.

If someone from WPF hackers tell you how to solve this problem more beautifully - write in the comments, plz.

Now the ContentChangedCallback method will be called up at the time of installation of new content for BusyIndicator 'a. The first line of this method is very important - because this method will be called for all BusyIndicator , and not just the one in which we set the AsyncIndicator.Attached="true" property. AsyncIndicator.Attached="true" . Therefore, we check that the value of this property is exactly true .

The second line of this method disables the event by changing the ItemsSource for the previous content, and the third, on the contrary, adds an event to the new content.

Consider the SetBusyIndicator method:
 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); } 

This code also requires some explanation:
First, the SetBusyIndicator method sets an event handler to change the dependency property ItemsSource change in a way we already know.
Secondly, you need to somehow save the link to the instance of busyIndicator in the contentObject control in contentObject for the contentObject know to which BusyIndicator 'y to change the IsBusy attribute. The simplest and most obvious solution for this, it seemed to me, was to use another private dependency property _busyIndicatorProperty .
Thirdly, in the UpdateBusyIndicator method UpdateBusyIndicator we set the reflection to set the IsBusy property of our BusyIndicator stored in _busyIndicatorProperty .

Full text of the example

Thanks for attention.

Source: https://habr.com/ru/post/142127/


All Articles