Search code examples
c#wpfbindingautocompletebox

AutoCompleteBox - How to select a specific item from a collection of matching items?


When using AutoCompleteBox - I have a list of items that have the matching display text - but different ID value.

[{Text:"John",Id:1},{Text:"John",Id:2},{Text:"John Doe",Id:3}]

enter image description here

When I select the second row (John #2) - the first value (John #1) is set to the SelectedItem property. When selecting a distinct value (John Doe #3) - it works correctly.

My observation is that if there are more matching items - it always takes the first of them.

What can I do so that the correct item (John #2) is set to SelectedItem?

<controls2:AutoCompleteBox 
   EraseTextIfNull="False"
   ValueMemberBinding="{Binding Converter={StaticResource AutocompleteItemTextConverter}}"
   ItemsSource="{Binding TestItemSource, RelativeSource={RelativeSource TemplatedParent}}"
   SelectedItem="{Binding TestSelectedItem,Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
   Text="{Binding TestText, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" 
   ItemTemplate="{Binding Source={StaticResource TestItemTemplate}}"
 />

ViewModel:

public class TestItemDto
{
    public int Id { get; set; }
    public string Text { get; set; }
}

private ObservableCollection<TestItemDto> _testItemSource = new ObservableCollection<TestItemDto>
{
    new TestItemDto
    {
        Id = 1,
        Text = "John"
    },
    new TestItemDto
    {
        Id = 2,
        Text = "John"
    },
    new TestItemDto
    {
        Id = 3,
        Text = "John Doe"
    },
};

public ObservableCollection<TestItemDto> TestItemSource
{
    get => _testItemSource;
    set
    {
        _testItemSource = value;
    }
}


private string _testText;

public string TestText
{
    get { return _testText; }
    set
    {
        _testText = value;
        Console.WriteLine($"TestText:{TestText}");
    }
}

private TestItemDto _testSelectedItem;

public TestItemDto TestSelectedItem
{
    get { return _testSelectedItem; }
    set
    {
        _testSelectedItem = value;
        Console.WriteLine($"TestText:{TestSelectedItem?.Id}:{TestSelectedItem?.Text}");
    }
}

Converter:

public class AutocompleteItemTextConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var adresarSearchResultItemDto = value as TestItemDto;
        return adresarSearchResultItemDto?.Text;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return null;
    }
}

Solution

  • The problem was with TryGetMatch method searching for a matching text and then choosing the first matched item.

    I added a priorityView argument (where SelectedItem is passed). Therefore before searching in the rest of the ItemSource - first it checks if the currently SelectedItem is already matching the text:

    private object TryGetMatch(string searchText, object priorityView,ObservableCollection<object> view, AutoCompleteFilterPredicate<string> predicate)
    {
        if (priorityView != null)
        {
            if (predicate(searchText, FormatValue(priorityView)))
            {
                return priorityView;
            }
        }
        
        if (view != null && view.Count > 0)
        {
            foreach (object o in view)
            {
                if (predicate(searchText, FormatValue(o)))
                {
                    return o;
                }
            }
        }
    
        return null;
    }
    

    This was also merged into the original repository