how to get notified in viewmodel when an element value of observablecollection changes

fsulserfsulser Member ✭✭
edited March 15 in Xamarin.Forms

I have a list of dynamic created switches.
I now want to be able to call a function when a change is made.
My App.xaml:

...
<StackLayout BindableLayout.ItemsSource="{Binding Settings}">
    <BindableLayout.ItemTemplate>
                <DataTemplate>
                    <StackLayout Orientation="Vertical">
                        <Grid HorizontalOptions="FillAndExpand">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="Auto" />
                            </Grid.ColumnDefinitions>
                            <Label Text="{Binding Name}" Grid.Column="0" Grid.Row="0" HorizontalOptions="FillAndExpand" />
                            <Switch IsToggled="{Binding IsEnabled}" Grid.Column="1" Grid.Row="0" HorizontalOptions="End" />
                        </Grid>
                    </StackLayout>
                </DataTemplate>
            </BindableLayout.ItemTemplate>
        </StackLayout>
...

My viewModel.cs:

public class ViewModel
{
    public ObservableCollection<Settings> Settings { get; set; };

    public ViewModel(INavigationService navigationService, MenuContainerPage page) : base(navigationService, page)
    {
    Settings = new ObservableCollection<Settings>(await settingVM.GetSettingsAsyncRequest());
    }
}

And a Model Settings.cs:

public class TeamsSetting
{
    public int Id { get; set; }
    public string Name { get; set; }    
    private bool _isSwitchToggled = false;
    public bool IsEnabled
    {
        get { return _isSwitchToggled; }
        set
        {
            _isSwitchToggled = value;
        }
    }
}

How can I now achieve a function call in the viewModel when the value of IsEnabled changed?
Is there a way to bind a call listener on the observablecollection object to listen on element changes, or is there any possibility to call from the Model on the set function a function of the ViewModel?

Best Answers

  • ClintStLaurentClintStLaurent US ✭✭✭✭✭
    Accepted Answer

    or is there any possibility to call from the Model on the set function a function of the ViewModel?

    You could have the model raise a MessageCenter message when this happens.
    Just place it in the set of the property. You don't want the model reaching into the VM - that's not its responsibility. Let the VM respond to the model yelling out its message. That way you can change behavior in the VM where it belongs instead of the model trying to control the VM.

    This worries me though
    private bool _isSwitchToggled = false; public bool IsEnabled
    Your model shouldn't know anything about UI. Why would it know about a switch being toggled. And which switch on which UI if the same model were displayed in 5 different controls at the same time?
    You already have IsEnabled - that should be enough. Either the object IsEnabled or it isn't. Let the UI handle itself off the state of the object.

  • fsulserfsulser ✭✭
    edited March 16 Accepted Answer

    Just an update. I actually encountered a problem later that I created kind of an endless loop with the provided solution. I could probably solve this by creating multiple private booleans. But then came up with the idea of a TrulyObservableCollection after some research and implemented that approach and it actually works.
    In case someone comes up, here's my code:

    ViewModel:

    public class SettingViewModel : INotifyPropertyChanged {
    
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs((propertyName)));
        }
    
        public TrulyObservableCollection<Setting> Settings { get; set; } = new TrulyObservableCollection<Setting>();
    
        public SettingViewModel()
        {
        }
    
        private async Task DataChangedAsync(TrulyObservableCollection<Setting> sender)
        {
        //update data on server
             await UpdateSelectedAsyncRequest(GetEnabled(sender));
        //reload all data from server
            await LoadData();
        }
    
        private string GetEnabled(TrulyObservableCollection<Setting> sender)
        {
            string enabledString = "";
            foreach (Setting setting in sender)
            {
                if (setting.IsEnabled)
                {
                    enabledString += setting.Id+ ",";
                }
            }
            return enabledString;
        }
    
        public static async Task<SettingViewModel> InitializeAsync()
        {
            if (_instance == null)
            {
                _instance = new SettingViewModel();
            }
            await _instance.LoadData();
            return _instance;
        }
    
        private async Task LoadData()
        {
            Settings = new TrulyObservableCollection<Setting>(await GetSelectedAsyncRequest());
            Settings.CollectionChanged += CollectionChangedAsync;
        }
    
        async void CollectionChangedAsync(object sender, EventArgs e)
        {
            TrulyObservableCollection<Setting> _settings = (TrulyObservableCollection<Setting>)sender;
            await DataChangedAsync(_settings);
            OnPropertyChanged("Settings");
        }
    }
    

    and the TrulyObservableCollection:

    public class TrulyObservableCollection<T> : ObservableCollection<T>
        where T : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler ItemPropertyChanged;
    
        public TrulyObservableCollection()
            : base()
        {
            CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollection_CollectionChanged);
        }
    
        public TrulyObservableCollection(IEnumerable<T> pItems) : this()
        {
            foreach (var item in pItems)
            {
                this.Add(item);
            }
        }
    
        void TrulyObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (Object item in e.NewItems)
                {
                    (item as INotifyPropertyChanged).PropertyChanged += item_PropertyChanged;
                }
            }
            if (e.OldItems != null)
            {
                foreach (Object item in e.OldItems)
                {
                    (item as INotifyPropertyChanged).PropertyChanged -= item_PropertyChanged;
                }
            }
        }
    
        void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
            OnCollectionChanged(args);
    
            ItemPropertyChanged?.Invoke(sender, e);
        }
    }
    

    The Settings class:

    public class Setting : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        private void NotifyPropertyChanged([CallerMemberName]String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    
        [JsonProperty("id")]
        public int Id { get; set; }
        [JsonProperty("name")]
        public string Name { get; set; }
    
        [JsonProperty("isEnabled")]
        private bool _isEnabled { get; set; }
        private bool _initial = true;
        public bool IsEnabled
        {
            get
            {
                return _isEnabled;
            }
            set
            {
                bool tmp = _isEnabled;
                _isEnabled = value;
                if (!_initial || !tmp) {
                    NotifyPropertyChanged("IsEnabled");
                }
                _initial = false;
            }
        }
    }
    

    My Settings class is still a little ugly and has some workaround. Because when isEnabled is true while loading NotifyPropertyChanged is fired and if it is false its not fired. If someone has a better solution for that problem, I'm happy to get a hint.

    the xaml remains as in the beginning

Answers

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭
    Accepted Answer

    or is there any possibility to call from the Model on the set function a function of the ViewModel?

    You could have the model raise a MessageCenter message when this happens.
    Just place it in the set of the property. You don't want the model reaching into the VM - that's not its responsibility. Let the VM respond to the model yelling out its message. That way you can change behavior in the VM where it belongs instead of the model trying to control the VM.

    This worries me though
    private bool _isSwitchToggled = false; public bool IsEnabled
    Your model shouldn't know anything about UI. Why would it know about a switch being toggled. And which switch on which UI if the same model were displayed in 5 different controls at the same time?
    You already have IsEnabled - that should be enough. Either the object IsEnabled or it isn't. Let the UI handle itself off the state of the object.

  • fsulserfsulser Member ✭✭

    @ClintStLaurent said:

    or is there any possibility to call from the Model on the set function a function of the ViewModel?

    You could have the model raise a MessageCenter message when this happens.
    Just place it in the set of the property. You don't want the model reaching into the VM - that's not its responsibility. Let the VM respond to the model yelling out its message. That way you can change behavior in the VM where it belongs instead of the model trying to control the VM.

    This worries me though
    private bool _isSwitchToggled = false; public bool IsEnabled
    Your model shouldn't know anything about UI. Why would it know about a switch being toggled. And which switch on which UI if the same model were displayed in 5 different controls at the same time?
    You already have IsEnabled - that should be enough. Either the object IsEnabled or it isn't. Let the UI handle itself off the state of the object.

    Thank you very much. That actually worked by adding:

    The reason for using having that:
    private bool _isSwitchToggled = false; public bool IsEnabled

    is that the data is initialized through await settingVM.GetSettingsAsyncRequest(). The data if a switch is selected or not is coming from a database.
    Therefore initially the values can also be true (done over Newtonsoft JSON mapping).
    In the current setup I luckily never will have the usecase where the same model is displayed multiple times. But it wouldn't really matter, as the logic will be that on a switch change I will send an update command to the backend and reload all data again (just to ensure that display represents whatever is in database). And with that I think even if the model is used in multiple views at the same time I can ensure that it would always show the same.

  • fsulserfsulser Member ✭✭
    edited March 16 Accepted Answer

    Just an update. I actually encountered a problem later that I created kind of an endless loop with the provided solution. I could probably solve this by creating multiple private booleans. But then came up with the idea of a TrulyObservableCollection after some research and implemented that approach and it actually works.
    In case someone comes up, here's my code:

    ViewModel:

    public class SettingViewModel : INotifyPropertyChanged {
    
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs((propertyName)));
        }
    
        public TrulyObservableCollection<Setting> Settings { get; set; } = new TrulyObservableCollection<Setting>();
    
        public SettingViewModel()
        {
        }
    
        private async Task DataChangedAsync(TrulyObservableCollection<Setting> sender)
        {
        //update data on server
             await UpdateSelectedAsyncRequest(GetEnabled(sender));
        //reload all data from server
            await LoadData();
        }
    
        private string GetEnabled(TrulyObservableCollection<Setting> sender)
        {
            string enabledString = "";
            foreach (Setting setting in sender)
            {
                if (setting.IsEnabled)
                {
                    enabledString += setting.Id+ ",";
                }
            }
            return enabledString;
        }
    
        public static async Task<SettingViewModel> InitializeAsync()
        {
            if (_instance == null)
            {
                _instance = new SettingViewModel();
            }
            await _instance.LoadData();
            return _instance;
        }
    
        private async Task LoadData()
        {
            Settings = new TrulyObservableCollection<Setting>(await GetSelectedAsyncRequest());
            Settings.CollectionChanged += CollectionChangedAsync;
        }
    
        async void CollectionChangedAsync(object sender, EventArgs e)
        {
            TrulyObservableCollection<Setting> _settings = (TrulyObservableCollection<Setting>)sender;
            await DataChangedAsync(_settings);
            OnPropertyChanged("Settings");
        }
    }
    

    and the TrulyObservableCollection:

    public class TrulyObservableCollection<T> : ObservableCollection<T>
        where T : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler ItemPropertyChanged;
    
        public TrulyObservableCollection()
            : base()
        {
            CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollection_CollectionChanged);
        }
    
        public TrulyObservableCollection(IEnumerable<T> pItems) : this()
        {
            foreach (var item in pItems)
            {
                this.Add(item);
            }
        }
    
        void TrulyObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (Object item in e.NewItems)
                {
                    (item as INotifyPropertyChanged).PropertyChanged += item_PropertyChanged;
                }
            }
            if (e.OldItems != null)
            {
                foreach (Object item in e.OldItems)
                {
                    (item as INotifyPropertyChanged).PropertyChanged -= item_PropertyChanged;
                }
            }
        }
    
        void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
            OnCollectionChanged(args);
    
            ItemPropertyChanged?.Invoke(sender, e);
        }
    }
    

    The Settings class:

    public class Setting : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        private void NotifyPropertyChanged([CallerMemberName]String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    
        [JsonProperty("id")]
        public int Id { get; set; }
        [JsonProperty("name")]
        public string Name { get; set; }
    
        [JsonProperty("isEnabled")]
        private bool _isEnabled { get; set; }
        private bool _initial = true;
        public bool IsEnabled
        {
            get
            {
                return _isEnabled;
            }
            set
            {
                bool tmp = _isEnabled;
                _isEnabled = value;
                if (!_initial || !tmp) {
                    NotifyPropertyChanged("IsEnabled");
                }
                _initial = false;
            }
        }
    }
    

    My Settings class is still a little ugly and has some workaround. Because when isEnabled is true while loading NotifyPropertyChanged is fired and if it is false its not fired. If someone has a better solution for that problem, I'm happy to get a hint.

    the xaml remains as in the beginning

  • TeymurAsadovTeymurAsadov USMember ✭✭

    Hi..I have tried your solution, but still getting the infinite loop..did you fix it ?

Sign In or Register to comment.