A Cross-platform Drag and Drop library for Blazor Part II

In Part I, I walked through the details of simple JavaScript module I created to support drag and drop functionality in Blazor. In this post I will go over the design of the C# class which leverages the JavaScript module and provides the ability to send movement data to Blazor components.


        public DragDropInterop(IJSRuntime jsRuntime)
        {
            ModuleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
                "import", "./_content/Finaltouch.DragDrop.Components/dragDropInterop.js").AsTask());
            ObjRef = DotNetObjectReference.Create(this);
        }

        public async ValueTask Initialize(Func<DragDropResult, Task>? func, DragDropOptions options)
        {
            if (func == null)
            {
                throw new ArgumentNullException(nameof(func));
            }
            Func = func;
            options ??= new DragDropOptions();
            JsonSerializerOptions SerializerOptions = new()
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                WriteIndented = false,
            };
            var jsonString = JsonSerializer.Serialize(options, SerializerOptions);
            var value = await ModuleTask.Value;
            await value.InvokeVoidAsync("initialize", ObjRef, jsonString);
        }
        
        public async ValueTask AddListeners()
        {
            var value = await ModuleTask.Value;
            await value.InvokeVoidAsync("addListeners");
        }

        [JSInvokable]
        public async Task OnPointerUp(DragDropResult result)
        {
            if (Func != null)
            {
                await Func(result);
            }
        }

The DragDropInterop class is intended for use as a dependency injection service. Following best practices described in the Microsoft documentation and examples for JavaScript interoperability, I use lazy instantiation to create a reference to the JavaScript module. Three exported methods are called by this class. Before any drag and drop functionality can be used, you have to call Initialize() on this class. Initialize receives a Func delegate and any non-default options that you want to specify, for example whether to disable sorting or non-default classnames for containers and draggable items in your Blazor components. The Func delegate is is used so you can apply changes to the component UI. It's deliberately meant to call an async method so that you can easily call external REST services, etc. when you update your UI.

The second method AddListeners() is called after Initialize has completed. You might wonder why I didn't simply add my task / item listeners as part of the initialization process. The reason is simple. The Func delegate we pass to this class must call StateHasChanged() in order to signal the Blazor framework that the UI needs to be updated. Remember that this JavaScript module was specifically designed not to mutate the DOM. The delegate makes sure that all DOM changes are managed by Blazor. Unfortunately, when you call StateHasChanged(), any event listeners which we added earlier are removed, thus we have to add back new ones before we can initiate another drag and drop operation. You call AddListeners() in the OnAfterRenderAsync() method of your Blazor components so that any task/item elements to which you add listeners are already created in the DOM.

The OnPointerUp() method is called by the JavaScript module when the user releases a mouse button, or removes their finger from a touch screen, etc. Note that the method is only called if a change in the DOM needs occurred. If the the pointer is released when it is not located over a container (target), then there is really no need to update the DOM.


	builder.Services.AddScoped(typeof(DragDropInterop));

Be sure to remember to register this service in the Program.cs class of your WASM client project so you can inject it into your Blazor pages and components. In Part III we'll examine my demo project to give you a better idea of how to apply this library in your own applications. Remember, all the code here is available from my public GitHub repository.

A Cross-platform Drag and Drop library for Blazor - Part 1

There is a long standing request from the Blazor development community for Microsoft to provide support for drag and drop functionality. In an article from Visual Studio Magazine (3/30/2022), author David Ramel details past efforts made towards adding this feature, but sadly, Blazor development manager Daniel Roth; "After investigating various approaches we've decided that a general purpose drag & drop feature isn't something we can easily add to the Blazor framework."

Like a few other developers, I experimented with the HTML5 Drag and Drop API, but its primary shortcoming is that it is not fully supported by some browsers, in particular mobile browsers. Roth's recommendation is to leverage JavaScript interoperability and rely on use of a JavaScript library for this functionality.

The Ramel article cites a 2021 presentation by Blazor creator Steve Sanderson which demonstrates use of the draggable.js library to create a calendar appointment application. I tried my hand at integrating draggable.js, as well as a few other well-known JavaScript libraries like lmdd.js (Lean and Mean Drag and Drop) and dragula.js (the name is rather clever) but found all of them lacking, at least for the use cases I have in mind.

The biggest problem with all of the libraries is that they mutate the Document Object Model (DOM), which according to the Blazor JavaScript interop documentation is a cardinal sin. It makes sense that you don't want to muck around under the covers with the DOM given how Blazor follows a different paradigm that says, "don't worry about the DOM, we'll manage that in our framework". Indeed, I quickly discovered that every time you call StateHasChanged() or initiated an automatic rerendering of the UI by changing parameter values, or use of an EventCallBack, any changes made by these third party libraries disappeared, or altered the drag and drop behavior in unexpected ways.

After numerous failed experiments using other peoples' libraries, I decided to create my own simple drag and drop JavaScript library that does not mutate the DOM. By using specific JavaScript events (pointerdown, pointermove and pointerup), my library displays the movement of objects on the screen, and uses a JSInvokable method to send data on what changes were made back to a .NET object. That object in turn, sends the data to one or more Blazor components which handle any changes to the UI.

I'll explain this process by walking through an example and showing the associated code. You can find all the code presented here, and a working sample in my GitHub repository. I noticed that a number of drag and drop demos are a form of draggable ToDo list. I borrowed the design of my sample from a demo created for lmdd.js. In the figure shown below, there are three colored containers labeled Tasks, High Priority and Done. I've predefined some tasks and placed some in each of the first two containers. The application is designed so that a user can drag a given task by it's handle (the menu hamburger icon on the left side of each task) to either of the other containers. In a later post, I'll demonstrate how to add some logic to limit where a task can be placed, but for now, any task can be dragged to any container.

Demo available at: https://rlebowitz.github.io/Finaltouch.DragDrop/

The two most important items in my solution are the JavaScript module, DragDropInterop, and the the C# class, DragDropInterop.cs which uses Javascript Interoperability to call specific methods and pass data to the module, and receives data sent back from the module at the end of a drop event. The post (Part I) will focus on the JavaScript module, my next blog post will discuss its C# counterpart.


function DragDropInterop() {
    let options, draggingElement, rect, sourceContainerId, sourceItemId, raf;
    let x, y, deltaX, deltaY;
    x = y = deltaX = deltaY = 0;

    /**
        Default options which can be overridden by passing in a DropDropOptions object 
     */
    const defaults = {
        componentClass: 'dd-component',
        containerClass: 'dd-container',
        itemClass: 'dd-item',
        handleClass: 'handle',
        sort: true
    };
    
    /**
     * Method used to merge DragDropOptions (C#) values with the predefined default option values.
     * Values passed in override the defaults automatically.
     * @param {any} settings
     */
    const assignOptions = function (settings) {
        var target = {};
        Object.keys(defaults).forEach(function (key) {
            target[key] = (Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : defaults[key]);
        });
        options = target;
    };

    /**
     * Public exported method used to initialize the various options associated with this module and 
     * store the DotNet object used to call methods labeled with JSInvokable.
     * @param {any} helper - the specified DotNet object
     * @param {any} jsonOptions - the serialized DragDropOptions object
     */
    this.initialize = function (helper, jsonOptions) {
        dragDropHelper = helper;
        // assign the options
        assignOptions(JSON.parse(jsonOptions));
    };
    
    ...
}

The initialize method is early in the lifecycle of any Blazor page or component that will use drag and drop. It accepts a JSON serialized object specifying CSS class values used to identify container, task item and handle elements in the DOM. There is also an option to determine whether to sort objects or not. In the current application, sorting is useful so I've set the property value to true. I added the ability to disable this functionality since there are likely other applications where sorting isn't used.


    /**
     * Method used to add event listeners to all of the draggable items/elements.
     */
    this.addListeners = function () {
        let items = Array.from(document.querySelectorAll(`.${options.itemClass}`));
        items.forEach(item => {
            item.addEventListener('pointerdown', pointerDown, { passive: true });
            if (!options.handleClass) {
                // change the item's cursor to the move cursor
                item.classList.add('moveCursor');
            }
            else {
                let handle = item.querySelector(`.${options.handleClass}`);
                if (!!handle) {
                    handle.classList.add('moveCursor');
                }
            }
        });
    };
    /**
     * Method used to remove the event listeners from all draggable items/elements.
     */
    const removeItemListeners = function () {
        let items = Array.from(document.querySelectorAll(`.${options.itemClass}`));
        items.forEach(item => {
            item.removeEventListener('pointerdown', pointerDown, { passive: true });
        });
    };

The next two methods are (obviously) used to add and remove a pointerdown event to each task item element in the DOM. The addListeners method is exported by the module; the removeItemListeners method is a private method called internally. Note that I've added a feature to change the cursor that appears when the cursor is over an item or its handle. I used handles in my app but you could have the cursor modified for the entire item by specifying the handleClass option as an empty string. You'll note that at this point in time, only event listeners for the pointerdown event are applied. This is deliberate; we don't want to listen for the other events just yet. I've used the various pointer events for convenience sake. These events allow the application to accept input from a mouse, touchscreen or other input device. In some third party libraries I've seen developers specify many different kinds of listeners to handle the different types of input; the pointer events seemed more convenient since they require less coding.


    /**
     * Private method called when the left button of a mouse is held down, or a touch screen is touched on
     * a draggable item/element.
     * @param {any} event - a 'pointerdown' event.
     */
    const pointerDown = function (event) {
        if (options.handleClass && event.target.classList.contains(options.handleClass)) {
            // the handle is presumably within the draggable item, so locate the item itself
            draggingElement = event.target.closest(`.${options.itemClass}`);
        }
        else {
            if (event.target.classList.contains(options.itemClass)) {
                draggingElement = event.target;
            }
            else {
                draggingElement = event.target.closest(`.${options.itemClass}`);
            }
        }
        if (!!draggingElement) {
            draggingElement.classList.add('dragging');
            x = event.clientX;
            y = event.clientY;
            rect = draggingElement.getBoundingClientRect();
            var container = draggingElement.closest(`.${options.containerClass}`);
            if (container) {
                sourceContainerId = container.dataset.containerId;
                sourceItemId = draggingElement.dataset.itemId;
            }
            document.addEventListener('pointermove', pointerMove, { passive: true });
            document.addEventListener('pointerup', pointerUp, { passive: true });
        }
    }

The pointerdown event handler is where we begin to track dragging movement. The initial code just identifies the actual element being dragged based on whether you are using a handle or not. After that we begin to store the input device's initial coordinates and those of the bouding rectangle of the element we're dragging. Next, we identify the container (task list) that the task element we're dragging is located in initially, as well as the unique identifier of the task item. Both these pieces of data are important to have when we send data back to the C# object that uses this module. In my component implementations I automatically assign random container and item identifiers using data attributes. These values are used in a couple of ways as will be shown later. Finally, I add the pointermove and pointerup event listeners to the document itself. In my application the ToDo component is the only component that will use drag and drop. In other applications, you may want to place your code in a top level div element and apply the listeners to that element instead. I found that if I went outside the boundary of my ToDo component, seen as a black border, the drag and drop functionality would stutter. The technical term I've seen used to describe this behavior is janky.


    /**
     * Private method called when the mouse or pointer device is moved.  
     * This method is called very frequently.  The request animation frame API is 
     * leveraged to smooth the appearance of dragging the item/element.
     * @param {any} event - a 'pointermove' event.
     */
    const pointerMove = function (event) {
        if (!raf) {
            deltaX = event.clientX - x;
            deltaY = event.clientY - y;
            raf = requestAnimationFrame(pointerMoveRAF);
        }
    };
    /**
     * Private method called by the request animation frame that animates the item/element's movement.
     * Once the element is finished rendering the frame variable is released to allow the next rendering.
     */
    const pointerMoveRAF = function () {
        draggingElement.style.transform = `translate3d(${deltaX}px, ${deltaY}px, 0px)`;
        raf = null;
    };

The two methods listed above are where the animation magic used to make the task element appear to move across the screen occurs. The pointerMove event handler method leverages the Request Animation Frame API which is designed to render the UI at a much higher frame rate. The important thing to remember is that the movement is simulated, there is no actual physical change to the DOM taking place.


    /**
     * Private method called when the mouse button or pointer device is released (dragging has completed).
     * @param {any} event - 'pointerup' event
     */
    const pointerUp = function (event) {
        // clean up
        document.removeEventListener('pointermove', pointerMove);
        document.removeEventListener('pointerup', pointerUp);
        // if the animation frame rendering was in process when the button or pointer device was release,
        // this will cancel the scheduled rendering.
        if (raf) {
            cancelAnimationFrame(raf);
            raf = null;
        };
        draggingElement.style.left = `${rect.left + deltaX}px`;
        draggingElement.style.top = `${rect.top + deltaY}px`;
        draggingElement.style.transform = 'translate3d(0px,0px,0px)';
        if (deltaX == 0 && deltaY == 0) {
            // item wasn't moved, so ignore the event
            return;
        }
        deltaX = deltaY = 0;
        // locate the container on which the item/element was dropped (if any)
        let element = document.elementFromPoint(event.clientX, event.clientY);he 
        let container = element.closest(`.${options.containerClass}`);
        if (container) {
            let afterElement;
            if (options.sort) { 
                afterElement = getDragAfterElement(container, event.clientY);
            }
            let targetItemId = !!afterElement ? afterElement.dataset.itemId : '';
            // create result object
            let result = new DragDropResult(sourceItemId, sourceContainerId, targetItemId, container.dataset.containerId);
            // remove the listeners to avoid possible memory leaks
            removeItemListeners();
            // pass back the DragDropResult to the C# object
            dragDropHelper.invokeMethodAsync('OnPointerUp', result);
        }
    }

The final event handler has several key functions. The first half of the code used to clean up a few things; remove the pointermove and pointerup event listeners, cancel the animation, and reset a couple of the module variables. The second half is going to gather data on which container and (if applicable) where the task item was dropped within the container. Note that if you don't drop the task item on a container, the task will simply remain where it was when you began dragging it. At the very end of this method we perform some cleanup by removing all the pointerdown event listeners, and finally we call the method OnPointerUp in the C# object that uses this module and passes it four pieces of data; the unique identifiers of the initial (source) container, the task item we dragged, the target container where we dropped the task item, and, if one exists, the task item upon which the dragged item was dropped.

There is one last vital bit of information that I need to add related to the pointermove event. There is another event called pointercancel which will have a deleterious effect on drag and drop functionality when you use a touch device like touch screens on mobile devices. If you want all the technical details, I suggest you check out this article. In a nutshell, we have to provide a small fix to the CSS that we apply to our tasks / items. Just add a touch-action: none to the item CSS and this problem is solved. By the way, you can't apply touch-action programmatically; once you start a gesture, applying element.style.touchAction = 'none' will have no effect! Just stick to defining it in a CSS class and you're all set.


    /**
     * Private method used to determine where the dragged item/element should be placed if sorting is enabled.
     * @param {any} container - the container on which the item/element was dropped.
     * @param {any} y - The y coordinate of the mouse or pointer device when the item was dropped.
     * @returns The element after which the dragged item/element should be inserted.  If the return value is null
     * the dragged item should be appended to end of the container's items.
     */
    const getDragAfterElement = function (container, y) {
        const draggableElements = [...container.querySelectorAll(`.${options.itemClass}:not(.dragging)`)];

        return draggableElements.reduce((closest, child) => {
            const box = child.getBoundingClientRect();
            const offset = y - box.top - box.height / 2;
            if (offset < 0 && offset > closest.offset) {
                return { offset: offset, element: child }
            } else {
                return closest
            }
        }, { offset: Number.NEGATIVE_INFINITY }).element
    }

I wish I could take credit for this last little bit of code, it's a very clever way to figure out where in an existing task list you have dropped the task item. It essentially figures out which existing task item in a list is closest to where you're dropping the dragged task item, and whether the dragged item is above or below the center of the existing task's x-axis. I found this snippet here. I only call this method if the sort option is set to true. Otherwise, we assume that the dropped item is appended to the end of the task list, and not necessarily somewhere within the list.

I'll detail the C# class that leverages all the JavaScript code in Part II.

Style Color Methods for Blazor

ו[יעקב] עשה לו [ליוסף] כתונת פסים. (פרשת וישב)

And [Jacob] made him [Joseph] a striped shirt.

I would wager that the best known reference to color in the Torah is found in Genesis 37:3. The King James translation describes Jacob's gift as a coat of many colors, yet the Hebrew phrase may not actually carry that meaning. In Modern Hebrew, the word passim refers to stripes. Some commentators over the centuries have described the coat (or tunic, or robe) as having multicolored stripes, but there is no universal agreement on the translation. In any event, this phrase provides a great title for the musical adaptation of Joseph's life by Andrew Lloyd Weber and Tim Rice.

While working on a new Razor component, I wanted to create a class to store CSS style values that I could pass as a parameter. I incorporate CSS variables in my stylesheets so that I can modify the appearance of components by constructing inline-style strings.

In one case, I wanted to select a base color, and be able to quickly derive both a lighter and darker variant of the color programmatically. Using some simple calculations, I can avoid having to define every single color I employ in my component; I just choose the base color and the variants are generated on the fly. It turns out that this is pretty easy to do if you work with RGB colors. To create a darker shade of a specified color, you can use the following method:


public static Color Shade(this Color color, double percentage = 0.25)
{
	return Color.FromArgb(Round(color.R * percentage), Round(color.G * percentage), Round(color.B * percentage));
}

private static int Round(double d)
{
	return d < 0 ? (int)(d - 0.5) : (int)(d + 0.5);
}

By specifying a percentage (any value between 0.0 and 1.0 - I use 25% or 0.25 as a default), you can generate a darker shade quickly. Note that I've provided a simple rounding method that converts double values to integers. The built-in Math.Round() method didn't quite work as I expected it to, so I whipped up this simple alternative method.

Generating a lighter tint is equally simple. The code for that is:


public static Color Tint(this Color color, double percentage = 0.25)
{
	return Color.FromArgb(Round(color.R + percentage * (255 - color.R)),
    	Round(color.G + percentage * (255 - color.G)), Round(color.B + percentage * (255 - color.B)));
}

Since I'm ultimately passing the colors I generate as hexadecimal string values to the style attribute of my components, I use a simple helper method to convert Color structs to hexadecimal strings:


public static string ToHexString(this Color color)
{
	return $"{color.R:X2}{color.G:X2}{color.B:X2}";
}

Remember to prepend a hash character (#) to the hexadecimal string before passing the color value to the style attribute, otherwise the browser won't know how to interpret it.

A couple of other helper methods that are useful if your base color is expressed as one of the many named colors, or a hexadecimal string:

public static Color NameToColor(string colorName)
{
	var value = Color.FromName(colorName);
    return value.IsNamedColor ? value : default;
}

public static Color HexStringToColor(string hex)
{
	var match = Regex.Match(hex);
    if (match.Success)
    {
    	var hexString = match.Groups[1].Value;
        var intValue = int.Parse(hexString, System.Globalization.NumberStyles.HexNumber);
        return Color.FromArgb(intValue);
	}
    return default;
}

private static readonly Regex Regex = HexRegex();

[GeneratedRegex("^#?([A-Fa-f\\d]{2}[A-Fa-f\\d]{2}[A-Fa-f\\d]{2})$", RegexOptions.Compiled)]
private static partial Regex HexRegex();