views:

1297

answers:

3

I am building a small wpf app in C#. When a button gets clicked a third party dll function constructs a tree like object. This object is bound to a treeview. This works fine but takes a bit of time to load. As the dll function constructs the object it prints progress info to the console. I want to redirect this into a TextBlock so that the user gets to see the progress messages.

My window ctor looks like this:

InitializeComponent(); 
StringRedir s = new StringRedir(ref ProgressTextBlock); 
Console.SetOut(s); 
Console.SetError(s); 
this.DataContext = s; 

xaml:

<TextBlock Text="{Binding Path=Text}" Width="244" 
x:Name="ProgressTextBlock" TextWrapping="Wrap"  /> 
<TreeView >...</TreeView> 

The StringRedir class is shown below. The problem is the TextBlock for some reason does not get updated with the messages until the TreeView gets loaded. Stepping through I see the Text property being updated but the TextBlock is not getting refreshed. I added a MessageBox.Show () at the point where Text gets updated and this seems to cause the window to refresh each time and I am able to see each message. So I guess I need some way to explicitly refresh the screen...but this doesnt make sense I thought the databinding would cause a visual refresh when the property changed. What am I missing here? How do I get it to refresh? Any advice is appreciated!

public class StringRedir : StringWriter , INotifyPropertyChanged 
{ 
    private string text; 
    private TextBlock local; 


    public string Text { 
        get{ return text;} 
        set{ 
            text = text + value; 
            OnPropertyChanged("Text"); 
        } 
    } 


    public event PropertyChangedEventHandler PropertyChanged; 
    protected void OnPropertyChanged(string name) 
    { 
        PropertyChangedEventHandler handler = PropertyChanged; 
        if (handler != null) 
        { 
            handler(this, new PropertyChangedEventArgs(name)); 
        } 
    } 


    public StringRedir(ref TextBlock t) 
    { 
        local = t; 
        Text = ""; 
    } 


    public override void WriteLine(string x) 
    { 
        Text = x +"\n"; 
        //MessageBox.Show("hello"); 
    } 


}
A: 

I believe the problem is in the constructor of your StringRedir class. You're passing in ProgessTextBlock, and you're doing this to it:

local.Text = "";

This is effectively overwriting the previously set value for ProgressTextBlock.Text, which was this:

{Binding Text}

See what I mean? By explicitly setting a value to the TextBlock's Text property, you've cancelled the binding.

If I'm reading right, it looks like the idea of passing a TextBlock into the StringRedir's ctor is a hangover from before you tried binding directly. I'd ditch that and stick with the binding idea as it's more in the "spirit" of WPF.

Matt Hamilton
Thanks Matt. Okay...that made me actually think about what I was doing with databinding :) The StringRedir class code, I found on msdn and blindly assummed I needed the local TextBlock reference to pass text back. Wow data binding...so thats what it does :)
Klerk
Don't forget to mark this as the answer if it does indeed fix the problem.
Matt Hamilton
Will definately do that. But I am still not seeing the textblock refresh (It does only once the entire treeview gets loaded). I have removed the local TextBlock from the StringRedir class and am just relying on binding now.
Klerk
+1  A: 

You haven't included the code that is loading the data for the TreeView, but I'm guessing it's being done on the UI thread. If so, this will block any UI updates (including changes to the TextBlock) until it has completed.

HTH, Kent

Kent Boogaart
Using the 2008 express edition, doesnt have the debug threads window. I know the TextBlock.Text is updated (it is firing the TargetUpdated event). I just need it to redraw itself. Tried all the invalidate methods but it seems like they are async or possibly low priority. Need a way to force redraw
Klerk
If you want to process all UI messages from the UI thread, check out my post here: http://kentb.blogspot.com/2008/04/dispatcher-frames.html. It will show you how to implement DoEvents() in WPF. Call that after you set the text and you should be fine.
Kent Boogaart
Ha! what timing I just posted an answer involving Dispatcher Invoke. Will check out your post. Thanks for the help. Really appreciated.
Klerk
Just read your post. Exactly what I needed!
Klerk
You're welcome - glad it helped.
Kent Boogaart
A: 

So after doing some reading on the WPF threading model ( http://msdn.microsoft.com/en-us/library/ms741870.aspx ) I finally got it to refresh by calling Dispatcher Invoke() with Dispatch priority set to Render. As Kent suggested above UI updates in the dispatcher queue were probably low priority. I ended up doing something like this.

XAML

<TextBox VerticalScrollBarVisibility="Auto"  
         Text="{Binding Path=Text, NotifyOnTargetUpdated=True}"
         x:Name="test" TextWrapping="Wrap" AcceptsReturn="True" 
         TargetUpdated="test_TargetUpdated"/>

C# target updated handler code

private void test_TargetUpdated(object sender, DataTransferEventArgs e)
{
    TextBox t = sender as TextBox;
    t.ScrollToEnd();
    t.Dispatcher.Invoke(new EmptyDelegate(() => { }), System.Windows.Threading.DispatcherPriority.Render);
}

Note: Earlier I was using a TextBlock but I changed to a TextBox as it comes with scrolling

I still feel uneasy about the whole flow though. Is there a better way to do this? Thanks to Matt and Kent for their comments. If I had points would mark their answers as helpful.

Klerk