Showing posts with label WPF. Show all posts
Showing posts with label WPF. Show all posts

Sunday, 16 November 2008

Using attached properties to compose new behaviour

I was inspired by a posting on stackoverflow.com - "How do I drag an image around a canvas in WPF" - where the first response in most peoples heads is to work with mouse up/down/move events, track offset positions and move the UI element around in response - pretty reasonable right?

Seeing as WPF's principles favours composition over inheritance and such like, what if instead we used the power of attached properties to attach new behaviour to a UIElement? Here's how to do it, first the code for the dependency property:

public class DraggableExtender : DependencyObject
{
    // This is the dependency property we're exposing - we'll 
    // access this as DraggableExtender.CanDrag="true"/"false"
    public static readonly DependencyProperty CanDragProperty =
        DependencyProperty.RegisterAttached("CanDrag",
        typeof(bool),
        typeof(DraggableExtender),
        new UIPropertyMetadata(false, OnChangeCanDragProperty));

    // The expected static setter
    public static void SetCanDrag(UIElement element, bool o)
    {
        element.SetValue(CanDragProperty, o);
    }

    // the expected static getter
    public static bool GetCanDrag(UIElement element)
    {
        return (bool) element.GetValue(CanDragProperty);
    }

    // This is triggered when the CanDrag property is set. We'll
    // simply check the element is a UI element and that it is
    // within a canvas. If it is, we'll hook into the mouse events
    private static void OnChangeCanDragProperty(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
    {
        UIElement element = d as UIElement;
        if (element == null) return;

        if (e.NewValue != e.OldValue)
        {
            if ((bool)e.NewValue)
            {
                element.PreviewMouseDown += element_PreviewMouseDown;
                element.PreviewMouseUp += element_PreviewMouseUp;
                element.PreviewMouseMove += element_PreviewMouseMove;
            }
            else
            {
                element.PreviewMouseDown -= element_PreviewMouseDown;
                element.PreviewMouseUp -= element_PreviewMouseUp;
                element.PreviewMouseMove -= element_PreviewMouseMove;
            }
        }
    }

    // Determine if we're presently dragging
    private static bool _isDragging = false;
    // The offset from the top, left of the item being dragged 
    // versus the original mouse down
    private static Point _offset;

    // This is triggered when the mouse button is pressed 
    // on the element being hooked
    static void element_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        // Ensure it's a framework element as we'll need to 
        // get access to the visual tree
        FrameworkElement element = sender as FrameworkElement;
        if (element == null) return;

        // start dragging and get the offset of the mouse 
        // relative to the element
        _isDragging = true;
        _offset = e.GetPosition(element);
    }

    // This is triggered when the mouse is moved over the element
    private static void element_PreviewMouseMove(object sender, 
        MouseEventArgs e)
    {
        // If we're not dragging, don't bother
        if (!_isDragging) return;

        FrameworkElement element = sender as FrameworkElement;
        if (element == null) return;

        Canvas canvas = element.Parent as Canvas;
        if( canvas == null ) return;
        
        // Get the position of the mouse relative to the canvas
        Point mousePoint = e.GetPosition(canvas);

        // Offset the mouse position by the original offset position
        mousePoint.Offset(-_offset.X, -_offset.Y);

        // Move the element on the canvas
        element.SetValue(Canvas.LeftProperty, mousePoint.X);
        element.SetValue(Canvas.TopProperty, mousePoint.Y);
    }

    // this is triggered when the mouse is released
    private static void element_PreviewMouseUp(object sender, 
        MouseButtonEventArgs e)
    {
        _isDragging = false;
    }
}

As you can see, we hook into the events exposed from the target element whenever we detect the property being changed. This allows us to inject any logic we like!

To use the behaviour, we include the namespace in XAML:

<Window x:Class="WPFFunWithDragging.Window1"
        xmlns:local="clr-namespace:WPFFunWithDragging"

And then just attach the behaviour to the elements we want to be able to drag like so;

<Canvas>
       <Image Source="Garden.jpg" 
              Width="50" 
              Canvas.Left="10" Canvas.Top="10" 
              local:DraggableExtender.CanDrag="true"/>
   </Canvas>

Cool huh? Sample code attached....

Friday, 15 August 2008

Exposing new properties for control templates using attached properties

There are many instances where you want to write a groovy looking control template to apply to your WPF controls only to find that the control you want to template falls short in the properties it provides - eg: Try specifying the over state gradient brush for a button using only it's exposed properties. In these situations you need some custom properties on the control to be able to tell your template what to do.

In this situation there are several options open to you - you could hijack a property - bad idea when it comes to maintainability. Alternatively you could inherit the control and introduce the new properties - again, not the most ideal situation under WPF's composition model.

By far the most flexible and easiest way to achieve this behaviour is with attached dependency properties.

In the following example, we want to extend all of our buttons to provide not just a single line of text as you'd find on normal buttons, but we also want to have customisable sub-title line. To achieve this we start with the custom dependency property;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ButtonPropertyExtender : DependencyObject
{
  public static readonly DependencyProperty SubTextProperty =
    DependencyProperty.RegisterAttached("SubText",
      typeof(String), typeof(ButtonPropertyExtender),
      new FrameworkPropertyMetadata(null,
        FrameworkPropertyMetadataOptions.AffectsMeasure |
        FrameworkPropertyMetadataOptions.AffectsArrange));

  public static void SetSubText(UIElement element, object o)
  {
    element.SetValue(SubTextProperty, o);
  }

  public static string GetSubText(UIElement element)
  {
    return (string)element.GetValue(SubTextProperty);
  }
}


Quite simply this registers a custom attached property with WPF and allows it to be attached to any element. The next thing we do is use the new property on our button thus;

1
<Button xc:ButtonPropertyExtender.SubText="Subtitle text">
Click Me!
</
Button>

And finally, to use our new property in the control template, we bind to it - notice however how we cannot use the {TemplateBinding} shortcut - instead we must use the full binding expression to get to the new custom property.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<ControlTemplate TargetType="Button">
  <Border Background="{TemplateBinding Background}"
      BorderBrush="Black"
      BorderThickness="2"
      CornerRadius="10" >

    <StackPanel HorizontalAlignment="Center">
      <ContentPresenter Content="{TemplateBinding Content}"/>
      <TextBlock
        Text="{Binding
RelativeSource={RelativeSource TemplatedParent},
         Path=(xc:ButtonPropertyExtender.SubText)}"
/>
    </StackPanel>
  </Border>
</ControlTemplate>

Sunday, 6 January 2008

Playing with WPF

I wanted to explore some of the features of WPF today, so decided to knock up a simple example that would provide a contemporary (AKA: crap looking as it's designed by a developer!) user interface to a fictitious application

I wanted the layout to be skinnable and be able to change various aspects and colours of the application by adding extra XAML. Here's a screenshot of how the basic UI functions;

image image

The left image shows how the UI should look by default (albeit with a distinct lack of controls), whilst the image on the right shows the "reports" panel overlaid on top of the default UI. The overlays fade in and out as they are brought up and closed.

Pretty basic right? Well yeah, but it was a good exercise in learning some of the cool features of WPF. Lets start with the basic layout for the application. The following XAML is for the main window and it's panels.

   1: <DockPanel>
   2:     <!-- Header Area - branding -->
   3:     <Border Height="50" DockPanel.Dock="Top" 
   4:             BorderBrush="{DynamicResource BrandingLow}" 
   5:             BorderThickness="0,0,0,1" Padding="10,0,10,0" >
   6:             <TextBlock Opacity="1" FontFamily="Segoe" FontSize="24" 
   7:                        FontStretch="Normal" FontWeight="Light" TextWrapping="Wrap" 
   8:                        Foreground="{DynamicResource BrandingHi}" 
   9:                        VerticalAlignment="Bottom" Margin="0,0,0,5">
  10:                 <Run Foreground="{DynamicResource BrandingLow}">DC</Run><Run FontWeight="Normal">.Finances</Run>
  11:             </TextBlock>
  12:     </Border>
  13:     
  14:     <!-- Menu Area -->
  15:     <Menu DockPanel.Dock="Top" Margin="5,0,0,0" Style="{DynamicResource MenuStyle}">
  16:         <MenuItem Header="_Reports" x:Name="ReportsMenu" Style="{DynamicResource MenuItemStyle}" Click="ReportsMenu_Click" />
  17:         <MenuItem Header="_Admin" x:Name="AdminMenu" Style="{DynamicResource MenuItemStyle}" Click="AdminMenu_Click"/>
  18:     </Menu>
  19:  
  20:     <!-- Content Area -->
  21:     <Grid>
  22:         <!-- Main content - the account register -->
  23:         <local:RegisterPanel x:Name="pnlContent"/>
  24:  
  25:         <!-- Reports Pane - hides and shows as necessary -->
  26:         <local:ReportsPanel x:Name="pnlReports" Visibility="Hidden" PanelClosed="PanelClosed"/>
  27:  
  28:         <!-- Administration Pane - hides and shows as necessary -->
  29:         <local:AdminPanel x:Name="pnlAdministration" Visibility="Hidden" PanelClosed="PanelClosed"/>
  30:     </Grid>
  31: </DockPanel>

As you can see, we basically split the form into 3 sections. The branding area at the top, the menu area and finally the content area which hosts 3 separate user controls, 2 of which are set to be hidden (the admin and reports panels).

Skinning using resources and styles

Notice that in order to allow the application to be "skinned" and have it's colours changed etc, we have made use of {DynamicResource} in a number of places for brushes and for styles. These are then encapsulated into a separate XAML file. For example, the XAML for the BrandingLow resource is as follows;

<SolidColorBrush x:Key="BrandingLow" Color="#FFCFD3DA"/>

This defines the branding low light brush, and can then be referenced as {DynamicResource BrandlingLow} anywhere in XAML that requires a brush. But to get the resource linked to the application, we must add it to the applications resource dictionary. This is done in app.xaml as follows;

   1: <Application x:Class="DC.Finances.App"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:     StartupUri="MainWindow.xaml">
   5:  
   6:     <Application.Resources>
   7:         
   8:         <!-- Pull in the merged resources - use default skin -->
   9:         <ResourceDictionary>
  10:             <ResourceDictionary.MergedDictionaries>
  11:                 <ResourceDictionary Source="Skins\Default\Skin.xaml"/>
  12:             </ResourceDictionary.MergedDictionaries>
  13:         </ResourceDictionary>
  14:  
  15:     </Application.Resources>
  16: </Application>

This embeds the skin.xaml file as a resource. Within this XAML we can either go ahead and define the elements we need or we can split it down into separate files. In this case I chose to split it into additional files to separate the brushes and styles etc.

Styles are interesting in WPF. They allow you to define a common look and feel for different element types and also add behaviour. Take for example, the style used for a menu item as seen in the example above;

   1: <MenuItem Header="_Reports" x:Name="ReportsMenu" 
   2:     Style="{DynamicResource MenuItemStyle}" Click="ReportsMenu_Click" />
   3:  
   4: <MenuItem Header="_Admin" x:Name="AdminMenu" 
   5:     Style="{DynamicResource MenuItemStyle}" Click="AdminMenu_Click"/>

The MenuItemStyle is then defined as;

   1: <Style x:Key="MenuItemStyle" TargetType="{x:Type MenuItem}">
   2:     <Setter Property="Background" Value="Transparent"/>
   3:     <Setter Property="Foreground" Value="{DynamicResource FontColor}"/>
   4:     <Setter Property="FontSize" Value="10"/>
   5:     <Setter Property="Padding" Value="8,5,20,5"/>
   6:     <Style.Triggers>
   7:         <Trigger Property="IsHighlighted" Value="true">
   8:             <Setter Property="Background" 
   9:                 Value="{DynamicResource MenuActiveBackgroundBrush}"/>
  10:             <Setter Property="Foreground" 
  11:                 Value="{DynamicResource MenuActiveFontColor}"/>
  12:             <Setter Property="BorderBrush" Value="#FF000000"/>
  13:         </Trigger>
  14:     </Style.Triggers>
  15: </Style>

This defines a style named MenuItemStyle that applies to elements of type MenuItem. It sets the background colour to transparent, the foreground colour to the standard font colour from the resources, the font to use 10 pixels and to use a specific padding. In addition it then defines some rudimentary behaviour in the form of triggers.

When the IsHighlighted property of the host item (a MenuItem element) is set to true, the three setters that are specified within the trigger are invoked. In this case it changes the background colour, foreground colour and outline brush to be of specific colours. This provides us with a nice rollover effect on the menus that use this style;

image image

Applying animations

As mentioned above, when we click reports or admin on the menu, we want to bring up the appropriate panel, and play a little animation to fade it in, then fade it out when we close it. This is incredibly easy in WPF.

Here's the Window.Resources section from the main form, which defines two storyboard based animations for fading up and down elements;

   1: <Window.Resources>
   2:         <!-- Animation for showing panels -->
   3:         <Storyboard x:Key="ShowPanel">
   4:             <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(UIElement.Opacity)">
   5:                 <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
   6:                 <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="1"/>
   7:             </DoubleAnimationUsingKeyFrames>
   8:             <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(UIElement.Visibility)">
   9:                 <DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Visible}"/>
  10:                 <DiscreteObjectKeyFrame KeyTime="00:00:00.3000000" Value="{x:Static Visibility.Visible}"/>
  11:             </ObjectAnimationUsingKeyFrames>
  12:         </Storyboard>
  13:         
  14:         <!-- Animation for hiding panels -->
  15:         <Storyboard x:Key="HidePanel">
  16:             <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(UIElement.Opacity)">
  17:                 <SplineDoubleKeyFrame KeyTime="00:00:00" Value="1"/>
  18:                 <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/>
  19:             </DoubleAnimationUsingKeyFrames>
  20:             <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(UIElement.Visibility)">
  21:                 <DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Visible}"/>
  22:                 <DiscreteObjectKeyFrame KeyTime="00:00:00.3000000" Value="{x:Static Visibility.Hidden}"/>
  23:             </ObjectAnimationUsingKeyFrames>
  24:         </Storyboard>
  25:     </Window.Resources>

Lines 3 - 12 show the animation declaration for fading up an object. The storyboard follows two animation paths, one on the opacity of the target object and one on the visibility property of the target object. For fading up, we see a spline keyframe animation moving the value of opacity from 0 to 1 over the course of 1/3 of a second. This then fades smoothly between the values over the time period specified. Also, we use a discrete keyframe animation to set the visibility property to true when the animation starts.

Lines 15-23 do the exact same thing but in reverse, and the discrete keyframe animation on the visibility property sets the object to hidden at the end.

Simple huh? So the only thing left to do is to trigger the animation when the menu item is clicked and bring the appropriate panel up.

The possible options here are;

a) User clicks the reports or admin menu

a1) No current panel - just fade up the selected panel

a2) Current panel visible - fade it down, then fade up the selected panel

b) User clicks the close button on a panel

b1) Fade down the active panel

To implement this logic, I chose to use the code behind to track an active FrameworkElement, then when I click on a menu option, I invoke the fade animation on the active element, fade up the new element and make it the active one.

Triggering animations is as straight forward as;

((Storyboard)this.Resources["HidePanel"]).Begin(_activeChildComponent);

As such, the entire logic for showing/hiding panels etc is shown below;

   1: public partial class Window1 : Window
   2: {
   3:     // This will be used to track any active open panels
   4:     FrameworkElement _activeChildComponent = null;
   5:     
   6:      /// <summary>
   7:     /// User clicked reports menu, hide the current panel if there is one and show the reports pane
   8:     /// </summary>
   9:     /// <param name="sender"></param>
  10:     /// <param name="e"></param>
  11:     private void ReportsMenu_Click(object sender, RoutedEventArgs e)
  12:     {
  13:         HideActivePanel();
  14:         ShowPanel(pnlReports);
  15:     }
  16:  
  17:     /// <summary>
  18:     /// User clicked admin menu, hide the current panel if there is one and show the reports pane
  19:     /// </summary>
  20:     /// <param name="sender"></param>
  21:     /// <param name="e"></param>
  22:     private void AdminMenu_Click(object sender, RoutedEventArgs e)
  23:     {
  24:         HideActivePanel();
  25:         ShowPanel(pnlAdministration);
  26:     }
  27:     
  28:     /// <summary>
  29:     /// User clicked to close the panel
  30:     /// </summary>
  31:     /// <param name="sender"></param>
  32:     /// <param name="e"></param>
  33:     private void PanelClosed(object sender, EventArgs e)
  34:     {
  35:         HideActivePanel();
  36:     }
  37:  
  38:     /// <summary>
  39:     /// If an active panel is open, this will fade it down
  40:     /// </summary>
  41:     private void HideActivePanel()
  42:     {
  43:         if (_activeChildComponent == null) return;
  44:         ((Storyboard)this.Resources["HidePanel"]).Begin(_activeChildComponent);
  45:         // TODO: Need to find a mechanism to wait until the fade has completed
  46:         _activeChildComponent = null;
  47:     }
  48:  
  49:     /// <summary>
  50:     /// Sets the panel specified to be the active panel
  51:     /// </summary>
  52:     /// <param name="target"></param>
  53:     private void ShowPanel(FrameworkElement target)
  54:     {
  55:         ((Storyboard)this.Resources["ShowPanel"]).Begin(target);
  56:         _activeChildComponent = target;
  57:     }
  58: }

Have you spotted the deliberate mistake? I've not yet worked out how to wait for the animation to complete, so when you go from panel to panel, the current panel doesn't get chance to fade down properly before the new panel fades up... I'll save that for another day though.