Hide Grid by Toggling a Switch - Data Binding

ShaunBauerShaunBauer USMember ✭✭
edited August 26 in Xamarin.Forms

Hi All,

Building a project which extracts questions and question categories from a database and then builds out a series of Editors, each within a Grid. At the top I have a label and a switch with the intent that the grid can be collapsed using the switch. An example picture below.

Essentially, I'm having a heck of a time working out how to link the correct button with the correct Grid so that it hides. I built it out in Xaml OK using static values, but as I'm using questions from a database, I have to do this purely in C#. I was trying to use bindingcontext and setbinding - all to no luck.

The switch is categoryTitleSwitch. The element I want to toggle is categoryGrid.

Any tips would be great

                 // loop through categories
                foreach (var category in categories)
                {

                    // Category Label and visibility switch
                    var categoryTitleGrid = new Grid { };

                    // category title column definitions
                    categoryTitleGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
                    categoryTitleGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });

                    // category title row defitions
                    categoryTitleGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

                    // category title label
                    var categoryTitleLabel = new Label { Text = category.name, Style=Device.Styles.SubtitleStyle };
                    var categoryTitleLabelLayout = new StackLayout { Orientation = StackOrientation.Vertical };
                    categoryTitleLabelLayout.Children.Add(categoryTitleLabel);

                    // category title switch
                    var categoryTitleSwitchLayout = new StackLayout { Orientation = StackOrientation.Vertical };
                    var categoryTitleSwitch = new Switch {  IsToggled = true };

                    categoryTitleSwitchLayout.Children.Add(categoryTitleSwitch);

                    // add layouts to grid
                    categoryTitleGrid.Children.Add(categoryTitleLabelLayout, 0, 0);
                    categoryTitleGrid.Children.Add(categoryTitleSwitchLayout, 1, 0);

                    // add now grid to the rest of the layout to get in right spot
                    parentLayout.Children.Add(categoryTitleGrid);

                    // wrapping grid - set early for binding context
                    var categoryGrid = new Grid { Padding = new Thickness(20, 20, 20, 40) };

                    categoryGrid.BindingContext = categoryTitleSwitch.IsToggled;
                    //categoryGrid.BindingContext = new { IsVisible = categoryTitleSwitch.IsToggled };
            //categoryGrid.SetBinding(Grid.IsVisibleProperty, new Binding());

                    // single columns
                    categoryGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });

                    // process each question
                    foreach (var question in questions)
                    {

            // processes each question
                    }

                    parentLayout.Children.Add(categoryGrid);
                }
Tagged:

Best Answer

  • ShaunBauerShaunBauer USMember ✭✭
    Accepted Answer

    Ok - so I parked this as it was making me angry - but the break did me well and I have it working. There wasn't much on the internet about dynamically making a view based on database results so I had to do a bit of trial and error to link the elements together.

    I achieved this by assigning a new instance of the ViewModel to each category. this treats each category / stacklayout as it's own view in a larger view. This may not be the best way, but it works. I'm interested to hear peoples thoughts if it's a terrible way - but until I find a better way, it will do :)

    My code is below if you are interested

    view

                            // surrounding content scroll view
                            var parentLayout = new StackLayout { Orientation = StackOrientation.Vertical, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.StartAndExpand };
    
                            // loop through categories
                            foreach (var category in categories)
                            {
    
                                // each category frame has it's own viewmodel to link to frame
                                var categoryFrame = new Frame { **BindingContext = new QuestionsViewModel()**, VerticalOptions = LayoutOptions.CenterAndExpand, HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(20, 20, 20, 20) };
    
                                // category lable and visibility switch grid
                                var categoryTitleGrid = new Grid { };
    
                                // category title column definitions
                                categoryTitleGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
                                categoryTitleGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
    
                                // category title row defitions
                                categoryTitleGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
                                categoryTitleGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
    
                                // category title label
                                var categoryTitleLabelLayout = new StackLayout { Orientation = StackOrientation.Vertical, VerticalOptions = LayoutOptions.Center };
                                var categoryTitleLabel = new Label { Text = category.name, FontSize = 20, FontAttributes = FontAttributes.Italic, TextColor = Color.FromRgb(74, 19, 38) };
    
                                categoryTitleLabelLayout.Children.Add(categoryTitleLabel);
    
                                // category title switch
                                var categoryTitleSwitchLayout = new StackLayout { Orientation = StackOrientation.Vertical, VerticalOptions = LayoutOptions.Center };
                                var categoryTitleSwitch = new Switch { };
    
            // bind the switch value                    
            categoryTitleSwitch.SetBinding(Switch.IsToggledProperty, "IsCategoryVisible");
    
                                categoryTitleSwitchLayout.Children.Add(categoryTitleSwitch);
    
                                // add layouts to grid
                                categoryTitleGrid.Children.Add(categoryTitleLabelLayout, 0, 0);
                                categoryTitleGrid.Children.Add(categoryTitleSwitchLayout, 1, 0);
    
                                // wrapping grid - set early for binding context
                                var categoryGrid = new Grid { Padding = new Thickness(0, 20, 0, 0) };
    
            // same binding as the switch                    
            categoryGrid.SetBinding(Grid.IsVisibleProperty, "IsCategoryVisible");*
    
    
                                // process each question
                                foreach (var question in categoryQuestions)
                                {
                            // add each question
                                }
    
    
                                // add question grid to parent
                                categoryTitleGrid.Children.Add(categoryGrid, 0, 1);
    
                                Debug.WriteLine("{0}", categoryGrid.Parent.ToString());
    
                                // set question grid col span
                                Grid.SetColumnSpan(categoryGrid, 2);
    
                                // add parent grid to frame
                                categoryFrame.Content = categoryTitleGrid;
    
                                // and finally, frame to th parent
                                parentLayout.Children.Add(categoryFrame);
    
                            }
    
                            // display the content
                            Content = new ScrollView { Content = parentLayout };
    

    ViewModel

                        public partial class QuestionsViewModel : INotifyPropertyChanged
                        {
                            private bool isCategoryVisible;
    
                            public event PropertyChangedEventHandler PropertyChanged;
    
                            private void OnPropertyChanged([CallerMemberName] string name = "")
                            {
                                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
                            }
    
                            public QuestionsViewModel()
                            {
                            }
    
                            public bool IsCategoryVisible
                            {
                                get { return isCategoryVisible; }
                                set
                                {
                                    isCategoryVisible = value;
                                    OnPropertyChanged();
                                }
                            }
    

Answers

  • ChaseFlorellChaseFlorell CAInsider, University mod
    edited August 26

    I would do something with the event if you're dealing purely with the view and not actual data at this point.

    // NOTE: THIS IS ALL PSEUDO CODE WRITTEN WITHOUT AN IDE, IT WON'T COMPILE
    
    private double _containerHeight;
    private bool _containerVisible; // = false; Start Collapsed
    
    public MyPage()
    {
        InitializeComponent();
    
        var switch = new Switch();
        switch.Toggled += VisibilityToggled;
    
        // do this so that you can store the original height and then start it as immediately collapsed.
        _containerHeight = SomeContainer.Height;
        SomeContainer.HeightRequest = 0;
    }
    
    private void VisibilityToggled(object sender, EventArgs args)
    {
        double start, end;
        this.AbortAnimation("SomeAnimation");
    
        // you can also check for `switch.IsToggled` if you prefer.
    
        if(_containerVisible)
        {
             _containerVisible = false;
            start = _containerHeight;
            end = 0;
        } else {
             _containerVisible = true;
            start = 0;
            end = _containerHeight;
        }
    
        // animate for beauty
        var animation = new Animation(v => SomeContainer.HeightRequest = v, start, end, Easing.SinInOut);
        animation.Commit(this, "SomeAnimation");
    }
    

    Also, this can totally be abstracted in a way that the animation bits can be re-used across multiple views. You can also run multiple animations a the same time if you want to collapse the unused containers when expanding another.

  • ShaunBauerShaunBauer USMember ✭✭

    Thanks @ChaseFlorell - I think this gets me a little closer, but the problem lies in the reference to the correct "SomeContainer". There can be multiple Categorys (each within it's own "SomeContainer") and each category will have a switch to toggle it's visibility.

    Below is a bit of a refactor to include the category loop... I think I jsut need to work out how to pass a SomeContainer reference to the event.

    public MyPage()
    {
        InitializeComponent();
    
        for (var category in categories)
        {
            var switch = new Switch();
            switch.Toggled += VisibilityToggled;
            var SomeContainer = new Grid { };
    
            // do this so that you can store the original height and then start it as immediately collapsed.
            _containerHeight = SomeContainer.Height;
            SomeContainer.HeightRequest = 0;
    }
    
    
    private void VisibilityToggled(object sender, EventArgs args)
    {
    ....
        // animate for beauty
        var animation = new Animation(v => **SomeContainer.HeightRequest** = v, start, end, Easing.SinInOut);
        animation.Commit(this, "SomeAnimation");
    }
    

    This thought is a bit of a hack but is there anyway I can associate a string ID when the switch is created that can be used in the event to find the correct container? something like the below (ack that the arguments used are indicative only)

    var switch = new Switch {ClassId = "unique-container-id"}
    var SomeContainer = new Grid {x:Name = "unique-container-id" }
    
    private void VisibilityToggled(object sender, EventArgs args)
        {
    SomeContainer = Container.FindByName<Grid> ("unique-container-id")
    var animation = new Animation(v => **SomeContainer.HeightRequest** = v, start, end, Easing.SinInOut);
            animation.Commit(this, "SomeAnimation");
    }
    
  • ShaunBauerShaunBauer USMember ✭✭

    An update.

    I created a BindableProperty for the switch.

        // cust webview to display pdfs using pdf.js
        public class ToggleTargetProperty : Switch
        {
            public static readonly BindableProperty ToggleTargetBindableProperty = BindableProperty.Create<ToggleTargetProperty, string>(p => p.ToggleTarget, default(string));
    
            public string ToggleTarget
            {
                get { return (string)GetValue(ToggleTargetBindableProperty); }
                set { SetValue(ToggleTargetBindableProperty, value); }
            }
        }
    

    So I can create the switch with the following and append a category identifier. I could use something different like category.ID maybe

    var categoryTitleSwitch = new Class.ToggleTargetProperty { ToggleTarget = category.slug, IsToggled = true };

    In the event handler, I can access that property like so

            async void Switch_HideMe (object sender, ToggledEventArgs e)
            {
                var senderData = sender as Class.ToggleTargetProperty;
    
                //var targetContainer = Content.FindByName<Grid>(senderData.ToggleTarget);
            }
    

    I'm about two lines of code away - If a magical function existed that could be Content.FindByID or I could work out what the name associated was. @AndrewMobile answered a post back in 2015 saying

    you can't set the x:Name from code behind.
    the name is registered internally by Xamarin Forms's XAML parser

    is it possible to access that registered name and call via the FindByName<> function...

  • ShaunBauerShaunBauer USMember ✭✭
    Accepted Answer

    Ok - so I parked this as it was making me angry - but the break did me well and I have it working. There wasn't much on the internet about dynamically making a view based on database results so I had to do a bit of trial and error to link the elements together.

    I achieved this by assigning a new instance of the ViewModel to each category. this treats each category / stacklayout as it's own view in a larger view. This may not be the best way, but it works. I'm interested to hear peoples thoughts if it's a terrible way - but until I find a better way, it will do :)

    My code is below if you are interested

    view

                            // surrounding content scroll view
                            var parentLayout = new StackLayout { Orientation = StackOrientation.Vertical, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.StartAndExpand };
    
                            // loop through categories
                            foreach (var category in categories)
                            {
    
                                // each category frame has it's own viewmodel to link to frame
                                var categoryFrame = new Frame { **BindingContext = new QuestionsViewModel()**, VerticalOptions = LayoutOptions.CenterAndExpand, HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(20, 20, 20, 20) };
    
                                // category lable and visibility switch grid
                                var categoryTitleGrid = new Grid { };
    
                                // category title column definitions
                                categoryTitleGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
                                categoryTitleGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
    
                                // category title row defitions
                                categoryTitleGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
                                categoryTitleGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
    
                                // category title label
                                var categoryTitleLabelLayout = new StackLayout { Orientation = StackOrientation.Vertical, VerticalOptions = LayoutOptions.Center };
                                var categoryTitleLabel = new Label { Text = category.name, FontSize = 20, FontAttributes = FontAttributes.Italic, TextColor = Color.FromRgb(74, 19, 38) };
    
                                categoryTitleLabelLayout.Children.Add(categoryTitleLabel);
    
                                // category title switch
                                var categoryTitleSwitchLayout = new StackLayout { Orientation = StackOrientation.Vertical, VerticalOptions = LayoutOptions.Center };
                                var categoryTitleSwitch = new Switch { };
    
            // bind the switch value                    
            categoryTitleSwitch.SetBinding(Switch.IsToggledProperty, "IsCategoryVisible");
    
                                categoryTitleSwitchLayout.Children.Add(categoryTitleSwitch);
    
                                // add layouts to grid
                                categoryTitleGrid.Children.Add(categoryTitleLabelLayout, 0, 0);
                                categoryTitleGrid.Children.Add(categoryTitleSwitchLayout, 1, 0);
    
                                // wrapping grid - set early for binding context
                                var categoryGrid = new Grid { Padding = new Thickness(0, 20, 0, 0) };
    
            // same binding as the switch                    
            categoryGrid.SetBinding(Grid.IsVisibleProperty, "IsCategoryVisible");*
    
    
                                // process each question
                                foreach (var question in categoryQuestions)
                                {
                            // add each question
                                }
    
    
                                // add question grid to parent
                                categoryTitleGrid.Children.Add(categoryGrid, 0, 1);
    
                                Debug.WriteLine("{0}", categoryGrid.Parent.ToString());
    
                                // set question grid col span
                                Grid.SetColumnSpan(categoryGrid, 2);
    
                                // add parent grid to frame
                                categoryFrame.Content = categoryTitleGrid;
    
                                // and finally, frame to th parent
                                parentLayout.Children.Add(categoryFrame);
    
                            }
    
                            // display the content
                            Content = new ScrollView { Content = parentLayout };
    

    ViewModel

                        public partial class QuestionsViewModel : INotifyPropertyChanged
                        {
                            private bool isCategoryVisible;
    
                            public event PropertyChangedEventHandler PropertyChanged;
    
                            private void OnPropertyChanged([CallerMemberName] string name = "")
                            {
                                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
                            }
    
                            public QuestionsViewModel()
                            {
                            }
    
                            public bool IsCategoryVisible
                            {
                                get { return isCategoryVisible; }
                                set
                                {
                                    isCategoryVisible = value;
                                    OnPropertyChanged();
                                }
                            }
    
Sign In or Register to comment.