- PVSM.RU - https://www.pvsm.ru -
В ответ на недавний пост про BusyIndicator решил поделиться своим опытом/виденьем данной проблемы. В статье представлена, на мой взгляд, более простая реализация индикатора занятости контрола. Сейчас любой может воспользоваться готовыми продуктами от маститых девелоперских контор, но проблема «Дырявой Абстракции» при этом становится весьма актуальной. Использование готовых индикаторов противоестественным для них образом неминуемо приводит к плачевным результатам. Поэтому очень важно представлять «как это работает».
Итак, да, но... дано:
<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 [3] (кстати, это мой макДоннальдс — 'cause I'm Lovin it...). В конструкторе главного окна сделана подписка на Action изменения IsBusy. В данном случае обработчик один, поэтому могу использовать Action. Иначе без делегата не обойтись было бы. Итак, в обработчике выставляется значение BusyAdorner у AdornableControl: null, чтобы отсоединить индикатор, не null чтобы присоединить.
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? надо попробовать! [4]).
Итак, выглядеть это будет так:
Не Бог весть что, но как демонстрация концепции сойдет.
<!-- В холодильнике мышь повесилась... скукотища.. смотреть не на что -->
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 достаточно сложная, и при потере бдительности можно огрести. В общем, я вас предупредил…
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.
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
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/5737
Ссылки в тексте:
[1] BusyIndicator: http://habrahabr.ru/post/141731/
[2] 10k Clock: http://designingwebinterfaces.com/wrong-way-ux
[3] TPL: http://msdn.microsoft.com/en-us/library/dd460717.aspx
[4] Game Loop? надо попробовать!: http://habrahabr.ru/post/136878/
Нажмите здесь для печати.