19 April 2012

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

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

Otma3ka


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


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


Итак, да, но... дано:
  • Главное окно, в котором размещена некая форма ввода данных
  • Форма в окне является экземпляром класса 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? надо попробовать!).
Итак, выглядеть это будет так:

Не Бог весть что, но как демонстрация концепции сойдет.

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». Но, возможно, кое-кому-то окажется полезным. Так что критикуем на здоровье, только давайте без холиваров насчет «коде-стайл-гайдов»… ОК?

UPD


К вопросу об оверхэде наследования… View для формы — это в общем случае UserControl. Неужеле написать в XAML, к примеру, UserControlEx или тот же BaseAdornableControl вместо UserControl — большой оверхэд?

К вопросу об использовании чисто MVVM подхода… Легко добавить в BaseAdornableControl DependencyProperty и вязать его к IsBusy ViewModel'и. В обработчике изменения этого свойства делать то же, что я прописал снаружи. Это куда надежнее, чем строить костыли с рефлекшном к внутреннему свойству «3rd Party» продукта. Кто знает, что удумают разработчики сторонней либы изменить у себя внутри?

К вопросу о привязки адорнера напрямую к свойству ViewModel… Как я писал в комментарии, придется завести в нем DependencyProperty, а для этого адорнер нужно унаследовать от FrameworkElement, к примеру. И вот как раз это будет очень серьезный оверхэд, особенно если адорнер будет висеть в памяти постоянно.
Для интереса поисследуйте код Visifire. Или хотя бы SNOOP'ом пройдитесь по дереву BarChart'а. Там на каждый бар в чарте создается один или два промежуточных канваса. Кроме того, DataPoint наследуется от FrameworkElement, то ли чтобы поиметь возможность биндить DataPoint к чему-либо, то ли чтобы свойство Color (которое, кстати, не Color, а отнюдь Brush) выставить. И прикол в том, что не эти DataPoint'ы, они же FrameworkElement'ы, в итоге оказываются на канвасе в чарте. По ним заново создается еще одна коллекция FrameworkElement'ов, которая и отрисовывается. В результате чарты Visifire начинают тормозить уже на 600+ элементах. Для сравнения: Dynamic Data Display -> Line Chart -> 60k элементов (особо при гладком графике) -> нормально рисуется.
Так что, решение биндить напрямую адорнеры к ViewModel как раз и приведет к совершенно ненужному оверхэду.

UPD2


К вопросу об использовании индикатора ToolKit'а
Есть коментарии… почитайте. К примеру, код:
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();

Представленный там индикатор — это по сути специализированный диалог, предназначенный для нотификации пользователя о прогрессе асинхронной таски. Этот диалог делается один на все окно/приложение. Представленное же тут индикатор можно навешивать каждому контролу в отдельности, не боясь большого потребления памяти. Просто категории разные.
Tags:mvvmиндикаторAdornerLayerasync patternbusy indicatorwpfwpf.mvvm
Hubs: Website development .NET
+1
5.2k 11
Comments 13