Sajjan Sarkar
Sajjan Sarkar

Reputation: 4198

Text updates not reflecting in WPF TextBlock - what am I missing?

OVERVIEW

I'm new to WPF, I have simple window with a textarea, a button and a textblock. On click of the button i want to update the textblock to say "started.. " or something, run a routine that takes a few minutes and at the end update it to "done".

PROBLEM:

Currently my textblock only updates with the "Done" message. :(

CODE:

public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private string myValue;
        public string MyValue
        {
            get { return myValue; }
            set
            {
                myValue = value;
                RaisePropertyChanged("MyValue");
            }
        }

        private void RaisePropertyChanged(string propName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
        public event PropertyChangedEventHandler PropertyChanged;
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;
        }

        private void ScriptToFileButton_Click(object sender, RoutedEventArgs e)
        {
            Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 50; i++)
                {
                    System.Threading.Thread.Sleep(100);
                    MyValue = i.ToString();
                }
            });

            MyValue = "Scripting, please wait..";
            String text = DBObjectsTextArea.Text;
            String[] args = text.Split(' ');
            SQLScripter scripter = new SQLScripter();
            scripter.script(args);
            MyValue = "Done!";

        }
        private void Window_Closed(object sender, EventArgs e)
        {
            Application.Current.Shutdown();
        }
    }

XAML:

 <TextBox Height="109" HorizontalAlignment="Left" Margin="45,12,0,0" Name="DBObjectsTextArea" VerticalAlignment="Top" Width="418" />
        <Button Content="Script To File" Height="23" HorizontalAlignment="Left" Margin="173,145,0,0" Name="ScriptToFileButton" VerticalAlignment="Top" Width="168" Click="ScriptToFileButton_Click" />
        <TextBlock Height="56" HorizontalAlignment="Left" Margin="45,197,0,0" Name="StatusTextBlock" Text="{Binding Path=MyValue}" VerticalAlignment="Top" Width="409" />

SIMILAR LINK:

I based my code off of this: How do I refresh visual control properties (TextBlock.text) set inside a loop?

Upvotes: 2

Views: 462

Answers (3)

Gayot Fow
Gayot Fow

Reputation: 8792

You can change your event handler to this...

   private void ScriptToFileButton_Click(object sender, RoutedEventArgs e)
        {
            String text = DBObjectsTextArea.Text;
            Task t = new Task(() => PerformOnDispatcher(() => { MyValue = "Scripting, please wait.."; }));
            ManualResetEvent mre = new ManualResetEvent(false);
            t.ContinueWith((x) =>
            {
                // scripting code goes here...
                mre.Set();
            }, TaskContinuationOptions.LongRunning);

            t.ContinueWith((y) => 
            { 
                mre.WaitOne();
                PerformOnDispatcher(() => { MyValue = "Scripting, please wait.."; });
            });
            t.Start();
        }

This segregates the work into three phases: telling the user that scripting has started, then doing the work, then telling the user that the work is done. Synchronization is controlled by the "ContinueWith" method. This method waits until the previous method has completed before starting.

Because it's in a Task, changes to the UI are marshalled back via the dispatcher, which uses a separate scheduler and thus will not be blocked.

The last statement, t.Start() kicks off the sequence of events, so the user experience of clicking the button is not hampered by a blocked UI. The event handler returns almost immediately.

If the work happens really quickly, you still may only see the "Done" because the previous messages were overwritten before you had a chance to read them. So the best way to debug these things is to anchor the debugger on this statement:

PropertyChanged(this, new PropertyChangedEventArgs(propName));

This will let you observe what is being piped out to the binding engine.

Finally, to make the event handler less cluttered so as to focus on the question, I used a convenience method...

    private void PerformOnDispatcher(Action a)
    {
        Dispatcher.InvokeAsync(a);
    }

For industrial strength apps, this would be an extension method or done within the property setter.

Upvotes: 1

d.moncada
d.moncada

Reputation: 17402

I got this working by using the following:

private void ScriptToFileButton_Click(object sender, RoutedEventArgs e)
{
    Task.Factory.StartNew(() =>
    {
        for (var i = 0; i < 50; i++)
        {
            Thread.Sleep(100);
            MyValue = i.ToString(CultureInfo.InvariantCulture);
        }
    })
    .ContinueWith(s =>
    {
        MyValue = "Scripting, please wait..";

        String text = DBObjectsTextArea.Text;
        String[] args = text.Split(' ');
        SQLScripter scripter = new SQLScripter();
        scripter.script(args);

        Thread.Sleep(3000); // This sleep is only used to simulate scripting
    })
    .ContinueWith(s =>
    {
        MyValue = "Done!";
    });
}

You need to create task continuations. The reason why you are only seeing "Done" is because you are setting MyValue directly after you start the Task. You are not waiting for the Task to complete it's initial processing.

Upvotes: 0

SpeedCoder5
SpeedCoder5

Reputation: 8998

If you wish the task to run on a background thread you must account for updating the WPF UI on the UI thread. This post from Dr. Xu explains it. Dispatcher.InvokeAsync was added with .Net Framework 4.5 and is preferred. Similar functionality can also be had in earlier versions of .Net with Dispatcher.BeginInvoke. Don't forget to clean up your task and determine how or if the app should close if the task is incomplete.

    // MainWindow...
    Task t; // keep for cleanup
    // Update on the UI thread
    private void SetMyValueAsync(string value)
    {
        Application.Current.Dispatcher.BeginInvoke((Action)(() => MyValue = value));
    }
    private void ScriptToFileButton_Click(object sender, RoutedEventArgs e)
    {
        t = Task.Factory.StartNew(() =>
        {
            SetMyValueAsync("Scripting, please wait..");
            // If the following lines will update the UI
            // they should do so on the UI thread per Dr. Xu's post.
            // String text = DBObjectsTextArea.Text;
            // String[] args = text.Split(' ');
            // SQLScripter scripter = new SQLScripter();
            //scripter.script(args);
            for (int i = 0; i < 50; i++)
            {
                System.Threading.Thread.Sleep(100);
                SetMyValueAsync(i.ToString());
            }
            SetMyValueAsync("Done!");
        });
    }

    protected override void OnClosing(CancelEventArgs e)
    {
        if (t != null)
        {
            try { t.Dispose(); }
            catch (System.InvalidOperationException)
            {
                e.Cancel = true;
                MessageBox.Show("Cannot close. Task is not complete.");
            }

        }
        base.OnClosing(e);
    }

Upvotes: 0

Related Questions