New to Telerik UI for BlazorStart a free 30-day trial

Drag and Drop

The Drag and Drop functionality for the TreeList rows allows you to move a row or a multitude of rows between different parents in the same TreeList or between different Telerik TreeList instances.

This article contains the following sections:

Basics

To enable the Drag and Drop functionality:

  1. Set the RowDraggable parameter of the <TelerikTreeList> to true

  2. Use the OnRowDrop event to handle the drag and drop operations and modify the data source as per your business logic.

The row drag and drop functionality works with a dedicated column which is always rendered as the first column when the feature is enabled.

OnRowDrop Event

The OnRowDrop event fires when the user drops a row into a new location. It allows you to manipulate your data collection based on where the user dropped the element.

Event Arguments

The OnRowDrop event provides an object of type TreeListRowDropEventArgs<T> to its event handler which exposes the following fields:

ParameterTypeDescription
ItemobjectRepresents the dragged row. You can cast this object to your model class.
DestinationItemobjectRepresents the row over which the Item is dropped. You can cast this object to your model class.
ItemsobjectRepresents the dragged row. You can cast this object to your model class.
DropPositionenumIts members allow you to determine the exact position of the dropped item relative to the position of the DestinationItem.
DestinationGridobjectThe reference of the Grid in which the row is dropped. This is applicable when you drag and drop rows between different grids.
DestinationIndexstringThe index where the drop will happen in the second component.
DestinationComponentIdstringThe Id of the second component in which the drop will happen.

TreeListRowDraggableSettings

The TreeListRowDraggableSettings is a child tag under the <TreeListSettings>, which is a child tag of the <TelerikTreeList>. It exposes the following parameters:

  • DragClueField - string - defines which field will be used to render the drag clue text. By default, this parameter will take the value of the first bound column of the first dragged row.

You can find examples of its usage below.

Examples

This section contains the following examples:

Drag and Drop a Row in the same TreeList

RAZOR
@* Drag a row and drop it in the TreeList. *@

<TelerikTreeList Data="@Data"
                 ItemsField="@(nameof(Employee.DirectReports))"
                 Pageable="true" Width="850px"
                 RowDraggable="true"
                 OnRowDrop="@((TreeListRowDropEventArgs<Employee> args) => OnRowDropHandler(args))">
    <TreeListSettings>
        <TreeListRowDraggableSettings DragClueField="@(nameof(Employee.Name))"></TreeListRowDraggableSettings>
    </TreeListSettings>
    <TreeListColumns>
        <TreeListColumn Field="Name" Expandable="true" Width="320px" />
        <TreeListColumn Field="Id" Editable="false" Width="120px" />
        <TreeListColumn Field="EmailAddress" Width="220px" />
        <TreeListColumn Field="HireDate" Width="220px" />
    </TreeListColumns>
</TelerikTreeList>

@code {
    private void OnRowDropHandler(TreeListRowDropEventArgs<Employee> args)
    {
        //The data manipulations in this example are to showcase a basic scenario.
        //In your application you should implement them as per the needs of the project.
        
        RemoveChildRecursive(Data, args.Item);

        var destinationParentItem = Data.FindRecursive(x => x.DirectReports?.Contains(args.DestinationItem) == true);

        var itemsCollection = destinationParentItem?.DirectReports ?? Data;

        int destinationItemIndex = itemsCollection.IndexOf(args.DestinationItem);

        if (args.DropPosition == TreeListRowDropPosition.Over)
        {
            if (args.DestinationItem.DirectReports == null)
            {
                args.DestinationItem.DirectReports = new List<Employee>();
            }

            args.DestinationItem.DirectReports.Add(args.Item);
        }
        else if (args.DropPosition == TreeListRowDropPosition.Before)
        {
            itemsCollection.Insert(destinationItemIndex, args.Item);
        }
        else
        {
            itemsCollection.Insert(destinationItemIndex + 1, args.Item);
        }
    }

    private void RemoveChildRecursive(List<Employee> collection, Employee item)
    {
        for (int i = 0; i < collection.Count(); i++)
        {
            if (item.Equals(collection[i]))
            {
                collection.Remove(item);

                return;
            }
            else if (collection[i].DirectReports?.Count > 0)
            {
                RemoveChildRecursive(collection[i].DirectReports, item);
            }
        }
    }

    #region Data
    public List<Employee> Data { get; set; }

    // data generation

    // used in this example for data generation and retrieval for CUD operations on the current view-model data
    public int LastId { get; set; } = 1;

    protected override async Task OnInitializedAsync()
    {
        Data = await GetTreeListData();
    }

    async Task<List<Employee>> GetTreeListData()
    {
        List<Employee> data = new List<Employee>();

        for (int i = 1; i < 15; i++)
        {
            Employee root = new Employee
            {
                Id = LastId,
                Name = $"root: {i}",
                EmailAddress = $"{i}@example.com",
                HireDate = DateTime.Now.AddYears(-i),
                DirectReports = new List<Employee>(), // prepare a collection for the child items, will be populated later in the code
            };
            data.Add(root);
            LastId++;

            for (int j = 1; j < 4; j++)
            {
                int currId = LastId;
                Employee firstLevelChild = new Employee
                {
                    Id = currId,
                    Name = $"first level child {j} of {i}",
                    EmailAddress = $"{currId}@example.com",
                    HireDate = DateTime.Now.AddDays(-currId),
                    DirectReports = new List<Employee>(), // collection for child nodes
                };
                root.DirectReports.Add(firstLevelChild); // populate the parent's collection
                LastId++;

                for (int k = 1; k < 3; k++)
                {
                    int nestedId = LastId;
                    // populate the parent's collection
                    firstLevelChild.DirectReports.Add(new Employee
                    {
                        Id = LastId,
                        Name = $"second level child {k} of {j} and {i}",
                        EmailAddress = $"{nestedId}@example.com",
                        HireDate = DateTime.Now.AddMinutes(-nestedId)
                    }); ;
                    LastId++;
                }
            }
        }

        return await Task.FromResult(data);
    }

    #endregion
}

Drag and Drop a Row between TreeLists

When you drag and drop items from one instance of the TreeList to another, the OnRowDrop event fires for both instances of the component so you can update both their data sources. All instances must be bound to the same model.

RAZOR
<TelerikTreeList Data="@Data"
                 ItemsField="@(nameof(Employee.DirectReports))"
                 Pageable="true" Width="850px"
                 @ref="@FirstTreeList"
                 RowDraggable="true"
                 OnRowDrop="@((TreeListRowDropEventArgs<Employee> args) => OnFirstRowDropHandler(args))">
    <TreeListSettings>
        <TreeListRowDraggableSettings DragClueField="@(nameof(Employee.Name))"></TreeListRowDraggableSettings>
    </TreeListSettings>
    <TreeListColumns>
        <TreeListColumn Field="Name" Expandable="true" Width="320px" />
        <TreeListColumn Field="Id" Editable="false" Width="120px" />
        <TreeListColumn Field="EmailAddress" Width="220px" />
        <TreeListColumn Field="HireDate" Width="220px" />
    </TreeListColumns>
</TelerikTreeList>


<TelerikTreeList Data="@SecondData"
                 ItemsField="@(nameof(Employee.DirectReports))"
                 Pageable="true" Width="850px"
                 RowDraggable="true"
                 OnRowDrop="@((TreeListRowDropEventArgs<Employee> args) => OnSecondRowDropHandler(args))">
    <TreeListSettings>
        <TreeListRowDraggableSettings DragClueField="@(nameof(Employee.Name))"></TreeListRowDraggableSettings>
    </TreeListSettings>
    <TreeListColumns>
        <TreeListColumn Field="Name" Expandable="true" Width="320px" />
        <TreeListColumn Field="Id" Editable="false" Width="120px" />
        <TreeListColumn Field="EmailAddress" Width="220px" />
        <TreeListColumn Field="HireDate" Width="220px" />
    </TreeListColumns>
</TelerikTreeList>

@code {
    private TelerikTreeList<Employee> FirstTreeList { get; set; }

    private void OnRowDrop(List<Employee> items, TreeListRowDropEventArgs<Employee> args)
    {
        foreach (var item in args.Items)
        {
            RemoveChildRecursive(items, item);
        }

        var destinationData = args.DestinationTreeList == FirstTreeList ? Data : SecondData;

        var destinationParentItem = destinationData.FindRecursive(x => x.DirectReports?.Contains(args.DestinationItem) == true);

        var itemsCollection = destinationParentItem?.DirectReports ?? Data;

        int destinationItemIndex = itemsCollection.IndexOf(args.DestinationItem);

        if (args.DropPosition == TreeListRowDropPosition.Over)
        {
            if (args.DestinationItem == null)
            {
                destinationData.AddRange(args.Items);
            }
            else
            {
                if(args.DestinationItem.DirectReports == null)
                {
                    args.DestinationItem.DirectReports = new List<Employee>();
                }

                args.DestinationItem.DirectReports.AddRange(args.Items);
            }
        }
        else if (args.DropPosition == TreeListRowDropPosition.Before)
        {
            itemsCollection.InsertRange(destinationItemIndex, args.Items);
        }
        else if(args.DropPosition == TreeListRowDropPosition.After)
        {
            itemsCollection.InsertRange(destinationItemIndex + 1, args.Items);
        }
    }

    private void OnFirstRowDropHandler(TreeListRowDropEventArgs<Employee> args)
    {
        //The data manipulations in this example are to showcase a basic scenario.
        //In your application you should implement them as per the needs of the project.
        
        OnRowDrop(Data, args);
    }

    private void OnSecondRowDropHandler(TreeListRowDropEventArgs<Employee> args)
    {
        //The data manipulations in this example are to showcase a basic scenario.
        //In your application you should implement them as per the needs of the project.
        
        OnRowDrop(SecondData, args);
    }

    private void RemoveChildRecursive(List<Employee> collection, Employee item)
    {
        for (int i = 0; i < collection.Count(); i++)
        {
            if (item.Equals(collection[i]))
            {
                collection.Remove(item);

                return;
            }
            else if (collection[i].DirectReports?.Count > 0)
            {
                RemoveChildRecursive(collection[i].DirectReports, item);
            }
        }
    }

    #region Data
    public List<Employee> Data { get; set; }
    public List<Employee> SecondData { get; set; }

    // data generation

    // used in this example for data generation and retrieval for CUD operations on the current view-model data
    public int LastId { get; set; } = 1;

    protected override async Task OnInitializedAsync()
    {
        Data = await GetTreeListData();
        SecondData = await GetSecondTreeListData();
    }

    async Task<List<Employee>> GetSecondTreeListData()
    {
        List<Employee> data = new List<Employee>();

        for (int i = 1; i < 15; i++)
        {
            Employee root = new Employee
            {
                Id = LastId,
                Name = $"root: {i}",
                EmailAddress = $"{i}@example.com",
                HireDate = DateTime.Now.AddYears(-i * 10),
                DirectReports = new List<Employee>(), // prepare a collection for the child items, will be populated later in the code
            };
            data.Add(root);
            LastId++;

            for (int j = 1; j < 4; j++)
            {
                int currId = LastId;
                Employee firstLevelChild = new Employee
                {
                    Id = currId,
                    Name = $"child {j} of {i}",
                    EmailAddress = $"{currId}@example.com",
                    HireDate = DateTime.Now.AddDays(-currId),
                    DirectReports = new List<Employee>(), // collection for child nodes
                };
                root.DirectReports.Add(firstLevelChild); // populate the parent's collection
                LastId++;

                for (int k = 1; k < 3; k++)
                {
                    int nestedId = LastId;
                    // populate the parent's collection
                    firstLevelChild.DirectReports.Add(new Employee
                    {
                        Id = LastId,
                        Name = $"second level child {k} of {j} and {i}",
                        EmailAddress = $"{nestedId}@example.com",
                        HireDate = DateTime.Now.AddMinutes(-nestedId)
                    }); ;
                    LastId++;
                }
            }
        }

        return await Task.FromResult(data);
    }

    async Task<List<Employee>> GetTreeListData()
    {
        List<Employee> data = new List<Employee>();

        for (int i = 1; i < 15; i++)
        {
            Employee root = new Employee
            {
                Id = LastId,
                Name = $"root: {i}",
                EmailAddress = $"{i}@example.com",
                HireDate = DateTime.Now.AddYears(-i),
                DirectReports = new List<Employee>(), // prepare a collection for the child items, will be populated later in the code
            };
            data.Add(root);
            LastId++;

            for (int j = 1; j < 4; j++)
            {
                int currId = LastId;
                Employee firstLevelChild = new Employee
                {
                    Id = currId,
                    Name = $"first level child {j} of {i}",
                    EmailAddress = $"{currId}@example.com",
                    HireDate = DateTime.Now.AddDays(-currId),
                    DirectReports = new List<Employee>(), // collection for child nodes
                };
                root.DirectReports.Add(firstLevelChild); // populate the parent's collection
                LastId++;

                for (int k = 1; k < 3; k++)
                {
                    int nestedId = LastId;
                    // populate the parent's collection
                    firstLevelChild.DirectReports.Add(new Employee
                    {
                        Id = LastId,
                        Name = $"second level child {k} of {j} and {i}",
                        EmailAddress = $"{nestedId}@example.com",
                        HireDate = DateTime.Now.AddMinutes(-nestedId)
                    }); ;
                    LastId++;
                }
            }
        }

        return await Task.FromResult(data);
    }

    #endregion
}

Drag and Drop between TreeList, Grid, TreeView and Scheduler

The functionality allows dragging items between TreeList, Grid, TreeView, and Scheduler. To achieve it, set the Draggable/RowDraggable parameter, and implement it through an event - OnDrop/OnRowDrop.

Drag and Drop from Scheduler to Grid, TreeList, TreeView is not yet supported. Only the reversed way.

Drag and Drop between Grid and TreeList

RAZOR
@using System.Collections.Generic;
@using System.Collections.ObjectModel;

@inject PersonService PersonService;
@inject TreeListService TreeService;

<TelerikGrid Data="@GridData"
             Id="Grid1"
             Pageable="true"
             Width="450px"
             PageSize="10"
             Sortable="true"
             RowDraggable="true"
             SelectionMode="@GridSelectionMode.Multiple"
             @ref="@GridRef"
             OnRowDrop="@((GridRowDropEventArgs<Person> args) => GridRowDrop(args))">
    <GridSettings>
        <GridRowDraggableSettings DragClueField="@nameof(Person.Name)"></GridRowDraggableSettings>
    </GridSettings>
    <GridColumns>
        <GridColumn Field=@nameof(Person.EmployeeId) Editable="false" />
        <GridColumn Field=@nameof(Person.Name) />
    </GridColumns>
</TelerikGrid>
<TelerikTreeList Data="@TreeData"
                 Id="TreeList1"
                 ItemsField="Items"
                 Pageable="true"
                 Width="550px"
                 RowDraggable="true"
                 @ref="@TreeListRef"
                 ParentIdField="ParentId"
                 OnRowDrop="@((TreeListRowDropEventArgs<FlatItem> args) => TreeListDrop(args))">
    <TreeListSettings>
        <TreeListRowDraggableSettings DragClueField="@nameof(FlatItem.StringProp)"></TreeListRowDraggableSettings>
    </TreeListSettings>
    <TreeListColumns>
        <TreeListColumn Field="Id"></TreeListColumn>
        <TreeListColumn Field="StringProp" Expandable="true"></TreeListColumn>
    </TreeListColumns>
</TelerikTreeList>

@code {
    public List<Person> GridData { get; set; }
    public TelerikGrid<Person> GridRef { get; set; }
    public TelerikTreeList<FlatItem> TreeListRef { get; set; }
    public List<FlatItem> TreeData { get; set; }
    protected override async Task OnInitializedAsync()
    {
        await LoadData();
    }
    private async Task LoadData()
    {
        var people = await PersonService.GetPeopleAsync();
        GridData = people.Take(10).ToList();
        var treeListData = await TreeService.GetFlatItemsAsync();
        TreeData = treeListData.Take(10).ToList();
    }
    private void GridRowDrop(GridRowDropEventArgs<Person> args)
    {
        foreach (var item in args.Items)
        {
            GridData.Remove(item);
        }
        if (args.DestinationComponentId == "TreeList1")
        {
            var destinationItem = (FlatItem)TreeListRef.GetItemFromDropIndex(args.DestinationIndex);
            args.Items
                .Select(item => new FlatItem() { StringProp = item.Name, Id = Guid.NewGuid() }).ToList()
                .ForEach(item => UpdateTreeList(item, destinationItem, (TreeListRowDropPosition)(int)args.DropPosition));
        }
        else if (args.DestinationComponentId == "Grid1")
        {
            InsertItemsIntoGrid(args.Items, args.DestinationItem, args.DropPosition);
        }
    }
    private void TreeListDrop(TreeListRowDropEventArgs<FlatItem> args)
    {
        var item = args.Item as FlatItem;
        if (args.DestinationComponentId == "TreeList1")
        {
            var destinationItem = (FlatItem)TreeListRef.GetItemFromDropIndex(args.DestinationIndex);
            UpdateTreeList(item, destinationItem, (TreeListRowDropPosition)(int)args.DropPosition);
        }
        else if (args.DestinationComponentId == "Grid1")
        {
            var sourceItems = TreeData
            .Where(x => x.ParentId == item.Id)
            .Select(item => new Person() { Name = item.StringProp, EmployeeId = GridData.Max(x => x.EmployeeId) + 1 });
            TreeData.Remove(item);
            var destinationItem = GridRef.GetItemFromDropIndex(args.DestinationIndex);
            InsertItemsIntoGrid(sourceItems, destinationItem, (GridRowDropPosition)(int)args.DropPosition);
            GridRef.Rebind();
        }
    }
    private void InsertItemsIntoGrid(IEnumerable<Person> items, Person destinationItem, GridRowDropPosition dropPosition)
    {
        var destinationIndex = 0;
        if (destinationItem != null)
        {
            destinationIndex = GridData.IndexOf(destinationItem);
            if (dropPosition == GridRowDropPosition.After)
            {
                destinationIndex += 1;
            }
        }
        GridData.InsertRange(destinationIndex, items);
        TreeListRef.Rebind();
    }
    private void UpdateTreeList(FlatItem item, FlatItem destinationItem, TreeListRowDropPosition dropPosition)
    {
        var destinationIndex = 0;
        if (destinationItem != null)
        {
            destinationIndex = TreeData.IndexOf(destinationItem);
            if (dropPosition == TreeListRowDropPosition.Over)
            {
                item.ParentId = destinationItem.Id;
            }
            else if (dropPosition == TreeListRowDropPosition.After)
            {
                destinationIndex += 1;
            }
            else if (dropPosition == TreeListRowDropPosition.Before)
            {
                destinationIndex += 1;
            }
            TreeData.Insert(destinationIndex, item);
        }
        TreeData = new List<FlatItem>(TreeData);
    }
}

See more applicable examples in the Grid Drag and Drop article.

Drag and Drop multiple Rows

You can drag and drop multiple rows in one or between multiple instances of the TreeList. To enable it, you should set the SelectionMode parameter of the TelerikTreeList to TreeListSelectionMode.Multiple. Then, if you drag a selected row, you will effectively drag all the selected rows.

When you select multiple rows, the row drag clue will be N items selected where N is the number of selected rows.

RAZOR
@* Select multiple rows and reorder them in the TreeList. *@

<TelerikTreeList Data="@Data"
                 ItemsField="@(nameof(Employee.DirectReports))"
                 Pageable="true" Width="850px"
                 SelectionMode="@TreeListSelectionMode.Multiple"
                 RowDraggable="true"
                 OnRowDrop="@((TreeListRowDropEventArgs<Employee> args) => OnRowDropHandler(args))">
    <TreeListSettings>
        <TreeListRowDraggableSettings DragClueField="@(nameof(Employee.Name))"></TreeListRowDraggableSettings>
    </TreeListSettings>
    <TreeListColumns>
        <TreeListColumn Field="Name" Expandable="true" Width="320px" />
        <TreeListColumn Field="Id" Editable="false" Width="120px" />
        <TreeListColumn Field="EmailAddress" Width="220px" />
        <TreeListColumn Field="HireDate" Width="220px" />
    </TreeListColumns>
</TelerikTreeList>

@code {
    private void OnRowDropHandler(TreeListRowDropEventArgs<Employee> args)
    {
        //The data manipulations in this example are to showcase a basic scenario.
        //In your application you should implement them as per the needs of the project.
        
        foreach (var item in args.Items)
        {
            RemoveChildRecursive(Data, item);
        }

        var destinationParentItem = Data.FindRecursive(x => x.DirectReports?.Contains(args.DestinationItem) == true);

        var itemsCollection = destinationParentItem?.DirectReports ?? Data;

        int destinationItemIndex = itemsCollection.IndexOf(args.DestinationItem);

        if (args.DropPosition == TreeListRowDropPosition.Over)
        {
            if (args.DestinationItem.DirectReports == null)
            {
                args.DestinationItem.DirectReports = new List<Employee>();
            }

            args.DestinationItem.DirectReports.AddRange(args.Items);
        }
        else if (args.DropPosition == TreeListRowDropPosition.Before)
        {
            itemsCollection.InsertRange(destinationItemIndex, args.Items);
        }
        else
        {
            itemsCollection.InsertRange(destinationItemIndex + 1, args.Items);
        }
    }

    private void RemoveChildRecursive(List<Employee> collection, Employee item)
    {
        for (int i = 0; i < collection.Count(); i++)
        {
            if (item.Equals(collection[i]))
            {
                collection.Remove(item);

                return;
            }
            else if (collection[i].DirectReports?.Count > 0)
            {
                RemoveChildRecursive(collection[i].DirectReports, item);
            }
        }
    }

    #region Data
    public List<Employee> Data { get; set; }

    // data generation

    // used in this example for data generation and retrieval for CUD operations on the current view-model data
    public int LastId { get; set; } = 1;

    protected override async Task OnInitializedAsync()
    {
        Data = await GetTreeListData();
    }

    async Task<List<Employee>> GetTreeListData()
    {
        List<Employee> data = new List<Employee>();

        for (int i = 1; i < 15; i++)
        {
            Employee root = new Employee
            {
                Id = LastId,
                Name = $"root: {i}",
                EmailAddress = $"{i}@example.com",
                HireDate = DateTime.Now.AddYears(-i),
                DirectReports = new List<Employee>(), // prepare a collection for the child items, will be populated later in the code
            };
            data.Add(root);
            LastId++;

            for (int j = 1; j < 4; j++)
            {
                int currId = LastId;
                Employee firstLevelChild = new Employee
                {
                    Id = currId,
                    Name = $"first level child {j} of {i}",
                    EmailAddress = $"{currId}@example.com",
                    HireDate = DateTime.Now.AddDays(-currId),
                    DirectReports = new List<Employee>(), // collection for child nodes
                };
                root.DirectReports.Add(firstLevelChild); // populate the parent's collection
                LastId++;

                for (int k = 1; k < 3; k++)
                {
                    int nestedId = LastId;
                    // populate the parent's collection
                    firstLevelChild.DirectReports.Add(new Employee
                    {
                        Id = LastId,
                        Name = $"second level child {k} of {j} and {i}",
                        EmailAddress = $"{nestedId}@example.com",
                        HireDate = DateTime.Now.AddMinutes(-nestedId)
                    }); ;
                    LastId++;
                }
            }
        }

        return await Task.FromResult(data);
    }

    #endregion
}

See Also