views:

2335

answers:

6

I've got a WPF application with a status bar.

<StatusBar Grid.Row="1"
           Height="23"
           Name="StatusBar1"
           VerticalAlignment="Bottom">
    <TextBlock Name="TextBlockStatus" />
</StatusBar>

I'd like to display text there and switch to the hourglass Wait cursor when I do a small amount of work.

This code will update the cursor, but the StatusBar text does not update...

Cursor = Cursors.Wait
TextBlockStatus.Text = "Loading..."
System.Threading.Thread.Sleep(New TimeSpan(0, 0, 3))
TextBlockStatus.Text = String.Empty
Cursor = Cursors.Arrow


Update

Inspired by Alexandra's answer...

It works if I do it this way, but I'm not at all happy with this solution. Is there a simpler way?

Delegate Sub Load1()
Sub Load2()
    System.Threading.Thread.Sleep(New TimeSpan(0, 0, 3))
End Sub
Dim Load3 As Load1 = AddressOf Load2

Sub Load()
    Cursor = Cursors.Wait
    TextBlockStatus.Text = "Loading..."
    Dispatcher.Invoke(DispatcherPriority.Background, Load3)
    TextBlockStatus.Text = String.Empty
    Cursor = Cursors.Arrow
End Sub

I'd rather it instead looked something like this...

Sub Load()
    Cursor = Cursors.Wait
    TextBlockStatus.Text = "Loading..."

    'somehow put all the Dispatcher, Invoke, Delegate,
     AddressOf, and method definition stuff here'

    TextBlockStatus.Text = String.Empty
    Cursor = Cursors.Arrow
End Sub

Or even better...

Sub Load()
    Cursor = Cursors.Wait
    ForceStatus("Loading...")
    System.Threading.Thread.Sleep(New TimeSpan(0, 0, 3))
    ForceStatus(String.Empty)
    Cursor = Cursors.Arrow
End Sub

Sub ForceStatus(ByVal Text As String)
    TextBlockStatus.Text = Text
    'perform magic'
End Sub


Update

I've also tried to bind the TextBlock to a public property and implement INotifyPropertyChanged as IanGilham suggested. This does not work.

XAML:

<TextBlock Text="{Binding Path=StatusText}"/>

Visual Basic:

Imports System.ComponentModel
Partial Public Class Window1
    Implements INotifyPropertyChanged

    Private _StatusText As String = String.Empty
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Property StatusText() As String
        Get
            Return _StatusText
        End Get
        Set(ByVal value As String)
            _StatusText = value
            OnPropertyChanged("StatusText")
        End Set
    End Property

    Shadows Sub OnPropertyChanged(ByVal name As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
    End Sub
    ...

    Sub Load()
        ...
        Cursor = Cursors.Wait
        StatusText = "Loading..."
        System.Threading.Thread.Sleep(New TimeSpan(0, 0, 3))
        StatusText = String.Empty
        Cursor = Cursors.Arrow
        ...
    End Sub
...
+1  A: 

You can easily call

TextBlockStatus.UpdateLayout();

right after you change the Text property which should refresh the control and change the text on the screen.

I also use

this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(delegate {
    /* your code here */
}));

to (try to) make sure that my task runs after the refreshing is done.

I have to admit, it works ~90% - 95% of the time (there are times when text changes only after the task has finished or it changes with a slight delay) but I couldn't find anything better.

EDIT for the question's edit:

I'm not an expert in VB but if it doesn't support anonymous inline methods then your second way is the one that should work. Try calling UpdateLayout() before calling the dispatcher

Cursor = Cursors.Wait
TextBlockStatus.Text = "Loading..."
TextBlockStatus.UpdateLayout(); //include the update before calling dispatcher
Dispatcher.Invoke(DispatcherPriority.Background, Load3)
TextBlockStatus.Text = String.Empty
Cursor = Cursors.Arrow
Alexandra
UpdateLayout() does not work for me.
Zack Peterson
See my update regarding the Dispatcher. I think VB does not support anonymous methods within a statement body.
Zack Peterson
+5  A: 

You should use BackgroundWorker. The work will take place an a separate thread, meaning your UI thread will be free and your application will still be responsive.

It's not going to be an incredibly compact solution code-wise, but it's the most robust and friendly to your users.

RandomEngy
Like this? http://stackoverflow.com/questions/783649/is-there-no-simple-way-to-set-wpf-statusbar-text
Zack Peterson
+1  A: 

I would create a public property to hold the text field and implement INotifyPropertyChanged. You can easily bind the TextBlock to the property in the xaml file.

<TextBlock Text="{Binding Path=StatusText}"/>

Then you just change the property and the UI will always be up to date.

In my work projects at the moment, the class representing the Window only contains method handlers, which call the public interface of a ViewModel/Presenter class. The ViewModel contains anything the UI needs to bind to and launches any and all functions by adding jobs to the ThreadPool.

Of course, all my projects at the moment involve batch processing so letting the ThreadPool deal with concurrency issues makes sense in my case.

Update: Can you try the properties approach again but making sure the property is public. According to this page, you can only bind to public properties.

Imports System.ComponentModel
Partial Public Class Window1
    Implements INotifyPropertyChanged

    Private _StatusText As String = String.Empty
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    // I think the property has to be public for binding to work
    Public Property StatusText() As String
        Get
            Return _StatusText
        End Get
        Set(ByVal value As String)
            _StatusText = value
            OnPropertyChanged("StatusText")
        End Set
    End Property

    Shadows Sub OnPropertyChanged(ByVal name As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
    End Sub
    ...

    Sub Load()
        ...
        Cursor = Cursors.Wait
        StatusText = "Loading..."
        System.Threading.Thread.Sleep(New TimeSpan(0, 0, 3))
        StatusText = String.Empty
        Cursor = Cursors.Arrow
        ...
    End Sub

...

IanGilham
A: 

User Ray submitted an answer that solves this problem in another question. His answer is based on Shaun Bowe's answer in a third question.

This is my implementation...

Sub UpdateStatus(ByVal Message As String)
    If Message = String.Empty Then
        Cursor = Cursors.Arrow
    Else
        Cursor = Cursors.Wait
    End If
    TextBlockStatus.Text = Message
    AllowUIToUpdate()
End Sub

Public Sub AllowUIToUpdate()
    Dim frame As New DispatcherFrame()
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Render, New DispatcherOperationCallback(AddressOf JunkMethod), frame)
    Dispatcher.PushFrame(frame)
End Sub

Private Function JunkMethod(ByVal arg As Object) As Object
    DirectCast(arg, DispatcherFrame).Continue = False
    Return Nothing
End Function

It might be good to combine this with XAML binding and INotifyPropertyChanged per IanGilham's suggestion.

Zack Peterson
Nice post +1. I'm a WPF newb and my statusbar text is not updating either. I think it's lame to tell the user to spin a new thread for everything. I'm giving these solution a go...
Steve
+1  A: 

Hi, here is my code (Status is a WPF TextBlock with x:Name="Status")

(It's c# but I think it should be portable to VB...)

    private void Button_Click(object sender, RoutedEventArgs e) {
        var bw = new BackgroundWorker();
        bw.DoWork += DoSomething;
        bw.RunWorkerAsync();
    }

    private void DoSomething(object sender, DoWorkEventArgs args) {
        UpdateStatus("Doing Something...");

        ...

        UpdateStatus("Done...");
    }

    private void UpdateStatus(string text) {
        Status.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => SetStatus(text)));
    }

    private void SetStatus(string text) {
        Status.Text = text;
    }

Nice greetings from Germany and thanks for your help above to get to this code... Markus

Mahop
+1  A: 

Hey,

Did you try like this?

TextBlockStatus.Dispatcher.Invoke(DispatcherPriority.Loaded, new Action(() => { TextBlockStatus.Text = message; }));

I had the same problem and for some reason using DispatcherPriority.Loaded worked for me. Also, no need for UpdateLayout.

I hope it works for you too, cheers,

André

andrecarlucci