Bug 21797 - When TextChanged event of SearchBar fires with my code in it, the keyboard dissapears
Summary: When TextChanged event of SearchBar fires with my code in it, the keyboard di...
Status: RESOLVED ANSWERED
Alias: None
Product: Forms
Classification: Xamarin
Component: Forms ()
Version: 1.2.2
Hardware: PC Windows
: Normal normal
Target Milestone: ---
Assignee: Bugzilla
URL:
Depends on:
Blocks:
 
Reported: 2014-08-04 12:09 UTC by noah.a.safian
Modified: 2015-05-29 10:37 UTC (History)
5 users (show)

Tags:
Is this bug a regression?: ---
Last known good build:

Notice (2018-05-24): bugzilla.xamarin.com is now in read-only mode.

Please join us on Visual Studio Developer Community and in the Xamarin and Mono organizations on GitHub to continue tracking issues. Bugzilla will remain available for reference in read-only mode. We will continue to work on open Bugzilla bugs, copy them to the new locations as needed for follow-up, and add the new items under Related Links.

Our sincere thanks to everyone who has contributed on this bug tracker over the years. Thanks also for your understanding as we make these adjustments and improvements for the future.


Please create a new report on Developer Community or GitHub with your current version information, steps to reproduce, and relevant error messages or log files if you are hitting an issue that looks similar to this resolved bug and you do not yet see a matching new report.

Related Links:
Status:
RESOLVED ANSWERED

Description noah.a.safian 2014-08-04 12:09:46 UTC
Trying to create a dynamic text search that updates the results as the user types. However, whenever the user types and TextChanged is fired when I have code in it, the keyboard disappears. When I switch TextChanged to SearchButtonPressed, I can type without the keyboard dissapearing. Would love any help or workaround you all can provide. I'm debugging on an iPad, writing in Visual Studio. The page i'm using is below, along with the engine controller class.

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using BusinessLayer.Engine;
using DAL;

namespace SCaN_Service_Catalogue
{
    class ServiceFilterPageV2 : MasterDetailPage
    {
        EngineInterface engine;
        public ServiceFilterPageV2(EngineInterface eng)
        {
            engine = eng;

            this.updateDetail();
            
            this.updateMaster(engine.CurrentFilter);
        }

        private void updateMaster(FilterTable key)
        {
            SearchBar sbar = new SearchBar
            {
                Placeholder = "Type service name here",
                BindingContext = engine
            };
            sbar.SetBinding(SearchBar.TextProperty, "TextSearch");

            sbar.SearchButtonPressed += (sender, args) =>
            {
                updateDetail();
            };

            Label title = new Label
            {
                Text = "Filter Options",
                HorizontalOptions = LayoutOptions.CenterAndExpand,
                Font = Font.BoldSystemFontOfSize(NamedSize.Large)
            };

            ListView filters = new ListView
            {
                ItemsSource = engine.Filters,
                ItemTemplate = new DataTemplate(typeof(TextCell))
            };
            filters.ItemTemplate.SetBinding(TextCell.TextProperty, "FilterTitle");
            filters.ItemSelected += async (sender, args) =>
            {
                this.updateMaster((FilterTable)args.SelectedItem);
                engine.CurrentFilter = (FilterTable)args.SelectedItem;
            };

            Label filterTitle = new Label
            {
                Text = key.FilterTitle,
                HorizontalOptions = LayoutOptions.Center,
                Font = Font.BoldSystemFontOfSize(NamedSize.Large)
            };

            ListView answerList = new ListView
            {
                ItemTemplate = new DataTemplate(typeof(SwitchCell)),
                VerticalOptions = LayoutOptions.FillAndExpand
            };
            answerList.ItemTemplate.SetBinding(SwitchCell.TextProperty, "Text");
            answerList.ItemTemplate.SetBinding(SwitchCell.OnProperty, "On");
            answerList.ItemSelected += async (sender, args) =>
            {
                answerList.SelectedItem = null;
            };



            ObservableCollection<SwitchCell> switches = new ObservableCollection<SwitchCell>();
            foreach (Answer a in key.Answers)
            {
                SwitchCell temp = new SwitchCell();
                temp.BindingContext = a;
                temp.SetBinding(SwitchCell.TextProperty, "Description");
                temp.SetBinding(SwitchCell.OnProperty, "SearchOn");
                temp.OnChanged += (sender, args) =>
                {
                    this.updateDetail();
                };
                switches.Add(temp);
            }
            answerList.ItemsSource = switches;


            Button clearButton = new Button
            {
                Text = "Clear Search",
                Font = Font.BoldSystemFontOfSize(30),
                HorizontalOptions = LayoutOptions.FillAndExpand,
                BackgroundColor = Color.Accent,
                TextColor = Color.White
            };
            clearButton.Clicked += async (sender, args) =>
            {
                engine.ClearSearch();
                updateDetail();
            };

            StackLayout masterStack = new StackLayout
            {
                Orientation = StackOrientation.Vertical,
                VerticalOptions = LayoutOptions.FillAndExpand,
                Children = 
                {
                    sbar,
                    title,
                    filters,
                    filterTitle,
                    answerList,
                    clearButton
                }
            };

            this.Master = new ContentPage
            {
                Title = "Filter Options",
                Content = masterStack
            };
        }

        private void updateDetail()
        {

            Label detailHeader = new Label
            {
                Text = "Matching Services",
                Font = Font.BoldSystemFontOfSize(30),
                HorizontalOptions = LayoutOptions.CenterAndExpand
            };

            ListView detailList = new ListView
            {
                //BindingContext = engine
                ItemsSource = engine.MatchingServices
            };
            //detailList.SetBinding(ListView.ItemsSourceProperty, "MatchingServices");
            detailList.ItemTemplate = new DataTemplate(typeof(TextCell));
            detailList.ItemTemplate.SetBinding(TextCell.TextProperty, "Title");
            detailList.ItemSelected += (asender, aargs) =>
            {
                Navigation.PushAsync(new ServiceDetailPage(aargs));
            };

            this.Detail = new ContentPage()
            {
                Title = "FilterDetailPage",

                Content = new StackLayout
                {
                    Children =
                                {
                                    detailHeader,
                                    detailList
                                }
                }
            };
        }
    }
}


/*
 * Current Engine in use. Had to redifine EngineInterface rather drastically to accomodate for
 * new implementation that makes heavy use of data binding on the front end and solves searching problem
 * by adding a "On" property to the Answer class.
 * 
 * The heavy filtering/searching work is done in the UpdateMatchesAnd() or UpdateMatchesOr() search,
 * one of which is called from the overarching UpdateMatches() method depending on the value of _andSearch.
 * 
 * This class relies heavily on data binding in the App's implementation, and can replacing the
 * FilterAnswered(Search) and FilterUnanswered(Service) methods by calling the UpdateMatches() whenever 
 * the PropertyChanged event of any of the answers is fired.
 */

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DAL;
using System.ComponentModel;

namespace BusinessLayer.Engine
{
    public class EngineV3 : EngineInterface, INotifyPropertyChanged
    {
        /*Field and corresponding property to determine whether search is AND or OR between FilterTables.
         * What this means is, if you have one Answer "On" in one FilterTable and another "On" in 
         * another FilterTable, if _andSearch == true (the default value) then only the services that match an Answer in both FilterTables
         * will be in _matchingServices. If _andSearch == false, then any Services matching any of the filters set will be in _matchingServices.
         * Two way binding is enabled on this property.
         */
        private bool _andSearch;
        public bool AndSearch
        {
            get { return _andSearch; }

            set
            {
                if (_andSearch != value)
                {
                    ClearSearch();//Clears the current search when you change modes
                    _andSearch = value;
                    OnPropertyChanged("AndSearch");
                }
            }
        }

        /*Private field to hold an instance of MyModel class, which fetches the necessary data from the database. It is the Business Layer's
         * way of talking to the data access layer
         */
        private MyModel data;

        /*Private field and public property to hold the collection of services that match the current search. Get and set enabled, and
         * along with two-way binding.
         */
        private ObservableCollection<Service> _matchingServices;
        public ObservableCollection<Service> MatchingServices
        {
            get { return _matchingServices; }

            set
            {
                _matchingServices = value;
                OnPropertyChanged("MatchingServices");
            }
        }

        //public property to allow access to the collection of all services
        public ObservableCollection<Service> AllServices { get { return data.Services; } }
        
        //public property to allow access to the collections of all sections
        public ObservableCollection<Section> Sections { get { return data.Sections; } }
        public ObservableCollection<FilterTable> Filters { get { return data.Filters; } }

        //private field and public property that holds the section the user is currently reading
        private Section _currentSection;
        public Section CurrentSection { get { return _currentSection; } set { _currentSection = value; } }

        //private field and public property that holds the filter the user is currently looking at
        private FilterTable _currentFilter;
        public FilterTable CurrentFilter { get { return _currentFilter; } set { _currentFilter = value; } }


        /*As for the text search, that executes a specific, case-independent search for services whose titles contain the desired text
         * within the services that match the filters and sets _matchingServices to those services. Two-way binding enabled
         */
        private string _textSearch;
        public string TextSearch { get { return _textSearch; } 
            set 
            { 
                _textSearch = value;
                UpdateMatches(TextSearch);
                OnPropertyChanged("TextSearch");
            } 
        }

        
        //Constructor, sets initial values
        public EngineV3(ICatalogDatabase database)
        {
            data = new MyModel(database);
            _matchingServices = new ObservableCollection<Service>(data.Services);
            _andSearch = true;
            _currentSection = data.Sections[0];
            _currentFilter = data.Filters[0];
            _textSearch = "";

            //Tells engine to update its matches whenever one of the filters is turned on or off
            foreach (FilterTable filter in Filters)
            {
                foreach (Answer a in filter.Answers)
                {
                    a.PropertyChanged += async (sender, args) =>
                        {
                            this.UpdateMatches(TextSearch);
                        };
                }
            }
        }

        //Helper method to implement INotifyPropertyChanged and enable two-way binding
        protected void OnPropertyChanged(string name)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(name));
            }
        }

        //Event to handle property changed, implements INotifyPropertyChanged for two-way binding
        public event PropertyChangedEventHandler PropertyChanged;

        //Helps with debugging. You probably should never use this
        public void setDebugServices()
        {
#if DEBUG_ENABLED
            _matchingServices = DebugAssistance.DebugHelper.GetDebugServiceObservableCollection();
#endif
        }

        //Clears the current search and resets the _matchingServices to all services
        public void ClearSearch()
        {
            foreach (FilterTable filter in Filters)
            {
                foreach (Answer a in filter.Answers)
                {
                    a.SearchOn = false;
                }
            }
            TextSearch = "";
            _matchingServices = new ObservableCollection<Service>(data.Services);
        }

        //Public method to update the matching services by calling the appropriate helper method based on
        //the value of _andSearch
        private void UpdateMatches(string search)
        {
            if (_andSearch)
            {
                updateMatchesAnd(search);
            }
            else
            {
                updateMatchesOr();
            }
        }

        //Helper method to update the matching services based on the current search parameters when _andSearch == true
        private void updateMatchesAnd(String search)
        {
            _matchingServices = new ObservableCollection<Service>();//empties _matchingServices
            foreach (Service current in data.Services)//cycles through each Service
            {
                if (current.Title.ToLower().Contains(search.ToLower()))//only considers them if they match the text search
                {
                    bool matches = true;//bool variable to hold whether current search matches filters
                    foreach (FilterTable filter in Filters)//cycles through each FilterTable
                    {
                        bool matchesFilter = true;//bool variable to hold whether current search matches current filter
                        foreach (Answer a in filter.Answers)//cycles through each Answer in filter
                        {
                            if (a.SearchOn)//Only considers filtering based on answers that are on
                            {
                                //if the current service doesn't match the a current answer, sets matchesFilter to false
                                //if it does match the current answer, sets matchesFilter to true and breaks from the Answer foreach
                                if (!EngineV3.followsTemplateOf(current, a.Template))
                                {
                                    matchesFilter = false;
                                }
                                else
                                {
                                    matchesFilter = true;
                                    break;
                                }
                            }
                        }
                        //If a service doesn't match a filter, sets matches to false and breaks from the FilterTable foreach
                        if (!matchesFilter)
                        {
                            matches = false;
                            break;
                        }
                    }
                    //Adds the current Service to _matchingServices if it matches the search
                    if (matches)
                    {
                        _matchingServices.Add(current);
                    }
                }
            }
        }

        //Helper method to update the matching services based on the current search parameters when _andSearch == false
        private void updateMatchesOr()
        {
            _matchingServices = new ObservableCollection<Service>();//empties _matchingServices
            foreach (Service current in data.Services)//cycles through each available service
            {
                bool matches = false;//bool variable to hold whether current Service matches the search
                bool filteringOn = false;//bool variable to hold whether we currently have any filters on
                foreach (FilterTable filter in Filters)//cycles through each FilterTable
                {
                    bool matchesFilter = false;//bool variable to hold whether the current service matches the current FilterTable
                    foreach (Answer a in filter.Answers)//cycles through each answer in teh current FilterTable
                    {
                        if (a.SearchOn)//Only considers searches that are on
                        {
                            filteringOn = true;//if a search is on, then we have a filter on so we set filteringOn to true

                            //If the current Service matches the current answer, sets matchesFilter to true and breaks the Answer loop
                            if (EngineV3.followsTemplateOf(current, a.Template))
                            {
                                matchesFilter = true;
                                break;
                            }
                        }
                    }
                    //If the service matches a filter, sets matches to true and breaks the FilterTable loop
                    if (matchesFilter)
                    {
                        matches = true;
                        break;
                    }
                }
                //If the service matches a filter or filtering is off, adds the service to the list of matching services
                if (matches || !filteringOn)
                {
                    _matchingServices.Add(current);
                }
            }
        }

        /*Static helper method that checks if original service has each of its fields equal to the non-null fields of the template service.
         *If all of the non-null properties of the template are equal to the corresponding properties of the original, then the method
         *returns true. Otherwise returns false. This is the method used for Service-to-service comparison, to see if a service matches
         *a filter.
         */
        private static bool followsTemplateOf(Service original, Service template)
        {
            if (template.Title != null && template.Title != original.Title)
            {
                return false;
            }
            if (template.Group != null && template.Group != original.Group)
            {
                return false;
            }
            if (template.Category != null && template.Category != original.Category)
            {
                return false;
            }
            if (template.TypicalUse != null && template.TypicalUse != original.TypicalUse)
            {
                return false;
            }
            if (template.Function != null && template.Function != original.Function)
            {
                return false;
            }
            if (template.Ioag_status != null && template.Ioag_status != original.Ioag_status)
            {
                return false;
            }
            if (template.Constraints != null && template.Constraints != original.Constraints)
            {
                return false;
            }
            if (template.Outputs != null && template.Outputs != original.Outputs)
            {
                return false;
            }
            if (template.ServiceEnds != null && template.ServiceEnds != original.ServiceEnds)
            {
                return false;
            }
            if (template.ServiceBegins != null && template.ServiceBegins != original.ServiceBegins)
            {
                return false;
            }
            if (template.IsValidated != null && template.IsValidated != original.IsValidated)
            {
                return false;
            }
            return true;
        }
    }
}
Comment 1 Seth Rosetter 2014-08-11 15:37:52 UTC
Noah, I have reviewed this code and noticed that your "updateDetail" method reassigns the detail page which causes the unexpected behavior. Underneath, iOS will recreate the ViewController when assigning to detail, causing the page to lose focus. However, if you only assign the detail once (i.e. in the ServiceFilterPageV2 ctor) and have the "updateDetail" only change the contents of your detail page (do not reassign the page), you should be able to hook into the SearchBar.TextChanged event and get the expected result.

 public ServiceFilterPageV2()
{			
    this.setDetail (); // Assign the Detail
    this.updateMaster();
}

...{
    sbar.TextChanged += (s, e) => updateDetail ();
}...

setDetail () 
{
   Detail = ... // Assign the detail
}...

updateDetail ()
{
    // Logic to update the contents of you detail page, do not reassign
    // page.
}