Setting an elements visibility within a composite control

Hi All,

I'm not quite sure where I'm going wrong here, I'm sure its data binding related, but basically I'm trying to build an autocomplete entry. I have an entry which when the user starts to type a list view below would be displayed of suggestions. This part works great, however I want to hide the listview when the suggestion is clicked or the entry isn't being typed into. This part I'm struggling with. I've tried using a grid (I was using a stack layout before) and setting the rows height to zero, I've tried directly setting the ListViews visibility in the code behind and I've tried binding the controls visibility to a property.

Do you have to implement INotifyProperty changed here?

XAML

<ContentView 
             xmlns:behaviors="clr-FB.Behaviors;assembly=FB"
             x:Class="FB.Controls.MyControl2"
             x:Name="This">

    <ContentView.Content>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="30"/>
                <RowDefinition Height="70"/>
            </Grid.RowDefinitions>
            <Entry x:Name="innerEntry" 
                   Grid.Row="0" FontFamily="{StaticResource Key='AbrilFatface'}">
                    <Entry.Behaviors>
                        <behaviors:EventToCommandBehavior EventName="TextChanged" 
                                                          Command="{Binding EntryTextChanged, Source={Reference This}}" />
                    </Entry.Behaviors>
            </Entry>
             <ListView x:Name="innerSuggestionBox" RowHeight="25"
                        Grid.Row="1"
                            IsVisible="{Binding IsEntryVisible, Source={Reference This}}"
                            behaviors:ItemTappedCommandListView.ItemTappedCommand="{Binding SuggestionItemTapped, Source={Reference This}}">
                        <ListView.ItemTemplate>
                            <DataTemplate>
                                <ViewCell>
                                    <ViewCell.View>
                                        <StackLayout Orientation="Horizontal">
                                            <Label Text="{Binding .}" HorizontalOptions="Start" />
                                        </StackLayout>
                                    </ViewCell.View>
                                </ViewCell>
                            </DataTemplate>
                        </ListView.ItemTemplate>
                </ListView>
        </Grid>
   </ContentView.Content>
</ContentView>

Code Behind

namespace FB.Controls
{
    public partial class MyControl2 : ContentView
    {
        public MyControl2()
        {
            InitializeComponent();

            privateSuggestionList = new ObservableRangeCollection<string>();

            this.IsEntryVisible = false;
            ListHeight = 0;
            innerEntry.SetBinding(Entry.PlaceholderProperty, new Binding("Placeholder", source: this));
            innerEntry.SetBinding(Entry.TextProperty, new Binding("Text", source: this));
            innerSuggestionBox.SetBinding(ListView.ItemsSourceProperty, new Binding("privateSuggestionList", source: this));

            innerEntry.Keyboard = Keyboard.Create(KeyboardFlags.CapitalizeSentence | KeyboardFlags.Spellcheck);
            //innerSuggestionBox.IsVisible = false;
            //innerSuggestionBox.SetBinding(ListView.IsVisibleProperty, new Binding("IsEntryVisible", source: this));
        }



        public static BindableProperty PlaceholderProperty =
            BindableProperty.Create("Placeholder", typeof(string), typeof(MyControl2), default(string));

        public string Placeholder
        {
            // ----- The display text for the composite control.
            get { return (string)base.GetValue(PlaceholderProperty); }
            set
            {
                if (value != this.Placeholder)
                    base.SetValue(PlaceholderProperty, value);
            }
        }

        public static BindableProperty TextProperty = BindableProperty.Create(
            propertyName: "Text",
            returnType: typeof(string),
            declaringType: typeof(MyControl2),
            defaultValue: string.Empty,
            defaultBindingMode: BindingMode.OneWay,
            propertyChanged: HandleTextPropertyChanged);

        public string Text
        {
            // ----- The display text for the composite control.
            get { return (string)base.GetValue(TextProperty); }
            set
            {
                if (value != this.Text)
                    base.SetValue(TextProperty, value);
            }
        }

        private static void HandleTextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
        {
            MyControl2 targetView;

            targetView = (MyControl2)bindable;
            if (targetView != null)
                targetView.Text = (string)newValue;
            targetView.IsEntryVisible = true;
        }

        private bool _isEntryVisible;
        public bool IsEntryVisible
        {
            get { return _isEntryVisible; }
            set
            {
                _isEntryVisible = value;
                //RaisePropertyChanged(() => IsEntryVisible);
            }
        }

        private int _listHeight;
        public int ListHeight
        {
            get { return _listHeight; }
            set
            {
                _listHeight = value;
                //RaisePropertyChanged(() => IsEntryVisible);
            }
        }

        public ObservableRangeCollection<string> privateSuggestionList { get; set; }

        public ICommand SuggestionItemTapped => new Command<string>(onSuggestionTapped);

        private void onSuggestionTapped(string item)
        {
            this.Text = item;
            Debug.WriteLine("List disabled");
            //innerSuggestionBox.IsVisible = false;
            IsEntryVisible = false;
            ListHeight = 0;
            Debug.WriteLine("IsEntryVisible value: {0}", IsEntryVisible);
        }

        public ICommand EntryTextChanged => new Command(onEntryTextChanged);

        private void onEntryTextChanged()
        {
            if (this.ItemsSource2 != null)
            {
                this.privateSuggestionList.Clear();
                this.privateSuggestionList.AddRange(ItemsSource2.Cast<string>()
                               .Where(x => x.ToLower().StartsWith(this.Text.ToLower(), StringComparison.Ordinal))
                               .OrderBy(x => x)
                               .ToList());
               Debug.WriteLine("List Enabled");
               //innerSuggestionBox.IsVisible = true;
               IsEntryVisible = true;
               ListHeight = 70;
               Debug.WriteLine("IsEntryVisible value: {0}", IsEntryVisible);
            }
        }

        public ICommand EntryTextLeft => new Command(onEntryTextLeft);

        private void onEntryTextLeft()
        {
            Debug.WriteLine("List disabled");
            IsEntryVisible = false;
            ListHeight = 0;
            //innerSuggestionBox.IsVisible = false;
        }

        public static readonly BindableProperty ItemsSource2Property =
            BindableProperty.Create(nameof(ItemsSource2), typeof(IEnumerable), typeof(MyControl2), null,
                                     BindingMode.Default, null, OnItemsSource2Changed);

        public IEnumerable ItemsSource2
        {
            get { return (IEnumerable)GetValue(ItemsSource2Property); }
            set { SetValue(ItemsSource2Property, value); }
        }

        static void OnItemsSource2Changed(BindableObject bindable, object oldvalue, object newvalue)
        {
            System.Diagnostics.Debug.WriteLine("source changed");
        }

    }
}

Best Answer

  • LandLuLandLu Xamurai
    Accepted Answer

    Since your SuggestionItemTapped command has a parameter item, when you bind to this command you should also define a command parameter or this command won't be triggered.
    Modify your Xaml like:

    <ContentView.Resources>
        <ResourceDictionary>
            <local:SelectedItemEventArgsToSelectedItemConverter x:Key="SelectedItemConverter"/>
        </ResourceDictionary>
    </ContentView.Resources>
    <ContentView.Content>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="30"/>
                <RowDefinition Height="70"/>
            </Grid.RowDefinitions>
            <Entry x:Name="innerEntry" 
                    Grid.Row="0">
                <Entry.Behaviors>
                    <local:EventToCommandBehavior EventName="TextChanged" 
                                                          Command="{Binding EntryTextChanged, Source={Reference This}}" />
                </Entry.Behaviors>
            </Entry>
            <ListView x:Name="innerSuggestionBox" RowHeight="25"
                        Grid.Row="1"
                        IsVisible="{Binding IsEntryVisible, Source={Reference This}}" >
                <!--behaviors:ItemTappedCommandListView.ItemTappedCommand="{Binding SuggestionItemTapped, Source={Reference This}}">-->
                <ListView.Behaviors>
                    <local:EventToCommandBehavior EventName="ItemTapped" 
                                                          Command="{Binding SuggestionItemTapped, Source={Reference This}}" Converter="{StaticResource SelectedItemConverter}"/>
                </ListView.Behaviors>
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell>
                            <ViewCell.View>
                                <StackLayout Orientation="Horizontal">
                                    <Label Text="{Binding .}" HorizontalOptions="Start" />
                                </StackLayout>
                            </ViewCell.View>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
    
        </Grid>
    </ContentView.Content>
    

    This is the SelectedItemEventArgsToSelectedItemConverter's code:

    public class SelectedItemEventArgsToSelectedItemConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var eventArgs = value as ItemTappedEventArgs;
            return eventArgs.Item;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    

    In your code behind file, the IsEntryVisible field should implement the OnPropertyChanged event:

    public bool IsEntryVisible
    {
        get { return _isEntryVisible; }
        set
        {
            _isEntryVisible = value;
            //RaisePropertyChanged(() => IsEntryVisible);
            OnPropertyChanged();
        }
    }
    

    Moreover we should define a field to avoid the onEntryTextChanged being triggered when listView's item is tapped. I modify these two command events like:

    bool didTapSuggestion = false;
    private void onSuggestionTapped(string item)
    {
        didTapSuggestion = true;
        this.Text = item;
        Debug.WriteLine("List disabled");
        //innerSuggestionBox.IsVisible = false;
        IsEntryVisible = false;
        Debug.WriteLine("IsEntryVisible value: {0}", IsEntryVisible);
    }
    
    private void onEntryTextChanged()
    {
        if (this.ItemsSource2 != null && !didTapSuggestion)
        {
            List<string> list = new List<string>();
            list.AddRange(ItemsSource2.Cast<string>()
                            .Where(x => x.ToLower().StartsWith(this.Text.ToLower(), StringComparison.Ordinal))
                            .OrderBy(x => x)
                            .ToList());
            this.privateSuggestionList.Clear();
            foreach (string s in list)
            {
                privateSuggestionList.Add(s);
            }
            Debug.WriteLine("List Enabled");
            //innerSuggestionBox.IsVisible = true;
            IsEntryVisible = true;
            ListHeight = 70;
            Debug.WriteLine("IsEntryVisible value: {0}", IsEntryVisible);
        }
        didTapSuggestion = false;
    }
    

    You can also refer to my whole sample here to see the effect.

Answers

  • LandLuLandLu Member, Xamarin Team Xamurai
    Accepted Answer

    Since your SuggestionItemTapped command has a parameter item, when you bind to this command you should also define a command parameter or this command won't be triggered.
    Modify your Xaml like:

    <ContentView.Resources>
        <ResourceDictionary>
            <local:SelectedItemEventArgsToSelectedItemConverter x:Key="SelectedItemConverter"/>
        </ResourceDictionary>
    </ContentView.Resources>
    <ContentView.Content>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="30"/>
                <RowDefinition Height="70"/>
            </Grid.RowDefinitions>
            <Entry x:Name="innerEntry" 
                    Grid.Row="0">
                <Entry.Behaviors>
                    <local:EventToCommandBehavior EventName="TextChanged" 
                                                          Command="{Binding EntryTextChanged, Source={Reference This}}" />
                </Entry.Behaviors>
            </Entry>
            <ListView x:Name="innerSuggestionBox" RowHeight="25"
                        Grid.Row="1"
                        IsVisible="{Binding IsEntryVisible, Source={Reference This}}" >
                <!--behaviors:ItemTappedCommandListView.ItemTappedCommand="{Binding SuggestionItemTapped, Source={Reference This}}">-->
                <ListView.Behaviors>
                    <local:EventToCommandBehavior EventName="ItemTapped" 
                                                          Command="{Binding SuggestionItemTapped, Source={Reference This}}" Converter="{StaticResource SelectedItemConverter}"/>
                </ListView.Behaviors>
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell>
                            <ViewCell.View>
                                <StackLayout Orientation="Horizontal">
                                    <Label Text="{Binding .}" HorizontalOptions="Start" />
                                </StackLayout>
                            </ViewCell.View>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
    
        </Grid>
    </ContentView.Content>
    

    This is the SelectedItemEventArgsToSelectedItemConverter's code:

    public class SelectedItemEventArgsToSelectedItemConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var eventArgs = value as ItemTappedEventArgs;
            return eventArgs.Item;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    

    In your code behind file, the IsEntryVisible field should implement the OnPropertyChanged event:

    public bool IsEntryVisible
    {
        get { return _isEntryVisible; }
        set
        {
            _isEntryVisible = value;
            //RaisePropertyChanged(() => IsEntryVisible);
            OnPropertyChanged();
        }
    }
    

    Moreover we should define a field to avoid the onEntryTextChanged being triggered when listView's item is tapped. I modify these two command events like:

    bool didTapSuggestion = false;
    private void onSuggestionTapped(string item)
    {
        didTapSuggestion = true;
        this.Text = item;
        Debug.WriteLine("List disabled");
        //innerSuggestionBox.IsVisible = false;
        IsEntryVisible = false;
        Debug.WriteLine("IsEntryVisible value: {0}", IsEntryVisible);
    }
    
    private void onEntryTextChanged()
    {
        if (this.ItemsSource2 != null && !didTapSuggestion)
        {
            List<string> list = new List<string>();
            list.AddRange(ItemsSource2.Cast<string>()
                            .Where(x => x.ToLower().StartsWith(this.Text.ToLower(), StringComparison.Ordinal))
                            .OrderBy(x => x)
                            .ToList());
            this.privateSuggestionList.Clear();
            foreach (string s in list)
            {
                privateSuggestionList.Add(s);
            }
            Debug.WriteLine("List Enabled");
            //innerSuggestionBox.IsVisible = true;
            IsEntryVisible = true;
            ListHeight = 70;
            Debug.WriteLine("IsEntryVisible value: {0}", IsEntryVisible);
        }
        didTapSuggestion = false;
    }
    

    You can also refer to my whole sample here to see the effect.

  • Thank so much @LandLu for your help and great idea on the didTapSuggestion flag

Sign In or Register to comment.