Wpf Observable collection and DataGrid not updating changes

28,958

An ObservableCollection will notify on change. There's no reason to do it manually, so you can remove all of the OnPropertyChanged("JobEntities");. This will get you to a cleaner solution.

MSDN

WPF provides the ObservableCollection class, which is a built-in implementation of a data collection that implements the INotifyCollectionChanged interface.

The next part is that an ObservableCollection will only notify on changes to the collection itself (add/remove). Any modifications to an element within the list will not have have the notify message sent. To do this, the simplest method is to implement the INotifyPropertyChanged to the elements used in the Observable Collection

I'm using PRISM 5 in the example, so it should be pretty equal to what you're doing. There's a couple of major design changes to you're code. First, I'm using a straight property for my Observable Collection. We know the framework will handle any add/remove operations to this collection. Then to notify when I change a property within the entity in an observable collection, I've used a notify property within the TestEntity class itself.

public class MainWindowViewModel : BindableBase
{
    //Notice no OnPropertyChange, just a property
    public ObservableCollection<TestEntity> TestEntities { get; set; }

    public MainWindowViewModel()
        : base()
    {
        this.TestEntities = new ObservableCollection<TestEntity> {
            new TestEntity { Name = "Test", Count=0},
            new TestEntity { Name = "Test1", Count=1},
            new TestEntity { Name = "Test2", Count=2},
            new TestEntity { Name = "Test3", Count=3}
        };

        this.UpCommand = new DelegateCommand(this.MoveUp);
    }

    public ICommand UpCommand { get; private set; }

    private void MoveUp()
    {
        //Here is a dummy edit to show the modification of a element within the observable collection
        var i = this.TestEntities.FirstOrDefault();
        i.Count = 5;

    }
}

Here's my entity, notice the BindableBase and the fact I notify on change. This allows the DataGrid or whatever you're using to be notified that the property changed.

public class TestEntity : BindableBase {
    private String _name;
    public String Name
    {
        get { return _name; }
        set { SetProperty(ref _name, value); }
    }
    //Notice I've implemented the OnPropertyNotify (Prism uses SetProperty, but it's the same thing)
    private Int32 _count;
    public Int32 Count
    {
        get { return _count; }
        set { SetProperty(ref _count, value); }
    }
}

Now really all the TestEntity needs to have implemented the INotifyPropertyChanged for this to work, but I'm using the PRISM BindableBase as an example.

EDIT

I found a similar question on SO. I think yours is slightly different, but they overlap on the concepts. It may help to look over it.

Observable Collection Notify when property changed in MVVM

EDIT

If the datagrid is sorted the previous method will not update the grid. To handle this you need to refresh the grid's view, but are unable to directly access it using MVVM. So to handle this you'll want to use a CollectionViewSource.

public class MainWindowViewModel : BindableBase
{
    //This will bind to the DataGrid instead of the TestEntities
    public CollectionViewSource ViewSource { get; set; }
    //Notice no OnPropertyChange, just a property
    public ObservableCollection<TestEntity> TestEntities { get; set; }

    public MainWindowViewModel()
        : base()
    {
        this.TestEntities = new ObservableCollection<TestEntity> {
        new TestEntity { Name = "Test", Count=0},
        new TestEntity { Name = "Test1", Count=1},
        new TestEntity { Name = "Test2", Count=2},
        new TestEntity { Name = "Test3", Count=3}
    };

        this.UpCommand = new DelegateCommand(this.MoveUp);

        //Initialize the view source and set the source to your observable collection
        this.ViewSource = new CollectionViewSource();
        ViewSource.Source = this.TestEntities;
    }

    public ICommand UpCommand { get; private set; }

    private void MoveUp()
    {
        //Here is a dummy edit to show the modification of a element within the observable collection
        var i = this.TestEntities.FirstOrDefault();
        i.Count = 5;
        //Now anytime you want the datagrid to refresh you can call this.
        ViewSource.View.Refresh();
    }
}

The TestEntity class does not change, but here's the class again:

public class TestEntity : BindableBase
{
    private String _name;
    public String Name
    {
        get { return _name; }
        set { SetProperty(ref _name, value); }
    }
    //Notice I've implemented the OnPropertyNotify (Prism uses SetProperty, but it's the same thing)
    private Int32 _count;
    public Int32 Count
    {
        get { return _count; }
        set { SetProperty(ref _count, value); }
    }
}

For clarification, here's my XAML showing the binding to the new CollectionViewSource.

<DataGrid  Grid.Row="1" ItemsSource="{Binding ViewSource.View}"></DataGrid>

For further reading you can refer to the MSDN article on this.

Here's another relevant question/answer - Re-sort WPF DataGrid after bounded Data has changed

Share:
28,958
nikhil
Author by

nikhil

Updated on November 29, 2020

Comments

  • nikhil
    nikhil over 3 years

    I have an observable collection in the view model that implements Bindable Base as follows Please have a look at the MoveUp and MoveDown methods where they are bound to two buttons in the view. When ever up button is pressed I want the selected row in the datagrid to move one step up in the based on the sequence column in the database and for down one step down.. Both the methods works PERFECTLY. Problem is the changes get shown in the datagrid only when the entire view is refreshed. My requirement is when the button is clicked I want the view to be automatically refreshed. I apologize for such long code. Please Help!!!!. I have some cs code as well for the both up and down buttons specified below the viewmodel. Only pointers in the code that needs to be emphasized is the ObservableCollection JobEntities, MoveUp and MoveDown commands.

    ViewModel.cs:

    public class JobConfigurationViewModel : BindableBase
    {
    
    
        public JobConfigurationLogic JobConfigurationLogic =
            new JobConfigurationLogic(new JobConfigurationResultsRepository());
    
        public SrcDestConfigurationLogic SrcDestConfigurationLogic =
            new SrcDestConfigurationLogic(new SrcDestCofigurationRepository());
    
        private string _enterprise;
    
        public string Enterprise
        {
            get { return _enterprise; }
            set { SetProperty(ref _enterprise, value); }
        }
    
        private int currentJobID;
        private int currentSequence;
        private int previousJobID;
        private int previousSequence;
        private string _site;
    
        public string Site
        {
            get { return _site; }
            set { SetProperty(ref _site, value); }
        }
    
        private int _siteID;
    
        public int SiteID
        {
            get { return _siteID; }
            set { SetProperty(ref _siteID, value); }
        }
    
        private ObservableCollection<JobConfigurationResults> _jobEntities;
    
        public ObservableCollection<JobConfigurationResults> JobEntities
        {
            get { return _jobEntities; }
            set
            {
                SetProperty(ref _jobEntities, value); 
                this.OnPropertyChanged("JobEntities");
            }
        }
    
        //Source System List for Job
        private List<SourceSiteSystem> _lstJobSrcSystems;
    
        public List<SourceSiteSystem> LstJobSrcSystems
        {
            get { return _lstJobSrcSystems; }
            set
            {
                //Using bindable base setproperty method instead of older inotify prop changed method
                SetProperty(ref _lstJobSrcSystems, value);
            }
        }
    
        //Deestination  System List for Job
        private List<DestinationSiteSystem> _lstJobDestSystems;
    
        public List<DestinationSiteSystem> LstJobDestSystems
        {
            get { return _lstJobDestSystems; }
            set
            {
                //Using bindable base setproperty method instead of older inotify prop changed method
                SetProperty(ref _lstJobDestSystems, value);
            }
        }
    
        //the Selected Source Site system ID 
        private int _selectedSrcSiteSystemId = 0;
    
        public int SelectedSrcSiteSystemId
        {
            get { return _selectedSrcSiteSystemId; }
            set
            {
                //Using bindable base setproperty method instead of older inotify prop changed method
                SetProperty(ref _selectedSrcSiteSystemId, value);
            }
        }
    
        //the Selected Source Site system from the dropdown
        private SourceSiteSystem _selectedSrcSiteSystem;
    
        public SourceSiteSystem SelectedSrcSiteSystem
        {
            get { return _selectedSrcSiteSystem; }
            set
            {
                //Using bindable base setproperty method instead of older inotify prop changed method
                if (value != null)
                {
                    SetProperty(ref _selectedSrcSiteSystem, value);
                    SelectedSrcSiteSystemId = SelectedSrcSiteSystem.SiteSystemId;
                }
            }
        }
    
        //the Selected Destination Site system ID 
        private int _selectedDestSiteSystemId = 0;
    
        public int SelectedDestSiteSystemId
        {
            get { return _selectedDestSiteSystemId; }
            set
            {
                //Using bindable base setproperty method instead of older inotify prop changed method
                SetProperty(ref _selectedDestSiteSystemId, value);
            }
        }
    
        //the Selected Destination Site system from the dropdown
        private DestinationSiteSystem _selectedDestSiteSystem;
    
        public DestinationSiteSystem SelectedDestSiteSystem
        {
            get { return _selectedDestSiteSystem; }
            set
            {
                //Using bindable base setproperty method instead of older inotify prop changed method
                if (value != null)
                {
                    SetProperty(ref _selectedDestSiteSystem, value);
                    SelectedDestSiteSystemId = SelectedDestSiteSystem.SiteSystemId;
                }
            }
        }
    
        private JobConfigurationResults _jeJobConfigurationResults;
    
        public JobConfigurationResults JEJobConfigurationResults
        {
            get { return _jeJobConfigurationResults; }
            set { _jeJobConfigurationResults = value; }
        }
    
        private List<JobTaskConfiguration> _taskSelectionList = new List<JobTaskConfiguration>();
    
        private CancellationTokenSource _source;
    
        private RelayCommand<object> _commandSaveInstance;
        private RelayCommand<object> _hyperlinkInstance;
        private RelayCommand<object> _commandRunJob;
        private RelayCommand<object> _upCommand;
        private RelayCommand<object> _downCommand;
        private IEventAggregator _aggregator;
    
    
        /// <summary>
        /// This is a Subscriber to the Event published by EnterpriseViewModel
        /// </summary>
        /// <param name="agg"></param>
        public JobConfigurationViewModel(IEventAggregator agg)
        {
            _aggregator = agg;
            PubSubEvent<Message> evt = _aggregator.GetEvent<PubSubEvent<Message>>();
            evt.Subscribe(message => Enterprise = message.Enterprise.ToString(), ThreadOption.BackgroundThread);
            evt.Subscribe(message => Site = message.Site.ToString(), ThreadOption.BackgroundThread);
            evt.Subscribe(message => SiteID = message.SiteID, ThreadOption.BackgroundThread);
            //evt.Unsubscribe();
            StartPopulate();
        }
    
        private async void StartPopulate()
        {
            await TaskPopulate();
        }
    
        //This is to ensure that the publisher has published the data that is needed for display in this workspace
        private bool TaskProc()
        {
            Thread.Sleep(500);
            PubSubEvent<Message> evt = _aggregator.GetEvent<PubSubEvent<Message>>();
            evt.Subscribe(message => Enterprise = message.Enterprise.ToString(), ThreadOption.BackgroundThread);
            evt.Subscribe(message => Site = message.Site.ToString(), ThreadOption.BackgroundThread);
            evt.Subscribe(message => SiteID = message.SiteID, ThreadOption.BackgroundThread);
            return DoPopulate();
        }
    
        private Task<bool> TaskPopulate()
        {
            _source = new CancellationTokenSource();
            return Task.Factory.StartNew<bool>(TaskProc, _source.Token);
        }
    
        /// <summary>
        /// This method handles the populating of the Source and Destination Dropdowns and the Job entity and Task Datagrid
        ///   This is mainly driven by the Site selected in the previous workspace
        /// </summary>
        /// <returns></returns>
    
        private bool DoPopulate()
        {
            PopulateSourceDestinations(this.SiteID);
    
            return true;
        }
    
        /// <summary>
        /// this method displays all entities and tasks for the site.
        /// This is done async so that the Publisher thread is not held up
        /// </summary>
        public void GetJobConfigurationResults()
        {
            if (SelectedSrcSiteSystem == null)
            {
                SelectedSrcSiteSystem = LstJobSrcSystems[0];
            }
            if (SelectedDestSiteSystem == null)
            {
                SelectedDestSiteSystem = LstJobDestSystems[0];
            }
            SelectedSrcSiteSystemId = SelectedSrcSiteSystem.SiteSystemId;
            SelectedDestSiteSystemId = SelectedDestSiteSystem.SiteSystemId;
            var jobConfigurationResults = new JobConfigurationResults
            {
                SourceId = SelectedSrcSiteSystemId,
                DestinationId = SelectedDestSiteSystemId
            };
            JobEntities = new ObservableCollection<JobConfigurationResults>();
            JobEntities = JobConfigurationLogic.GetResults(jobConfigurationResults.SourceId,
                jobConfigurationResults.DestinationId);
            _taskSelectionList = new List<JobTaskConfiguration>(JobEntities.Count * 3);
        }
    
        /// <summary>
        /// //Adding a method to pupulate the Source and Destination dropdown lists. 
        /// This is done async so that the Publisher thread is not held up
        /// </summary>
        /// 
        /// 
        public async void PopulateSourceDestinations(int siteId)
        {
            this.LstJobSrcSystems = SrcDestConfigurationLogic.LoadSourceSiteSystems(siteId);
            this.LstJobDestSystems = SrcDestConfigurationLogic.LoadDestinationSystems(siteId);
            GetJobConfigurationResults();
        }
    
        public ICommand HyperlinkCommand
        {
            get
            {
                if (_hyperlinkInstance == null)
                    _hyperlinkInstance = new RelayCommand<object>(openDialog);
                return _hyperlinkInstance;
            }
        }
    
        private void openDialog(object obj)
        {
            JobConfigurationResults results = obj as JobConfigurationResults;
            JEJobConfigurationResults = JobEntities.SingleOrDefault(x => x.JobEntityId == results.JobEntityId);
    
        }
    
        public ICommand CommandSave
        {
            get
            {
                if (_commandSaveInstance == null)
                    _commandSaveInstance = new RelayCommand<object>(saveJobConfigurationChanges);
                return _commandSaveInstance;
            }
        }
    
        public ICommand CommandRunJob
        {
            get { return _commandRunJob ?? (_commandRunJob = new RelayCommand<object>(RunJob)); }
        }
    
        /// <summary>
        /// this saves all the changes in the selection made by the user 
        /// </summary>
        /// <param name="ob"></param>
        public void saveJobConfigurationChanges(object ob)
        {
            foreach (var job in JobEntities)
            {
                int jobEntityId = job.JobEntityId;
                foreach (var task in job.TaskDetails)
                {
                    int id = task.JobTask_ID;
                    bool isSelected = task.IsSelected;
                    _taskSelectionList.Add(task);
                }
            }
            JobConfigurationLogic.UpdateTaskSelection(_taskSelectionList);
        }
    
    
        public ICommand UpCommand
        {
            get
            {
                if (_upCommand == null)
                    _upCommand = new RelayCommand<object>(MoveUp);
                return _upCommand;
            }
        }
    
        private void MoveUp(object obj)
        {
    
            if (obj != null)
            {
                JobConfigurationResults results = obj as JobConfigurationResults;
                currentJobID = results.JobEntityId;
                currentSequence = results.SequenceOrder - 1;
                try
                {
                    JobConfigurationResults res = _jobEntities.SingleOrDefault(n => n.SequenceOrder == currentSequence);
                    previousJobID = res.JobEntityId;
                    previousSequence = res.SequenceOrder + 1;
                    // JobConfigurationLogic.UpdateSequence(currentJobID, previousSequence, previousJobID, currentSequence);
                    JobConfigurationLogic.UpdateSequence(currentSequence, currentJobID, previousSequence, previousJobID);
                    OnPropertyChanged("JobEntities");
                }
                catch (NullReferenceException)
                {
                    MessageBox.Show("Can't move the top record");
                }
    
            }
            else
            {
                MessageBox.Show("Please Select a row that you want to sort");
            }
        }
    
        public ICommand DownCommand
        {
            get
            {
                if (_downCommand == null)
                    _downCommand = new RelayCommand<object>(MoveDown);
                return _downCommand;
            }
        }
    
        private void MoveDown(object obj)
        {
            if (obj != null)
            {
                JobConfigurationResults results = obj as JobConfigurationResults;
                currentJobID = results.JobEntityId;
                currentSequence = results.SequenceOrder + 1;
                try
                {
                    JobConfigurationResults res = _jobEntities.SingleOrDefault(a => a.SequenceOrder == currentSequence);
                    previousJobID = res.JobEntityId;
                    previousSequence = res.SequenceOrder - 1;
                    JobConfigurationLogic.UpdateSequence(currentSequence, currentJobID, previousSequence, previousJobID);
                    OnPropertyChanged("JobEntities");
                }
                catch (NullReferenceException)
                {
    
                    MessageBox.Show("You have reached the end");
                }
    
            }
            else
            {
                MessageBox.Show("Please Select a row that you want to sort");
            }
        }
    
        /// <summary>
        /// Execute an etl job using the current job id
        /// </summary>
        private void RunJob(object obj)
        {
            JobEngine jobEngine = new JobEngine();
            var jobId = JobEntities[0].JobId;
            jobEngine.ProcessJob(jobId);
        }
    } 
    

    CS CODE:

    private void btnup_Click(object sender, RoutedEventArgs e)
    {
    
        dgEntities.Items.Refresh();
        //dgEntities.GetBindingExpression(DataGrid.ItemsSourceProperty).UpdateTarget();
    }
    
    private void btndown_Click(object sender, RoutedEventArgs e)
    {
        dgEntities.GetBindingExpression(DataGrid.ItemsSourceProperty).UpdateTarget();
    }
    
  • nikhil
    nikhil over 9 years
    Thanks Nathan I will give it a try. So much appreciated for the code and explanation.
  • nikhil
    nikhil over 9 years
    Nathan I have tried this but its still not refreshing the DataGrid. Its working only when I refresh the entire View. Is there a way to refresh the view on button click..
  • Nathan
    Nathan over 9 years
    @nikhil: I'm sorry I must be missing what you're trying to do. What I've shown will update the screen when you modify elements within the observable collection, which appears to be what you're trying to do. The only other piece I could see is you've possibly got the datagrid sorted, in which case the grid will not update and resort the items. So I'll update my answer to account for this.
  • Nathan
    Nathan over 9 years
    @nikhil: I've added a new piece to the code, it now uses a CollectionViewSource to handle refreshing the grid. Hopefully this is what you're after.
  • nikhil
    nikhil over 9 years
    Hey Nathan I am trying the ViewSource.View, but the problem is its not displaying any record in the DataGrid. On the other hand if I just bind to the Observable Collection it displays all the records in the datagrid. I also kept the break point and the count for the ViewSource is same as the Observable Collection but for some reason it dosent display any rows in the datagrid.
  • Nathan
    Nathan over 9 years
    @nikhil: I've posted the code I was using to test on Github for you. I added two buttons, one to modify a record in the datagrid and another to add a new row. If you sort the datagrid on the count column, it'll keep the grid sorted while elements are being edited/added. Let me know if you have any problems with it.
  • nikhil
    nikhil over 9 years
    Sorry Nathan the issue was because of the thread thats causing the issue. The Pub Sub model is completely starting a new thread which is preventing the view to be notified. Thank you very much for you help.
  • Nathan
    Nathan over 9 years
    @nikhil: no worries, glad you got it figured out.
  • nikhil
    nikhil over 9 years
    Hey Nathan I have one small question. Is there a way to check if there are any changes in the Observable Collection in the ViewModel.
  • Nathan
    Nathan over 9 years
    @nikhil: It depends on your definition of change. If you just want to know when things get added/removed on the ObservableCollection then look at the CollectionChanged event. However, if you actually want to know when an item on the collection changes, you may need to alter the approach. I don't really have enough room in a comment to go into the details, but the answer to this SO question should get you going.
  • nikhil
    nikhil over 9 years
    Thanks you very much Nathan. The post is so helpful.