How to prevent UI blocking in WPF

MattNorman

Well-known member
Joined
May 22, 2021
Messages
98
Programming Experience
1-3
I have a dashboard in my app that retrieves data from the database every 3 seconds. I am currently using a dispatcher timer in the view model for this however it slows down the apps responsiveness significantly.

Can anyone advise on the best way to do this without locking up the UI thread? Once the data has been pulled from the database it will need to be updated in the data grid which is bound to a property in the ViewModel.

Appreciate any help.

Regards
Matt
 
I think line 16 is okay since you are getting back a list. On line 28, though, see if you can also get back a list. After awaiting the list, then create a new observable collection seeded with the the list, and then assign that new collection to the the dependency property on line 31.

(It would also likely help if you created a minimal repro case of your problem and play with things there instead of your current "big" project so that you can focus on just the issue.)
I have tried amending but still get the same result. It runs once and loads the data but then throws the same exception.

I will create a simple test project to try and replicate this there.

C#:
private async void UpdateDashboardData(object sender, EventArgs e)
{
    string methodName = "UpdateDashboardData(object sender, EventArgs e)";

    try
    {
        //Stop the timer until code execution is complete.
        ((DispatcherTimer)sender).Stop();

        //Get list of selected agent groups.
        List<string> selectedGroups = AgentGroups.Where(a => a.IsChecked == true).Select(a => a.GroupName).ToList();

        //Check if server has updated real time skill data.
        if (DataManagerRealTimeSkill.HasServerDataRefreshed(LastSkillRealTimeUpdate))
        {
            List<RealTimeSkillModel> data = await Task.Run(() => DataManagerRealTimeSkill.GetSkillData(selectedGroups));
            if (data.Count != 0)
            {
                RealTimeSkillData = data[0];
                UpdateThresholds();
                LastSkillRealTimeUpdate = DateTime.Now;
            }
        }

        //Check if server has updated real time agent data.
        if (DataManagerRealTimeAgent.HasServerDataRefreshed(LastAgentRealTimeUpdate))
        {
            List<RealTimeAgentModel> newData = await Task.Run(() => DataManagerRealTimeAgent.GetAgentData(selectedGroups));
            ObservableCollection<RealTimeAgentModel> data = new ObservableCollection<RealTimeAgentModel>(newData);
            if (data.Count != 0)
            {
                RealTimeAgentData = data;
                UpdateThresholds();
                LastAgentRealTimeUpdate = DateTime.Now;
            }
        }

        //Check if data has updated historical skill data.
        if (DataManagerHistoricalSkill.HasServerDataRefreshed(LastHistoricalSkillUpdate))
        {
            GetHistoricalSkillTileData();
            GetHistoricalSkillTileComparisonData();
            GetHistoricalSkillTableData();

            UpdateThresholds();
            LastHistoricalSkillUpdate = DateTime.Now;
        }

        //Start the timer.
        ((DispatcherTimer)sender).Start();
    }
    catch (Exception ex)
    {
        DataManagerLogs.WriteToErrorLog($"{classNamespace}.{methodName}", ex.ToString(), "");
        MsgBoxDialogContent = new CustomMessageBoxView("Error", $"An error has been encountered in {classNamespace}.{methodName}" + Environment.NewLine + Environment.NewLine +
                                                       "Exception: " + ex.Message);
        MsgBoxDialogOpen = true;
        DialogHostStates.CustomMessageDialogOpen = true;
        dialogTimer.Start();
    }
}
 
Doing some testing it appears to be the fact that I am using an ObservableCollection that is the issue. A standard list works fine.
 
Starting to lose hope on this one.

I recreated the same scenario in a stripped down app and it runs without any issues:

C#:
public class MainViewModel : BaseViewModel
    {
        public MainViewModel()
        {
            realtimeDataTimer.Tick += new EventHandler(UpdateDashboardData);
            realtimeDataTimer.Start();
        }

        public ObservableCollection<TestModel> TestData { get; set; } = new ObservableCollection<TestModel>();

        private readonly DispatcherTimer realtimeDataTimer = new DispatcherTimer();

        private async void UpdateDashboardData(object sender, EventArgs e)
        {
            ObservableCollection<TestModel> data = new ObservableCollection<TestModel>(await Task.Run(() => DataManager.GetData()));
            TestData = data;
        }
    }

If I use the exact same approach in my main app I still get the exception.
 
I have tried some toner methods again and still end up in the same place.

This works fine:
C#:
while (true)
{
    App.Current.Dispatcher.BeginInvoke(new Action(() =>
    {
        ObservableCollection<RealTimeAgentModel> data = DataManagerRealTimeAgent.GetAgentData(selectedGroups));

        RealTimeAgentData = data;
    }));

    Thread.Sleep(3000);
}

As soon as I move the data fetching outside of the Dispatcher.BeginInvoke it throws the exception. It seems to be the fact that the new data list is created in the background thread that is the issue rather then editing the original bound collection.
 
Just to confirm I understand, are you saying this fails:
C#:
var realData = DataManagerRealTimeAgent.GetAgentData(selectedGroups));

App.Current.Dispatcher.BeginInvoke(new Action(() =>
{
    ObservableCollection<RealTimeAgentModel> data = new ObservableCollection<RealTiemAgentModel>(realData);

    RealTimeAgentData = data;
}));

But this succeeds:
C#:
App.Current.Dispatcher.BeginInvoke(new Action(() =>
{
    ObservableCollection<RealTimeAgentModel> data = DataManagerRealTimeAgent.GetAgentData(selectedGroups));

    RealTimeAgentData = data;
}));
 
Just to confirm I understand, are you saying this fails:
C#:
var realData = DataManagerRealTimeAgent.GetAgentData(selectedGroups));

App.Current.Dispatcher.BeginInvoke(new Action(() =>
{
    ObservableCollection<RealTimeAgentModel> data = new ObservableCollection<RealTiemAgentModel>(realData);

    RealTimeAgentData = data;
}));

But this succeeds:
C#:
App.Current.Dispatcher.BeginInvoke(new Action(() =>
{
    ObservableCollection<RealTimeAgentModel> data = DataManagerRealTimeAgent.GetAgentData(selectedGroups));

    RealTimeAgentData = data;
}));
That's correct.

As soon as I move the data load outside of the Dispatched.BeginInvoke it fails.

Moving it inside works but slows down the UI as expected.

This suggests that the updating of the property is not the issue, it's that the var holding the output of the data fetch is created on a different thread.

I have however also tried using a BackgroundWorker which handles the passing of data between threads and that still throws the exception.

Constructor
C#:
BackgroundWorker dataRefreshWorker = new BackgroundWorker();
dataRefreshWorker.DoWork += dataRefreshWorker_DoWork;
dataRefreshWorker.RunWorkerCompleted += dataRefreshWorker_RunWorkerCompleted;
dataRefreshWorker.RunWorkerAsync();

Methods
C#:
void dataRefreshWorker_DoWork(object sender, DoWorkEventArgs e)
{
    List<string> selectedGroups = AgentGroups.Where(a => a.IsChecked).Select(a => a.GroupName).ToList();
    List<RealTimeAgentModel> data = DataManagerRealTimeAgent.GetAgentData(selectedGroups);
    e.Result = data;
    Thread.Sleep(1000);
}

void dataRefreshWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    List<RealTimeAgentModel> data = e.Result as List<RealTimeAgentModel>;
    RealTimeAgentData = new ObservableCollection<RealTimeAgentModel>(e.Result as List<RealTimeAgentModel>);
}

The RunWorkerCompleted method should be running on the main thread so no idea why it is still throwing the same exception.
 
For grins, can you replace the property RealTimeAgentData just be an ObservableCollection<string> and then populate that with dummy strings? Perhaps the RealTimeAgentModel is not a POCO, but rather a node in an object graph and some of the other objects in that graph are created in another thread.
 
So I finally caught a break and it appears the issue specifically relates to one column within the data grid that contains a button with it's background color bound to a property within the RealTimeAgentModel.

Commenting this out from the XAML allows it to run in the background fine when just using the BindingOperations.EnableCollectionSynchronization method.

Know I need to figure out why the color binding specifically is causing this.
 
Next part of the puzzle is to understand why the color property is the problem and why it is not created on the UI thread if that is indeed the case.

The RealTimeAgentModel has a Brush property called BackgroundColor. This is given a default value directly within the model and the color only changes under certain conditions.

Does setting a default color within the model class mean that it is set on a non UI thread?

I have taken out the code that changes the color and it still fails.

It seems that it is the fact that the property is being re-created as the list is re-populated.
 
Final piece of the puzzle resolved. I changed the model to have a Color property which gets changes when data updates. I then amended the SolidColorBrush property to return a new SolidColorBrush based on the Color property. The SolidColorBrush can then be bound fine as it is not being created/changed on a non UI thread.

C#:
public Color BackColor { get; set; } = Colors.Green;

public SolidColorBrush BackgroundColor
{
    get
    {
        return new SolidColorBrush(BackColor);
    }
}

Thanks for your help with this Skydiver
 
Another approach is to make your model UI agnostic and use a value converter to to make the view UI specific.
 
Have you tried something along the lines of this?
C#:
Task.Factory
  .StartNew(() => DataManagerRealTimeAgent.GetAgentData(selectedGroups))
  .ContinueWith(t =>
     App.Current.Dispatcher.BeginInvoke(new Action(() => {
        RealTimeAgentData = t.Result;
     })));
 
Back
Top Bottom