📜 ⬆️ ⬇️

How would I do BusyIndicator

In response to a recent post about BusyIndicator decided to share his experience / vision of this problem. The article presents, in my opinion, a simpler implementation of the employment control indicator. Now anyone can take advantage of ready-made products from venerable developer offices, but the problem of "Leaky Abstraction" at the same time becomes very relevant. The use of ready-made indicators in an unnatural way for them inevitably leads to disastrous results. Therefore, it is very important to present "how it works."

Otma3ka




Formulation of the problem


So yes, but ... given:


Main window


<Window x:Class="MyBusyAdorner.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="clr-namespace:MyBusyAdorner.Views" xmlns:adorners="clr-namespace:MyBusyAdorner.Adorners" Title="MainWindow" Height="350" Width="525"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <views:BaseAdornableControl x:Name="AdornableControl" BusyAdorner="{x:Null}" Margin="15"/> <Button Content="Attach/Detach" Grid.Row="1" Click="Button_Click"/> </Grid> </Window> 

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using MyBusyAdorner.ViewModels; using MyBusyAdorner.Adorners; namespace MyBusyAdorner { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private SimpleBusyAdornerDemoViewModel _viewModel; public MainWindow() { InitializeComponent(); DataContext = _viewModel = new SimpleBusyAdornerDemoViewModel(); _viewModel.IsBusyChanged = new Action<bool>((newValue) => { AttachDetachBusyAdorner(newValue); }); } private void AttachDetachBusyAdorner(bool isBusy) { AdornableControl.BusyAdorner = isBusy ? new BusyAdorner(AdornableControl) : null; } private void Button_Click(object sender, RoutedEventArgs e) { _viewModel.IsBusy = !_viewModel.IsBusy; } } } 

It's simple. In the window is the form that we want to mark. Under it is a button that changes the value of the IsBusy property in the ViewModel. As I already wrote, this button imitates the beginning and end of work of a certain task (asynchronous). How the logic of interaction between the asynchronous task and the ViewModel is implemented is not important in this case. We will assume that the TPL library has been used (by the way, this is my macDonnalds - 'cause I'm Lovin it ...). In the designer of the main window, the subscription to the Action is modified IsBusy . In this case, there is only one handler, so I can use Action. Otherwise, it would not be possible to do without a delegate. So, in the handler, the BusyAdorner value of AdornableControl : null is set to detach the indicator, not null to attach.
')

Busyadorner


 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Documents; using System.Windows; using System.Windows.Media; namespace MyBusyAdorner.Adorners { public class BusyAdorner : Adorner { public BusyAdorner(UIElement adornedElement) : base(adornedElement) { } protected override void OnRender(DrawingContext drawingContext) { var adornedControl = this.AdornedElement as FrameworkElement; if (adornedControl == null) return; Rect rect = new Rect(0,0, adornedControl.ActualWidth, adornedControl.ActualHeight); // Some arbitrary drawing implements. SolidColorBrush renderBrush = new SolidColorBrush(Colors.Green); renderBrush.Opacity = 0.2; Pen renderPen = new Pen(new SolidColorBrush(Colors.Navy), 1.5); double renderRadius = 5.0; double dist = 15; double cntrX = rect.Width / 2; double cntrY = rect.Height / 2; double left = cntrX - dist; double right = cntrX + dist; double top = cntrY - dist; double bottom = cntrY + dist; // Draw four circles near to center. drawingContext.PushTransform(new RotateTransform(45, cntrX, cntrY)); drawingContext.DrawEllipse(renderBrush, renderPen, new Point { X = left, Y = top}, renderRadius, renderRadius); drawingContext.DrawEllipse(renderBrush, renderPen, new Point { X = right, Y = top }, renderRadius, renderRadius); drawingContext.DrawEllipse(renderBrush, renderPen, new Point { X = right, Y = bottom }, renderRadius, renderRadius); drawingContext.DrawEllipse(renderBrush, renderPen, new Point { X = left, Y = bottom }, renderRadius, renderRadius); } } } 

It is understood that this is a kind of "twist", generating terrible memorials indicating the employment of the ViewModel. In this case, the picture will be static, but for the rotational dynamics there is not enough timer to update the angle of the RotateTransform . Here you can give free rein to the animation. You can, by the way, use the same TASK from the TPL to smoothly change the angle of rotation of the pattern (HMM ... Task as Game Loop? You have to try! ).
So, it will look like this:

Not God knows what, but as a demonstration of the concept will come down.

BaseAdornableControl


 <!--    ... ..     --> 

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using MyBusyAdorner.Adorners; namespace MyBusyAdorner.Views { /// <summary> /// Interaction logic for BaseAdornableControl.xaml /// </summary> public partial class BaseAdornableControl : UserControl { #region [Fields] //private List<Adorner> _adorners = new List<Adorner>(); private BusyAdorner _busyAdorner; #endregion [/Fields] #region [Properties] public BusyAdorner BusyAdorner { get { return _busyAdorner; } set { DetachBusyAdorner(); _busyAdorner = value; if (value != null) { AttachBusyAdorner(); } } } private void AttachBusyAdorner() { if (_busyAdorner == null) return; var adornerLayer = AdornerLayer.GetAdornerLayer(this); adornerLayer.Add(_busyAdorner); } private void DetachBusyAdorner() { var adornerLayer = AdornerLayer.GetAdornerLayer(this); if (adornerLayer != null && _busyAdorner != null) { adornerLayer.Remove(_busyAdorner); } } #endregion [/Properties] public BaseAdornableControl() { InitializeComponent(); this.Unloaded += new RoutedEventHandler(BaseAdornableControl_Unloaded); } void BaseAdornableControl_Unloaded(object sender, RoutedEventArgs e) { DetachBusyAdorner(); } } } 

Important note . Before unloading the control wrapped in the adorner, it is necessary, away from sin (memory leaks), to disconnect the adorner. The logic of the AdornerLayer is quite complex, and with a loss of vigilance you can ogresti. In general, I warned you ...

SimpleBusyAdornerDemoViewModel


 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel; namespace MyBusyAdorner.ViewModels { public class SimpleBusyAdornerDemoViewModel : INotifyPropertyChanged { #region [Fields] private bool _isBusy; #endregion [/Fields] #region [Properties] public bool IsBusy { get { return _isBusy; } set { if (value != _isBusy) { _isBusy = value; RaisePropertyChanged("IsBusy"); RaiseIsBusyChanged(); } } } public Action<bool> IsBusyChanged { get; set; } #endregion [/Properties] #region [Private Methods] private void RaiseIsBusyChanged() { if (IsBusyChanged != null) { IsBusyChanged(_isBusy); } } #endregion [/Private Methods] #region [INotifyPropertyChanged] public event PropertyChangedEventHandler PropertyChanged; private void RaisePropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } #endregion [/INotifyPropertyChanged] } } 

Nothing special for those familiar with the MVVM pattern, except “WTF-code” with Action instead of event .

Additional feature - BusyAdornerManager


 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Collections.ObjectModel; using MyBusyAdorner.Adorners; using System.Windows; using System.Windows.Documents; namespace MyBusyAdorner.Services { public sealed class BusyAdornerManager { #region [Fieds] private List<BusyAdorner> _adorners; #endregion [/Fieds] #region [Public Methods] public void AddBusyAdorner(UIElement adornedElement) { if (adornedElement == null) return; var adorner = new BusyAdorner(adornedElement); _adorners.Add(adorner); } public void RemoveAllAdorners(UIElement adornedElement) { if (adornedElement == null) return; var adornerLayer = AdornerLayer.GetAdornerLayer(adornedElement); foreach (var adorner in adornerLayer.GetAdorners(adornerLayer)) { adornerLayer.Remove(adorner); } } #endregion [/Public Methods] #region Singleton private static volatile BusyAdornerManager instance; private static object syncRoot = new Object(); private BusyAdornerManager() { } public static BusyAdornerManager Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new BusyAdornerManager(); } } return instance; } } #endregion } } 

This is a service designed to facilitate the hanging of adorners on arbitrary controls. Kakulka too, it was possible to make it not a singleton, but simply a static class, but there is no list of adrenners there.

Conclusion


To spread on git or else where I don’t see the point, and I don’t want to, frankly, bother with such a trifle. For me this post is a snippet, an attempt to bring thoughts / knowledge in order, as well as a ticket to the “habreview board”. But maybe someone will be useful. So we criticize on health, but let's not talk about “code-style-guides” without holivars ... OK?

UPD


To the question of the overhead of inheritance ... View for a form is generally a UserControl. Is it possible to write in XAML, for example, UserControlEx or the same BaseAdornableControl instead of UserControl - a big overhead?

To the question of using the pure MVVM approach ... It is easy to add to the BaseAdornableControl DependencyProperty and knit it to the IsBusy ViewModel'and. In the handler for changing this property, do the same that I have specified outside. This is much safer than building crutches with reflex to the internal property of the “3rd Party” product. Who knows what third-party developers will think to change inside themselves?

To the question of binding the adorner directly to the ViewModel property ... As I wrote in the comment, you will have to have a DependencyProperty in it, and for this you need to inherit the Adorner from Framework Element, for example. And just this will be a very serious overhead, especially if the adner is constantly in memory.
For interest, look for the Visifire code. Or at least SNOOP'om walk on the tree BarChart'a. There, for each bar in the chart, one or two intermediate canvases are created. In addition, DataPoint is inherited from FrameworkElement, either to have the opportunity to bind DataPoint to something, or to set the Color property (which, by the way, is not Color, but Brush). And the joke is that not these DataPoint'y, they are FrameworkElement'y, end up on the canvas in the chart. On them another collection of FrameworkElements is created anew, which is drawn. As a result, the Visifire charts are starting to slow down already on 600+ elements. For comparison: Dynamic Data Display -> Line Chart -> 60k elements (especially with smooth graphics) -> is normally drawn.
So, the decision to directly bind the adrenners to the ViewModel will just lead to a completely unnecessary overhead.

UPD2


On the use of the ToolKit indicator ...
There are comments ... read. For example, the code:
 BackgroundWorker worker = new BackgroundWorker(); worker.DoWork += (o, ea) => { //long-running-process code System.Threading.Thread.Sleep(10000); DispatchService.Dispatch((Action)(() => { //update the UI code goes here // ... })); }; worker.RunWorkerCompleted += (o, ea) => { this.ResetIsBusy(); //here the BusyIndicator.IsBusy is set to FALSE }; this.SetIsBusy(); //here the BusyIndicator.IsBusy is set to TRUE worker.RunWorkerAsync(); 

The indicator presented there is essentially a specialized dialogue designed to notify the user about the progress of the asynchronous task. This dialog is done alone for the entire window / application. The indicator presented here can be hung to each control separately, without fear of large memory consumption. Just different categories.

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


All Articles