Search code examples
c#wpfdatagrid

WPF DataGrid SelectedItem strange behaviour


We are seeing some strange behavior surrounding the SelectedItem property in our DataGrid. Some background information:

The DataGrid displays the result of a query to our database. There is a button that allows the user to manually refresh the results in the DataGrid. There is an auto-refresh mechanism whereby the results will automatically refresh every 30 seconds.

What we are seeing is the SelectedItem property will always become index 0 of the ItemsSource for the Datagrid when the auto-refresh occurs. But we want the currently selected row to remain the selected row after the refresh. However, if the user manually clicks refresh, the selected row remains the same after the refresh which is strange because the same code is running for the refresh logic. And yes, we have code that remembers the currently selected item which then gets set again after the refresh has been completed.

Here is some of the relevant code:

<UserControl.Resources>         
    <CollectionViewSource Source="{Binding DataGridResults}" x:Key="ReferralItemsSource"/>
</UserControl.Resources>

<customControls:CustomDataGrid x:Name="GridControl"
                              ItemsSource="{Binding Source={StaticResource ReferralItemsSource}}"
                              SelectedItem="{Binding DataContext.SelectedReferral, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
                              IsReadOnly="False"
                              IsSynchronizedWithCurrentItem="True"
                              SelectionMode="Single">
private async void RefreshWorklist(bool invokedByAutoRefresh = false)
{
   try
   {
       if (Initialising || ShowSpinner || IsProcessing || ShowRefreshSpinner || IsCurrentWorklistDeleted || !_sessionData.IsActive()) return;

       IsProcessing = true;
       RefreshWorklistCommand.RaiseCanExecuteChanged();

       if (CurrentWorklistId != null)
       {
           var selectedReferralId = SelectedReferral.pk_Referral_ID;

           if (invokedByAutoRefresh)
           {
               // Refresh has been invoked by _timer, so show spinner on the results page only
               ShowRefreshSpinner = true;
           }
           else
           {
               // User has manually clicked refresh button so show app wide spinner
               ShowSpinner = true;
               if (_timer != null)
               {
                   SetupWorklistRefreshTimer(); // Setup _timer again so that it will refresh again at an appropriate time
               }
           }

           Referrals = await _referralRepository.GetReferralsFromWorklistAsync(CurrentWorklistId.Value, invokedByAutoRefresh);

           if (Filters.Count > 0)
           {
               var listOfReferralPks = ReferralFiltering.GetFilteredResults(Referrals, Filters.Where(f => f.HasBeenApplied).ToList());
               var filteredResults = Referrals.Where(r => listOfReferralPks.Contains(r.pk_Referral_ID)).ToList();
               DataGridResults = MapReferralLookupItemsToReferralLookupItemViewModels(filteredResults);
           }
           else
           {
               DataGridResults = MapReferralLookupItemsToReferralLookupItemViewModels(Referrals);
           }

           SelectedReferral = DataGridResults.FirstOrDefault(r => r.pk_Referral_ID == selectedReferralId);
       }
   }
   catch (Exception e)
   {
       _errorHandler.DisplayError(e);
   }
}

As explained earlier, RefreshWorklist() is called by the manual refresh invoked through a Command:

private void Execute_RefreshWorklist()
{
     RefreshWorklist();
}

Or automatically through the use of a Timer:

private void SetupWorklistRefreshTimer()
{
   _timer?.Dispose();
   var refreshInterval = _userSettingsRepository.GetIntegerSystemSetting("ReferralsWorklistRefreshInterval");
   if (refreshInterval <= 0) return; // If this is 0 or below then the refresh should be disabled

   if (refreshInterval < 10) // If it is less than 10 then set it to 10 to avoid too many MT calls
   {
       refreshInterval = 10;
   }

   var timeUntilFirstTick = refreshInterval * 1000;
   _timer = new Timer((s) => RefreshWorklist(true), null, timeUntilFirstTick, refreshInterval * 1000);
}


And finally the SelectedItem property view model binding property:

public ReferralLookupItemViewModel SelectedReferral
{
   get { return _selectedReferral; }
   set
   {
       if (_selectedReferral != value)
       {
           _selectedReferral = value;
           OnPropertyChanged();
       }
   }
}

Does anybody have any idea as to why this behavior is occurring? Is it something to do with the Timer? I appreciate this is not a simple question so please ask away for more information.


Solution

  • You need to assign properties in Binding with the UI on the UI thread.

    Replace your Timer with a DispatcherTimer or use Dispatcher.Invoke or Dispatcher.BeginInvoke inside the existing Timer callback when calling RefreshWorklist.

    By pressing the Button you are already on the UI thread, but Timer has its own thread that is different from the UI thread. DispatcherTimer callback are called on the UI thread instead https://learn.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatchertimer?view=netframework-4.0