Blazor Webassembly SVG Drag And Drop

Alexey Boyko
CodeX
Published in
9 min readOct 4, 2021

Demo | GitHub

Pic 4. Draggable supports nested Draggable, two-way binding, initialization without parameter binding
Blazor Webassembly SVG Drag And Drop Demo. Draggable component supports nested Draggable, two-way binding, initialization without parameter binding

This article describes a way to implement drag and drop of SVG objects. Along the way, the following points of development with Blazor were considered:

  • Templated components. Content of the templated component can be set in the parent component;
  • Passing events from parent component to child (Parent -> Child);
  • Problem of rewiring component input parameters inside component (Overwritten parameters problem);
  • Two-way binding between parent and child component. Those. an input parameter of a child component can modify both the parent component and the child;
  • How to use stopPropagation with Blazor.

What we will get in the end

The result is a Blazor component — Draggable. Example of use:

@inject MouseService mouseSrv;<svg xmlns="http://www.w3.org/2000/svg"
@onmousemove=@(e => mouseSrv.FireMove(this, e))
@onmouseup=@(e => mouseSrv.FireUp(this, e))>>
<Draggable X=250 Y=150>
<circle r="60" fill="#ff6600" />
<text text-anchor="middle"
alignment-baseline="central" style="fill:#fff;">Sun</text>
</Draggable>
</svg>

Listing 1. Using of Draggable component

Draggable along with it’s content will be dragged.

X and Y parameters supports two-way binding:

@inject MouseService mouseSrv;<svg xmlns="http://www.w3.org/2000/svg"
@onmousemove=@(e => mouseSrv.FireMove(this, e))
@onmouseup=@(e => mouseSrv.FireUp(this, e))>>
<Draggable @bind-X=X @bind-Y=Y>
<circle r="60" fill="#ff6600" />
<text text-anchor="middle"
alignment-baseline="central" style="fill:#fff;">Sun</text>
</Draggable>
</svg>
@code {
double X = 250;
double Y = 150;
}

Listing 2. Use of Draggable with two-way binding of X, Y

If you just need a ready-made solution, you don’t need to read the article — go straight to the block “How to use the Draggable component in your project” at the end.

Main idea

It is convenient to use the g grouping element and “translate” to the position of SVG objects:

<svg style="width:500px; height:300px"
xmlns="http://www.w3.org/2000/svg">
<g transform="translate(250, 150)">
<circle r="60" fill="#ff6600" />
<text text-anchor="middle"
alignment-baseline="central" style="fill:#fff;">Sun</text>
</g>
</svg>

Listing 3. Positioning a group of SVG objects using translate

Pic 1. Positioning a group of SVG objects using translate
Pic 1. Positioning a group of SVG objects using translate

To “drag” it is needed to subscribe to the mouse move event and change the “translate” values.

Templated component Draggable

Listing 1 shows the use of the Draggable component. Draggable is a templated component that wraps content in <g>:

<g transform="translate(@x, @y)">
@ChildContent
</g>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
double x = 250;
double y = 150;
}

Listing 4. Templated component Draggable wraps content in <g>

When x, y changes, the position of <g> along with the content will change.

To drag (to change x, y correctly), the following events are needed:

  1. Event when user starts dragging.
    It’s “onmousedown” on the <g> element. Note that <g> is not a rectangle, the borders of <g> follow the contours of nested objects. Those. for Listing 3, onmousedown will only fire inside the circle, which is exactly what is needed.
  2. Events of cursor movement when the mouse is pressed.
    Thinking “onmousemove” on the <g> element? No. With fast movements, the mouse goes beyond the bounds of <g> and “onmousemove” stops working. Therefore, “onmousemove” needs to be subscribed to the entire <svg> and the event must be forwarded to Draggable.
  3. Event when user finishes dragging — ups (lifts) the mouse.
    “onmouseup” on <g> is also not suitable because of the same reason: with fast movements, the mouse goes beyond the bounds of <g> and “onmouseup” doesn’t work. It turns out that “onmouseup” needs to be subscribed for the entire <svg>.

Passing events from parent component to child (Parent -> Child)

It turns out that we need a way to subscribe Draggable to the “onmousemove” and “onmouseup” events of the parent <svg>. This can be done using a singleton service:

// inject IMouseService into subscribers
public interface IMouseService {
event EventHandler<MouseEventArgs>? OnMove;
event EventHandler<MouseEventArgs>? OnUp;
}
// use MouseService to fire events
public class MouseService : IMouseService {
public event EventHandler<MouseEventArgs>? OnMove;
public event EventHandler<MouseEventArgs>? OnUp;
public void FireMove(object obj, MouseEventArgs evt)
=> OnMove?.Invoke(obj, evt);
public void FireUp(object obj, MouseEventArgs evt)
=> OnUp?.Invoke(obj, evt);
}

Listing 5. Use IMouseService into components that need to be subscribed to events. Use MouseService where events are fired

MouseService needs to be registered as a singleotn so that all components receive one service instance:

builder.Services
.AddSingleton<MouseService>()
.AddSingleton<IMouseService>(ff
=> ff.GetRequiredService<MouseService>());

Listing 6. MouseService registered as singleton

The MouseService can now be used.

Subscribing to <svg> events and fiering MouseService events:

@inject MouseService mouseSrv;<svg style="width:500px; height:300px" 
xmlns="http://www.w3.org/2000/svg"
@onmousemove=@(e => mouseSrv.FireMove(this, e))
@onmouseup=@(e => mouseSrv.FireUp(this, e))>>
<Draggable>
<circle r="60" fill="#ff6600" />
<text text-anchor="middle"
alignment-baseline="central" style="fill:#fff;">Sun</text>
</Draggable>
</svg>

Listing 7. Firing <svg> “onmousemove” and “onmouseup” events to the global singleton service

Subscribing to <svg> events inside Draggable:

@inject IMouseService mouseSrv;

<g transform="translate(@x, @y)" @onmousedown=OnDown>
@ChildContent
</g>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
double x = 250;
double y = 150;
protected override void OnInitialized() { mouseSrv.OnMove += OnMove;
mouseSrv.OnUp += OnUp;
base.OnInitialized();
}
void OnDown(MouseEventArgs e) {...}
void OnMove(object? _, MouseEventArgs e) {... x=... y=...}
void OnUp(object? _, MouseEventArgs e) {...}
public void Dispose() {
mouseSrv.OnMove -= OnMove;
mouseSrv.OnUp -= OnUp;
}
}

Listing 8. Draggable component. Subscribing to MouseService events and “onmousedown” of the <g> element

Pay attention to the Dispose method: you need to unsubscribe from subscriptions.

Now Draggable has all the necessary events inside:

  • OnDown — drag start event,
  • OnMove — changing mouse position,
  • OnUp — drag end event.

We can implement drag and drop algorithm:

  1. On “OnDown”, set the “mouse pressed” flag and remember the cursor position;
  2. On “OnMove” if “mouse pressed”:
    - Calculate the delta between the old and new cursor position;
    - Remember current cursor position;
    - Change x, y — add to the current delta values.
    The specific coordinates of the cursor are not important, the deltas are important.

For brevity, the code for the algorithm is not included here — take a look at GitHub.

Input parameters rewriting problem

Now Draggable is already working, but there is no way to set the starting position — i.e. set parameters x, y.

If we make the internal x, y fields as input parameters (Listing 9), then we can set the start position (Listing 10), but drag and drop will stop working.

...
@code {

[Parameter] double x { get; set; };
[Parameter] double y { get; set; };

void OnMove(object? _, MouseEventArgs e) {... x=... y=...}

Listing 9. Draggable component. Private x,y are made as public input parameters. Inside the component, x, y are updated (in the OnMove method). Not a working option.

<Draggable x=250 y=150>
...
</Draggable>

Listing 10. Setting Draggable input parameters in parent component

Drag and drop stops working due to Overwritten parameters problem. Those. overwriting input parameters inside the component. The following happens:

  • Draggable updates x, y
  • this causes the component to be rerendered
  • rerendering leads to resetting of input parameters, i.e. x, y become 250, 150 again.

From this follows the general rule: it is better to avoid updating the input parameters inside the component — this can lead to unexpected behavior.

We can solve the problem as follows:

  • leave the internal fields x, y as they were — do not make input parameters of them,
  • create separate properties for parameters,
  • set initial values of internal fields x, y on OnInitialized
...
@code {
...
double x;
double y;
[Parameter] double X { get; set; }
[Parameter] double Y { get; set; }
protected override void OnInitialized() {
x = X;
y = Y;
...
}

void OnMove(object? _, MouseEventArgs e) {... x=... y=...}

Listing 11. Draggable component. The x, y fields are initialized on OnInitialized

The disadvantage of this solution is that updating the input parameters after initialization does not affect anything: we cannot change the position of the object from the parent component. Further, this drawback will be fixed.

Two-way binding between parent and child component

Child -> Parent binding: the parameter is updated inside the component, the parent is notified of the change

Child-> Parent binding is done by adding “XChanged”, “YChanged” input parameters of type EventCallback. Naming rule: “{parameter name}Changed”.

...
@code {
..
double x;
double y;
[Parameter] double X { get; set; }
[Parameter] public EventCallback<double> XChanged { get; set; }
[Parameter] double Y { get; set; }
[Parameter] public EventCallback<double> YChanged { get; set; }
...
void OnMove(object? _, MouseEventArgs e) {
...
x=... y=...
XChanged.InvokeAsync(x);
XChanged.InvokeAsync(xy;
}

Listing 12. Draggable component. X, Y parameters with Child -> Parent binding

Now we can track X, Y changes in the parent component:

Solar system position: @X , @Y
<svg>
<Draggable @bind-X=X @bind-Y=Y>
...
</Draggable>
</svg>
@code {
double X = 250;
double Y = 150;
}

Listing 13. Using Draggable with X, Y binding

Pic 2. The child component changes the input parameters, the parent is subscribed to the changes
Pic 2. The child component changes the input parameters, the parent is subscribed to the changes

Parent -> Child binding: the parent component updates the parameters of the child

Now the parent can track changes in the Draggable position, can set the initial position, but cannot change the position after initialization.

The child component “out of the box” receives changes to the input parameters: setters X, Y are called every time a change is made in the parent component. The whole question is how to handle these events and avoid the problem of overwriting input parameters (see above).

Draggable to support both options:

  • setting the initial position without tracking changes,
  • tracking changes, and the ability to change position from the parent component.
setting the starting position without tracking changes
<Draggable X=250 Y=150>
...
</Draggable>

tracking changes
<Draggable @bind-X=X @bind-Y=Y>
...
</Draggable>

Listing 14. Two use cases for Draggable: with and without change tracking

The code becomes ugly.

...
double? x;
[Parameter]
public double X {
get { return x ?? 0; }
set { if (!x.HasValue || (!isDown & XChanged.HasDelegate)) {
x = value; } }
}
[Parameter] public EventCallback<double> XChanged { get; set; }
...
protected override void OnInitialized() {
mouseSrv.OnMove += OnMove;
mouseSrv.OnUp += OnUp;
base.OnInitialized();
}
bool isDown;void OnDown(MouseEventArgs e) {... isDown = true; }
void OnMove(object? _, MouseEventArgs e) {... }
void OnUp(object? _, MouseEventArgs e) {isDown = false; }

Listing 15. Draggable component. X, Y input parameters can be changed from parent component

Algorithm:

  • if initialization (!x.HasValue) — set the initial value of x,
  • if the component is currently being moved by the user (isDown) — we ignore the setting of the input parameter X
  • if the component is not currently being moved by the user and the input parameter is bound to the parent property (XChanged.HasDelegate) — update x.

Same for Y.

Pic 3. Two-way binding
Pic 3. Two-way binding

Draggable inside Draggable inside Draggable. stopPropagation with Blazor

Of course we should try putting Draggable into Draggable.

<Draggable @bind-X=X @bind-Y=Y>
<circle r="60" fill="#ff6600" />
<text text-anchor="middle"
alignment-baseline="central" style="fill:#fff;">Sun</text>
<Draggable X=173 Y=-15>
<circle r="35" fill="#1aaee5" stroke="#fff" />
<Draggable X=-57 Y=-38>
<text>Earth</text>
</Draggable>
<Draggable X=51 Y=-25>
<circle r="15" fill="#04dcd2" stroke="#fff" />
<Draggable X=-5 Y=-20>
<text>Moon</text>
</Draggable>
</Draggable>
</Draggable>
</Draggable>

Listing 16. Draggable inside Draggable

Pic 4. Draggable inside Draggable without stopPropagation

Nice, but impossible to use. If we drag the nested Draggable, the external one starts to drag too.

HTML mouse events such as “onmousedown” float from bottom to top. Those. first “onmousedown” fired for the inner element, after for the parent.

In Draggable, the drag start event is “onmousedown”. If we do not allow the event to bubble up to the parent component, then only the nested one will be dragged.

<g transform="translate(@x, @y)" cursor=@cursor @onmousedown=OnDown 
@onmousedown:stopPropagation="true">
@ChildContent
</g>

Listing 17. Draggable component. Prevent bubbling of the “onmousedown” event

Now we can draw the solar system:

  • The Moon moves around the Earth, so it drags at the same time as the Earth.
  • The Earth moves around the Sun, so the Earth and the Moon drags at the same time as the Sun.
Pic 4. Draggable supports nested Draggable, two-way binding, initialization without parameter binding
Pic 4. Draggable supports nested Draggable, two-way binding, initialization without parameter binding

How to use the Draggable component in your project

  1. Create MouseService — listing 5
  2. Register MouseService in Program.cs — listing 6
  3. Create Draggable component form GitHub
  4. Subscribe on SVG events onmousemove and onmouseup, and fire MouseService events — listing 1

Links

https://docs.microsoft.com/en-us/aspnet/core/blazor/components/?view=aspnetcore-5.0#overwritten-parameters-1

https://docs.microsoft.com/en-us/aspnet/core/blazor/components/data-binding?view=aspnetcore-3.1#parent-to-child-binding-with-component-parameters

https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/

https://visualstudiomagazine.com/articles/2020/01/27/suppressing-events-blazor.aspx

--

--