bdhar
bdhar

Reputation: 22973

Cross-Thread property behavior

I have a windows form with a text box txtOutput. I have some content in it. I have written a property to get and set the txtOutput.Text both from within the same thread and across threads like this:

public string OutputString
{
    get
    {
        string text = string.Empty;
        if (txtOutput.InvokeRequired)
        {
            txtOutput.BeginInvoke(new MethodInvoker(delegate
                {
                    text = txtOutput.Text;
                }));
        }
        else
        {
            text = txtOutput.Text;
        }
        return text;
    }
    set
    {
        if (txtOutput.InvokeRequired)
        {
            txtOutput.BeginInvoke(new MethodInvoker(delegate
                {
                    txtOutput.Text = value;
                }));
        }
        else
        {
            txtOutput.Text = value;
        }
    }
}

If I set/get the property from the same thread, the behavior is as expected when calling the below function like PrintMessage().

public void PrintMessage()
{
    MessageBox.Show(OutputString);
}

But when I call like this new Thread(PrintMessage).Start(). The get does not retrieve the value in the text box (i.e., the MessageBox shows empty string). When I do the same by keeping a breakpoint on the line:

txtOutput.BeginInvoke(new MethodInvoker(delegate
{
    text = txtOutput.Text;
}));

while debug, the value is retrieved (i.e., the MessageBox shows the txtOutput content)

Should I sleep somewhere? Where am I making the mistake?

Upvotes: 2

Views: 935

Answers (2)

Unril
Unril

Reputation: 106

You can use Tasks with TaskScheduler form UI thread. When you pass the dispatcher form UI thread to task factory, this task performed in UI thread and return result to the thread where this task was created on.

namespace WpfTest {
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

public partial class MainWindow : Window {
    private readonly TaskScheduler _taskScheduler;

    public MainWindow() {
        InitializeComponent();

        _taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
        button.Click += ButtonOnClick;
    }

    public string Text {
        get {
            if (text.CheckAccess()) {
                return text.Text;
            }
            return Task.Factory.StartNew(
                () => text.Text, CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Result;
        }
        set {
            if (text.CheckAccess()) {
                text.Text = value;
            } else {
                Task.Factory.StartNew(
                    () => { text.Text = value; }, CancellationToken.None, TaskCreationOptions.None, _taskScheduler);
            }
        }
    }

    private void ButtonOnClick(object sender, RoutedEventArgs routedEventArgs) {
        Text += "Test1";
        new Thread(() => { Text += "Test2"; }).Start();
    }
}
}

Upvotes: 0

Ryan Wright
Ryan Wright

Reputation: 3433

The problem is you're calling MessageBox.Show() with a reference to the text variable before the UI thread can handle the request you placed with the dispatcher. I would avoid using Thread.Sleep() as you could end up with some nasty side effects. Ideally you should re-factor your code to get rid of the property, which is synchronous, in favor of a more asynchronous solution. Something similar to the code below should give you the result you're looking for:

public void PrintMessage(Action<string> displayAction)
{
    string text = string.Empty;
    if (txtOutput.InvokeRequired)
    {
        txtOutput.BeginInvoke(new MethodInvoker(delegate
        {
            displayAction.Invoke(txtOutput.Text);
        }));
    }
    else
    {
        displayAction.Invoke(txtOutput.Text);
    }
}

And Invoke it:

// Get the text asynchronously
PrintMessage(s => MessageBox.Show(s));

Upvotes: 2

Related Questions