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 👍