2

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?

Community
  • 1
  • 1
Sajjan Sarkar
  • 3,900
  • 5
  • 40
  • 51
  • @GayotFow how do I do that? Can u share some code samples? Maybe somehting like http://stackoverflow.com/questions/4205502/wpf-not-updating-textbox-while-in-progress ? – Sajjan Sarkar May 22 '14 at 15:02
  • @GayotFow That was the link I used initally, so do I need to add the Dispatch code in addition to it? If you could update my code as a separate answer, ill be happy to accept it as the answer if it works – Sajjan Sarkar May 22 '14 at 15:10

3 Answers3

1

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.

Gayot Fow
  • 8,710
  • 1
  • 35
  • 48
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/54225/discussion-between-sajjan-sarkar-and-gayot-fow). – Sajjan Sarkar May 22 '14 at 17:00
  • I'm curious, why is TaskContinuationOptions.LongRunning being used? This will create a new Thread every time as compared to removing it, and letting the ThreadPool handle the Task. – d.moncada May 22 '14 at 18:47
  • It gives the option to detach it from the local queue so that it will not interfere with competing threads in the local queue. – Gayot Fow May 22 '14 at 18:57
0

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);
    }
SpeedCoder5
  • 8,188
  • 6
  • 33
  • 34
0

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.

d.moncada
  • 16,900
  • 5
  • 53
  • 82