views:

50

answers:

2

I'm trying to come up with an unobtrusive way to to display minor error messages to a user. So I've added a statusbar to my form,

    <StatusBar Margin="0,288,0,0" Name="statusBar" Height="23" VerticalAlignment="Bottom">
        <TextBlock Name="statusText">Ready.</TextBlock>
    </StatusBar>

And then when they click an "Add" button, it should do some stuff, or display an error message:

private void DownloadButton_Click(object sender, RoutedEventArgs e)
{
    addressBar.Focus();
    var url = addressBar.Text.Trim();
    if (string.IsNullOrEmpty(url))
    {
        statusText.Text = "Nothing to add.";
        return;
    }
    if (!url.Contains('.'))
    {
        statusText.Text = "Invalid URL format.";
        return;
    }
    if (!Regex.IsMatch(url, @"^\w://")) url = "http://" + url;
    addressBar.Text = "";

But the message just sits there for the life of the app... I think I should reset it after about 5 seconds... how can I set a timer to do that?

Bonus: How do I give it a nifty fade-out effect as I do so?


I've created a System.Timers.Timer,

    private Timer _resetStatusTimer = new Timer(5000);

    void _resetStatusTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
        statusText.Text = "Ready";
    }

But the Elapsed event runs on a different thread than the UI, which it doesn't like... how to I get around that?

+8  A: 

You can use a Storyboard to do the trick.

<Storyboard x:Key="Storyboard1">
        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="statusBarItem">
            <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
            <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/>
            <EasingDoubleKeyFrame KeyTime="0:0:3" Value="1"/>
            <EasingDoubleKeyFrame KeyTime="0:0:4" Value="0"/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>

When the message has to be displayed you just call programmatically the Begin method of the StoryBoard or insert a trigger as below.

<Window.Triggers>
    <EventTrigger RoutedEvent="TextBoxBase.TextChanged" SourceName="textBox">
        <BeginStoryboard Storyboard="{StaticResource Storyboard1}"/>
    </EventTrigger>
</Window.Triggers>

EDIT: Another option is to do like this:

<TextBlock Name="statusText" Text="{Binding Path=StatusBarText, NotifyOnTargetUpdated=True}">
        <TextBlock.Triggers>
            <EventTrigger RoutedEvent="Binding.TargetUpdated">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Opacity">
                            <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                            <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="1"/>
                            <EasingDoubleKeyFrame KeyTime="0:0:4" Value="1"/>
                            <EasingDoubleKeyFrame KeyTime="0:0:5" Value="0"/>
                        </DoubleAnimationUsingKeyFrames>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </TextBlock.Triggers>

Then to create a DependencyProperty called StatusBarText in this case that is implemented as follow:

public string StatusBarText
    {
        get { return (string)GetValue(StatusBarTextProperty); }
        set { SetValue(StatusBarTextProperty, value); }
    }

    // Using a DependencyProperty as the backing store for StatusBarText.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty StatusBarTextProperty =
        DependencyProperty.Register("StatusBarText", typeof(string), typeof(MyOwnerClass), new UIPropertyMetadata(""));

Hope this helps.

AS-CII
Where do I put that `<StoryBoard>`? Anywhere in the XAML?
Mark
It can be in any resource dictionary in the window that contains `statusBarItem`. There's probably no reason not to put it in `Window.Resources`.
Robert Rossney
Thanks Robert.. got it working now! But is there a way I can tie it to the statusText.Changed event directly in the XAML? Then I wouldn't have to call any `Begin()` methods, wouldn't have to find the resource in the code-behind, and I'd never have to worry about forgetting to start the storyboard...
Mark
You can add a trigger into the XAML but you cannot know when the text inside the textblock changes. You can use a TextBox instead and create a trigger to TextChanged. See at my post.
AS-CII
*Unless* you make the text a property, and bind the TextBlock.Text to this property, then you can be notified when *that* property is changed instead, no? Having trouble doing so though: http://stackoverflow.com/questions/3971786/stackoverflowexception-on-initializecomponent
Mark
What kind of problems you have? I've tried to implement the code in the link you've posted and it seems that there are no problems. Maybe for you doesn't work because you haven't created the StatusBarText property well. Try to create it as a DependencyProperty, I've updated again the post.
AS-CII
Is it better to use a dependency property than one that implements INotifyPropertyChanged?
Mark
http://stackoverflow.com/questions/291518/inotifypropertychanged-vs-dependencyproperty-in-viewmodel You can use both of them. Personally I prefer INotifyPropertyChanged but I've used DependencyProperty in my example to avoid the implementation of the interface.
AS-CII
+1  A: 

Your timer is a good approach, and you even identified your problem: you just need a way to access statusText.Text on the UI thread. (In WPF, threads other than the UI thread are prohibited from accessing UI elements). Here comes the WPF Dispatcher to the rescue:

http://msdn.microsoft.com/en-us/magazine/cc163328.aspx#S5

You can use the DispatcherTimer class to do exactly what you were attempting (here's their code):

// Create a Timer with a Normal Priority
_timer = new DispatcherTimer();

// Set the Interval to 2 seconds
_timer.Interval = TimeSpan.FromMilliseconds(2000); 

// Set the callback to just show the time ticking away
// NOTE: We are using a control so this has to run on 
// the UI thread
_timer.Tick += new EventHandler(delegate(object s, EventArgs a) 
{ 
    statusText.Text = string.Format(
        "Timer Ticked:  {0}ms", Environment.TickCount); 
});

// Start the timer
_timer.Start();
Aphex
Thanks! This works well, and it was pretty easy to do. AS-CII went for the bonus though!
Mark