Как бы я делал BusyIndicator

в 22:37, , рубрики: .net, async pattern, mvvm, wpf, индикатор, разработка, метки: , , ,

В ответ на недавний пост про BusyIndicator решил поделиться своим опытом/виденьем данной проблемы. В статье представлена, на мой взгляд, более простая реализация индикатора занятости контрола. Сейчас любой может воспользоваться готовыми продуктами от маститых девелоперских контор, но проблема «Дырявой Абстракции» при этом становится весьма актуальной. Использование готовых индикаторов противоестественным для них образом неминуемо приводит к плачевным результатам. Поэтому очень важно представлять «как это работает».

Otma3ka

  • шКодил, будучи вдохновленным соседним постом про "BusyIndicator"
  • В порыве лютого энтузиазма писал код без оглядки на «Best Practice Guides»
  • Собственно и было интересно насколько хорошо я усвоил уроки и обновить свои внутренние "10k Clock"
  • В связи с вышеизложенным и катастрофической нехваткой времени код отнюдь не блещет элегантностью
  • Также код не является универсальным; возможно, его встраивание в уже имеющуюся архитектуру приложения окажется затруднительным
  • Зато просто и понятно (мне по крайней мере), а главное [hehe]в нем нет РЕФЛЕКШНА[/hehe]

Постановка задачи

Итак, да, но... дано:

  • Главное окно, в котором размещена некая форма ввода данных
    Как бы я делал BusyIndicator
  • Форма в окне является экземпляром класса BaseAdornableControl: UserControl (или наследника), который имеет свойство public BusyAdorner BusyAdorner
  • В сеттере этого свойства выполняется присоединение/отсоединение индикатора занятости
  • DataContext'у главного окна присвоено значение экземпляра демонстрационной ViewModel («а эту переменную мы назовем Пи с душкой» SimpleBusyAdornerDemoViewModel)
  • ViewModel имеет одно булиновое свойство IsBusy и вместо эвента изменения этого свойства имеется Action[bool]
  • Не стал заморачиваться с эвентом для простоты (не хотел объявлять дополнительно класс хэндлера и его аргумента)
  • Логика такова: при смене значения IsBusy дергается Action[bool] IsBusyChanged с новым значением IsBusy в качестве аргумента
  • Подписавшийся на Action[bool] IsBusyChanged производит выставку значения для свойства BusyAdorner экземпляра BaseAdornableControl либо в null (отсоединить адорнер), либо в ненулловое значение (присоединить адорнер)
  • Опять же для простоты положил в окно кнопку, которая инвертирует значение IsBusy во ViewModel, но МЫ ТО С ВАМИ ЗНАЕМ, ЧТО ViewModel САМА ДОЛЖНА ЭТИМ ЗАНИМАТЬСЯ, к примеру, при отправке запроса веб-сервису и приеме ответа

Главное окно

<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;
        }
    }
}

Тут все просто. В окне лежит форма, которую мы хотим пометить. Под ней кнопка, которая меняет во ViewModel значение свойства IsBusy. Как я уже написал, кнопка эта имитирует начало и конец работы некоей таски (асинхронной). Как реализована логика взаимодействия асинхронной таски с ViewModel'ю в данном случае не важно. Будем считать, что использована библиотека TPL (кстати, это мой макДоннальдс — 'cause I'm Lovin it...). В конструкторе главного окна сделана подписка на Action изменения IsBusy. В данном случае обработчик один, поэтому могу использовать Action. Иначе без делегата не обойтись было бы. Итак, в обработчике выставляется значение BusyAdorner у AdornableControl: null, чтобы отсоединить индикатор, не null чтобы присоединить.

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);

            
        }
    }
}

Подразумевается, что это некая «крутилка», порождающая жуткие меморилики индицирующая занятость ViewModel. В данном случае картинка будет статичная, но для вращательной динамики не хватает таймера для обновления угла у RotateTransform. Тут можно дать волю фантазии для анимации. Можно, кстати, использовать ту же таску из TPL для плавного изменения угла поворота рисунка (ХММ… Task в качестве Game Loop? надо попробовать!).
Итак, выглядеть это будет так:
Как бы я делал BusyIndicator
Не Бог весть что, но как демонстрация концепции сойдет.

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();
        }
    }
}

Важное замечание. Перед выгрузкой обернутого в адорнер контрола, следует, от греха (утечек памяти) подальше, отсоединять адорнер. Логика работы AdornerLayer достаточно сложная, и при потере бдительности можно огрести. В общем, я вас предупредил…

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]
    }
}

Ничего особенного для знакомых с паттерном MVVM, кроме «WTF-code» с Action'ом вместо event.

Дополнительная фишка — 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

    }
}

Это сервис, призванный облегчить навешивание адорнеров на произвольные контролы. Тоже какулька -можно было сделать его не синглтоном, а просто статическим классом, а список адорнеров там ПОКА ни к чему.

Заключение

Выкладывать на git или еще куда не вижу смысла, да и не хочется, честно говоря, с такой мелочью возиться. Для меня данный пост — сниппет, попытка привести мысли/знания в порядок, а также тикет на «habreview board». Но, возможно, кое-кому-то окажется полезным. Так что критикуем на здоровье, только давайте без холиваров насчет «коде-стайл-гайдов»… ОК?

Автор: HomoLuden


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


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