Card Item Sorting in .NET MAUI Kanban (SfKanban)
21 Jul 202624 minutes to read
The Kanban control supports sorting cards within columns based on data fields such as Priority, DueDate, or Status. Configure sorting programmatically and update it dynamically at runtime using the following properties:
-
SortingMappingPath - Used to map the sorting field to a property name in the KanbanModel or
CustomModel. The default value isstring.Empty, in which case the cards will not be sorted. -
SortingOrder - Used to define the direction of cards sorting within each column.
- Ascending - Cards with lower values appear first.
- Descending - Cards with higher values appear first.
NOTE
The SortingOrder property is applicable only when a valid value is assigned to SortingMappingPath.
Customize card order with sorting configuration
Sorting in the Kanban control can be implemented using the following approaches.
- Custom
- Index
Custom Field Sorting
Map a valid property name from the ItemsSource to the SortingMappingPath property to enable custom sorting. This mapping sorts cards by the corresponding property value during initialization and drag-and-drop operations.
The following example shows how card positions are updated based on sorting configuration and property mapping.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:kanban="clr-namespace:Syncfusion.Maui.Kanban;assembly=Syncfusion.Maui.Kanban"
xmlns:local="clr-namespace:YourAppNamespace;assembly=YourAppName"
x:Class="YourAppNamespace.MainPage">
<ContentPage.BindingContext>
<local:SortingViewModel />
</ContentPage.BindingContext>
<kanban:SfKanban x:Name="kanban"
SortingMappingPath="Index"
SortingOrder="Ascending"
ItemsSource="{Binding Cards}"
ColumnMappingPath="Category">
<kanban:SfKanban.CardTemplate>
<DataTemplate>
<Border Stroke="Black"
StrokeThickness="1"
StrokeShape="RoundRectangle 8"
Background="#F3CFCE">
<Grid RowDefinitions="Auto,Auto,Auto"
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
Padding="8">
<HorizontalStackLayout Grid.Row="0"
Grid.ColumnSpan="2"
Spacing="4"
VerticalOptions="Center"
HeightRequest="20">
<Label Grid.Row="0"
Grid.ColumnSpan="2"
Text="{Binding Priority, StringFormat='• {0}'}"
FontSize="14"
FontAttributes="Bold"
TextColor="Orange"
VerticalOptions="Center"
VerticalTextAlignment="Center"
HeightRequest="20" />
</HorizontalStackLayout>
<Label Grid.Row="1"
Grid.ColumnSpan="2"
Text="{Binding Title}"
FontAttributes="Bold"
FontSize="14"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
Margin="5" />
<Label Grid.Row="2"
Grid.ColumnSpan="2"
Text="{Binding Description}"
FontSize="12"
HorizontalTextAlignment="Center"
LineBreakMode="WordWrap"
Margin="5" />
</Grid>
</Border>
</DataTemplate>
</kanban:SfKanban.CardTemplate>
<kanban:SfKanban.Columns>
<kanban:KanbanColumn Title="Open"
Categories="Open" />
<kanban:KanbanColumn Title="In Progress"
Categories="In Progress" />
<kanban:KanbanColumn Title="Done"
Categories="Done"
AllowDrag="False" />
</kanban:SfKanban.Columns>
</kanban:SfKanban>
</ContentPage>var kanban = new SfKanban
{
ItemsSource = new SortingViewModel().Cards
};
kanban.DragEnd += OnCardDragEnd;
this.Content = kanban;
private void OnCardDragEnd(object sender, KanbanDragEndEventArgs e)
{
kanban.RefreshKanbanColumn();
}public class CardDetails
{
public string? Title { get; set; }
public string? Description { get; set; }
public string? Priority { get; set; }
public string? Category { get; set; }
}using System.Collections.ObjectModel;
public class SortingViewModel
{
public SortingViewModel()
{
this.Cards = new ObservableCollection<CardDetails>()
{
new CardDetails() { Title = "Task - 1", Priority = "Medium", Category = "Open", Description = "Fix the issue reported in the Edge browser." },
new CardDetails() { Title = "Task - 3", Priority = "Low", Category = "In Progress", Description = "Analyze the new requirements gathered from the customer." },
new CardDetails() { Title = "Task - 4", Priority = "Critical", Category = "Open", Description = "Arrange a web meeting with the customer to get new requirements." },
new CardDetails() { Title = "Task - 2", Priority = "High", Category = "In Progress", Description = "Test the application in the Edge browser." },
new CardDetails() { Title = "Task - 5", Priority = "Medium", Category = "Done", Description = "Enhance editing functionality." },
new CardDetails() { Title = "Task - 8", Priority = "Medium", Category = "In Progress", Description = "Improve application performance." },
new CardDetails() { Title = "Task - 9", Priority = "Medium", Category = "Done", Description = "Improve the performance of the editing functionality." },
new CardDetails() { Title = "Task - 10", Priority = "High", Category = "Open", Description = "Analyze grid control." },
new CardDetails() { Title = "Task - 12", Priority = "Low", Category = "Done", Description = "Analyze stored procedures." }
};
}
public ObservableCollection<CardDetails> Cards { get; set; }
}
NOTE
NOTE
- To apply sorting after a drop operation, handle the DragEnd event and explicitly call the RefreshKanbanColumn method. This ensures the column updates to reflect the new card order based on the defined sorting logic.
- When using a custom data model, the default card UI is not applicable. To render the card content, you must define a custom
DataTemplateusing the CardTemplate property.
Index-Based Sorting
Use the index-based approach to drop cards at precise positions within a column. On drop, the card’s Index is updated based on the index of the previous card. The index of the next card is also incremented to maintain continuous ordering. The Index property of the data model must always be set to a non-null value because the helper code uses Convert.ToInt32 (which throws on null).
NOTE
The SortingMappingPath property must be mapped to a valid numeric property name from the ItemsSource to enable index-based sorting updates.
The following example updates a card’s numeric property using the index-based sorting approach.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:kanban="clr-namespace:Syncfusion.Maui.Kanban;assembly=Syncfusion.Maui.Kanban"
xmlns:local="clr-namespace:YourAppNamespace;assembly=YourAppName"
x:Class="YourAppNamespace.MainPage">
<ContentPage.BindingContext>
<local:SortingViewModel />
</ContentPage.BindingContext>
<kanban:SfKanban x:Name="kanban"
SortingMappingPath="Index"
SortingOrder="Ascending"
ItemsSource="{Binding Cards}"
ColumnMappingPath="Category">
<kanban:SfKanban.CardTemplate>
<DataTemplate>
<Border Stroke="Black"
StrokeThickness="1"
StrokeShape="RoundRectangle 8"
Background="#F3EADC">
<Grid RowDefinitions="Auto,Auto,Auto"
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
Padding="8">
<HorizontalStackLayout Grid.Row="0"
Grid.ColumnSpan="2"
Spacing="4"
VerticalOptions="Center"
HeightRequest="20"
HorizontalOptions="End">
<Label Text="{Binding Index, StringFormat='Rank #{0}'}"
FontSize="14"
FontAttributes="Bold"
TextColor="#026B6E"
VerticalOptions="Center"
VerticalTextAlignment="Center"
HeightRequest="20" />
</HorizontalStackLayout>
<Label Grid.Row="1"
Grid.ColumnSpan="2"
Text="{Binding Title}"
FontAttributes="Bold"
FontSize="14"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
Margin="5" />
<Label Grid.Row="2"
Grid.ColumnSpan="2"
Text="{Binding Description}"
FontSize="12"
HorizontalTextAlignment="Center"
LineBreakMode="WordWrap"
Margin="5" />
</Grid>
</Border>
</DataTemplate>
</kanban:SfKanban.CardTemplate>
<kanban:SfKanban.Columns>
<kanban:KanbanColumn Title="Open"
Categories="Open" />
<kanban:KanbanColumn Title="In Progress"
Categories="In Progress" />
<kanban:KanbanColumn Title="Done"
Categories="Done"
AllowDrag="False" />
</kanban:SfKanban.Columns>
</kanban:SfKanban>
</ContentPage>using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Syncfusion.Maui.Kanban;
var kanban = new SfKanban
{
ItemsSource = new SortingViewModel().Cards
};
kanban.DragEnd += OnCardDragEnd;
this.Content = kanban;
private void OnCardDragEnd(object sender, KanbanDragEndEventArgs e)
{
ApplySortingWithoutPositionChange(e);
kanban.RefreshKanbanColumn();
}
private void ApplySortingWithoutPositionChange(KanbanDragEndEventArgs eventArgs)
{
if (eventArgs.Data == null
|| eventArgs.TargetColumn?.Items == null
|| eventArgs.SourceColumn == null
|| (eventArgs.SourceColumn == eventArgs.TargetColumn && eventArgs.SourceIndex == eventArgs.TargetIndex))
{
return;
}
// Retrieve sorting configuration
var sortMappingPath = kanban.SortingMappingPath;
var sortingOrder = kanban.SortingOrder;
if (string.IsNullOrEmpty(sortMappingPath))
{
return;
}
// Extract and cast items from the target column
var targetColumnItems = eventArgs.TargetColumn.Items is IList<object> items
? items.Cast<object>().ToList() : new List<object>();
// Sort items based on the sorting order
if (targetColumnItems.Count > 0)
{
Func<object, int> keySelector = item => Convert.ToInt32(GetPropertyInfo(item.GetType(), sortMappingPath)?.GetValue(item) ?? 0);
targetColumnItems = sortingOrder == KanbanSortingOrder.Ascending
? targetColumnItems.OrderBy(keySelector).ToList()
: targetColumnItems.OrderByDescending(keySelector).ToList();
}
// Determine the index to insert the dragged card.
int currentCardIndex = eventArgs.TargetIndex;
if (currentCardIndex >= 0 && currentCardIndex <= targetColumnItems.Count)
{
targetColumnItems.Insert(currentCardIndex, eventArgs.Data);
}
else
{
targetColumnItems.Add(eventArgs.Data);
currentCardIndex = targetColumnItems.Count - 1;
}
// Update index property of all items using smart positioning logic
ApplySmartIndexUpdate(targetColumnItems, sortingOrder, currentCardIndex);
}
private void ApplySmartIndexUpdate(List<object> items, KanbanSortingOrder sortingOrder, int currentCardIndex)
{
if (items == null || items.Count == 0)
{
return;
}
if (sortingOrder == KanbanSortingOrder.Ascending)
{
HandleAscendingIndexSorting(items, currentCardIndex);
return;
}
HandleDescendingIndexSorting(items, currentCardIndex);
}
private void HandleAscendingIndexSorting(List<object> items, int currentCardIndex)
{
int afterCardIndex = -1;
int lastItemIndex = -1;
// Get the index of the card after the insertion point
if (currentCardIndex < items.Count - 1)
{
var afterCard = items[currentCardIndex + 1];
afterCardIndex = GetCardIndex(afterCard) ?? -1;
}
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
if (item == null)
{
continue;
}
PropertyInfo? propertyInfo = GetPropertyInfo(item.GetType(), "Index");
if (propertyInfo == null)
{
continue;
}
int itemIndex = Convert.ToInt32(propertyInfo.GetValue(item) ?? 0);
bool isFirstItem = i == 0;
if (isFirstItem)
{
// If the inserted card is at the beginning, assign a smart index
if (currentCardIndex == 0)
{
lastItemIndex = afterCardIndex > 1 ? afterCardIndex - 1 : 1;
propertyInfo.SetValue(item, lastItemIndex);
}
else
{
lastItemIndex = itemIndex;
}
}
else
{
// Increment index for subsequent items
lastItemIndex++;
propertyInfo.SetValue(item, lastItemIndex);
}
}
}
private void HandleDescendingIndexSorting(List<object> items, int currentCardIndex)
{
int beforeCardIndex = -1;
int lastItemIndex = -1;
// Get the index of the card before the insertion point
if (currentCardIndex > 0 && currentCardIndex < items.Count)
{
var cardBefore = items[currentCardIndex - 1];
beforeCardIndex = GetCardIndex(cardBefore) ?? -1;
}
for (int i = items.Count - 1; i >= 0; i--)
{
var item = items[i];
if (item == null)
{
continue;
}
PropertyInfo? propertyInfo = GetPropertyInfo(item.GetType(), "Index");
if (propertyInfo == null)
{
continue;
}
int itemIndex = Convert.ToInt32(propertyInfo.GetValue(item) ?? 0);
bool isLastItem = i == items.Count - 1;
if (isLastItem)
{
// If the inserted card is at the end, assign a smart index
if (currentCardIndex == items.Count - 1)
{
lastItemIndex = beforeCardIndex > 1 ? beforeCardIndex - 1 : 1;
propertyInfo.SetValue(item, lastItemIndex);
}
else
{
lastItemIndex = itemIndex;
}
}
else
{
lastItemIndex++;
propertyInfo.SetValue(item, lastItemIndex);
}
}
}
private int? GetCardIndex(object cardDetails)
{
if (cardDetails == null)
{
return null;
}
PropertyInfo? propertyInfo = GetPropertyInfo(cardDetails.GetType(), "Index");
if (propertyInfo == null)
{
return null;
}
var indexValue = propertyInfo.GetValue(cardDetails);
if (indexValue != null)
{
return Convert.ToInt32(indexValue);
}
return null;
}
private PropertyInfo? GetPropertyInfo(Type type, string key)
{
return GetPropertyInfoCustomType(type, key);
}
private PropertyInfo? GetPropertyInfoCustomType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, string key)
{
return type.GetProperty(key);
}public class CardDetails
{
public string? Title { get; set; }
public string? Description { get; set; }
public int? Index { get; set; }
public string? Category { get; set; }
}using System.Collections.ObjectModel;
public class SortingViewModel
{
public SortingViewModel()
{
this.Cards = new ObservableCollection<CardDetails>()
{
new CardDetails() { Title = "Task - 1", Index = 5, Category = "Open", Description = "Fix the issue reported in the Edge browser." },
new CardDetails() { Title = "Task - 3", Index = 9, Category = "In Progress", Description = "Analyze the new requirements gathered from the customer." },
new CardDetails() { Title = "Task - 4", Index = 2, Category = "Open", Description = "Arrange a web meeting with the customer to get new requirements." },
new CardDetails() { Title = "Task - 2", Index = 1, Category = "In Progress", Description = "Test the application in the Edge browser." },
new CardDetails() { Title = "Task - 5", Index = 8, Category = "Done", Description = "Enhance editing functionality." },
new CardDetails() { Title = "Task - 8", Index = 3, Category = "In Progress", Description = "Improve application performance." },
new CardDetails() { Title = "Task - 9", Index = 6, Category = "Done", Description = "Improve the performance of the editing functionality." },
new CardDetails() { Title = "Task - 10", Index = 4, Category = "Open", Description = "Analyze grid control." },
new CardDetails() { Title = "Task - 12", Index = 7, Category = "Done", Description = "Analyze stored procedures." }
};
}
public ObservableCollection<CardDetails> Cards { get; set; }
}
NOTE
NOTE
- The Index-based sorting can be achieved at the sample level after a drag-and-drop action. To implement this handle the DragEnd event, access the items in the target column using e.TargetColumn.Items, and update the numeric field used for sorting to maintain a continuous order. Finally, call RefreshKanbanColumn method to update the UI with the new order.
- To disable sorting logic, avoid assigning a value to the SortingMappingPath property. This ensures that card positions remain static and reflect the order of the ItemsSource collection, making it suitable for scenarios where sorting is not required or is managed externally.