joemarini.com

Interactivity in XAML - Hold the Script, Please

Article Summary: Demonstrates how to add interactive behavior to an Avalon application without writing C# code by using PropertyTriggers, a generic mechanism for changing property values of a styled object in response to a changed property.

UPDATE: This example was updated Feb 15, 2005 for the public Avalon CTP Build

download Download the code for this tutorial

One of the most common features of any modern application, whether web-based or client-based, is the use of interactive user interface elements. These include buttons that highlight when focused or clicked, images that change when the mouse is rolled over them, text boxes that change color to indicate errors, etc.

In almost all cases, this kind of interactivity is achieved through the use of some kind of scripting mechanism. In web pages, for example, there is usually some JavaScript code that is assigned to execute when certain events are triggered on a given page element, like an <IMG> tag whose src attribute changes in response to mouse-over and mouse-out events. In Windows Forms and Macromedia Flash applications, the controls themselves listen for certain events and then repaint their appearance in reaction to those events, like buttons that appear to be pushed in when clicked on.

In Avalon, you can achieve the same kinds of interactive effects without writing a single line of script code by using a feature of XAML known as Property Triggers. These triggers are defined within styles to respond to changes on a given property of the object that the style is applied to. When they are triggered, they can then set the values of other properties on the object.

PropertyTriggers Define Interactive Behavior

The Property Trigger construct itself is very simple. It consists of a <PropertyTrigger> tag, which usually encloses one or more <Set> tags. The <PropertyTrigger> tag also has attributes that govern its actions. The Property attribute indicates the property of the styled object that the trigger is listening for changes on. The Value attribute contains the value that will cause the PropertyTrigger to fire when it is equal to the value of the property that is being watched. It usually looks something like this:

<PropertyTrigger Property="IsMouseOver" Value="true">
   <Set PropertyPath="Fill" Value="green"/>
</PropertyTrigger>

In this case, the PropertyTrigger is watching the IsMouseOver property of an object (I'll get to how you do this in a moment). When this property's value is equal to true (that is, the mouse is in fact over the object), the PropertyTrigger will fire and the Set condition will set the Fill property of the object to "green".

Associating PropertyTriggers with Objects

Of course, for any of this to happen, the PropertyTrigger has to know which object it is watching. This is accomplished using XAML Styles. PropertyTriggers are a subclass of the VisualTrigger class, and are defined inside a style's <Style.VisualTriggers> section. You can have as many PropertyTriggers defined as there are properties that you want to watch. Let's look at a complete example to see how this happens:

<Style>
  <Rectangle Fill="Gold" Stroke="Red" StrokeThickness="3" Width="150"
      Height="100" Margin="10" RadiusX="10" RadiusY="10"/>
  <Style.VisualTriggers>
    <PropertyTrigger Property="IsMouseOver" Value="true">
      <Set PropertyPath="Fill" Value="green"/>
    </PropertyTrigger>
  </Style.VisualTriggers>
</Style>

This style defines the appearance of all Rectangles (OK, not exactly a best practice, but fine for our example) to have a fill color of Gold and a Red stroke color. It also defines their height and width and gives them rounded corners. The PropertyTrigger inside the VisualTriggers section of the style is watching the IsMouseOver property of the Rectangle to see when its value matches the Value attribute of the trigger; in this case "true". When it does, the <Set> tag will set the "Fill" property of the Rectangle to "green".

Two key things to notice here:

  1. There is no logic code needed to achieve this effect. The interactivity happens in the XAML markup (all 3 lines of it).
  2. You do not need to have another <Set> tag to counteract the first <Set> tag. When the triggering condition no longer applies, the effect of the Set is removed automatically for you. In this case, when the IsMouseOver property is no longer true, the rectangle will revert back to its original appearance.

Now we just need to define some rectangles to see what happens. Here is the complete XAML code:

<Border Background="white" ID="root"
xmlns="http://schemas.microsoft.com/2003/xaml">
<DockPanel>
<DockPanel.Resources>
<Style>
  <Rectangle Fill="Gold" Stroke="Red" StrokeThickness="3" Width="150"
      Height="100" Margin="10" RadiusX="10" RadiusY="10"/>
  <Style.VisualTriggers>
    <PropertyTrigger Property="IsMouseOver" Value="true">
      <Set PropertyPath="Fill" Value="green"/>
    </PropertyTrigger>
  </Style.VisualTriggers>
</Style>
</DockPanel.Resources>

<Rectangle />
<Rectangle />
<Rectangle />

</DockPanel>
</Border>

And here's what it looks like when you run the XAML file in WindowsXP with the Avalon Community Technology Preview (CTP) installed:

Simple PropertyTrigger

Using Several Triggers and Sets

You're not limited to just one PropertyTrigger. You can have as many PropertyTriggers as you like, and each one can contain as many Set tags as you like. Let's take the original example and modify it slightly to demonstrate this.

Here's the same example, with a few changes that I've highlighted for clarity:

<Border Background="white" ID="root"
xmlns="http://schemas.microsoft.com/2003/xaml">
<DockPanel>
<DockPanel.Resources>
<Style>
  <Rectangle Fill="Gold" Stroke="Red" StrokeThickness="3" Width="150"
      Height="100" Margin="10" RadiusX="10" RadiusY="10" Opacity="0.25"/>
  <Style.VisualTriggers>
    <PropertyTrigger Property="IsMouseOver" Value="true">
      <Set PropertyPath="Fill" Value="green"/>
      <Set PropertyPath="Opacity" Value="1.0"/>
    </PropertyTrigger>
    <PropertyTrigger Property="IsEnabled" Value="false">
      <Set PropertyPath="Fill" Value="gray"/>
      <Set PropertyPath="Stroke" Value="gray"/>
    </PropertyTrigger>
  </Style.VisualTriggers>
</Style>
</DockPanel.Resources>

<Rectangle />
<Rectangle />
<Rectangle IsEnabled="false" />

</DockPanel>
</Border>

Let's go over each of the changes:

  1. I've added an Opacity setting to the style so that each rectangle is faded in appearance by default.
  2. The first PropertyTrigger now sets the Opacity to 1.0 when the mouse is over the rectangle in addition to setting the Fill property.
  3. I've added another PropertyTrigger that looks at the IsEnabled property, and grays out the rectangle when it is disabled.
  4. I've disabled one of the rectangles in the layout so that the second PropertyTrigger will fire.

And here's what the results look like:

Using Several PropertyTriggers

Testing Several Properties with MultiPropertyTrigger

While all of this is pretty neat by itself, I can just imagine many of you saying "well, that's pretty neat by itself, but what if I want something special to happen only when two different triggers match their Value conditions at the same time?" For example, you might want to apply a particular style to a control only when that control both has the input focus and has the mouse over it.

To accomplish this, you use the <MutliPropertyTrigger> tag, which will test its contained PropertyTriggers to see if they are all the same as their trigger condition. This example applies a style that tests multiple properties to a set of buttons. The style defines property triggers for the IsFocused, IsMouseOver, and IsEnabled properties of the button to set the appearance for each state. It also defines a MultiPropertyTrigger for both the IsFocused and the IsMouseOver properties, which is highlighted in the code below:

<Border Background="white" ID="root"
xmlns="http://schemas.microsoft.com/2003/xaml">
<DockPanel>

  <DockPanel.Resources>
    <Style>
      <Button Margin="5" FontSize="16"/>

      <Style.VisualTriggers>
        <PropertyTrigger Property="IsFocused" Value="True" >
          <Set PropertyPath="Foreground" Value="#6699CC" />
        </PropertyTrigger>
        <PropertyTrigger Property="IsMouseOver" Value="True" >
          <Set PropertyPath="Foreground" Value="#aaee11" />
        </PropertyTrigger>
        <PropertyTrigger Property="IsEnabled" Value="False" >
          <Set PropertyPath="Foreground" Value="#aeaeae" />
          <Set PropertyPath="Background" Value="#efefef" />
        </PropertyTrigger>

        <MultiPropertyTrigger>
          <MultiPropertyTrigger.Conditions>
            <Condition Property="IsFocused" Value="True" />
            <Condition Property="IsMouseOver" Value="True" />
          </MultiPropertyTrigger.Conditions>
          <Set PropertyPath="Foreground" Value="#ff0000" />
        </MultiPropertyTrigger>

      </Style.VisualTriggers>
    </Style>
  </DockPanel.Resources>

  <Button Height="30" Width="150">Button One</Button>
  <Button Height="30" Width="150">Button Two</Button>
  <Button Height="30" Width="150">Button Three</Button>
  <Button Height="30" Width="150" IsEnabled="False">Button Four</Button>

</DockPanel>
</Border>

The MultiPropertyTrigger works the same as the standard PropertyTrigger, but fires only when all of its sub-conditions (specified in the Condition tag) are all met at the same time. When you run this example, it looks like this:

Individual PropertyTriggers Firing

Notice that Button Two has the focus, and its foreground color is set appropriately. Button One has the mouse over it, so its foreground is set to green. When Button Two has both the mouse over it and the focus, however, the foreground color is set to red:

MultiPropertyTrigger in Action

But Wait — It Gets Better

One of the "a-ha!" moments for me came when I realized that PropertyTriggers can listen to any dependency property on an Avalon object, not just things like mouse events or focus messages. In this example, I have created a Button style that tests the Button's HasContent property, and makes the button invisible if it that property's value is "false" (in other words, it has no content). This example shows the behavior in action - note that when you delete the content from the third button, it disappears from the layout:

<DockPanel xmlns="http://schemas.microsoft.com/2003/xaml"
Background="#ffffffff"
>

  <DockPanel.Resources>
    <Style>
      <Button Margin="5"/>
      <Style.VisualTriggers>
        <PropertyTrigger Property="Button.HasContent" Value="False" >
          <Set PropertyPath="Visibility" Value="Collapsed" />
          <Set PropertyPath="Margin" Value="0" />
        </PropertyTrigger>
      </Style.VisualTriggers>
    </Style>
  </DockPanel.Resources>

  <Button Height="30" Width="100">Button One</Button>
  <Button Height="30" Width="100">Button Two</Button>
  <Button Height="30" Width="100">Button Three</Button>
  <Button Height="30" Width="100" IsEnabled="False">Button Four</Button>

</DockPanel>

When Button three has content (in this case, the string "Button Three"), it shows up in the layout just as you would expect. However, when you delete the button's content, it no longer appears, because the HasContent property now evaluates to "false", which is what our property trigger is looking for in order to make the button collapse:

Button Three is Visible When it Has Content ...

... but it Automatically Disappears When it Doesn't Have Content

So, just by creating a style, I now have a Button control that knows how to hide itself when it doesn't have any content to display. Such a button would be useful for something like a "Next" button in a Wizard dialog that would automatically disappear when there was no "Next" condition to navigate to, and I wouldn't need to write special code to get it to happen - I'd just delete the string from the button and it would go away.

Conclusion

Avalon PropertyTriggers enable a wide range of interactive behavior that was previously only achievable using customized scripting logic. By building this kind of mechanism into declarative XAML, creating user interfaces that are responsive to the user is now as simple as creating a style sheet. Furthermore, since these triggers are in fact defined inside styles, you can not only achieve separation of content from presentation, you can also separate your control's interactive appearance from its structure.

download Download the code for this tutorial (NOTE: you need to have the Community Technology Preview of Avalon installed to run this sample. You can get this version of Avalon from MSDN).

For information about how to obtain permission to re-publish this material, please contact us at info@joemarini.com.