Let’s consider how to implement the ability to drag items with Rx.NET.
The classic way to implement the drag and drop functionality is to implement event handlers for mouse down, mouse up and mouse move events. Then save the current drag state (enable it when the mouse button is pressed and disable it when the mouse button is released) and handle a mouse movement.
Using Rx.NET, you can create the same logic in a more compact form.
This example assumes using WinUI to create the UI. Other frameworks have different mouse event arguments.
Add the NuGet package to start using Rx.NET:
dotnet add package System.Reactive --version 6.0.1
You also need to use the following namespace:
using System.Reactive.Linq;
The following code implements the drag and drop logic:
var down = Observable
.FromEventPattern<PointerRoutedEventArgs>(this, nameof(Control.PointerPressed))
.Where(i => IsLeftButton(i.EventArgs));
var up = Observable
.FromEventPattern<PointerRoutedEventArgs>(this, nameof(Control.PointerReleased))
.Where(i => IsLeftButton(i.EventArgs));
var move = Observable
.FromEventPattern<PointerRoutedEventArgs>(this, nameof(Control.PointerMoved))
.Where(i => IsLeftButton(i.EventArgs))
.TakeUntil(up);
mouseMoveDisposable = down.Select(i => GetStartPoint(i.EventArgs))
.SelectMany(start => move.Select(i => CalculateDelta(start, i.EventArgs)))
.Subscribe(i => SetOffset(i.X, i.Y));
private bool IsLeftButton(PointerRoutedEventArgs e)
{
return e.GetCurrentPoint(this).Properties.IsLeftButtonPressed;
}
Pretty compact, huh?
Let’s consider the code line-by-line.
We create observables from events. The first two observables (down
and up
) generate the sequences when the mouse left button is pressed and released. The third (move
) generates the sequence while the mouse button isn’t released.
The logic is as follows: for each down
event subscribe to the move
event and pass the result to the SetOffset
method to apply the drag result. mouseMoveDisposable
is used to keep the reference to this subscription to dispose it after all.
Let’s consider the example where an object is placed at the center of the control:
x = ActualWidth / 2;
y = ActualHeight / 2;
We can use an offset to move an object:
x = offsetX + ActualWidth / 2;
y = offsetY + ActualHeight / 2;
All we need is to update the offset as the mouse moves. The following methods implement capturing the initial position, handling the offset and applying the result:
private Point GetStartPoint(PointerRoutedEventArgs eventArgs)
{
var start = eventArgs.GetCurrentPoint(this).Position;
// the coordinates where the mouse key is pressed
return new Point(start.X, start.Y);
}
private Point CalculateDelta(Point start, PointerRoutedEventArgs eventArgs)
{
var current = eventArgs.GetCurrentPoint(this).Position;
// calculate the delta from the start and the current mouse pointer positions
return new Point(current.X - start.X, current.Y - start.Y);
}
private void SetOffset(double x, double y)
{
// apply the offset
offsetX = x;
offsetY = y;
// update the control
Invalidate();
}
But there is one critical problem with this code.
When applying an offset, the object is moved away from the center by (offsetX, offsetY). But this will only work once: offsetX and offsetY are actually just the delta of the mouse pointer position from the mouse down and mouse up events. If a user tries to move the object again, it will be moved away from the center again (instead of its current position).
Let’s update GetStartPoint
method in the following way:
private Point GetStartPoint(PointerRoutedEventArgs eventArgs)
{
var start = eventArgs.GetCurrentPoint(this).Position;
return new Point(start.X - offsetX, start.Y - offsetY);
}
In this case, we capture not only the position of the mouse pointer but also the offset.
And… that’s it. A very simple and compact solution!
Learn Reactive Programming with the Reactive Extensions for .NET