How to prevent UI blocking in WPF

MattNorman

Well-known member
Joined
May 22, 2021
Messages
51
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
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,431
Location
Chesapeake, VA
Programming Experience
10+
Is it the screen updates that is causing the freezing, or is the database requests causing the freezing?

Use a profiler and find the bottleneck.

Hopefully, you are not doing that database query right in the timer event handler.

Normally, you would want to do your database refreshes in a background thread and update your model. Then in your dispatch timer you'll want to have your view model pull that new data from the model and have it exposed to the view. Freezing can happen if you (or the system) put a lock on the model while it is being updated, and the UI's timer event handler is stuck waiting on that lock before it can read the data. At typical way to get around this is to do a virtual double buffering of the data in the view model.

Most of the WPF controls are pretty smart about on updating only the part of the UI that is visible, but if your profiler is telling you that it is really the screen updates, then your only recourse is to send less data to the UI. For example, if your database is returning thousands of rows, but the UI can only display 24 rows, does it really make sense to have the UI bind to a collection with thousands of items when the collection can only contain those 24 items that are visible?
 

endofunk

Member
Joined
Jan 7, 2022
Messages
16
Programming Experience
10+
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
Your description is IMHO lacking some details:
  • What is the DB
    • Is it running on separate hardware; separate from UI.
    • How are you accessing the DB; e.g. SQL, SOAP, JSON, ...
    • Brief description of how the DB is updated; is this e.g. from the UI(s), or other...
    • Frequency of data updates; does the UI really need to be refreshed so frequently?
  • Re APPs (i.t.o. "slows down the apps responsiveness significantly")
    • Does this refer to more than the dashboard UI? e.g. other apps running on the same client hardware?
 
Last edited:

MattNorman

Well-known member
Joined
May 22, 2021
Messages
51
Programming Experience
1-3
Is it the screen updates that is causing the freezing, or is the database requests causing the freezing?

Use a profiler and find the bottleneck.

Hopefully, you are not doing that database query right in the timer event handler.

Normally, you would want to do your database refreshes in a background thread and update your model. Then in your dispatch timer you'll want to have your view model pull that new data from the model and have it exposed to the view. Freezing can happen if you (or the system) put a lock on the model while it is being updated, and the UI's timer event handler is stuck waiting on that lock before it can read the data. At typical way to get around this is to do a virtual double buffering of the data in the view model.

Most of the WPF controls are pretty smart about on updating only the part of the UI that is visible, but if your profiler is telling you that it is really the screen updates, then your only recourse is to send less data to the UI. For example, if your database is returning thousands of rows, but the UI can only display 24 rows, does it really make sense to have the UI bind to a collection with thousands of items when the collection can only contain those 24 items that are visible?
So currently I am doing everything in the dispatcher timer, both fetching the new data and updating the collections that are bound to the UI.

I have tried moving things into a background thread however always seem to run into some issues. I have reverted my most recent attempt so don't have a note of the exact error I was getting.

If I move the data refresh to a background thread, can that thread update the GUI or would I need the dispatcher timer to update the collection? If I need to separate them I assume the newly fetched data would need to be stored somewhere so that the dispatcher timer can then update the collection?
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,431
Location
Chesapeake, VA
Programming Experience
10+

MattNorman

Well-known member
Joined
May 22, 2021
Messages
51
Programming Experience
1-3
I have done some testing on this however I am still running in to issues where I get an 'Must create DependencySource on same thread as the DependecnyObject' exception.

I am confused as it allows the code in the timer to execute the first time but then fails the second time around.

C#:
if (DataManagerRealTimeAgent.HasServerDataRefreshed(LastAgentRealTimeUpdate))
{
    ObservableCollection<RealTimeAgentModel> data = new ObservableCollection<RealTimeAgentModel>(DataManagerRealTimeAgent.GetAgentData(selectedGroups));
    if (data.Count != 0)
    {
        DispatcherHelper.CheckBeginInvokeOnUI(
            () =>
            {
                RealTimeAgentData = data;
                LastAgentRealTimeUpdate = DateTime.Now;
            });

        //UpdateThresholds();
    }
}

The exception is thrown on line 3 on the second time it runs however i'm not sure why as I am creating a new local variable.
 

MattNorman

Well-known member
Joined
May 22, 2021
Messages
51
Programming Experience
1-3
Thinking about this more, would it be better to have the timer running in the helper class that actually retrieves the data and just have an event fire that my view model will register to so that it knows when the data needs to be updated?
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,431
Location
Chesapeake, VA
Programming Experience
10+
That would work too. Be aware that most non-UI framework timers run on a separate thread so you will need to use the Dispatcher when 8t comes time to notify the UI.

Have you considered using async/await?
 

MattNorman

Well-known member
Joined
May 22, 2021
Messages
51
Programming Experience
1-3
That would work too. Be aware that most non-UI framework timers run on a separate thread so you will need to use the Dispatcher when 8t comes time to notify the UI.

Have you considered using async/await?
In that case I am likely to just have the same issue as I have already tried using the dispatcher when updating the UI. Unfortunately the exception doesn't tell me which line caused it.

I'll have a look in to async/await. I haven;t had much experience with this.

It baffles me why it seems to difficult to do this as surely all apps that need to collect data in a background thread need to be able to update the UI at some point. I have tried various methods from articles I have found and still run into the same exception every time.
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,431
Location
Chesapeake, VA
Programming Experience
10+
In that case I am likely to just have the same issue as I have already tried using the dispatcher when updating the UI.
But this time you already have the data in memory. The slow database work was already completed in another thread. Now it is just the UI that needs to be updated. Compare this to your previous situation where you were doing the database query in the UI thread, the. updating the UI.
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,431
Location
Chesapeake, VA
Programming Experience
10+
It baffles me why it seems to difficult to do this as surely all apps that need to collect data in a background thread need to be able to update the UI at some point.
See post #5. It seems to be one of MS's responses to the problem.

Another approach is to use async/await. Start the database query in an async task, then await the results.
 

MattNorman

Well-known member
Joined
May 22, 2021
Messages
51
Programming Experience
1-3
But this time you already have the data in memory. The slow database work was already completed in another thread. Now it is just the UI that needs to be updated. Compare this to your previous situation where you were doing the database query in the UI thread, the. updating the UI.
Sorry by same issue I mean't the exception I keep getting despite using the dispatcher to update the UI.

I have rolled back to my working version where everything was running in a dispatcher timer and tried async/await. This ultimately gives me the same exception as everything else I have tried. I thought it may have been due to the timer ticks overlapping so I added a stop and start to the method. This still fails unfortunately. If I step through it line by line it gets to the very end of the method fine and then throws the exception:
System.ArgumentException: 'Must create DependencySource on same Thread as the DependencyObject.'

I don't even know where to look as the method executes fine and the data in the UI is updated without any issues.

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))
        {
            ObservableCollection<RealTimeAgentModel> data = await Task.Run(() => new ObservableCollection<RealTimeAgentModel>(DataManagerRealTimeAgent.GetAgentData(selectedGroups)));
            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();
    }
}
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,431
Location
Chesapeake, VA
Programming Experience
10+
When you call Task.Run(), the lambda that you pass in is executed in another thread. Your lambdas are creating the ObservableCollection<T>s.
 

MattNorman

Well-known member
Joined
May 22, 2021
Messages
51
Programming Experience
1-3
When you call Task.Run(), the lambda that you pass in is executed in another thread. Your lambdas are creating the ObservableCollection<T>s.
Thanks, should I be doing something different then? How do I get the result of the task back to a property in my view model without causing the exception?

Regards
Matt
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,431
Location
Chesapeake, VA
Programming Experience
10+
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.)
 

MattNorman

Well-known member
Joined
May 22, 2021
Messages
51
Programming Experience
1-3
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();
    }
}
 

MattNorman

Well-known member
Joined
May 22, 2021
Messages
51
Programming Experience
1-3
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.
 

MattNorman

Well-known member
Joined
May 22, 2021
Messages
51
Programming Experience
1-3
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.
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,431
Location
Chesapeake, VA
Programming Experience
10+
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;
}));
 
Top Bottom