В этом посте я хочу сравнить два решения одной и той же задачи. Одна реализация строится на классических событиях; вторая — на observable-коллекциях из библиотеки Rx.
Постановка задачи
Задача очень простая:- Есть WPF приложение с одним окном. Окно допускает изменение размеров и ограничено по минимальной вышине и ширине.
- В окне расположена картинка.
- Эту картинку нужно таскать внутри окна с помощью мышки.
- Нельзя допускать, чтобы картинка (или её часть) оказалась за границами окна, ни при перетаскивания, ни при изменения размеров окна.
Реализация через события
Разметка окна:<Window x:Class="Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Drag'n'Drop Sample (Events)" Height="400" Width="400" WindowStartupLocation="CenterScreen" MinHeight="150" MinWidth="150"> <Canvas x:Name="canvas" SizeChanged="Canvas_OnSizeChanged"> <Image x:Name="image" Source="http://upload.wikimedia.org/wikipedia/commons/7/7a/Rx_symbol.png" Width="100" Height="100" Canvas.Left="0" Canvas.Top="0" MouseDown="Image_OnMouseDown" MouseUp="Image_OnMouseUp" MouseMove="Image_OnMouseMove"/> </Canvas> </Window>Код:
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; public sealed partial class Window1 { private bool inDragMode; private Point offset; public Window1() { InitializeComponent(); } private void Image_OnMouseDown(object sender, MouseButtonEventArgs e) { // Переходим в режим перетаскивания inDragMode = true; // Запоминаем позицию мыши относительно картинки offset = e.GetPosition(image); // Захватываем мышь image.CaptureMouse(); } private void Image_OnMouseUp(object sender, MouseButtonEventArgs e) { // Отпускаем мышь image.ReleaseMouseCapture(); // Выходим из режима перетаскивания inDragMode = false; } private void Image_OnMouseMove(object sender, MouseEventArgs e) { if (!inDragMode) // Только в режиме перетаскивания return; // меняем позицию картинки // Позиция мыши относительно рабочей области var pointerPosition = e.GetPosition(canvas); // Корректируем новую позицию картинки ... var newPosition = CoerceImagePosition(pointerPosition.X - offset.X, pointerPosition.Y - offset.Y); // ... и передвигаем её SetImagePosition(newPosition); } private void Canvas_OnSizeChanged(object sender, SizeChangedEventArgs e) { // Если после изменения размеров рабочей области // картинка вылезает за её границы вправо или вниз, то ... if (canvas.ActualWidth < Canvas.GetLeft(image) + image.Width || canvas.ActualHeight < Canvas.GetTop(image) + image.Height) { // ... корректируем позицию картинки ... var newPosition = CoerceImagePosition(Canvas.GetLeft(image), Canvas.GetTop(image)); // ... и передвигаем её SetImagePosition(newPosition); } } private Point CoerceImagePosition(double x, double y) { // Корректируем позицию картинки таким образом, // чтобы она не вылезала за границы рабочей области return new Point(Math.Min(Math.Max(x, 0.0), canvas.ActualWidth - image.Width), Math.Min(Math.Max(y, 0.0), canvas.ActualHeight - image.Height)); } private void SetImagePosition(Point position) { // Передвигаем картинку Canvas.SetLeft(image, position.X); Canvas.SetTop(image, position.Y); } }
Реализация через Rx
Разметка окна:<Window x:Class="Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Drag'n'Drop Sample (Rx)" Height="400" Width="400" WindowStartupLocation="CenterScreen" MinHeight="150" MinWidth="150"> <Canvas x:Name="canvas"> <Image x:Name="image" Source="http://upload.wikimedia.org/wikipedia/commons/7/7a/Rx_symbol.png" Width="100" Height="100" Canvas.Left="0" Canvas.Top="0" /> </Canvas> </Window>Код:
using System; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; public sealed partial class Window1 { public Window1() { InitializeComponent(); // 1) Нажатия кнопки мыши // 2) преобразуются в позицию мыши относительно картинки // 3) и приводят к захвату мыши var mouseDown = /*1*/ Observable.FromEvent<MouseButtonEventArgs>(image, "MouseDown") /*2*/ .Select(evt => evt.EventArgs.GetPosition(image)) /*3*/ .Do(_ => image.CaptureMouse()); // 1) Отпускания кнопки мыши // 2) приводят к отпусканию мыши var mouseUp = /*1*/ Observable.FromEvent<MouseButtonEventArgs>(image, "MouseUp") /*2*/ .Do(_ => image.ReleaseMouseCapture()); // 1) Движения мыши // 2) преобразуются в позицию мыши относительно рабочей области var mouseMove = /*1*/ Observable.FromEvent<MouseEventArgs>(image, "MouseMove") /*2*/ .Select(evt => evt.EventArgs.GetPosition(canvas)); // 1) Каждую позицию мыши относительно картинки, // полученную в момент нажатия кнопки мыши // 2) и каждую позицию мыши относительно рабочей области, // полученную при движении мыши с зажатой кнопкой // 3) преобразуем в новую позицию картинки var dragPositions = /*1*/ from offset in mouseDown /*2*/ from mousePosition in mouseMove.TakeUntil(mouseUp) /*3*/ select CoerceImagePosition(mousePosition.X - offset.X, mousePosition.Y - offset.Y); // 1) Каждое изменение размеров рабочей области // 2) после которого картинка вылезает за её границы вправо или вниз // 3) преобразуем в откорректированную позицию картинки var canvasSizeChangedPositions = /*1*/ from evt in Observable .FromEvent<SizeChangedEventArgs>(canvas, "SizeChanged") /*2*/ where canvas.ActualWidth < Canvas.GetLeft(image) + image.Width || canvas.ActualHeight < Canvas.GetTop(image) + image.Height /*3*/ select CoerceImagePosition(Canvas.GetLeft(image), Canvas.GetTop(image)); // 1) Позиции картинки, полученные при перетаскивании, // 2) объединяем с позициями картинки, // откорректированными после изменения размеров рабочей области, // 3) и передвигаем картинку в новую позицию. /*1*/ dragPositions /*2*/ .Merge(canvasSizeChangedPositions) /*3*/ .Subscribe(SetImagePosition); } private Point CoerceImagePosition(double x, double y) { // Корректируем позицию картинки таким образом, // чтобы она не вылезала за границы рабочей области return new Point(Math.Min(Math.Max(x, 0.0), canvas.ActualWidth - image.Width), Math.Min(Math.Max(y, 0.0), canvas.ActualHeight - image.Height)); } private void SetImagePosition(Point position) { // Передвигаем картинку Canvas.SetLeft(image, position.X); Canvas.SetTop(image, position.Y); } }
Кто победил?
На мой взгляд, the Oscar goes to Rx — и вот почему:- Решение более декларативное за счёт LINQ.
- Отсутствует изменяемое состояние.
- Код работы с потоками событий сосредоточен в одном месте, а не разбросан по обработчикам.
- Код короче (не намного, но всё же).
Эдуард, спасибо за пример!
ОтветитьУдалитьЕсть один вопрос, имена событий указываются как строковые "магические константы". Если события берутся из стандартной библиотеки, то их еще можно вынести в отдельные константы. Но что если события описываем мы сами, и в последствии, в результате рефакторинга, их имена заменят. Не возникнет ли здесь проблема при рантайме?
Не думал ли ты над подходом получения имени события, как например:
public static class PropertyHelper
{
public static string GetPropertyName(Expression> expr)
{
return (MemberExpression)expr.Body).Member.Name;
}
}
ИМХО, Есть несколько но:
ОтветитьУдалитьа) придет какой-нибудь начинающий разработчик и хрен разберется с тем кодом, который у вас написан с Rx.
б) Код будет посложнее - хрен его отдебажите нормально.
Может быть я и не прав. Особо Rx не пользовался.
linq дебажить та ещё забава, на самом деле
ОтветитьУдалитьTo Тимур Ф.:
ОтветитьУдалитьУ метода Observable.FromEvent есть три перегрузки:
1. Observable.FromEvent<MouseButtonEventArgs>(
image, "MouseDown");
2. Observable.FromEvent<MouseButtonEventArgs>(
rxHandler => image.MouseDown += rxHandler.Invoke,
rxHandler => image.MouseDown -= rxHandler.Invoke);
3. Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
rxHandler => rxHandler.Invoke,
eventHandler => image.MouseDown += eventHandler,
eventHandler => image.MouseDown -= eventHandler);
Последние две reflection не используют.
>Zagrebelin комментирует.
ОтветитьУдалитьДля этого существуют UnitTests, а Rx очень и очень здорово тестируется
> а) придет какой-нибудь начинающий разработчик и хрен разберется с тем кодом, который у вас написан с Rx
ОтветитьУдалитьСферический начинающий разработчик в вакууме в состоянии засрать код любой степени тривиальности, Rx тут особой погоды не делает. Но это совершенно не повод писать приложение через GoTo.
Я может быть не прав, но выскажусь.
ОтветитьУдалитьМне второй код вариант нравится - он, скажем так, изящный за счет своей декларативности и реализации всей логики в одном месте.
Но это сферический код в вакууме. Реалиный код будет сложнее и сложнее для чтения/понимания. А насчет проблем с дебагом сдесь уже писали.