DEV Community

Cover image for 🔄 Creating a Syncfusion MultiSelect Component with Two-Way Binding in Blazor
Spyros Ponaris
Spyros Ponaris

Posted on • Edited on

🔄 Creating a Syncfusion MultiSelect Component with Two-Way Binding in Blazor

In this example, we build a reusable Blazor component using Syncfusion’s SfMultiSelect that supports:

  • Dynamic option filtering based on a Picklist type (e.g., Color, Unit)
  • Two-way data binding with a parent component
  • Internal state management to avoid unnecessary updates

đź’ˇ Why This Pattern?

Often in enterprise forms, you need a dynamic dropdown that updates based on a type and preselects values passed from a parent. This setup ensures:

  • Clean encapsulation
  • Correct @bind support
  • Lightweight rendering

âś… Component Markup

<SfMultiSelect TValue="List<PicklistSetDto>"
               TItem="PicklistSetDto"
               @ref="multiObj"
               DebounceDelay="@NumericValue"
               Placeholder="@Label"
               DataSource="@Options"
               @bind-Value="InternalSelectedValues"
               Mode="VisualMode.Box"
               AllowFiltering="true"
               FilterType="Syncfusion.Blazor.DropDowns.FilterType.Contains"
               PopupHeight="300px">
    <MultiSelectFieldSettings Text="Text" Value="Value" />
    <MultiSelectEvents TValue="List<PicklistSetDto>" TItem="PicklistSetDto" Filtering="OnFilter"></MultiSelectEvents>
</SfMultiSelect>
Enter fullscreen mode Exit fullscreen mode

đź§  Component Logic

 @code {
    /// <summary>
    /// 
    /// </summary>
    SfMultiSelect<List<PicklistSetDto>, PicklistSetDto> multiObj { get; set; }
    /// <summary>
    /// /
    /// </summary>
    private int NumericValue = 300;

    // Label to show in the input placeholder
    [Parameter]
    public string Label { get; set; } = "Select Options";

    // The Picklist type (e.g., Color, Unit, etc.)
    [Parameter]
    public Picklist Name { get; set; }

    // Value passed from the parent component
    [Parameter]
    public IEnumerable<PicklistSetDto> SelectedValues { get; set; } = new List<PicklistSetDto>();

    // EventCallback to notify parent of selection changes
    [Parameter]
    public EventCallback<IEnumerable<PicklistSetDto>> SelectedValuesChanged { get; set; }

    // List of options to be shown in the dropdown
    protected List<PicklistSetDto> Options = new();

    // Backing field for internal selected values
    private List<PicklistSetDto> _internalSelectedValues = new();

    private Picklist _previousName;

    // Internal property used for binding to the MultiSelect component
    private List<PicklistSetDto> InternalSelectedValues
    {
        get => _internalSelectedValues;
        set
        {
            // Only update if the list actually changed (prevents unnecessary updates)
            if (!_internalSelectedValues.SequenceEqual(value ?? new()))
            {
                _internalSelectedValues = value ?? new();
                // Notify parent of the new selected values
                SelectedValuesChanged.InvokeAsync(_internalSelectedValues);
            }
        }
    }

    // Initialize the list of options and apply any preselected values
    protected override async Task OnInitializedAsync()
    {
        // Load options based on the Picklist type (Name)
        Options = await Task.FromResult(PicklistService.DataSource
            .Where(x => x.Name == Name)
            .OrderBy(x => x.Name)
            .ToList());

    }

    // Ensure selected values are refreshed if parameters change after initialization
    public override async Task SetParametersAsync(ParameterView parameters)
    {
        await base.SetParametersAsync(parameters);

        if (!EqualityComparer<Picklist>.Default.Equals(_previousName, Name))
        {
            _previousName = Name;

            PicklistService.Refresh();

            Options = PicklistService.DataSource
                .Where(x => x.Name == Name)
                .ToList();

            InternalSelectedValues = Options
                .Where(x => SelectedValues.Any(s => s.Value == x.Value))
                .ToList();
        }
    }

    private async Task OnFilter(FilteringEventArgs args)
    {
        args.PreventDefaultAction = true;
        var query = new Query().Where(new WhereFilter() 
        { Field = "Name", 
        Operator = "contains", 
        value = args.Text, IgnoreCase = true });

        query = !string.IsNullOrEmpty(args.Text) ? query : new Query();

        await multiObj.FilterAsync(this.Options, query);
    }
}
Enter fullscreen mode Exit fullscreen mode

đź§© Example Usage in Parent Component

<PicklistMultiSelect Label="Select Colors"
                         Name="Picklist.Color"
                         SelectedValues="@colorData"
                         SelectedValuesChanged="OnColorChanged" />

    private List<PicklistSetDto> colorData = new();
    private void OnColorChanged(IEnumerable<PicklistSetDto> values)
    {
        colorData = values.ToList();
    }
Enter fullscreen mode Exit fullscreen mode

To efficiently handle large datasets in Syncfusion’s SfMultiSelect in Blazor, you should avoid loading the entire dataset into memory and instead use server-side filtering and paging. Here's how to implement it:

âś… 1. Enable Virtualization

For large datasets, enable virtualization to load items on-demand:

<SfMultiSelect TValue="List<PicklistSetDto>"
               TItem="PicklistSetDto"
               @ref="multiObj"
               Placeholder="@Label"
               @bind-Value="InternalSelectedValues"
               AllowFiltering="true"
               EnableVirtualization="true"
               PopupHeight="300px"
               Filtering="OnFilter">
    <MultiSelectFieldSettings Text="Text" Value="Value" />
</SfMultiSelect>
Enter fullscreen mode Exit fullscreen mode

âš  EnableVirtualization="true" improves rendering performance but requires paging logic in OnFilter.

âś… 2. Implement OnFilter with Server-Side Querying
Replace your in-memory filter with actual server-side filtering:

private async Task OnFilter(FilteringEventArgs args)
{
    args.PreventDefaultAction = true;

    // Server-side call (adjust as needed)
    var filtered = await PicklistService.GetFilteredAsync(Name, args.Text);

    await multiObj.FilterAsync(filtered, new Query());
}
Enter fullscreen mode Exit fullscreen mode

âś… 3. Modify Your Service to Support Filtering and Paging

public async Task<List<PicklistSetDto>> GetFilteredAsync(Picklist name, string? filter)
{
    using var context = _dbContextFactory.CreateDbContext();

    var query = context.PicklistSets
        .Where(p => p.Name == name);

    if (!string.IsNullOrWhiteSpace(filter))
        query = query.Where(p => p.Text.Contains(filter));

    return await query
        .OrderBy(p => p.Text)
        .Take(100) // Limit for performance
        .Select(p => new PicklistSetDto
        {
            Value = p.Value,
            Text = p.Text,
            Name = p.Name
        })
        .ToListAsync();
}

Enter fullscreen mode Exit fullscreen mode

✅ 4. Optional – Add Debounce to Reduce API Calls

DebounceDelay="300"

This delays filtering execution until the user stops typing, avoiding too many API hits.

🚀 Why Use Syncfusion in Blazor?

Syncfusion’s Blazor UI components offer a wide range of benefits for modern web apps. Here’s why it’s a great choice for developers:

âś… 1. Rich Component Library
Syncfusion provides 80+ production-ready components, including:

SfGrid, SfMultiSelect, SfDatePicker, SfDialog, SfChart, and more

Covers everything from data entry to dashboards

⚡ 2. Performance Optimized
Built-in virtualization and lazy loading

Handles large datasets smoothly (e.g., thousands of records in a Grid)

🎨 3. Consistent UI/UX
Beautiful, modern design out of the box

Theme support: Fluent, Bootstrap5, Material, Tailwind, or your custom styles

đź§° 4. Developer Productivity
Intuitive APIs that follow Blazor standards (@bind-Value, DataSource, events)

  • Extensive documentation and real-world samples
  • Native Blazor — not just JS interop wrappers

đź”’ 5. Enterprise-Ready Features

  • Built-in support for localization, accessibility (WCAG 2.0), and RTL
  • Works seamlessly with validation frameworks like FluentValidation

🔄 6. Excellent Integration with Modern Blazor Patterns
Compatible with @inject, CascadingParameter, and EventCallback

  • MVVM-friendly (e.g., CommunityToolkit.MVVM)
  • Supports reactive forms and dynamic configuration

👨‍💻 7. Strong Commercial Support
Regular updates and new feature rollouts

  • Dedicated support via forums and ticket system
  • Source code access included in paid plans

Top comments (2)

Collapse
 
j_parson profile image
Jim Parson •

This is a great example of how to wrap Syncfusion controls with meaningful encapsulation in Blazor. The separation of state and parent binding is textbook clean.

Collapse
 
stevsharp profile image
Spyros Ponaris •

Glad that you liked. Thanks 👍