Trying to implement a custom BottomSheetComboBox
Hello, I am trying to implement a custom combobox control, which uses a SfBottomSheet to render the items that can be selected.
<?xml version="1.0" encoding="utf-8" ?>
<ContentView
x:Class="SnapSpend.Shared.Controls.BottomSheetComboBox"
x:Name="This"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:helpers="clr-namespace:SnapSpend.Infrastructure;assembly=SnapSpend.Infrastructure"
xmlns:toolkit="clr-namespace:Syncfusion.Maui.Toolkit.BottomSheet;assembly=Syncfusion.Maui.Core"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Grid x:Name="MainGrid">
<!-- Selected Value Display -->
<Border
BackgroundColor="{AppThemeBinding Light={StaticResource SurfaceLightVariant},
Dark={StaticResource SurfaceDarkVariant}}"
Padding="12,8"
StrokeShape="RoundRectangle 8"
x:Name="Border">
<Grid ColumnDefinitions="*, Auto">
<Label
Grid.Column="0"
Text="{Binding Source={x:Reference This}, Path=DisplayText}"
TextColor="{AppThemeBinding Light={StaticResource TextLight},
Dark={StaticResource TextDark}}"
VerticalOptions="Center" />
<Label
FontFamily="{x:Static helpers:RemixIcons.FONT_FAMILY}"
FontSize="16"
Grid.Column="1"
HorizontalOptions="End"
Text="{x:Static helpers:RemixIcons.RiArrowDownSLine}"
TextColor="{AppThemeBinding Light={StaticResource TextLight},
Dark={StaticResource TextDark}}"
VerticalOptions="Center" />
</Grid>
</Border>
</Grid>
</ContentView>
using System.Collections;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using Syncfusion.Maui.Toolkit.BottomSheet;
namespace SnapSpend.Shared.Controls;
public partial class BottomSheetComboBox : IValueConverter
{
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(
nameof(ItemsSource),
typeof(IEnumerable),
typeof(BottomSheetComboBox));
public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create(
nameof(SelectedItem),
typeof(object),
typeof(BottomSheetComboBox),
null,
BindingMode.TwoWay,
propertyChanged: OnSelectedItemChanged);
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
nameof(Title),
typeof(string),
typeof(BottomSheetComboBox),
"Select an item");
public static readonly BindableProperty DisplayTextProperty = BindableProperty.Create(
nameof(DisplayText),
typeof(string),
typeof(BottomSheetComboBox),
"Select an item");
public static readonly BindableProperty DisplayConverterProperty = BindableProperty.Create(
nameof(DisplayConverter),
typeof(IValueConverter),
typeof(BottomSheetComboBox),
null);
private SfBottomSheet? _bottomSheet;
private CollectionView? _collectionView;
public IEnumerable ItemsSource
{
get => (IEnumerable)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public object SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public string DisplayText
{
get => (string)GetValue(DisplayTextProperty);
private set => SetValue(DisplayTextProperty, value);
}
public IValueConverter? DisplayConverter
{
get => (IValueConverter?)GetValue(DisplayConverterProperty);
set => SetValue(DisplayConverterProperty, value);
}
public event EventHandler<object>? SelectedItemChanged;
public ICommand ShowBottomSheetCommand { get; }
public ICommand SelectItemCommand { get; }
public BottomSheetComboBox()
{
InitializeComponent();
ShowBottomSheetCommand = new RelayCommand(ShowBottomSheet);
SelectItemCommand = new RelayCommand<object>(SelectItem);
Border.GestureRecognizers.Add(
new TapGestureRecognizer
{
Command = ShowBottomSheetCommand,
NumberOfTapsRequired = 1,
});
// Listen for parent page changes
ParentChanged += OnParentChanged;
}
private void OnParentChanged(object? sender, EventArgs e)
{
// Clean up any previous bottom sheet
if (_bottomSheet == null)
{
return;
}
_bottomSheet = null;
_collectionView = null;
}
private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (BottomSheetComboBox)bindable;
control.DisplayText = control.GetDisplayText(newValue);
control.SelectedItemChanged?.Invoke(control, newValue);
}
public string GetDisplayText(object? item)
{
if (item == null)
return "Select an item";
if (DisplayConverter != null)
return DisplayConverter.Convert(item, typeof(string), null, null)?.ToString() ?? item.ToString();
return item.ToString() ?? "Select an item";
}
private void ShowBottomSheet()
{
var page = GetParentPage();
if (page == null)
{
return;
}
// Create bottom sheet if it doesn't exist yet
if (_bottomSheet == null)
{
CreateBottomSheet(page);
}
// Show the bottom sheet
_bottomSheet?.Show();
}
private void CreateBottomSheet(ContentPage page)
{
// Create bottom sheet
_bottomSheet = new SfBottomSheet
{
IsModal = true,
AllowedState = BottomSheetAllowedState.HalfExpanded,
CornerRadius = 16,
ShowGrabber = true,
// Background = Application.Current?.RequestedTheme == AppTheme.Dark
// ? Application.Current?.Resources["SurfaceDark"] as Color
// : Application.Current?.Resources["SurfaceLight"] as Color,
ContentPadding = new Thickness(16)
};
// Create content
var contentGrid = new Grid
{
RowDefinitions =
{
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Star }
},
RowSpacing = 16
};
// Header
var headerLabel = new Label
{
Text = Title,
FontAttributes = FontAttributes.Bold,
FontSize = 20,
// TextColor = Application.Current?.RequestedTheme == AppTheme.Dark
// ? Application.Current?.Resources["TextDark"] as Color
// : Application.Current?.Resources["TextLight"] as Color
};
contentGrid.Add(headerLabel, 0, 0);
// Collection view for items
_collectionView = new CollectionView
{
SelectionMode = SelectionMode.Single,
ItemsSource = ItemsSource,
// Set up item template
ItemTemplate = new DataTemplate(() =>
{
var grid = new Grid
{
Padding = new Thickness(16, 12)
};
var label = new Label
{
// TextColor = Application.Current?.RequestedTheme == AppTheme.Dark
// ? Application.Current?.Resources["TextDark"] as Color
// : Application.Current?.Resources["TextLight"] as Color,
VerticalOptions = LayoutOptions.Center
};
// Bind the label text through our converter
label.SetBinding(Label.TextProperty, new Binding(".", converter: this));
// Add tap gesture
var tapGesture = new TapGestureRecognizer();
tapGesture.SetBinding(TapGestureRecognizer.CommandProperty, new Binding(nameof(SelectItemCommand), source: this));
tapGesture.SetBinding(TapGestureRecognizer.CommandParameterProperty, new Binding("."));
label.GestureRecognizers.Add(tapGesture);
grid.Add(label);
return grid;
})
};
contentGrid.Add(_collectionView, 0, 1);
_bottomSheet.BottomSheetContent = contentGrid;
// Find the parent layout to attach the bottom sheet
if (page.Content is Layout layout)
{
// We need to track when the bottom sheet is closed
// _bottomSheet.Closed += (s, e) => { _bottomSheet.IsOpen = false; };
// Add the bottom sheet to the page
page.Content = new Grid
{
Children = { layout, _bottomSheet }
};
}
}
private ContentPage? GetParentPage()
{
Element? parent = this;
while (parent != null)
{
parent = parent.Parent;
if (parent is ContentPage page)
{
return page;
}
}
return null;
}
private void SelectItem(object? item)
{
if (item == null)
{
return;
}
SelectedItem = item;
_bottomSheet!.IsOpen = false;
SelectedItemChanged?.Invoke(this, item);
}
public object Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
{
return GetDisplayText(value);
}
public object ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
I am using this control in HomePageNew like this:
<controls:BottomSheetComboBox
Grid.Column="1"
ItemsSource="{Binding DateFilterOptions}"
SelectedItem="{Binding RecentScansDateFilter}"
Title="Select Period" />
This code partially works because the bottom sheet only gets displayed once, when tapping the control. If you tap 2nd time or more, nothing gets displayed.
Looking on the debug window I can see the following warning:
Microsoft.Maui.Controls.Element: Warning: Microsoft.Maui.Controls.Grid is already a child of HomePageNew. Remove Microsoft.Maui.Controls.Grid from HomePageNew before adding to Microsoft.Maui.Controls.Grid.
Could you advice how this can be fixed? Do you see any peformance problems with this implementation?
Hi Mihai,
Greetings from Syncfusion support!
We have reviewed your implementation and identified that the issue occurs due to the way the SfBottomSheet is added to the MainPage layout. Specifically, in your current approach, you are replacing the existing page content with a new Grid containing both the original layout and the SfBottomSheet. This can cause issues with retaining the original structure and behavior of your layout.
Issue in Your Current Implementation:
|
private void CreateBottomSheet(ContentPage page) { // Create bottom sheet _bottomSheet = new SfBottomSheet { IsModal = true, AllowedState = BottomSheetAllowedState.HalfExpanded, CornerRadius = 16, ShowGrabber = true, ContentPadding = new Thickness(16) }; … if (page.Content is Layout layout) { // Add the bottom sheet to the page page.Content = new Grid { Children = { layout, _bottomSheet } }; _bottomSheet.Show(); } } |
In the above code, setting page.Content = new Grid { ... } results in replacing the existing page content, which can lead to unintended layout issues.
To properly integrate the SfBottomSheet into your existing layout without replacing the ContentPage.Content, please use the following updated code:
|
private void CreateBottomSheet(ContentPage page) { // Create bottom sheet _bottomSheet = new SfBottomSheet { IsModal = true, AllowedState = BottomSheetAllowedState.HalfExpanded, CornerRadius = 16, ShowGrabber = true, ContentPadding = new Thickness(16) };
… if (page.Content is Layout layout) { if (layout is Grid) { // Add the bottom sheet to the layout dynamically if (!layout.Children.Contains(_bottomSheet)) { layout.Children.Add(_bottomSheet); } } _bottomSheet.Show(); } } |
Please try this updated implementation and let us know the details.
Regards,
Brundha V
- 1 Reply
- 2 Participants
-
MA Mihai Alexandru Dumitru
- Mar 3, 2025 07:33 AM UTC
- Mar 4, 2025 03:45 PM UTC