Tuesday, January 20, 2009

Responsive WPF User Interfaces Part 1

Introduction

WPF is a fantastic platform for creating User Interfaces (UI) that allow all the sexy and powerful fashions of the day. Sexy features like reflections, animations, flow documents & 3D are all native to WPF. Powerful programming concepts such as declarative programming & test driven development are available and encouraged. While many new developers to WPF have a hard enough time learning XAML and the binding model that is new with WPF, I feel that a key concept of Usable Interfaces can get forgotten. This therefore is the first in a series of posts on producing Responsive UI in WPF.
This series of posts will try to keep a narrow focus on producing UI that are responsive. Where necessary I will digress to cover essential background concepts. There is an assumption, however, that the audience has basic WPF skills and has at least covered the WPF Hands on Lab 1.

Declarative Programming

I thought it would be nice to jump straight to some code that can give us some responsive User Interfaces straight out of the box. WPF has a the concept of Storyboards that allow values to transition from one value to another over a given time. The great thing about Storyboards is they cater for Binding, they don't have to be a linear transition, they are smart enough to cancel out when they are no longer valid and you can program them declaratively in XAML.
In the example below we show the usage of 2 Storyboards. These Storyboards declare that the opacity value on the target ("glow") shall change from its current value to 1 (Timeline1) or 0 (TimeLine2). Each Storyboard is set to run for 0.3 seconds. The first timeline is started by the trigger associated to the IsMouseOver event. The second trigger is associated with the exit of the condition that started the first time line. This means when we mouse-over a button, a glow will gradually illuminate. When we mouse-off the illumination will fade away. What is great about Storyboards is that in this example, if you mouse-over then mouse-off quicker than 0.3 seconds the first story board will hand over to the second story board without completing and the second story board will fade from a value somewhere between 0 and 1 until it becomes 0.

<Window x:Class="ArtemisWest.Demo.ResponsiveUI._1_Storyboards.StoryboardAnimation"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="StoryboardAnimation" Background="Black" Width="300" Height="200">
  <Window.Resources>
    <ControlTemplate x:Key="GlassButton" TargetType="{x:Type Button}">
      <ControlTemplate.Resources>
        <Storyboard x:Key="Timeline1">
          <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="glow"
             Storyboard.TargetProperty="(UIElement.Opacity)">
            <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="1"/>
          </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Key="Timeline2">
          <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="glow"
            Storyboard.TargetProperty="(UIElement.Opacity)">
            <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/>
          </DoubleAnimationUsingKeyFrames>
        </Storyboard>
      </ControlTemplate.Resources>
      <Border BorderBrush="#FFFFFFFF" BorderThickness="1" CornerRadius="4">
        <Border x:Name="border" Background="#7F000000" BorderBrush="#FF000000" CornerRadius="2" >
          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition Height="0.507*"/>
              <RowDefinition Height="0.493*"/>
            </Grid.RowDefinitions>
            <Border x:Name="glow" Opacity="0" 
              HorizontalAlignment="Stretch"  Width="Auto" 
              Grid.RowSpan="2">
              <Border.Background>
                <RadialGradientBrush>
                  <RadialGradientBrush.RelativeTransform>
                    <TransformGroup>
                      <ScaleTransform ScaleX="1.702" ScaleY="2.243"/>
                      <SkewTransform AngleX="0" AngleY="0"/>
                      <RotateTransform Angle="0"/>
                      <TranslateTransform X="-0.368" Y="-0.152"/>
                    </TransformGroup>
                  </RadialGradientBrush.RelativeTransform>
                  <GradientStop Color="#B28DBDFF" Offset="0"/>
                  <GradientStop Color="#008DBDFF" Offset="1"/>
                </RadialGradientBrush>
              </Border.Background>
            </Border>
            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" 
              Width="Auto" Grid.RowSpan="2"/>
            <Border x:Name="shine" HorizontalAlignment="Stretch" Width="Auto" Margin="0,0,0,0" CornerRadius="4,4,0,0">
              <Border.Background>
                <LinearGradientBrush EndPoint="0.494,0.889" StartPoint="0.494,0.028">
                  <GradientStop Color="#99FFFFFF" Offset="0"/>
                  <GradientStop Color="#33FFFFFF" Offset="1"/>
                </LinearGradientBrush>
              </Border.Background>
            </Border>
          </Grid>
        </Border>
      </Border>
      <ControlTemplate.Triggers>
        <Trigger Property="IsPressed" Value="True">
          <Setter Property="Opacity" TargetName="shine" Value="0.4"/>
          <Setter Property="Background" TargetName="border" Value="#CC000000"/>
          <Setter Property="Visibility" TargetName="glow" Value="Hidden"/>
        </Trigger>
        <Trigger Property="IsMouseOver" Value="True">
          <Trigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource Timeline1}"/>
          </Trigger.EnterActions>
          <Trigger.ExitActions>
            <BeginStoryboard x:Name="Timeline2_BeginStoryboard" Storyboard="{StaticResource Timeline2}"/>
          </Trigger.ExitActions>
        </Trigger>
      </ControlTemplate.Triggers>
    </ControlTemplate>

    <Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
      <Setter Property="Template" Value="{StaticResource GlassButton}"/>
      <Setter Property="MinWidth" Value="100"/>
      <Setter Property="MinHeight" Value="30"/>
      <Setter Property="Margin" Value="5"/>
      <Setter Property="Foreground" Value="GhostWhite"/>
    </Style>
  </Window.Resources>
  <WrapPanel Orientation="Horizontal">
    <Button>Button 1</Button>
    <Button>Button 2</Button>
    <Button>Button 3</Button>
  </WrapPanel>
</Window>

The reasons I thought it would be useful to illustrate this example are:

  • many animations can run concurrently
  • this example has no relevant C# code
  • code is declarative
  • the end result is a very "sexy" and responsive UI

So, hopefully that has been a nice gentle start to our journey over Responsive WPF User Interfaces.

Next: The WPF Threading model and the dispatcher in Responsive WPF User Interfaces in Part 2.

Back to series Table Of Contents

Working version of the code can be found here

No comments: