Question

I got a DataGrid which is getting filled with several items after an update function has been processed which is executed within a BackgroundWorker.

The BackgroundWorker actually requests a JSON API and parses the incoming response. In that process, the UI is not locked at all, everything is still responsive. Now, when filling the DataGrid with the previously fetched response, the UI locks and freezes until all items have been added.

Code:

Update method:

public override void Update()
{
    CanUpdate = false;
    AddEverythingButtonVisible = false;

    var worker = new BackgroundWorker();
    worker.DoWork += (s, e) =>
    {
        // Item.Search is sending a HTTP request, everything is responding
        foreach (Item item in Item.Search(_SearchText, _SearchLevelMin, _SearchLevelMax, _SearchRarity, _SearchType,_SearchRemoveUnavailable))
        {
            // after the response was parsed, the lock begins at this point when adding items by using the UI thread
            Execute(() => ItemCollection.Add(new ProfitItemViewModel(new ProfitItem(item))));
        }
     }
 }

Execute is a little helper function in order to access the UI thread while being in an seperate thread (BackgroundWorker) and looks as follows:

protected void Execute(System.Action action)
{
    if (Dispatcher.CheckAccess())
    {
        action.Invoke();
    }
    else
    {
        Dispatcher.BeginInvoke(DispatcherPriority.DataBind, action);
    }
}

I had a read about BeginInvoke and Invoke and came to the decision, that the UI thread must not stop responding when the actions are performed. So I ended up implementing BeginInvoke but the UI still freezes and locks when updating.

Well, is there any solution for not locking my entire application while my DataGrid is getting filled?


Updated and working code:

var searchResults = Item.Search(_SearchText, _SearchLevelMin, _SearchLevelMax, _SearchRarity, _SearchType, _SearchRemoveUnavailable);
var compiledSearchResults = searchResults.Select(item => new ProfitItemViewModel(new ProfitItem(item)));
foreach (ProfitItemViewModel item in compiledSearchResults)
    Execute(() => ItemCollection.Add(item));
Was it helpful?

Solution

You are adding items individually, each as a separate invoke call, which would very much slow down the UI. Rather, add as many items as you can in a single UI call. It may cause a temporary glitch, but will be much faster than a bunch of single calls.

Also, you are creating objects inside the UI call. If they aren't dependency objects, create them before you make the UI call to save some UI power as well.

Rule of thumb: do was much as you can outside the UI thread as you can, and only do work on the UI thread that has to be done on the UI thread (in your case, updating the ItemCollection).

Example:

var searchResult = Item.Search(_SearchText, _SearchLevelMin, _SearchLevelMax, _SearchRarity, _SearchType,_SearchRemoveUnavailable));

var compiledList = searchResult.Select(item => new ProfitItemViewModel(new ProfitItem(item))).ToArray();

Execute(() => {
    foreach (Item item in compiledList)
    {
        ItemCollection.Add(item);
    }
});

Also consider using DeferRefresh to speed up the adds like so:

Execute(() => {
    using(ItemCollection.DeferRefresh()) {
        foreach (Item item in compiledList)
        {
            ItemCollection.Add(item);
        }
    }
});

OTHER TIPS

I think you should load the items in another thread, and once loaded, then update the grid. For instance, I use a methdo like this:

public void LockAndDoInBackground(Action action, string text, Action beforeVisualAction = null, Action afterVisualAction = null)
{
    if (IsBusy)
        return;
    var currentSyncContext = SynchronizationContext.Current;
    ActiveThread = new Thread((_) =>
    {                
        currentSyncContext.Send(t =>
        {
            IsBusy = true;
            BusyText = string.IsNullOrEmpty(text) ? "Wait please..." : text;
            if (beforeVisualAction != null)
                beforeVisualAction();
        }, null);
        action();
        currentSyncContext.Send(t =>
        {
            IsBusy = false;
            BusyText = "";
            if (afterVisualAction != null)
                afterVisualAction();
        }, null);
    });
    ActiveThread.Start();
}

IsBusy and BusyText are optional variables in the class (I use this method in a Base ViewModel Class) The action parameter should be the load logic, and the afterVisualAction parameter should be the load data action in the grid. For instance:

USAGE:

public void Test()
{
    var tcollection = new List<ProfitItemViewModel>();
    Action tloadAction = ()=> 
    {
        foreach (Item item in Item.Search(_SearchText, _SearchLevelMin, _SearchLevelMax, _SearchRarity, _SearchType,_SearchRemoveUnavailable))
        {                
            tcollection.Add(new ProfitItemViewModel(new ProfitItem(item))));
        }
    };
    Action tupdateGridAction = ()=> 
    {
        foreach (Item item in tcollection)
        {                
            ItemCollection.Add(item);
        }
    };
    LockAndDoInBackground(tloadAction, "Generating Information...", null,  tupdateGridAction);
}

This is the idea I propose. Hope it helps.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top