1

I have fairly straightforward code that I want to run in order to edit the UI. I know that the UI will freeze if I run it in the Main Thread so I have been working at running it in a separate Thread, but this is not working. Here is what I want to happen: The user populates two lists using the UI. One list keeps track of what is on the screen and the other keeps track of what will need to go on the screen later. When the user clicks a certain button it clears the first list and the UI. I then display each item on the UI again using the second list with a certain length of time in between the return of each object. Here it is in a simplified form:

myList = [/*items to be displayed*/];
myMessages = [/*items currently displayed*/]
private void ButtonAnimationRun(object sender, RoutedEventArgs e)
{
    Thread thread = new Thread(() =>
    {
        Run();
    });
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();

}
public void Run()
{
    foreach (Message message in myList)
    {
        Thread.Sleep(message.Time * 1000);
        CreateText(message.Text);
    }          
}
public void CreateText(string text)
{
    Dispatcher.Invoke(() => myGrid.Children.Add(myMessages[myMessages.Count - 1]));
    // text is added to the child of myGrid here
    Dispatcher.Invoke(() => myScrollBar.ScrollToBottom());
}

That is not all my code, but it is the code that is related to my problem. If I use await or tasks I get an Apartment State error. With the code above I get this error: 'The calling thread cannot access this object because a different thread owns it.' on the line of the first Invoke, with or without the Invoke I still get it. The second Invoke works just fine when I comment out the previous one, but then, or course, I don't get anything shown on the UI because that is the code that adds the UI element to the UI. I have searched stackoverflow and all the answers I find cause others to crop up or inhibit the functionality of my app. If you need more information I can provide it.

Hans
  • 21
  • 1
  • 4
  • With resaonable data types code should behave fine without freeing UI... Please [edit] code to post [MCVE]. In particular you should not need STA for code shown and use list of strings for `myList`. If the fact `myList` is actually list of visible controls you need to include that in the code sample. Also `CreateText` does not use its argument... and for some reason have 2 `.Invoke` calls - you don't need that for proper sample either. – Alexei Levenkov Apr 22 '20 at 22:19

2 Answers2

2

The reason you get this exceotion is that you are using the wrong Dispatcher.

In WPF each thread has it's own Dispatcher. All UIElement objects also derive from DispatcherObject. Every DispatcherObject has a dispatcher affinity, which means they are associated to a specific Dispatcher - the Dispatcher of the thread the DispatcherObject (or UIElement) was created on. That's why this affinity is also known as thread affinity.

Now, you understand how to trigger the cross thread exception "The calling thread cannot access this object because a different thread owns it.": access the DispatcherObject e.g. a Grid from the wrong thread or via the wrong Dispatcher.

To get the right Dispatcher i.e. the Dispatcher that is associated with the DispatcherObject, you can access the DispatcherObject.Dispatcher property:

var dispatcher = myGrid.Dispatcher;

Since most of the time the DispatcherObjects are created on the application's (primary) UI thread, you can use it's Dispatcher too. You can access the UI thread Dispatcher via the static property

var dispatcher = Application.Current.Dispatcher;

If you have a long running dispatcher job, consider to execute it asynchronously and in the background (or at a lower priority than the default). This also helps to prevent the UI from becoming sluggish:

myGrid.Dispatcher.InvokeAsync(() => myScrollBar.ScrollToBottom(), DispatcherPriority.Background);

If you create a new UI thread manually (I guess you have a very good reason to do this), you may want to kick off the Dispatcher loop (or frame) by calling Dispatcher.Run. Consider to use Task.Run instead of Thread and the asynchronous Task.Delay instead of the blocking Thread.Sleep.


After reading your question once more, I understood that you are only creating this thread because you want to prevent the UI from freezing. It also occurred to me, that the way you have created the thread is possibly because you didn't know it better. If this is the case I strongly recommend to implement the following solution, which may also solve your problem:

private async void ButtonAnimationRun(object sender, RoutedEventArgs e)
{
  foreach (Message message in myList)
  {
    await Task.Delay(message.Time * 1000);
    await CreateTextAsync(message.Text);
  }          
}

public async Task CreateTextAsync(string text)
{
  // Do the heavy work asynchronously on a background thread
  await Task.Run(() =>
    {
      Application.Current.Dispatcher.InvokeAsync(() => myGrid.Children.Add(myMessages[myMessages.Count - 1]), DispatcherPriority.Background);

      // text is added to the child of myGrid here

      Application.Current.Dispatcher.InvokeAsync(() => myScrollBar.ScrollToBottom(), DispatcherPriority.Background);
    }
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • I have tried `myGrid.Dispatcher`, `Application.Current.Dispatcher`, and `InvokeAsync()` and I still got the 'The calling thread cannot access this object because a different thread owns it.' error on all of them. – Hans Apr 22 '20 at 22:54
  • 1
    Where was `myGrid` created, on the main UI thread or your new STA thread? – BionicCode Apr 22 '20 at 23:15
  • Agree with comment about where the control was created. Also should be using `BeginInvoke` not `Invoke`. No reason to block. – Zer0 Apr 22 '20 at 23:16
  • `myGrid` was created in the XAML file. – Hans Apr 22 '20 at 23:18
  • 1
    @Hans - He's referring to the thread it was created on. The construction and initialization of a control/window dictates which STA thread it uses. This looks like your real problem, unless you use a better design as he mentioned. – Zer0 Apr 22 '20 at 23:21
  • Are you saying that I can create a new grid within the Thread and use it there? – Hans Apr 22 '20 at 23:36
  • Hang on. I just got it all to work. I backed up and ran the entire `CreateText` method with `Application.Current.Dispatcher` and it worked just fine and exactly how it is supposed to. Thanks, @BionicCode, for the tip on `Task.Delay`. That helped, too. – Hans Apr 22 '20 at 23:47
  • I have updated my answer to show you how to use the recommended Task library. You should always prefer the `Task` over a `Thread`. What I was asking, where or how do you create the view? Does `myGrid` belong to the main view? To which thread does `myGrid`belong? Please try my edited code. It should solve the problem, otherwise the problem is hidden in code you didn't show. – BionicCode Apr 22 '20 at 23:47
  • I was really wondering. Then why didn't `Application.Current.Dispatcher` worked in the first run? Have you made a mistake? – BionicCode Apr 22 '20 at 23:48
  • Still, you should check my improved implementation. – BionicCode Apr 22 '20 at 23:50
  • Wait, are the `message` items also controls (of type `DispatrherObject`)? This would explain everything. Because `message.Text` would then actually be a forbidden access the `DispatrherObject` `Message` from a different thread. – BionicCode Apr 22 '20 at 23:57
  • I tried the `await` way before and it complained about having the wrong Apartment State. The `Message` is a class I made in my application because I needed a certain type of information stored in a certain way. It has properties I can conveniently access to write to the UI. – Hans Apr 23 '20 at 00:02
  • You obviously used `await` very wrong. If you show me what you did I can tell you what went wrong. Check my code. It is completely different from yours. It will never throw any STA exception. This is the way to program. `Thread` is too heavy and too complex, don't use it. And making it `STA` for no special reason is also useless and can be expensive. You rather should mark it as background thread. The way you are using `Thread` wrong, is a good example why to avoid implementing multithreading this (old) way. – BionicCode Apr 23 '20 at 00:09
  • I called only the `Run` method with `await` and `Task.Run`. I didn't use it to the extent you did. – Hans Apr 23 '20 at 00:15
  • Yes, but you called it from the `Thread`, which was set to STA. This doesn't work, because you never started the dispatcher frame. Also, `Task.Run` replaces `Thread`. Don't combine them. Never mind. It's working now. I just had to say that your multithreading needs improvement. Async/await was made to solve your specific problem - keep the UI responsive. It was introduced to make the heavy `Thread` obsolete and intriduce a far more convenient API – BionicCode Apr 23 '20 at 00:24
  • Thank you. But I wasn't bugging you to make you accept my answer. You should seriously consider to let go the `Thread`. – BionicCode Apr 23 '20 at 00:32
  • I think I will. The reason I used the `Thread` is because that is what I was taught threading on, but you are probably right. – Hans Apr 23 '20 at 12:57
0

I would be tempted to simplify BionicCode's answer even more, by eliminating any usage of the Dispatcher altogether. I would also get rid of Task.Run, since there is no heavy work that would freeze the UI in the posted code. The beauty of async-await is that after each await we are back in the UI thread, without having to do anything at all.

private async void ButtonAnimationRun(object sender, RoutedEventArgs e)
{
    foreach (Message message in myList)
    {
        await Task.Delay(message.Time * 1000);
        CreateText(message.Text);
    }
}

public void CreateText(string text)
{
    myGrid.Children.Add(myMessages[myMessages.Count - 1]);
    // text is added to the child of myGrid here
    myScrollBar.ScrollToBottom();
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104