Search code examples
c#.netwpfxaml

WPF Listbox updates to bound data source not triggering updates (but new inserts do)


I'm just getting back into WPF/XAML programming and have run into a problem I can't figure out. I have an object, think of it as an Order, each order contains a list of Store objects, and each Store contains a list of Planogram objects. The unprocessed orders are retrieved from the database and placed into an observable collection.

Using the MVVM Toolkit, each property uses INotify to indicate when a property is changed. This also sets the IsDirty flag indicating that changes have been made. Both the Store object and the Order object have an event handler attached to the collection changed event of the child list to indicate that some contained element changed.

The view model contains the observable collection of orders and also has an event handler on the collection changed event of that list, so the form can be updated.

The form has a List box that contains the name of the order. When an order is selected the details are displayed on other controls on the form.

That's the background...Here's the problem...I have a text block in the Item Template of the List Box for each order name. The visibility is bound to the Is Dirty flag using the BoolToVis converter. If I select an order and make a change to any of the child properties, the changes bubble up to the parent and the is dirty flag is set correctly. But the source is never updated and the "*" in the text box is never displayed.

However, there's an option to add a blank order. A blank order is created and added to the orders list. At that point, the new order is displayed in the List Box with the * beside the order name.

I've been searching for a couple of days and read through everything that I can find on SO. Most of what I've seen is that people don't have INotify setup on the properties. I do and every answer I've found says that this should be working...but it's not.

Base Object that contains the IsDirty Flag

public class BaseOneOffStorePlanogram : ObservableObject, IOneOffData
    {
        private bool _isDirty = false;
        

        public bool IsDirty 
        { 
            get { return _isDirty; } 
            set { 
                if(value != _isDirty)
                {
                    _isDirty = value;
                    base.OnPropertyChanged("IsDirty");
                }
            } 
        } 
        public string LastModifiedBy { get; set; }
        public DateTime? LastModifiedOn { get; set; }

        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (e.PropertyName != "IsDirty")
            {
                base.OnPropertyChanged(e);

                IsDirty = true;
            }
         }

       
    }

This is a snippet of the Store object where each property is calling the OnPropertyChanged event and also where I have the Collection changed wired up.


        public string StoreNumber
        {
            get { return _StoreNumber; }
            set
            {
                _StoreNumber = value;
                base.OnPropertyChanged("StoreNumber");
            }
        }
        public DateTime? PricingDateOverride
        {
            get
            {
                return _PricingDateOverride;
            }
            set
            {
                _PricingDateOverride = value;
                base.OnPropertyChanged("PricingDateOverride");
            }
        }

     public OneOffStore()
        {
            _OneOffStorePlanograms = new ObservableCollection<OneOffStorePlanogram>();
            _OneOffStorePlanograms.CollectionChanged += _OneOffStorePlanograms_CollectionChanged;
          
        }

        private void _OneOffStorePlanograms_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if(e.OldItems != null)
            {
                foreach( OneOffStorePlanogram oosp in e.OldItems)
                {
                  oosp.PropertyChanged += InvokeCollectionChanged;
                }
            }
            if( e.NewItems != null)
            {
                foreach(OneOffStorePlanogram oosp in e.NewItems)
                {
                    oosp.PropertyChanged += InvokeCollectionChanged;
                }
            }
        }

        private void InvokeCollectionChanged(object sender, PropertyChangedEventArgs e)
        {
            OnPropertyChanged("OneOffStorePlanograms");
        }

This is the portion of the view model where the observable collection is defined


     public ObservableCollection<OneOff> OneOffs
        {
            get { return _oneOffs; }
        }

     private void InitializeForm(){
            _oneOffs = new ObservableCollection<OneOff>();

            _selectedStores = new ObservableCollection<Store>();
            _selectedPlanograms = new ObservableCollection<Models.Planogram>();
            _useEffectiveDate = false;
           
            LoadPlanograms();
            LoadStores();
            LoadOneOffs();
            //GetUnprocessedOneOffs();
           
            _oneOffs.CollectionChanged += OneOffs_CollectionChanged;`
          }

Here's the XAML snippet

 <ListBox x:Name="lstActiveJobs"   VerticalAlignment="Stretch" HorizontalAlignment="Stretch" ItemsSource="{Binding OneOffs, NotifyOnSourceUpdated=True,UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding SelectedOneOff}" >

                                    <ListBox.ItemTemplate>
                                        <DataTemplate>
                                            <Grid>
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="*"/>
                                                    <ColumnDefinition Width="10"/>
                                                </Grid.ColumnDefinitions>
                                                <Grid.RowDefinitions>
                                                    <RowDefinition Height="*"/>
                                                </Grid.RowDefinitions>
                                                <TextBlock Text="{Binding OneOffName, PresentationTraceSources.TraceLevel=High}" Grid.Row="0" Grid.Column="0" />
                                                <TextBlock Text="*" x:Name="HasChangeModifier" Grid.Row="0" Grid.Column="1"  Visibility="{Binding IsDirty, Converter={StaticResource BoolToVis}, UpdateSourceTrigger=PropertyChanged}"/>
                                            </Grid>

                                        </DataTemplate>
                                    </ListBox.ItemTemplate>
                                </ListBox>

I'm sure it's something simple or that I'm doing wrong. What's got me confused is that the newly added, non-saved item works but the ones retrieved from the db don't.

Any advice would be appreciated.


Solution

  • The overridden OnPropertyChanged(PropertyChangedEventArgs e) method does not call base.OnPropertyChanged(e) for the IsDirty property, hence Bindings to that property are not notified.

    The method should look like this:

    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (!IsDirty && e.PropertyName != nameof(IsDirty))
        {
            IsDirty = true;
        }
        base.OnPropertyChanged(e);
    }
    

    Be aware that OnPropertyChanged(PropertyChangedEventArgs e) is called by OnPropertyChanged(string propertyName).


    As a note, with CommunityToolkit.MVVM you do not need to write the property declaration yourself. Just use the ObservableProperty attribute:

    public partial class BaseOneOffStorePlanogram : ObservableObject, IOneOffData
    {
        [ObservableProperty] // this will generate the IsDirty property
        private bool isDirty;
    
        ...
    
        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (!IsDirty && e.PropertyName != nameof(IsDirty))
            {
                IsDirty = true;
            }
            base.OnPropertyChanged(e);
        }
    }