Select All Switch with MVVM

GraverobberGraverobber Member ✭✭✭
edited May 20 in Xamarin.Forms

Hello everyone,

I wanted to implement a select all switch, which, when selected it selects all other switches. If it is deselected it deselects all. If all switches are selected one by one and the last one is selected, the switch should also be selected. If all are selected and one is deselected the switch should deselect as well but the other switches should remain selected.

The last step is what is giving me a hard time right now.

I'm kind of new to MVVM so perhaps I'm missing something. I will give you a short example of what I did:

We have a content page which has the UI:

<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:SelectAllSwitchTest" x:Class="SelectAllSwitchTest.MainPage">
    <StackLayout Orientation="Vertical" HorizontalOptions="Center">
        <!-- Place new controls here -->
        <Switch x:Name="SelectAll" IsToggled="{Binding AllSelected}"/>
        <Switch x:Name="Joe" IsToggled="{Binding Joe.IsSelected}"/>
        <Switch x:Name="John" IsToggled="{Binding John.IsSelected}"/>
        <Switch x:Name="Fred" IsToggled="{Binding Fred.IsSelected}"/>
    </StackLayout>
</ContentPage>

And the .cs sets the Binding Context:

public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            BindingContext = new MainPageViewModel();
        }
    }

Then the MainPageViewModel is like this:

public class MainPageViewModel : INotifyPropertyChanged
    {
        public bool AllSelected
        {
            get 
            {
                foreach (var person in persons)
                    if (!person.IsSelected)
                        return false;
                return true;
            }
            set
            {
                foreach (var person in persons)
                    person.IsSelected = value;
            }
        }

        public Person Joe { get; set; }
        public Person John { get; set; }
        public Person Fred { get; set; }

        protected ObservableCollection<Person> persons;

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public MainPageViewModel()
        {
            John = new Person("John", true);
            Joe = new Person("Joe", false);
            Fred = new Person("Fred", false);

            persons = new ObservableCollection<Person>();
            persons.Add(John);
            persons.Add(Joe);
            persons.Add(Fred);

            foreach(var p in persons)
            {
                p.PropertyChanged += (sender, e) =>
                {
                    NotifyPropertyChanged(nameof(AllSelected));
                };
            }
        }
    }

and last but not least the Person model looks like this:

public class Person : INotifyPropertyChanged
    {
        public string Name { get; set; }
        public bool IsSelected { get { return _isSelected; } set { _isSelected = value; NotifyPropertyChanged(); } }

        private bool _isSelected { get; set; }
        public Person()
        {
        }
        public Person(string name, bool isSelected)
        {
            Name = name;
            IsSelected = isSelected;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

So as said, the part where I deselect a single switch while all are selected and it should deselect the Allselected switch as well is the one where I just can't figure out how to do that.

Before anyone says: I fully understand why in this simple example that I provided it is not working. But I tried tons of ideas, I looked together with a colleague but for the moment we didn't come up with a solution. I just can't name all the ways I tried to solve that or give an example to each and every single one that would be just too much. So I went with the simplest solution that comes to someones mind when he thinks of a solution as the starting point. With this solution, all cases are already covered except for the last one.
Does anyone have an idea, how to solve it?

Thank you already!

Answers

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭
    edited May 20

    TIP: If you're naming your controls with x:name for something other than referencing them from within the same XAML = you're already off to a bad start in your code design. As a rule, XAML UI doens't get named. Its been that way for 15+ years since WPF.

    TIP: If you're manipulating UI for logic reasons - Again, you're headed way off course.

    TIP: If you've got more than 10 additional lines in your xaml.cs file that didn't come from the template - start being concerned. We've got entire enterprise-grade apps that don't have 50 lines of code behind changes combined in the entire app. The UI is not were logic belongs. Its just a reflection of your data in the VIewModel.

    And you're manually making your people in the UI code behind of the page?

    Stop. Please just stop. You can't be a software architect and design your app from the ground up if you haven't done any coding or learning about the foundation concepts first. Its like trying to learn how to build a house by buying the lumber first then asking "How do I use a saw?"

    Binding and C# proficiency are foundation concepts within XAML/MVVM development. You really can't make a program without an understanding of these. I strongly encourage you to stop coding on your project until you take the time to study these. A proper understanding of these concepts will dramatically change the way your approach your solution and alter the direction of how you architect the app. So there's little point gerry-rigging something now that you'll rip out later once you understand how it should have been done. In other words, 40 hours of learning now will save you hundreds of wasted hours later - not to mention all the money you'll save in aspirin if you aren't suffering all the headaches from trying to build your app without a good foundation of understanding.

    Xamarin University YouTube channel
    https://www.youtube.com/channel/UCykEmj8H8O0aE6QB1965XCg

    Microsoft Learn, Xamarin search results
    https://docs.microsoft.com/en-us/search/index?search=Xamarin

    Red Pill Xamarin tutorial series
    http://redpillxamarin.com/2018/03/12/2018-101-vs2017-new-solution/

  • GraverobberGraverobber Member ✭✭✭

    Thank you for your response @ClintStLaurent.
    I sure will have another look into the Links you provided, from your text it seems I fundamentally misunderstood something in MVVM. I didn't expect it to be that bad :D
    Anyway a few points to your message:

    @ClintStLaurent said:
    TIP: If you're naming your controls with x:name for something other than referencing them from within the same XAML = you're already off to a bad start in your code design. As a rule, XAML UI doens't get named. Its been that way for 15+ years since WPF.

    I don't see why I can't name my UI elements for better readability? I named them, but in the code nothing happens with it so I don't see the problem here? Of course in this super simple example it is not needed, but in a more complex UI where many elements might repeat, a name in the beginning might help to quicker find what I'm searching for.

    @ClintStLaurent said:
    TIP: If you're manipulating UI for logic reasons - Again, you're headed way off course.

    I'm sorry but I can't follow what you mean? (English is not my mother tongue so perhaps it's the reason ^^) Can you elaborate on that a bit? Why shouldn't the UI correspond to logic changes?
    For example the UI of a radio button changes to the logic that only one button can be selected at a time, no?

    @ClintStLaurent said:
    TIP: If you've got more than 10 additional lines in your xaml.cs file that didn't come from the template - start being concerned. We've got entire enterprise-grade apps that don't have 50 lines of code behind changes combined in the entire app. The UI is not were logic belongs. Its just a reflection of your data in the VIewModel.

    Thanks for the tip, so far the only extra line(s) I have within the xaml.cs is to set the BindingContext to the ViewModel.

    @ClintStLaurent said:
    And you're manually making your people in the UI code behind of the page?

    In this example, yes, so people can just copy paste my code to a project and try it themself.

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭
    edited May 20

    @Graverobber said:
    Thank you for your response @ClintStLaurent.
    I sure will have another look into the Links you provided, from your text it seems I fundamentally misunderstood something in MVVM. I didn't expect it to be that bad :D
    Anyway a few points to your message:

    @ClintStLaurent said:
    TIP: If you're naming your controls with x:name for something other than referencing them _from within the same XAML

    I don't see why I can't name my UI elements for better readability?

    Again, "for something other than referencing {...}". If you're just doing it because you need the name to remember what its for... its not 'bad' per sae. But its not standard. Any most software houses will look sideways at your for it. Its a flag of a new developer so I don't recommend it in your job interview submitted samples. As you gain experience with XAML you'll get used to reading it. There's not much need for a name x:Name fredGenderSwitch if you can see its binded to Fred.IsMale or whatever. You get used to looking at the binding for the purpose of the element.

    ut in a more complex UI where many elements might repeat, a name in the beginning might help to quicker find what I'm searching for.

    Honestly, its in a more complex UI that you should see this less. Complex UI tend to have more structure to them. Nested groups that have a shared purpose for example. So you don't look at the entire page. You just at the Grid for the HomeAddress block or whatever. And since that is a small amount of code you don't really need naming to know what is the StreetAddress since its binded to the StreetAddyFirstLine property or whatever. And once you start using collections you only have 1 element such as a ListVIew or a FlexLayout that is binded to a collection. We don't do this bit of hand coding 200 items for too people in a collection.

    @ClintStLaurent said:
    TIP: If you're manipulating UI for logic reasons - Again, you're headed way off course.

    I'm sorry but I can't follow what you mean? (English is not my mother tongue so perhaps it's the reason ^^) Can you elaborate on that a bit? Why shouldn't the UI correspond to logic changes?

    No. Never. The UI is only a reflection of the data. Period. That's how this whole MVVM pattern is designed. UI does not manipulate logic. In theory your code should work just fine if there was no UI. All the logic should work absolutely fine if it were headless and just responding to REST calls, or electronic rain sensors, or GoogleAssistant sending a command.

    For example the UI of a radio button changes to the logic that only one button can be selected at a time, no?

    No. What happens when you have 10 different views all binded back to the same VIewModel? Which one is in charge of the logic then? How do you keep them all synchronized? No. The logic controls the logic and the data. The UI is just a way to display and interact with the data. If the data changes, that becomes a change for the logic to handle.

    @ClintStLaurent said:
    And you're manually making your people in the UI code behind of the page?

    In this example, yes, so people can just copy paste my code to a project and try it themself.

    what people? No offense... but nobody should be using that code or structure as a guide. Please don't tell me you're trying to write tutorials or code for others while you're still getting a handle on it yourself. Please don't be offended when I say "That's really just the blind leading the blind" - and leads to a big uptick in questions here and other sites where mentors have to undo the misconceptions and misunderstanding being spread.

  • GraverobberGraverobber Member ✭✭✭
    edited May 20

    Thank you again @ClintStLaurent,

    no no, don't worry I don't want to write Tutorials :)
    What I meant was if someone comes to this Thread and wants to help solving my problem, by coding themselve a bit, this person can just copy my examples here and can save the time to write it themselve. It's optional not a must.

    Thank you also for explaining more about the naming issue you mentioned before. It makes sense and of course, if the binding name is that obvious like John.Name then it is even less needed. I will consider it and remove it from my code where not needed. Thanks a lot.

    Regarding the UI logical change (and please, for all that follows, feel free to correct me :)), if the UI is a reflection of the data, and my data are several persons that are either selected or not selected, then this AllSelected switch I want to have represents data, more precisely the data that says if all users are selected or not. So updating a single selected person data will also update the AllSelected data switch as a logical consequence.
    I mean, the SelectedAll parameter that is used as Binding for the Switches Toggle state is a boolean based on the state of all person models IsSelected parameter.
    I could use a REST API (or Unit-test) which just passes the Person(s) data and get a Data result for the AllSelected boolean.
    The UI will update based on this parameter through the binding.
    When the SelectAll switch is pressed, I manipulate the data of the persons, not directly their UI representations.

    My missing puzzle piece, or perhaps that's the part that I didn't understand yet, is how to do the same for the AllSelect switch, to tell the AllSelected boolean to also update when a single Person-datas IsSelected was updated.
    That's what I tried to achieve by calling OnPropertyChanged(nameof(AllSelected))

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    My missing puzzle piece, or perhaps that's the part that I didn't understand yet, is how to do the same for the AllSelect switch, to tell the AllSelected boolean to also update when a single Person-datas IsSelected was updated.

    That's what I tried to achieve by calling OnPropertyChanged(nameof(AllSelected))

    I don't grasp what it is you're doing/using these like.

    So if the switch is flipped on Fred you want it flipped on Barney, Betty, and Wilma also?
    Just make the property static on the model. Then the same bool and its value is used on all instances of the person class.

  • GraverobberGraverobber Member ✭✭✭

    @ClintStLaurent
    Thank you once again.

    The scenario is:
    no one is selected, I select Fred -> only Fred is selected
    now I select barney, Betty and Wilma -> All are selected + The overall switch to select all at once is selected
    I deselect the overall switch -> Fred, Barney,Betty and Wilma are deselected.
    I select Fred, Barney, Betty and Wilma again separately, the SelectAll switch will also turn on.

    Now if I deselect only Fred, the SelectAll switch should turn off, but Barney, Betty and Wilma should stay selected.

    The point is that there is supposed to be a switch that allows to select all persons at once, but should react to the information if a single person was changed afterwards.

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭
    edited May 20

    Properties have both get and set methods.
    You can run code in those.

    So based on a whole 30 seconds of thinking about it....
    in the get return if all the other objects are true. Therefore if any 1 is false it will return false.
    In the set, if the value is true, then go through all the other objects and set their IsSelected to true. Do nothing if the value is false.
    UPDATE: Oh, and set up a CollectionChanged event handler on your collection of people, so that if any element has any change, raise PropertyChanged("SelecteAll") so the UI performs a get on the property. Which then re-evalues if SelectAll is true or not.

  • GraverobberGraverobber Member ✭✭✭

    Thank you @ClintStLaurent for taking and spending the time to help me, I appreciate it.

    What you suggested is what I tried to do, the problem with that solution is:

    Fred, Barney, Betty and Wilma are selected (So all are selected)
    -> Toggling the SelectAll switch
    == All are still selected because we check if(value == true) but it isn't so we don't set it to the single switches.

    Your solution would work if I could determine if the set is caused by tapping on the switch or by call to OnPropertyChanged. Then I could check what caused the set to trigger and apply the state accordingly. Is there a way to know that?

    P.S. Thank you for CollectionChanged, I was not aware of it. It didn't work right now but I'll spend some time to see how it works. It is much better than having PropertyChanged on every single Collection item.

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    @Graverobber said:
    Fred, Barney, Betty and Wilma are selected (So all are selected)
    -> Toggling the SelectAll switch
    == All are still selected because we check if(value == true) but it isn't so we don't set it to the single switches.

    There shouldn't be any checking. Its not needed if we are using MVVM binding correctly.

    Your solution would work if I could determine if the set is caused by tapping on the switch or by call to OnPropertyChanged. Then I could check what caused the set to trigger and apply the state accordingly. Is there a way to know that?

    Again, shouldn't be needed. That kind of code micromanagement just leads to way too much work going on.

    Maybe I can find some time this morning to play with a switch group like you're describing to see if I can make it work. No promises. But I'll try.

  • GraverobberGraverobber Member ✭✭✭

    Thanks a lot @ClintStLaurent no promises needed, you're trying to help me that is all that counts.
    Let me know if you figured something out. I tried this morning but still failed.

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    Ok class... Here's a little something to study then come back and tell me how it does what it does.

    To me the key points are:
    Collection based
    Event based
    Binding based
    No UI handling events of the actual switches that then parse through the elements. All the work is in the VIewModel.

    XAML

                <StackLayout Orientation="Horizontal">
                    <Switch
                        HorizontalOptions="Start"
                        VerticalOptions="Start"
                        IsToggled="{Binding IsAllSelected}" />
                    <Label Text="Select All" />
                </StackLayout>
                <ScrollView>
                    <FlexLayout
                        VerticalOptions="Start"
                        AlignContent="Center"
                        AlignItems="Center"
                        BindableLayout.ItemsSource="{Binding Flintstones}"
                        Direction="Row"
                        JustifyContent="Center"
                        Wrap="Wrap">
                        <BindableLayout.ItemTemplate>
                            <DataTemplate>
                                <Grid
                                    HeightRequest="75"
                                    WidthRequest="200"
                                    Margin="5"
                                    BackgroundColor="DarkSlateGray">
                                    <Switch
                                        HorizontalOptions="Start"
                                        VerticalOptions="Center"
                                        IsToggled="{Binding IsSelected}" />
                                    <Label
                                        HorizontalOptions="End"
                                        VerticalOptions="Center"
                                        Margin="0,0,10,0"
                                        BackgroundColor="Transparent"
                                        FontAttributes="Bold"
                                        HorizontalTextAlignment="Center"
                                        Text="{Binding Name, Mode=OneWay}"
                                        TextColor="Black"
                                        VerticalTextAlignment="Center" />
                                </Grid>
    
                            </DataTemplate>
                        </BindableLayout.ItemTemplate>
                    </FlexLayout>
                </ScrollView>
    
    

    Widget class

    NOTE: The command isn't required for this demo. It was just there already from another test. But might be handy later

        public class Widget : ModelBase
        {
            #region C'tors 
    
            public Widget()
            {
                ModelCommand = new Command(OnModelCommand);
            }
    
            private void OnModelCommand(object passedCommandParam)
            {
                Console.WriteLine("INFORMATION> This instance executed the ModelCommand");
            }
    
            #endregion C'tors
    
            #region IsSelected (bool)
            private bool _IsSelected;
            public bool IsSelected
            {
                [DebuggerStepThrough]
                get
                {
                    //if (_IsSelected == null) IsSelected = new bool();
                    return _IsSelected;
                }
    
                [DebuggerStepThrough]
                set
                {
                    if (_IsSelected == value) return;
                    _IsSelected = value;
                    OnPropertyChanged();
                }
            }
            #endregion IsSelected  (bool)
    
            #region Name (string)
            private string _Name = string.Empty;
            public string Name
            {
                [DebuggerStepThrough]
                get
                {
                    if (_Name == null) Name = string.Empty;
                    return _Name;
                }
    
                [DebuggerStepThrough]
                set
                {
                    if (_Name == value) return;
                    _Name = value;
                    OnPropertyChanged();
                }
            }
            #endregion Name  (string)
    
    
            #region SomeDate (DateTime)
            private DateTime _SomeDate;
            public DateTime SomeDate
            {
                get
                {
                    //if (_SomeDate == null) SomeDate = new DateTime();
                    return _SomeDate;
                }
    
                set
                {
                    if (_SomeDate == value) return;
                    _SomeDate = value;
                    OnPropertyChanged();
                }
            }
            #endregion SomeDate  (DateTime)
    
    
    
            public ICommand ModelCommand { get; set; }
        }
    
    

    ViewModel

            #region Flintstones (ObservableCollection<Widget>)
            private ObservableCollection<Widget> _Flintstones;
            public ObservableCollection<Widget> Flintstones
            {
                get
                {
                    if (_Flintstones == null) Flintstones = new ObservableCollection<Widget>();
                    return _Flintstones;
                }
    
                set
                {
                    if (_Flintstones == value) return;
                    _Flintstones = value;
                    OnPropertyChanged();
                }
            }
            #endregion Flintstones  (ObservableCollection<Widget>)
    
    
            #region IsAllSelected (boo)
            private bool _IsAllSelected;
            public bool IsAllSelected
            {
                get
                {
                    var nope = Flintstones.Where(x => !x.IsSelected); 
                    if (nope.Any()) 
                        return false;
                    return _IsAllSelected;
                }
    
                set
                {
                    if (value)
    
                        foreach (var flintstone in Flintstones)
                        {
                            flintstone.IsSelected = true;
                        }
    
                    _IsAllSelected = value;
                    OnPropertyChanged();
                }
            }
            #endregion IsAllSelected  (boo)
    
    
            private void Flintstones_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
            {
                if (e.NewItems != null)
                    foreach (var eNewItem in e.NewItems)
                    {
                        ((Widget) eNewItem).PropertyChanged += WidgetPropertyChanged;
                    }
    
                if (e.OldItems != null)
                    foreach (var eNewItem in e.OldItems)
                    {
                        ((Widget) eNewItem).PropertyChanged -= WidgetPropertyChanged;
                    }
            }
    
    
    

    constructor

                Flintstones.CollectionChanged += Flintstones_CollectionChanged;
                Flintstones.Add(new Widget() { Name = "Fred" });
                Flintstones.Add(new Widget() { Name = "Wilma" });
                Flintstones.Add(new Widget() { Name = "Barney" });
                Flintstones.Add(new Widget() { Name = "Betty" });
                Flintstones.Add(new Widget() { Name = "Pebbles" });
                Flintstones.Add(new Widget() { Name = "Bambam" });
    

    There might be some difference in what this does due to my interpretation of your need description. But I'm confident that once you have this much working you should be able to tweak it's behavior.

  • GraverobberGraverobber Member ✭✭✭
    edited May 21

    Thank you very much for all the time you invest @ClintStLaurent .

    I took you example and tried to understand it and I think I did.
    I will shortly explain so you can tell yes or no :)

    The SelectAll switch is based on the state of the Widgets. As the get is responsible for telling the UI Element in which state it has to be, it checks if any of the widgets is not selected, if so it will return false in order to go into the off state.
    If all are selected it will return the state it was in (stored in _isAllSelected private field). (This is btw one point why it is not completely what I' m trying to achieve, but later more to that)

    Now the Collection has a CollectionChanged event that is fired when a Widget is Added (or removed) from the collection. When a widget is added the PropertyChanged event is configuredn to trigger the WidgetPropertyChanged method. You didn't share the code of this function but I assume all it does is calling OnPropertyChanged(nameof(IsAllSelected)) in order to trigger the get again.
    Question to this:
    Would it result in the same effect if I iterate over the Widgets and apply PropertyChanged to the individual items? Or is this bad practice?

    So now, perhaps I didn't explain well enough what I try to achieve or I'm still not understanding something but with this implementation, the scenario where all are selected and I click the Select All button will not unselect All Widgets.
    That is because the set will be called when the Users click on SelectAll, it will check if value is true but it isn't so it skips "falsing" the Widgets. I just don't find the way how to get all 4 possibilities into that, I got 3 but not 4.

    I made some pictures like you did to make (finally) clear what I expect to have to provide a good UX:

    1. Case Nothing is selected and the SelectAll Switch gets clicked

    2. Case All are selected and the SelectAll Switch gets clicked

    3. Case All but one is selected and the remaining one is clicked.

    4. Case All are selected and then one gets deselected

    Thank you very much once again, I already learned a lot from you, even if I still didn't manage to solve my particular problem :D

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    Would it result in the same effect if I iterate over the Widgets and apply PropertyChanged to the individual items? Or is this bad practice?

    Well... Its going to cause more work. If you iterate over a large collection every time that's a lot of processing. If you react to an event (what we call 'event driven') you only have one unit of work.

    the scenario where all are selected and I click the Select All button will not unselect All Widgets

    Right. Because I wrote it as "select all" not "toggle all" or "de-select all". But that's an easy enough change for you to make.

    Thank you very much once again, I already learned a lot from you, even if I still didn't manage to solve my particular problem

    Well... You now have a working mechanism that does 80% of what you need. The skeleton of the feature. Bluntly: If you can't handle it from there then you need to go to your boss and honestly admit you're in over your head. There is no shame saying you have more learning to do... or that its going to take 50% more hours than you expected because there is more to this than you first thought. Bosses (well, good bosses) respect honesty and an employee that knows and admits there limits and capabilities.

Sign In or Register to comment.