Search code examples
xamarinxamarin.formsobservablecollection

Using SearchBar with ObservableCollection in MVVM


I've created a search page in my app and I'd like to be able to search through my ObservableCollection of items in the ViewModel and display them onto a CollectionView. So far this is what I've done and I get an exception i.e System.Reflection.TargetInvocationException: 'Exception has been thrown by the target of an invocation.' every time I run the app.
SearchPage XAML

<!--Doctors Search Result-->
        <Grid Grid.Row="1">
            <CollectionView ItemsSource="{Binding RecentDoctors}">

                <CollectionView.ItemsLayout>
                    <ListItemsLayout Orientation="Vertical" ItemSpacing="15"/>
                </CollectionView.ItemsLayout>

                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <StackLayout Orientation="Horizontal">
                            <!--Image-->
                            <Frame BackgroundColor="Black"
                                   HeightRequest="20"
                                   WidthRequest="20"
                                   CornerRadius="100"
                                   Margin="20,0,0,0"
                                   HorizontalOptions="Start"
                                   VerticalOptions="Center"
                                   IsClippedToBounds="True">

                                <Image HorizontalOptions="Center"
                                       VerticalOptions="Center"/>
                            </Frame>

                            <StackLayout Orientation="Vertical"
                                         VerticalOptions="Center"
                                         Spacing="-3">
                                <!--Fullname-->
                                <Label Text="{Binding DoctorsName}"
                                       FontSize="19"
                                       FontAttributes="Bold"/>

                                <!--Specialization-->
                                <Label Text="{Binding Specialization}"
                                       FontSize="14"
                                       TextColor="LightGray"/>
                            </StackLayout>
                        </StackLayout>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </Grid>

        <!--Doctors Search Bar-->
        <Grid Grid.Row="0" ColumnSpacing="0" RowSpacing="0">
            <pancake:PancakeView BackgroundColor="#0F8DF4"
                                 HasShadow="True">
                <Grid>
                    <!--The SearchBar-->
                    <renderers:CustomSearchBar x:Name="doctorsSearchBar"
                               Placeholder="Search Doctors by Name, Specialization"
                               VerticalOptions="Center"
                               FontSize="17"
                               TextColor="Black"
                               WidthRequest="320"
                               Text="{Binding SearchedText}"
                               SearchCommand="{Binding SearchBarCommand}"
                               SearchCommandParameter="{Binding Text, Source={x:Reference doctorsSearchBar}}"/>
                </Grid>
            </pancake:PancakeView>
        </Grid>

SearchPage ViewModel

public class TelemedSearchPageViewModel : BaseViewModel
    {
        private string _searchedText;
        public string SearchedText
        {
            get { return _searchedText; }

            set
            {
                _searchedText = value;
                OnPropertyChanged();
                Search();
            }
        }

        public ObservableCollection<RecentDoctorsInfo> RecentDoctors { get; set; } = new ObservableCollection<RecentDoctorsInfo>();

        public ICommand SearchBarCommand { get; set; }

        /// <summary>
        /// Main Constructor
        /// </summary>
        public TelemedSearchPageViewModel()
        {
            SearchBarCommand = new RelayCommand(Search);

            //RecentDoctorsList
            RecentDoctors.Add(new RecentDoctorsInfo()
            {
                DoctorsName = "Steven Strange",
                Specialization = "Sorcerer Supreme",
                Location = "177a Bleecker St. | USA"
            });

            RecentDoctors.Add(new RecentDoctorsInfo()
            {
                DoctorsName = "Peter Parker",
                Specialization = "Spiderman",
                Location = "177a Bleecker St. | USA"
            });

            RecentDoctors.Add(new RecentDoctorsInfo()
            {
                DoctorsName = "Bruce Banner",
                Specialization = "The Hulk",
                Location = "177a Bleecker St. | USA"
            });

            RecentDoctors.Add(new RecentDoctorsInfo()
            {
                DoctorsName = "Reed Richards",
                Specialization = "Mr.Fantastic",
                Location = "177a Bleecker St. | USA"
            });  
        }

        #region METHODS
        public void Search()
        {
            if (RecentDoctors != null && RecentDoctors.Count >0)
            {
                var temp = RecentDoctors.Where(x => x.DoctorsName.ToLower().Contains(SearchedText.ToLower()));

                foreach (var item in temp)
                {
                    RecentDoctors.Add(item);
                }
            }
        }

        #endregion
    }

Edit3:

if (RecentDoctors != null && RecentDoctors.Count > 0)
            {
                var results = RecentDoctors.Where(x => x.DoctorsName.ToLower().Contains(SearchedText.ToLower()));
                SearchResults.Clear();
                foreach (RecentDoctorsInfo item in results)
                {
                    SearchResults.Add(item);
                }
            }
            else
            {
                RecentDoctors.Clear();
            }

Solution

  • If you want to execute the search when user type you should use a behavior as doscs suggest

    public class SearchBarTextChangedBehavior : Behavior<SearchBar>
    {
        protected override void OnAttachedTo(SearchBar bindable)
        {
            base.OnAttachedTo(bindable);
            bindable.TextChanged += this.SearchBar_TextChanged;
        }
    
        protected override void OnDetachingFrom(SearchBar bindable)
        {
            base.OnDetachingFrom(bindable);
            bindable.TextChanged -= this.SearchBar_TextChanged;
        }
    
        private void SearchBar_TextChanged(object sender, TextChangedEventArgs e)
        {
            ((SearchBar)sender).SearchBarCommand?.Execute(e.NewTextValue);
        }
    }
    

    Then attach the behavior to your SearchBar

    <renderers:CustomSearchBar
        x:Name="doctorsSearchBar"
        Placeholder="Search Doctors by Name, Specialization"
        VerticalOptions="Center"
        FontSize="17"
        TextColor="Black"
        WidthRequest="320"
        Text="{Binding SearchedText}"
        SearchCommand="{Binding SearchBarCommand}">
        <renderers:CustomSearchBar.Behaviors>
            <behaviors:SearchBarTextChangedBehavior />
        </renderers:CustomSearchBar.Behaviors>
    </renderers:CustomSearchBar>
    

    By the other hand, you should create a private copy of the original list and add the same items as the public collection

    private List<RecentDoctorsInfo> originalRecentDoctorsList = new List<RecentDoctorsInfo>();
    
    public ObservableCollection<RecentDoctorsInfo> RecentDoctors { get; set; } = new ObservableCollection<RecentDoctorsInfo>();
    
    public ICommand SearchBarCommand { get; set; }
    
    public TelemedSearchPageViewModel()
    {
        SearchBarCommand = new RelayCommand(Search);
    
        //RecentDoctorsList
        RecentDoctors.Add(new RecentDoctorsInfo()
        {
            DoctorsName = "Steven Strange",
            Specialization = "Sorcerer Supreme",
            Location = "177a Bleecker St. | USA"
        });
    
        RecentDoctors.Add(new RecentDoctorsInfo()
        {
            DoctorsName = "Peter Parker",
            Specialization = "Spiderman",
            Location = "177a Bleecker St. | USA"
        });
    
        RecentDoctors.Add(new RecentDoctorsInfo()
        {
            DoctorsName = "Bruce Banner",
            Specialization = "The Hulk",
            Location = "177a Bleecker St. | USA"
        });
    
        RecentDoctors.Add(new RecentDoctorsInfo()
        {
            DoctorsName = "Reed Richards",
            Specialization = "Mr.Fantastic",
            Location = "177a Bleecker St. | USA"
        });
    
        // Backup copy list.
        originalRecentDoctorsList.Add(new RecentDoctorsInfo()
        {
            DoctorsName = "Steven Strange",
            Specialization = "Sorcerer Supreme",
            Location = "177a Bleecker St. | USA"
        });
    
        originalRecentDoctorsList.Add(new RecentDoctorsInfo()
        {
            DoctorsName = "Peter Parker",
            Specialization = "Spiderman",
            Location = "177a Bleecker St. | USA"
        });
    
        originalRecentDoctorsList.Add(new RecentDoctorsInfo()
        {
            DoctorsName = "Bruce Banner",
            Specialization = "The Hulk",
            Location = "177a Bleecker St. | USA"
        });
    
        originalRecentDoctorsList.Add(new RecentDoctorsInfo()
        {
            DoctorsName = "Reed Richards",
            Specialization = "Mr.Fantastic",
            Location = "177a Bleecker St. | USA"
        });  
    }
    

    And by last, your Search method should clean the public collection (the one you're showing) and use the private as backup

    private void Search()
    {
        if (!string.IsNullOrEmpty(SearchedText))
        {
            var filteredDoctors = RecentDoctors
                        .Where(x =>
                            x.DoctorsName.ToLower().Contains(SearchedText.ToLower()))
                        .ToList();
    
            RecentDoctors.Clear();
    
            foreach(var recentDoctor in filteredDoctors)
                RecentDoctors.Add(recentDoctor);
        }
        else
        {
            // This is when you clean the text from the search
    
            RecentDoctors.Clear();
    
            foreach(var originalRecentDoctor in originalRecentDoctorsList)
                RecentDoctors.Add(originalRecentDoctor);
        }
    }