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

    В ответ на недавний пост про 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();
    

    Представленный там индикатор — это по сути специализированный диалог, предназначенный для нотификации пользователя о прогрессе асинхронной таски. Этот диалог делается один на все окно/приложение. Представленное же тут индикатор можно навешивать каждому контролу в отдельности, не боясь большого потребления памяти. Просто категории разные.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 13

      +2
      Вам, конечно, спасибо за старание, хоть какое-то.

      Не считая кода, значительная часть статьи — перечисление множественных недостатков вашей работы. Вас кто-то гнал, что «В связи с вышеизложенным и катастрофической нехваткой времени код отнюдь не блещет элегантностью», «В порыве лютого энтузиазма писал код без оглядки на «Best Practice Guides»»? Имейте уважение к читателям.

      А конкретно по реализации — по-моему, она чудовищна. Я запутался в адорнерах, бизиадорнерах, АдорнаблеЛейерах, адорнерманагерах, аттачах-детачах и прочем. В вашем подходе, получается, чтобы иметь возможность крутить индикатор занятости, нужно наследоваться от BaseAdornableControl?
      Для назначения адорнера у вас нужно писать кучу кода, подписываться на событие вьюмодели. Это не MVVM-подход.

      Простейшая схема:
      Контрол BusyIndicator, который занимается исключительно отрисовкой самого себя, имеет единственное депенденси-свойство IsRunning
      BusyIndicator хостится в том контроле, на котором нужно показывать прогресс, биндим его свойство IsRunning к свойству IsBusy вьюмодели.
      Будет как-то так:

      BusyIndicator IsHitTestVisible=«False» CacheMode=«BitmapCache» IsRunning="{Binding IsBusy}"

      И все.
      0
      > В вашем подходе, получается, чтобы иметь возможность крутить индикатор занятости, нужно наследоваться от BaseAdornableControl?
      Совсем не обязательно. Для добавления адорнера в произвольный контрол есть менеджер.

      > Для назначения адорнера у вас нужно писать кучу кода, подписываться на событие вьюмодели.
      Во-первых, один обработчик не есть «куча кода». Во вторых, вы получаете контроль над добавлением и удалением адорнера. В-третьих, адорнер не должен висеть в памяти, когда он не нужен.

      > BusyIndicator IsHitTestVisible=«False»
      Конкретно в ситуации адорнера занятости IsHitTestVisible должен быть равен True, чтобы перехватывать события мыши.

      > BusyIndicator хостится в том контроле, на котором нужно показывать прогресс, биндим его свойство IsRunning к свойству IsBusy вьюмодели.
      Для этого нужно будет делать адорнер на основе контрола, а это достаточно тяжелое решение. Опять же, нежелательно, чтобы выключенный, невидимый контрол потреблял память.
        +1
        А почему просто не взять готовый контрол из WPF Toolkit?
        Как вариант просто посмотерть как там в красках Good practice все сделано и не изобретать свои квадратноколесые велосипеды.
          0
          > А почему просто не взять готовый контрол из WPF Toolkit?
          Какой контрол вы имеете в виду?

          В начале поста я написал, что важно понимать каким образом можно их сделать. Какие варианты есть. Без понимания как работают адорнеры можно сделать ошибки, из-за которых потом придется наворачивать жуткие костыли.
          Пример тому пост, на который я ответил. Функционал готового продукта не устроил разработчика — пришлось лезть рефлекшном внутрь. И в итоге кода будет столько же, но «опасного» в плане стабильности.
          К тому же, сроки с заказчиком утверждены, и они не позволяют пройти процедуру соглосования лицензий того же Toolkit'а на стороне заказчика.
            0
            > Какой контрол вы имеете в виду?
            Вопрос снимается — перешел по ссылке… В статье дополнил про использование контролов. Это на самом деле целый диалог, а не легковесный адорнер.
              0
              Скопирую со своего комментария
              >Как вариант просто посмотерть как там в красках Good practice все сделано и не изобретать свои квадратноколесые велосипеды.

              То, что вы тут приводите в качестве примера набор говнокода, я полностью соглсен с первым комментарием.

              А потом, странно что вы на стадии создание архитектуры и ее согласования не понимали что вам нужны будут дополнительные контролы и задумались об этом только когда проект подходит к концу.
                0
                Во-первых, тут приведен код не из реального проекта.
                Во-вторых, раз уж началась-таки критика, то извольте указывать, что конкретно вы называете ГК и/или «как правильно писать.»
                В-третьих, это Agile. Сегодня кастомеру не нужны индикаторы, а завтра он захочет их поиметь. Будь добр исполни прихоть.
                  0
                  >Во-первых, тут приведен код не из реального проекта.
                  В виду того, что мы, как выяснелось, обсуждаем сферического коня в вакуме с надуманными вами гепотетическими ситуациями дисскусию предлагаю закрыть.
                    0
                    Ну раз у вас конкретных замечаний нет, то можно было и не начинать дискуссию. Тем более в заголовке ясно написано «Как бы… делал..».
                    А концепция как раз приближенная к практике. Если вы считаете использование Adorner'ов «говнокодом», то мне тоже нечего добавить.
                      –1
                      Я всего лишь хотел сказать и повторить слова, которые пишутеся почти на каждой странице книги Руководство Microsoft по проектированию архитектуры приложений. 2е издание: не изобретайте велосипеды, используйте готовые решения.
                      А от себя добавить, что если не знаете как изобретать — посмотрите как длеают это люди, кторые знают.
                        0
                        В качестве примера вы указываете на тулкит? Про него я отписался уже. Это решение тяжеловесное и сделано оно как ProgressBar.

                        Если у вас есть другие примеры — приведите.

                        > посмотрите как длеают это люди, кторые знают.
                        Вы думаете, что Telerik, DevExpress, Visifire, написанные известными компаниями, идеальны? Или в этих конторах тоже «незнающие девелоперы»? Нет и нет. Заюзав Visifire или Toolkit'овский DataGrid вы столкнетесь с конфликтом его логики и логики, которую требует заказчик.

                        Вот к примеру DatePicker тулкитовский. При размещении его в выпадающем меню возникают проблемы. Скрыть Popup с этим пикером логично по событию смены даты. Но проблема в том, что смена даты происходит по MouseLeftButtonDown. Из-за этого MouseLeftButtonUp срабатывает уже на контроле, находящемся под Popup'ом.
                        На лицо неправильное использование.

                        А с индикаторами, наследованными от контролов может получиться также. Новичок начнет втыкать их для каждой формочки или отдельного контрола, разведет таких диалого большое количество, а когда увидит, что его приложение жутко тормозит и жрет память немеренно, то станет лишь ругать разработчиков тулкита, визифайра и т.д., НО НЕ СЕБЯ.
          –1
          >Во-первых, тут приведен код не из реального проекта.
          В виду того, что мы, как выяснелось, обсуждаем сферического коня в вакуме с надуманными вами гепотетическими ситуациями дисскусию предлагаю закрыть.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое