Easy form layout in WPF Part 1 – Introducing FormPanel

The WPF layout system is extremely powerful, there’s almost nothing you can’t do with Grid and maybe a few DockPanel objects – but that power comes at a price and that price is a lot of typing.

I find it hard to believe that anyone who has written any form in WPF isn’t sick of  <RowDefinition Height="Auto"/> and Grid.Column=”1” Grid.Row=”1” – and of course things get worse when you have to add a new row at the beginning of the form and you have to manually update all those Grid.Row definitions.

So, in this series I will try to solve the problem.

Now it’s important to remember we are trying to simplify our code here, we will not write a powerful do-everything control, we will write something that will cover the simple cases (that are around 80% of cases) and the rest we will code with Grid.

This series has 3 parts:

  1. Easy form layout in WPF Part 1 – Introducing FormPanel (You are here).
  2. Easy form layout in WPF Part 2 – How to deal with more complicated scenarios (scheduled for August 3rd).
  3. Easy form layout in WPF Part 3 – Adding Groups (scheduled for August 10th).

You can find the complete source code with a sample project at the end of the last post.

Let’s start from the desired end result and work back to the code:

I’m writing a bug tracking product and I want a form with the usual bug tracking fields:

I want all the labels to be the same size and all the text boxes and combo boxes to be the same size, I want the labels centered vertically and the text/combo boxes to fill all the available width, I want constant spacing between labels and controls and between row and columns.

And most of all I want everything without writing any code or XAML on every windows.

This is what I want the XAML for the window above to look like (a real app would obviously need some data binding code to fill the controls):

<Window x:Class="FormPanelApp.Window3"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:l="clr-namespace:FormPanelApp"
    Title="Window3" Height="300" Width="500">
    <l:FormPanel Margin="10">
        <TextBlock Text="Title:"/>
        <TextBox/>
        <TextBlock Text="Area:"/>
        <ComboBox/>
        <TextBlock Text="Category:"/>
        <ComboBox/>
        <TextBlock Text="Assigned To:"/>
        <ComboBox/>
        <TextBlock Text="Status:"/>
        <ComboBox/>
        <TextBlock Text="Estimate:"/>
        <TextBox/>
        <TextBlock Text="Tags:"/>
        <TextBox/>
        <TextBlock Text="Version:"/>
        <TextBox/>
    </l:FormPanel>
</Window>

So what am I going to do? easy, write a custom panel.

We will Create a FormPanel class that inherits from the WPF Panel.

using System;
using System.Windows.Controls;
using System.Windows;

namespace FormPanelApp
{
    public class FormPanel : Panel
    {

Now we will add some dependency properties for things we would like to be configurable in the panel.

The first and most important is the number of columns (each column is a label/control pair):

        public static readonly DependencyProperty ColumnsProperty =
            DependencyProperty.Register("Columns", typeof(int), typeof(FormPanel),
            new FrameworkPropertyMetadata(2, FrameworkPropertyMetadataOptions.AffectsMeasure));
        public int Columns
        {
            get { return (int)GetValue(ColumnsProperty); }
            set { SetValue(ColumnsProperty, value); }
        }

The spacing between rows and columns:

        public static readonly DependencyProperty ColumnSpacingProperty =
            DependencyProperty.Register("ColumnSpacing", typeof(double), typeof(FormPanel),
            new FrameworkPropertyMetadata(15.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
        public double ColumnSpacing
        {
            get { return (double)GetValue(ColumnSpacingProperty); }
            set { SetValue(ColumnSpacingProperty, value); }
        }

        public static readonly DependencyProperty RowSpacingProperty =
            DependencyProperty.Register("RowSpacing", typeof(double), typeof(FormPanel),
            new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
        public double RowSpacing
        {
            get { return (double)GetValue(RowSpacingProperty); }
            set { SetValue(RowSpacingProperty, value); }
        }

And the space between labels and controls:

        public static readonly DependencyProperty LabelControlSpacingProperty =
            DependencyProperty.Register("LabelControlSpacing", typeof(double), typeof(FormPanel),
            new FrameworkPropertyMetadata(5.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
        public double LabelControlSpacing
        {
            get { return (double)GetValue(LabelControlSpacingProperty); }
            set { SetValue(LabelControlSpacingProperty, value); }
        }

We will also create dependency properties for the size of labels and controls as calculated by the panel, in the next post we will see how useful this will be:

        public static readonly DependencyProperty LabelSizeProperty =
            DependencyProperty.Register("LabelSize", typeof(Size), typeof(FormPanel));
        public Size LabelSize
        {
            get { return (Size)GetValue(LabelSizeProperty); }
            set { SetValue(LabelSizeProperty, value); }
        }

        public static readonly DependencyProperty ControlSizeProperty =
            DependencyProperty.Register("ControlSize",typeof(Size),typeof(FormPanel));
        public Size ControlSize
        {
            get { return (Size)GetValue(ControlSizeProperty); }
            set { SetValue(ControlSizeProperty, value); }
        }

Now I'm going to add something called a coordinator, in the third post we will use it to quickly make the FormPanel even more useful - you can ignore it for now:

        public IFormPanelCoordinator Coordinator { get; set; }

All the actual work in a panel is done in two methods: MeasureOverride and ArrangeOverride.

The MeasureOverride method calculates the required size for the panel, in our case we just scan all the panel's children and look for the maximum label width and height and maximum control width and height.

We save the results in the LabelSize and ControlSize properties we defined earlier and calculate the required size based on those sizes, the number of columns and the spacing properties we defined.

        protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
        {
            double labelMaxWidth = 0;
            double labelMaxHeight = 0;
            double controlMaxWidth = 0;
            double controlMaxHeight = 0;
            for (int i = 0; i < Children.Count-1; i += 2)
            {
                Children[i].Measure(availableSize);
                Children[i + 1].Measure(availableSize);
                labelMaxWidth = Math.Max(labelMaxWidth, Children[i].DesiredSize.Width);
                labelMaxHeight = Math.Max(labelMaxHeight, Children[i].DesiredSize.Height);
                controlMaxWidth = Math.Max(controlMaxWidth, Children[i+1].DesiredSize.Width);
                controlMaxHeight = Math.Max(controlMaxHeight, Children[i+1].DesiredSize.Height);
            }

            var oldLabelSize = LabelSize;
            var oldControlSize = ControlSize;
            var newLabelSize = new Size(labelMaxWidth, labelMaxHeight);
            var newControlSize = new Size(controlMaxWidth, controlMaxHeight);
            LabelSize = newLabelSize;
            ControlSize = newControlSize;

            if (Coordinator != null &&
                (newLabelSize != oldLabelSize || newControlSize != oldControlSize))
            {
                Coordinator.ControlOrLabelSizeChanged(this);
            }

            return new Size(
                Columns * (LabelSize.Width + ControlSize.Width + LabelControlSpacing) + (Columns - 1) * ColumnSpacing,
                ((Children.Count/2) / Columns) * Math.Max(LabelSize.Height, ControlSize.Height) + (((Children.Count/2) / Columns) - 1) * RowSpacing);
        }

We also notify the coordinator if the size changed, but we will talk about that in the third post

The ArrangeOverride method actually places all the labels and controls, it just loops over all the panel's children and calculates their final location:

        protected override Size ArrangeOverride(Size finalSize)
        {
            double controlWidth = (finalSize.Width - (Columns - 1) * ColumnSpacing - Columns * (LabelSize.Width + LabelControlSpacing)) / Columns;
            double rowHeight = Math.Max(LabelSize.Height, ControlSize.Height) + RowSpacing;
            double columnWidth = LabelSize.Width + LabelControlSpacing + controlWidth + ColumnSpacing;
            for (int i = 0; i < Children.Count - 1; i += 2)
            {
                var labelRect = new Rect(
                    columnWidth * ((i / 2) % Columns), rowHeight * ((i / 2) / Columns),
                    LabelSize.Width, rowHeight - RowSpacing);
                Children[i].Arrange(
                    new Rect(
                        labelRect.Left, 
                        labelRect.Top+(labelRect.Height-Children[i].DesiredSize.Height)/2,
                        Children[i].DesiredSize.Width,Children[i].DesiredSize.Height));
                Children[i + 1].Arrange(new Rect(
                    columnWidth * ((i / 2) % Columns) + LabelSize.Width + LabelControlSpacing, rowHeight * ((i / 2) / Columns),
                    controlWidth, rowHeight - RowSpacing));
            }
            return new Size(finalSize.Width, rowHeight * ((Children.Count/2) / Columns + 1));
        }

and of course, we have to close the class:

    }
}

also, I'm listing the IFormPanelCoordinator interface here because the FormPanel wouldn't compile without it.

    public interface IFormPanelCoordinator
    {
        void ControlOrLabelSizeChanged(FormPanel sender);
    }

And, as you see, we wrote a completely trivial class that take care of the annoying task of manually setting the grid layout.

I said in the beginning this is a simple class that will only cover the common cases, in the next post we will see how this class still saves us a lot of typing in the not-so-simple case.

Blogging Software Updated

I’ve just finished upgrading my blogging software to the latest version, you shouldn’t notice any difference.

If you see any problems with the blog or the rest of the web site please e-mail me (the address is in the page footer) or use the contact form.

Thanks

Give your application the Hollywood user experience with WPF

Have you ever noticed the completely ridicules UI of computers in movies and TV shows?

You rarely see any windowing systems (but that’s ok, the hero never has to switch windows), the fonts and icons are huge (also ok, there’s never anything more complex than 3 text boxes and 2 buttons) except for lists that always display in a grid with 2 points font size (also ok, the hero never needs more than a second to scan the entire list).

But it sure is dramatic, logging into a secure system looks like opening a safe door, sending or receiving e-mail is accompanied by huge animated envelopes flying on screens and when you delete a file you see it slowly shredded (slowly – because the hero is on his way and has to arrive just in time to stop the deletion).

Today I’m going to give you a movie style login control.

Initially the control will display a user name and password controls inside a round area along with a semi circler login button surrounded by gray “closed doors” (if this was for production I would have used a brushed metal texture instead of gray – but I wanted to keep the download small).

If you try to login with incorrect credentials you will get a scary “access denied” message.

When you enter the correct user name and password the circular “lock” are will rotate and then the “doors” will slide open the reveal the application.

Now, we are all serious software engineers here, so this is going to be a proper WPF control, it’s going to be “lookless” with replaceable templates and it’s going to be fully MVVM friendly.

We start by creating a class, we will call it LoginControl and we will inherit from Control – because that’s were we get the control template support from.

Let’s start with the interface with the control template.

Our control needs a TextBox for entering the user name, a PasswordBox for entering password and a button for actually trying to log in.

There is no cancel button because it’s out of scope for this control (the hero will either successfully log in or be dragged away by evil security guards – he will never close the application).

To accommodate the TextBox we just need a string dependency property that can be bound to the TextBox.Text property (that way the control template writer can also replace the TextBox with a completely different control if needed) – we will call this property LoginName:

public static readonly DependencyProperty LoginNameProperty =
    DependencyProperty.Register("LoginName", typeof(string), typeof(LoginControl));
public string LoginName
{
    get { return (string)GetValue(LoginNameProperty); }
    set { SetValue(LoginNameProperty, value); }
}

The PasswordBox is a little tricky because you can’t data bind to the PasswordBox.Password property, so we will have to access the PasswordBox ourselves, we do that by specifying the control template must have a PasswordBox named PART_PasswordBox, this is called a template part and the convention is the start the name with an uppercase PART_, we specify this using the TemplatePart attribute on the control calss:

[TemplatePart(Name="PART_PasswordBox", Type=typeof(PasswordBox))]
public class LoginControl : Control
{

For the login button we will use a routed command – this will let the template writer use any control that can send commands instead of a button (hyperlink, for example) and thankfully it’s just one line of code:

public static readonly RoutedCommand TryLoginCommand = new RoutedCommand();

Ok, two lines, we also need to use CommandBindings to attach code to this command, we will do it in our constructor (we will see the TryLoginExecute method later):

public LoginControl()
{
    CommandBindings.Add(new CommandBinding(TryLoginCommand, TryLoginExecute));
}

That took care of the data and notifications the template have to send to the control, we also need two notifications from the control to the template: one for failed login attempts and one for successful login attempts.

When the user enters incorrect name or password we want to show a message like “Incorrect user name of password, please check your Caps Lock and try again” (or in the movies “ACCESS DENIED”) and continue, we don’t change the state of the control so a property that can be bound to doesn’t make sense – buy an event does (we use a routed event and set it’s routing strategy to “Tunnel” so it is sent into the control template and not to the parent window).

public static readonly RoutedEvent FailedLoginEvent = EventManager.RegisterRoutedEvent(
    "FailedLogin", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(LoginControl));
public event RoutedEventHandler FailedLogin
{
    add { AddHandler(FailedLoginEvent, value); }
    remove { RemoveHandler(FailedLoginEvent, value); }
}

When the login is successful we want to run our unlock animation, we could use a property if we want but we will just deal with it the same way as the failed login:

public static readonly RoutedEvent SuccessfulLoginEvent = EventManager.RegisterRoutedEvent(
    "SuccessfulLogin", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(LoginControl));
public event RoutedEventHandler SuccessfulLogin
{
    add { AddHandler(SuccessfulLoginEvent, value); }
    remove { RemoveHandler(SuccessfulLoginEvent, value); }
}

That’s everything the template needs, but we also need to communicate with the application hosting the control.

First we need a way to ask the application if the user name and password are valid, we will do it with an event (note that this time the routing strategy is “Bubble”):

public class CheckLoginEventArgs : RoutedEventArgs
{
    public string LoginName;
    public string Password;
    public bool LoginSuccessful;
}

public delegate void CheckLoginEventHandler(object sender, CheckLoginEventArgs e);

public static readonly RoutedEvent CheckLoginEvent = EventManager.RegisterRoutedEvent(
    "CheckLogin", RoutingStrategy.Bubble, typeof(CheckLoginEventHandler), typeof(LoginControl));
public event CheckLoginEventHandler CheckLogin
{
    add{AddHandler(CheckLoginEvent,value);}
    remove{RemoveHandler(CheckLoginEvent,value);}
}

But wait, I said this control will be MVVM friendly so we will also add a command options:

public static readonly DependencyProperty CheckLoginCommandProperty =
    DependencyProperty.Register("CheckLoginCommand", typeof(ICommand), typeof(LoginControl));
public ICommand CheckLoginCommand
{
    get { return (ICommand)GetValue(CheckLoginCommandProperty); }
    set { SetValue(CheckLoginCommandProperty, value); }
}

And last but not least, a command so signal that the login was successful in an MVVM friendly way:

public static readonly DependencyProperty SuccessfulLoginCommandProperty =
    DependencyProperty.Register("SuccessfulLoginCommand", typeof(ICommand), typeof(LoginControl));
public ICommand SuccessfulLoginCommand
{
    get { return (ICommand)GetValue(SuccessfulLoginCommandProperty); }
    set { SetValue(SuccessfulLoginCommandProperty, value); }
}

We could also add a bubbling successful login event, but it’s easy enough to attach to the tunneling event in code behind – so we won’t bother.

You may have noticed we wrote a whole lot of code that just defines properties, commands and events but we didn’t write any code that actually does anything, so it’s time to write the actual control logic.

The control logic is ridiculously simple:

  • if you have a CheckLoginCommand execute it, otherwise send the CheckLogin event.
  • It the command/event indicated the login was successful send the LoginSuccessful event and execute the SuccessfulLoginCommand.
  • Otherwise send the FailedLogin event.

or in code:

private void TryLoginExecute(object sender, ExecutedRoutedEventArgs e)
{
    CheckLoginEventArgs args = new CheckLoginEventArgs()
    {
        RoutedEvent = CheckLoginEvent,
        LoginName = LoginName,
        Password = GetPassword(),
    };

    var cmd = CheckLoginCommand;
    if (cmd != null)
    {
        cmd.Execute(args);
    }
    else
    {
        RaiseEvent(args);
    }

    if (args.LoginSuccessful)
    {
        var result = new RoutedEventArgs(SuccessfulLoginEvent);
        RaiseEvent(result);
        var successCommand = SuccessfulLoginCommand;
        if (successCommand != null)
        {
            successCommand.Execute(null);
        }
    }
    else
    {
        ClearPassword();
        var result = new RoutedEventArgs(FailedLoginEvent);
        RaiseEvent(result);
    }
}

There are also the GetPassword and ClearPassword methos we use to access the PasswordBox defined as a template part – note that we have to run correctly also when it doesn’t exist (even if there’s no point to do so in the control).

private string GetPassword()
{
    var pbox = Template.FindName("PART_PasswordBox", this) as PasswordBox;
    if (pbox != null) return pbox.Password;
    return string.Empty;
}
private void ClearPassword()
{
    var pbox = Template.FindName("PART_PasswordBox", this) as PasswordBox;
    if (pbox != null) pbox.Password = string.Empty;
}

And that’s all we need to apply our movie style template.

To see the control template and view the entire animation just download the project here (~20Kb). the control template is in the app.xaml file.

It’s a VS2010 project targeting .net 3.5SP1 but the code is fully compatible with VS2008, I’ve also included the sample executable so you can view the animation without compiling (the correct user name/password are “user”/”1234” without the quotes).

yaTimer 2.6 Now an auto-update

If you have automatic updates enabled and you are running version 2.5 or later you should get the updated version of yaTimer within a week.

If you can’t wait you can always click the “Check for updates” button located in the about window (accessed from the last option on the main menu) or the options window (Accessed from the bottom right of the main menu).

There were a few problems with the last auto update – and the reason it has been a full week between yaTimer’s release and it’s availability as an automatic update is that I’ve been working on fixing the problems – and hopefully everything will work now.

If you have any problems upgrading you can e-mail me (the address is on the bottom of every page on the site) or use the contact form.

As always, you can also upgrade manually from the upgrade page.

WPF Adorners Part 4 – Simple and Powerful System for Using Adorners

This is Part 4 of a series, you may want to read part 1, part 2 and part 3 first.

In the previous post I promised a cool-looking popup – so we’ll start with the screenshots and follow with the code to create them.

We start with the same window-with-a-button we used in the first post in the series:

But when we click the button we get a speech bubble popup!

How did we do that?

First, let’s look at the Window.xaml file:

<Window x:Class="AdornerDemo.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:a="clr-namespace:AdornerDemo"
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <ControlTemplate x:Key="PopupTemplate">
            <Grid HorizontalAlignment="Left" VerticalAlignment="Top">
                <a:AdornedPlaceholder/>
                <Grid Width="150" Height="100" Margin="5 10 0 0">
                    <Rectangle Stroke="Black" Fill="Yellow" RadiusX="6" RadiusY="6" Margin="0 20 0 0"/>
                    <Path Stroke="Black" Fill="Yellow" Data="M 25 20 L 20 0 33 20" Margin="0 1 0 0"/>
                    <TextBlock Text="What are you doing?" Margin="5 25 0 0"/>
                    <TextBox Margin="5 45 5 0" VerticalAlignment="Top"/>
                    <Button Content="Tweet" Margin="5" VerticalAlignment="Bottom" HorizontalAlignment="Right"/>
                </Grid>
            </Grid>
        </ControlTemplate>
    </Window.Resources>
    <Grid>
        <Button Content="Click Me" HorizontalAlignment="Center" VerticalAlignment="Center" a:Adorners.Template="{StaticResource PopupTemplate}" Click="ShowPopup" Name="Btn"/>
    </Grid>
</Window>

We have a control template that describes the popup, similar to the validation we have a placeholder element – this time it’s AdornerDemo.AdornedPlaceholder – that we use to position the popup.

There’s also an AdornerDemo.Adorners class and we use the Adorners.Template attached property to connect the template to the button.

The Button click event shows the popup, let’s look at the event code:

private void ShowPopup(object sender, RoutedEventArgs e)
{
    Adorners.SetIsVisible(Btn, true);
}

To show the adorner we just set the Adorners.IsVisible attached property to true, in this example we did it in code but we can also do so in XAML with a trigger.

Now, let’s look at that Adorners class that does all that magic:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Threading;

namespace AdornerDemo
{
    public class Adorners
    {
        // Template attached property

        public static readonly DependencyProperty TemplateProperty =
            DependencyProperty.RegisterAttached("Template", typeof(ControlTemplate), typeof(Adorners),
            new PropertyMetadata(TemplateChanged));

        public static ControlTemplate GetTemplate(UIElement target)
        {
            return (ControlTemplate)target.GetValue(TemplateProperty);
        }
        public static void SetTemplate(UIElement target, ControlTemplate value)
        {
            target.SetValue(TemplateProperty, value);
        }
        private static void TemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            UpdateAdroner((UIElement)d, GetIsVisible((UIElement)d), (ControlTemplate)e.NewValue);
        }

        // IsVisible attached property

        public static readonly DependencyProperty IsVisibleProperty =
            DependencyProperty.RegisterAttached("IsVisible", typeof(bool), typeof(Adorners),
            new PropertyMetadata(IsVisibleChanged));
        public static bool GetIsVisible(UIElement target)
        {
            return (bool)target.GetValue(IsVisibleProperty);
        }
        public static void SetIsVisible(UIElement target, bool value)
        {
            target.SetValue(IsVisibleProperty, value);
        }
        private static void IsVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            UpdateAdroner((UIElement)d, (bool)e.NewValue, GetTemplate((UIElement)d));
        }

        // InternalAdorner attached property

        public static readonly DependencyProperty InternalAdornerProperty =
            DependencyProperty.RegisterAttached("InternalAdorner", typeof(ControlAdorner), typeof(Adorners));

        public static ControlAdorner GetInteranlAdorner(DependencyObject target)
        {
            return (ControlAdorner)target.GetValue(InternalAdornerProperty);
        }
        public static void SetInternalAdorner(DependencyObject target, ControlAdorner value)
        {
            target.SetValue(InternalAdornerProperty, value);
        }

        // Actually do all the work:

        private static void UpdateAdroner(UIElement adorned)
        {
            UpdateAdroner(adorned, GetIsVisible(adorned), GetTemplate(adorned));
        }

        private static void UpdateAdroner(UIElement adorned, bool isVisible, ControlTemplate controlTemplate)
        {
            var layer = AdornerLayer.GetAdornerLayer(adorned);

            if (layer == null)
            {
                // if we don't have an adorner layer it's probably
                // because it's too early in the window's construction
                // Let's re-run at a slightly later time
                Dispatcher.CurrentDispatcher.BeginInvoke(
                    DispatcherPriority.Loaded,
                    new Action(o => UpdateAdroner(o)), adorned);
                return;
            }

            var existingAdorner = GetInteranlAdorner(adorned);

            if (existingAdorner == null)
            {
                if (controlTemplate != null && isVisible)
                {
                    // show
                    var newAdorner = new ControlAdorner(adorned);
                    newAdorner.Child = new Control() { Template = controlTemplate, Focusable = false, };
                    layer.Add(newAdorner);
                    SetInternalAdorner(adorned, newAdorner);
                }
            }
            else
            {
                if (controlTemplate != null && isVisible)
                {
                    // switch template
                    Control ctrl = existingAdorner.Child;
                    ctrl.Template = controlTemplate;
                }
                else
                {
                    // hide
                    existingAdorner.Child = null;
                    layer.Remove(existingAdorner);
                    SetInternalAdorner(adorned, null);
                }
            }
        }
    }
}

This class doesn’t do much, out of 115 lines 7 are declarations and 50 are attached property boilerplate code, what’s left is the UpdateAdorner method that just create a ControlAdorner (slightly different then the one we used in part 2) add a control to it and set the control template.

Now let’s look at the AdornedPlaceholder class:

using System;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;

namespace AdornerDemo
{
    public class AdornedPlaceholder : FrameworkElement
    {
        public Adorner Adorner
        {
            get
            {
                Visual current = this;
                while (current != null && !(current is Adorner))
                {
                    current = (Visual)VisualTreeHelper.GetParent(current);
                }

                return (Adorner)current;
            }
        }

        public FrameworkElement AdornedElement
        {
            get
            {
                return Adorner == null ? null : Adorner.AdornedElement as FrameworkElement;
            }
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            var controlAdorner = Adorner as ControlAdorner;
            if (controlAdorner != null)
            {
                controlAdorner.Placeholder = this;
            }

            FrameworkElement e = AdornedElement;
            return new Size(e.ActualWidth, e.ActualHeight);
        }
    }
}

The only things it does is keep the same size as the adorned element and register itself with the ControlAdorner.

And the new version of the ControlAdorner has one extra method at the end:

using System;
using System.Windows.Documents;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;

namespace AdornerDemo
{
    public class ControlAdorner : Adorner
    {
        private Control _child;
        public AdornedPlaceholder Placeholder { get; set; }

        public ControlAdorner(UIElement adornedElement)
            : base(adornedElement)
        {
        }

        protected override int VisualChildrenCount
        {
            get
            {
                return 1;
            }
        }

        protected override Visual GetVisualChild(int index)
        {
            if (index != 0) throw new ArgumentOutOfRangeException();
            return _child;
        }

        public Control Child
        {
            get { return _child; }
            set
            {
                if (_child != null)
                {
                    RemoveVisualChild(_child);
                }
                _child = value;
                if (_child != null)
                {
                    AddVisualChild(_child);
                }
            }
        }

        protected override Size MeasureOverride(Size constraint)
        {
            _child.Measure(constraint);
            return _child.DesiredSize;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            _child.Arrange(new Rect(new Point(0, 0), finalSize));
            UpdateLocation();
            return new Size(_child.ActualWidth, _child.ActualHeight);
        }

        private void UpdateLocation()
        {
            if (Placeholder == null) return;
            Transform t = (Transform)TransformToDescendant(Placeholder);
            if (t == Transform.Identity) return;
            var oldTransfor = RenderTransform;
            if (oldTransfor == null || oldTransfor == Transform.Identity)
            {
                RenderTransform = t;
            }
            else
            {
                TransformGroup g = new TransformGroup();
                g.Children.Add(oldTransfor);
                g.Children.Add(t);
                RenderTransform =
                    new MatrixTransform(g.Value);
            }
        }

    }
}

What it does, except for a lot of error checking, is use RenderTranform to move the entire adorner so that the AdornedPlaceholder is directly above the adorned element.

All this messing around with transforms and TransformToDescendant instead of just finding the X,Y offset is important because in WPF you can set any transform you want on any element – so the button could be rotated, for example:

This is all for the adorner series, hope you enjoyed it.

yaTimer 2.6 Released

I’ve just finished uploading yaTimer 2.6 to the server, this version is a free upgrade for all existing customers, you can download the new version from the upgrade page.

What’s new in this version:

  • You can now edit timing information for a task when its timer is running (you can edit everything except the start time and duration of the currently running timing event) – this is the most requested feature for this version.
  • Open and closed groups on the task list are now saved when you close and reopen yaTimer.
  • The main task list is now much faster, uses less memory and has better animations than previous versions.
  • And last but most definitely not least – you can now synchronize multiple copies of yaTimer over the internet with yaTimer Central.
    While this version has only a short list of new features I’m very excited about it, especially about yaTimer Central – I have great plans for this service.

WPF Adorners Part 3 – Adorners and Validation

This is part 3 of a series, you may want to read part 1 and part 2 first.

Before we continue let’s take a quick look at WPF validation.

Let’s add a property to our window class that will just not validate:

public Window1()
{
    DataContext = this;
    InitializeComponent();
}

public string BadProperty
{
    get { return "Bad"; }
    set { throw new Exception("Value must be \"Bad\""); }
}

And bind to this property:

<TextBox VerticalAlignment="Center" HorizontalAlignment="Center" Width="150" >
    <TextBox.Text>
        <Binding Path="BadProperty" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <ExceptionValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

Now every time we try to type something into this control we get a red border around it:

Not very impressive, is it? WPF actually let us customize this to get something a a little better, just drop this into your window:

<Window.Resources>
    <Style TargetType="{x:Type TextBox}">
        <Setter Property="Validation.ErrorTemplate">
            <Setter.Value>
                <ControlTemplate>
                    <DockPanel>
                        <Grid DockPanel.Dock="Right" Width="16" Height="16" VerticalAlignment="Center" Margin="3 0 0 0">
                            <Ellipse Width="16" Height="16" Fill="Red"/>
                            <Ellipse Width="3" Height="8" VerticalAlignment="Top" HorizontalAlignment="Center" Margin="0 2 0 0" Fill="White"/>
                            <Ellipse Width="2" Height="2" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="0 0 0 2" Fill="White"/>
                        </Grid>
                        <Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
                            <AdornedElementPlaceholder/>
                        </Border>
                    </DockPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>

And our validation graphics magically turn into this:

Ok, so what does this has to do with adorners?

When the WPF validation system detects an invalid control it creates and adorner that holds a control (like the one we wrote in the previous post), inserts a control into it and sets that control template to the content of the Validation.ErrorTemplate attached property.

It positions the adorner above the original control so that the AdornedElementPlaceholder is exactly above the control and that let us easily place the control template content relative to the original control.

This is much easier to use than to code the adorner content in C# but the AdornedElementPlaceholder only works in validation, it does not do anything in adorners created by us.

In thenext post we will duplicate the validation system for out own use – and use it to pop up a small cool-looking form inside our application window.

Some notes about validation

  • Avoid using exceptions for validations, I used it in the example because it’s less code than the other options but in real code you should either use IDataErrorInfo or write your own VlidationRule.
  • There’s a bug in the validation system, the control inside the adorner can receive focus and will interfere with keyboard navigation, add this to your Resources section to fix it:
<Style TargetType="{x:Type Control}">
    <Setter Property="Focusable" Value="False"/>
</Style>

WPF Adorners Part 2 – Placing any control on the adorner layer

This is part 2 of a series you may want to read part 1 first.

In the previous post in this series we created a trivial adorner and placed it over a button – in this post we will look at placing something more complex inside an adorner.

We’ve already seen Adroner is a FrameworkElement and we know FrameworkElement can contain other visuals inside it – so let’s just write an adorner that contains a control:

using System;
using System.Windows.Documents;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;

namespace AdornerDemo
{
    class ControlAdorner : Adorner
    {
        private Control _child;

        public ControlAdorner(UIElement adornedElement)
            : base(adornedElement)
        {
        }

        protected override int VisualChildrenCount
        {
            get
            {
                return 1;
            }
        }

        protected override Visual GetVisualChild(int index)
        {
            if (index != 0) throw new ArgumentOutOfRangeException();
            return _child;
        }

        public Control Child
        {
            get { return _child; }
            set
            {
                if (_child != null)
                {
                    RemoveVisualChild(_child);
                }
                _child = value;
                if (_child != null)
                {
                    AddVisualChild(_child);
                }
            }
        }

        protected override Size MeasureOverride(Size constraint)
        {
            _child.Measure(constraint);
            return _child.DesiredSize;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            _child.Arrange(new Rect(new Point(0, 0), finalSize));
            return new Size(_child.ActualWidth, _child.ActualHeight);
        }
    }
}

This is a completely standard implementation of a FramwrorkElement that contains a child control.

You may be asking yourself why a control and not a Visual or a UIElement, this is because the way we will use this adorner in a future post.

Now let’s use our new adorner by placing a button over the button from our previous program:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    Button inAdorner = new Button()
    {
        HorizontalAlignment = HorizontalAlignment.Right,
        Content = "X",
    };
    ControlAdorner adorner = new ControlAdorner(Btn)
    {
        Child = inAdorner,
    };
    AdornerLayer.GetAdornerLayer(Btn).Add(adorner);
}

Yes, this is a silly example, but think what you can do by placing a UserControl over an existing control.

In the next post we will look into the potential of the adorner layer.

WPF Adorners Part 1 – What are adorners

WPF adorners are visuals that live in their own layer above the normal controls, they were originally designed to support adding notes inside the document viewer (that is why they’re in the System.Windows.Documents namespace) but they can be useful in many other situations.

In this series we will look at some ways to use adorners in an application and I will introduce a simple system to simplify the use of adorners.

Before we begin let’s look at a simple example:

We will create an adorner that draws four red boxes at it’s four corners:

using System;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows;

namespace AdornerDemo
{
    class FourBoxes : Adorner
    {
        public FourBoxes(UIElement adornedElement) :
            base(adornedElement)
        {
        }

        protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
        {
            drawingContext.DrawRectangle(Brushes.Red, null, new System.Windows.Rect(0, 0, 5, 5));
            drawingContext.DrawRectangle(Brushes.Red, null, new System.Windows.Rect(0, ActualHeight-5, 5, 5));
            drawingContext.DrawRectangle(Brushes.Red, null, new System.Windows.Rect(ActualWidth-5, 0, 5, 5));
            drawingContext.DrawRectangle(Brushes.Red, null, new System.Windows.Rect(ActualWidth-5, ActualHeight-5, 5, 5));
        }
    }
}

You can see that adorners have a OnRender method you can override just like any other WPF visual – actually Adorner inherits from FrameworkElement and you can use every WPF feature you like inside adorners (more about that in a later post).

Another thing you can see is that an adorner is tied to a UIElement, it’s connected to this element in the constructor and remains connected to this element.

Now let’s use this adorner, we will start from the standard Window1 class and add a button and a Loaded event handler:

<Window x:Class="AdornerDemo.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300" Loaded="Window_Loaded">
    <Grid>
        <Button Content="Look at me" VerticalAlignment="Center" HorizontalAlignment="Center" Name="Btn"/>
    </Grid>
</Window>

And now we will attach our adorner to this button:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    AdornerLayer.GetAdornerLayer(Btn).Add(new FourBoxes(Btn));
}

You can see we use AdornerLayer.GetAdornerLayer to get the adorner layer, the adorner layer is the floating surface that adorners live in.

Once we have the layer we use AdornerLayer.Add and we’re done.

Important note: error checking was removed to make the sample easier to read, you should check the return value from AdornerLayer.GetAdornerLayer.

Now running our code we get:

In the next post in this series we will look into adding something a little more complex into the adorner layer.

yaTimer Central Pre-Registration Open

Yesterday I announced the soon to be released yaTimer Central and I urged you to register to get an e-mail the moment the service is available – but I forgot the upload the registration form (yes, I’m embarrassed).

So, Visit the yaTimer Central page now to register, if a lot of people register I’ll open the service in several small “waves” so the sooner you register the sooner you can synchronize yaTimer on all your computers.