Andrej Tozon's blog

In the Attic

NAVIGATION - SEARCH

WPF: When's a Button not a Button

This is a button:

<Button Name="button">Button</Button>

This is a button too:

<ToolBar>
    <Button Name="toolbarButton">Toolbar button</Button>
</ToolBar>

Or is it?

Let's declare a style, targeting a Button type:

<Style TargetType="{x:Type Button}">
    <Setter Property="Background" Value="Green" />
    <Setter Property="Foreground" Value="White" />
</Style>

The result might not be exactly what you expected:

image

Standalone button gets painted according to the declared style, while the button in the ToolBar doesn't. Setting the style explicitly helps:

<ToolBar>
    <Button Name="toolbarButton" 
Style
="{StaticResource ButtonStyle}">Toolbar button</Button
> </ToolBar>

... but that's probably not something we want as we need to apply this style to all the buttons in the given scope.

So what's going on? Well, a ToolBar is a special beast of a control, liking things to be exactly its way, ignoring most of non-direct calls to changing its looks and behavior. It defines and uses some specific resource keys to style its children, and we need to plug into ButtonStyleKey to push our Button style through:

<Style TargetType="{x:Type Button}" x:Key="{x:Static ToolBar.ButtonStyleKey}">
<Setter Property="Background" Value
="Green" />
<Setter Property="Foreground" Value
="White" />
</Style>

Finally, to apply the same style to both common and ToolBar Buttons, we're going to use some of that style inheritance magic:

<Style TargetType="{x:Type Button}" >
    <Setter Property="Background" Value="Green" />
    <Setter Property="Foreground" Value="White" />
</Style>
<Style x:Key="{x:Static ToolBar.ButtonStyleKey}" 
TargetType="{x:Type Button}"
BasedOn="{StaticResource {x:Type Button}}" />
image 
<ToolBar>
    <Button Name="toolbarButton">Toolbar button</Button>
</ToolBar>
<Button Name="button">Button</Button>

Three ways to make your WPF images pop out on MouseOver

There are a couple of ways in WPF to make an image pop out when moving mouse over it. Of course we want the image to pop out smoothly, so in this quick rundown we're going to use the following animation storyboards:

<!-- This storyboard will make the image grow to double its size in 0.2 seconds -->
<Storyboard x:Key
="expandStoryboard">
    <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleX"
        To="2" Duration
="0:0:0.2" />
    <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY"
        To="2" Duration
="0:0:0.2" />
</Storyboard
>
<!-- This storyboard will make the image revert to its original size -->
<Storyboard x:Key
="shrinkStoryboard">
    <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleX"
        To="1" Duration
="0:0:0.2" />
    <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY"
        To="1" Duration
="0:0:0.2" />
</Storyboard
>

One thing worth noticing here is the lack of "From" attribute in animation elements. That's because we want the animation pick up from whatever animation state the image is in to make it smoother. Specifying the beginning value of animation (with e.g. "From='1'" in expandStoryboard) would mean the image growing would always start at its original, not current size.

Triggering storyboards with event handlers

If you like imperative coding, you'll probably rush to implement image's MouseEnter and MouseLeave event handlers to trigger the animations. Image declaration would in Xaml look somewhat like this:

<Image Name="image1" Source="Image1.png" 
UIElement.MouseEnter="image_MouseEnter"
UIElement.MouseLeave="image_MouseLeave">
<Image.RenderTransform>
<!-- Initial values we're going to animate -->
<ScaleTransform ScaleX="1" ScaleY="1"/> </Image.RenderTransform> </Image>

... complemented with a couple of event handlers:

private void image_MouseEnter(object sender, MouseEventArgs e)
{
    Storyboard story = (Storyboard)FindResource("expandStoryboard");
    Image image = sender as Image;
    image.BeginStoryboard(story);
}

private void image_MouseLeave(object sender, MouseEventArgs e)
{
    Storyboard story = (Storyboard)FindResource("shrinkStoryboard");
    Image image = sender as Image;
    image.BeginStoryboard(story);
}

[Both storyboards are declared as resources, hence the use of FindResource method for retrieving them.]

Using Event triggers instead of event handlers

Although there's nothing wrong with the previous method, why not do it all in Xaml? Enter EventTriggers:

<Image Name="image2" Source="Image2.png">
    <Image.Triggers>
        <EventTrigger RoutedEvent="Image.MouseEnter">
            <BeginStoryboard Storyboard="{StaticResource expandStoryboard}" />
        </EventTrigger>
        <EventTrigger RoutedEvent="Image.MouseLeave">
            <BeginStoryboard Storyboard="{StaticResource shrinkStoryboard}" />
        </EventTrigger>
    </Image.Triggers>
    <Image.RenderTransform>
       <ScaleTransform ScaleX="1" ScaleY="1"/>
    </Image.RenderTransform>
</Image>

Looks better, doesn't it?

Finishing with Property triggers

The third method is very similar to the second, except it uses Property triggers instead of Event triggers. Currently, Property triggers have to be declared within a style:

<Style TargetType ="{x:Type Image}">
    <Setter Property="RenderTransform">
       
<Setter.Value>
           
<ScaleTransform ScaleX="1" ScaleY="1"/>
       
</Setter.Value>
   
</Setter>
    <Style.Triggers>
        <Trigger Property="IsMouseOver" Value
="True">
            <Trigger.EnterActions
>
                <BeginStoryboard Storyboard="{StaticResource expandStoryboard
}" />
            </Trigger.EnterActions
>
            <Trigger.ExitActions
>
                <BeginStoryboard Storyboard="{StaticResource shrinkStoryboard
}" />
            </Trigger.ExitActions
>
        </Trigger
>
    </Style.Triggers
>
</Style
>

Additional benefit of using styles is the ability to reuse/apply them to all elements of specified type within the scope the style is declared in. For example, declaring the above style (together with both storyboards) on the application level would generally make all images within application behave the same way. Unless, of course, some images specify their own styles, overriding this behavior.
And the image?

<Image Name="image3" Source="Image3.png" />

[Note that specifying RenderTransform on this Image is no longer needed, because it's already set with the Style.]

WPF vs. Windows Forms (data) binding

When talking to people about (data) binding in Windows Presentation Foundation, I find that a lot of them are amazed with how easy it is to bind some control's property to another, and the first one being able to react to other's changes. I'm surprised how many people still use data binding in windows forms for just that - binding to data sources, mostly datasets. But, the data can be much more that just some information, contain in data carriers like datasets and other objects.

Let's say you have to write some code to control a control's size with a slider (trackbar). In WPF and XAML, this is trivial:

<Canvas>
    <Slider Height="21" Name="slider1" 
        Width="100" Canvas.Left="178" Canvas.Top="10" 
        Minimum="50" Maximum="200" />
    <Rectangle Canvas.Left="50" Canvas.Top="50" 
        Name="rectangle1" Fill="Red" 
        Height="{Binding ElementName=slider1, Path=Value}" 
        Width="{Binding ElementName=slider1, Path=Value}" />
</Canvas>

Rectangle's Width and Height properties are bound to the value of the slider so when the slider's position changes, the rectangle resizes accordingly. How would you do something like that in Windows Forms?

Event handlers?

private void trackBar1_Scroll(object sender, EventArgs e)
{
    panel1.Width = trackBar1.Value;
    panel1.Height = trackBar1.Value;
}

Sure, why not... [panel1 with Red background is used instead of a Rectangle, and TrackBar is a slider]

But, there's another way, using - you guessed it - binding. Instead of responding to every TrackBar's value change for yourself, you can let the framework do the job:

public Form1()
{
    InitializeComponent();
    panel1.DataBindings.Add("Width", trackBar1, "Value");
    panel1.DataBindings.Add("Height", trackBar1, "Value");
}

Another example: main Form's title should mirror whatever user enters in the TextBox. XAML code:

<Window x:Class="BindingSample.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="{Binding ElementName=textBox1, Path=Text}" Height="300" 
Width="300" Loaded
="Window_Loaded"> ...
</Window>

And instead of doing this with Windows Forms:

private void textBox1_TextChanged(object sender, EventArgs e)
{
    Text = textBox1.Text;
}

You could simply do this:

public Form1()
{
    InitializeComponent();
    DataBindings.Add("Text", textBox1, "Text");
}

Now, how about binding to a custom object? For example, we want the size of our rectangle change randomly through time, and come up with a class like:

public class Randomizer
{
    private int number;
    private Timer timer;

    public Randomizer()
    {
        PickNumber();
        timer = new Timer();
        timer.Interval = 500;
        timer.Tick += new EventHandler(timer_Tick);
        timer.Enabled = true;
    }

    public int Value
    {
        get { return number; }
    }

    private void timer_Tick(object sender, EventArgs e)
    {
        PickNumber();
    }

    private void PickNumber()
    {
        number = new Random().Next(50, 201);
// Fixed for simplicity
    }
}

The Value property of this class will change twice per second, each time holding a number between 50 and 200.
Let's bind panel's Width and Height properties to the Value this class produces...

private Randomizer r = new Randomizer();
public Form1()
{
    InitializeComponent();
    panel1.DataBindings.Add("Width", r, "Value");
    panel1.DataBindings.Add("Height", r, "Value");
}

... and run the application... but... nothing happens, the panel doesn't change its size. That's because it doesn't know that Value property ever changes. To let panel's bindings be aware of Value changes, our Randomizer class has to send some notifications to any clients, bound to Value property. We'll do this by implementing INotifyPropertyChanged interface. Here's the whole class again:

public class Randomizer: INotifyPropertyChanged
{
    private int number;
    private Timer timer;

    public Randomizer()
    {
        PickNumber();
        timer = new Timer();
        timer.Interval = 500;
        timer.Tick += new EventHandler(timer_Tick);
        timer.Enabled = true;
    }

    public int Value 
    {
        get { return number; }
    }

    private void timer_Tick(object sender, EventArgs e)
    {
        PickNumber();
    }

    private void PickNumber()
    {
        number = new Random().Next(50, 201); // Fixed for simplicity
        OnPropertyChanged("Value");
    }

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

With INotifyPropertyChanged interface implemented, our rectangle finally starts animating. To conclude, here's how we would use the Randomizer class in WPF:

1. Declare the class as a resource:

<Window.Resources>
    <local:Randomizer x:Key="r" />
</Window.Resources>

2. Bind the rectangle to the Randomizer class, declared as a resource:

<Rectangle Canvas.Left="50" Canvas.Top="50" 
    Name="rectangle1" Fill="Red" 
    Height="{Binding Source={StaticResource r}, Path=Value}" 
    Width="{Binding Source={StaticResource r}, Path=Value}" />

One thing, worth observing here is that rectangle animates even at design time in Visual Studio's IDE, not only at runtime. Enjoy.

Slides from my WPF talks

This is the PowerPoint presentation [in Slovenian language] from my WPF talks I did yesterday and two weeks ago. This was my first time delivering full-day courses and not everything went as originally planned. The first talk suffered from some technical difficulties and classic demos trouble, and for the yesterday, some evil virus got me so there were some fever/sore throat issues, leaving me powerless at the end.

Anyway, I'm not posting any sample code this time, instead I'll be writing some more blog posts on the subject, OK?