User Tools

Site Tools


notes:wpf:other_topics

Other WPF Topics

Routed Events

There are three types of routed events in WPF:

  • Bubbling events: These events travel up the containment hierarchy.
  • Tunneling events: These events travel down the containment hierarchy. The tunneling event always fires before the bubbling event. If you mark the tunneling event as handled, the bubbling event won't occur because the two events share the same instance of the RoutedEventArgs class.
  • Direct events: these events are ordinary .NET events

Event routing allows an event to be raised by one element but be handled in another one.

Related therminology:

  • Sender - an object that has an event handler attached.
  • Source - an object that raised an event: for a keyboard event, it is a control that had a focus when the event occurred; for a mouse event, it is the topmost element under the mouse pointer.
  • OriginalSource - an object that originally raised the event; for example if a button contains an image and a TextBlock, OriginalSource refers to an element that was actually clicked: a TextBlock or an image.

Example: Handle bubbling and tunneling events:

<Window x:Class="QuickTests.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="300" Width="650">
 
    <DockPanel LastChildFill="True" Grid.IsSharedSizeScope="True">
 
        <StackPanel Orientation="Horizontal" DockPanel.Dock="Top" 
                    MouseDown="MouseDownHandler" PreviewMouseDown="PreviewMouseDownHandler">
            <Button Name="TestButton" Margin="5" Click="ClickButton" 
                    MouseDown="MouseDownHandler" PreviewMouseDown="PreviewMouseDownHandler">
                <Grid MouseDown="MouseDownHandler" PreviewMouseDown="PreviewMouseDownHandler">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <Canvas Width="20" Height="18" VerticalAlignment="Center"
                            MouseDown="MouseDownHandler" PreviewMouseDown="PreviewMouseDownHandler">
                        <Ellipse Canvas.Left="0" Canvas.Top="0" Width="18" Height="18" 
                                 Fill="GreenYellow" Stroke="Navy" />
                        <Ellipse Canvas.Left="4" Canvas.Top="4" Width="10" Height="10" 
                                 Fill="Tomato" Stroke="Navy" />
                    </Canvas>
                    <TextBlock Grid.Column="1" VerticalAlignment="Center"
                               MouseDown="MouseDownHandler" 
                               PreviewMouseDown="PreviewMouseDownHandler">Test Button</TextBlock>
                </Grid>
            </Button>
            <Button Name="SimpleButton" Margin="5" Click="ClickButton"
                    MouseDown="MouseDownHandler" 
                    PreviewMouseDown="PreviewMouseDownHandler">Simple Button</Button>
            <Button Name="ClearButton" Margin="5" Click="ClearButton_Click">Clear Events</Button>
        </StackPanel>
 
        <!-- Header -->
        <Grid DockPanel.Dock="Top">
            <Grid.ColumnDefinitions>
                <ColumnDefinition SharedSizeGroup="EventName" />
                <ColumnDefinition SharedSizeGroup="Sender" />
                <ColumnDefinition SharedSizeGroup="Source" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
 
            <Border Grid.ColumnSpan="4" BorderBrush="Gray" BorderThickness="1">
                <Rectangle Fill="AliceBlue" />
            </Border>
            <TextBlock Grid.Column="0" Margin="3">Event Name</TextBlock>
            <TextBlock Grid.Column="1" Margin="3">Sender</TextBlock>
            <TextBlock Grid.Column="2" Margin="3">Source</TextBlock>
            <TextBlock Grid.Column="3" Margin="3">Original Source</TextBlock>
        </Grid>
 
        <ListBox Name="ListBox1" ItemsSource="{Binding}" BorderThickness="1">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition SharedSizeGroup="EventName" />
                            <ColumnDefinition SharedSizeGroup="Sender" />
                            <ColumnDefinition SharedSizeGroup="Source" />
                            <ColumnDefinition />
                        </Grid.ColumnDefinitions>
 
                        <TextBlock Grid.Column="0" Margin="3" Text="{Binding Path=EventName}" />
                        <TextBlock Grid.Column="1" Margin="3" Text="{Binding Path=Sender}" />
                        <TextBlock Grid.Column="2" Margin="3" Text="{Binding Path=Source}" />
                        <TextBlock Grid.Column="3" Margin="3" Text="{Binding Path=OriginalSource}" />
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
 
    </DockPanel>
 
</Window>
using System;
using System.Collections.ObjectModel; // ObservableCollection
using System.Windows;
using System.Windows.Controls; // Button
using System.Windows.Input; // MouseButtonEventArgs
 
namespace QuickTests
{
    public partial class MainWindow : Window
    {
        private ObservableCollection<TestEvent> events = new ObservableCollection<TestEvent>();
 
        public MainWindow()
        {
            InitializeComponent();
            ListBox1.DataContext = events;
        }
 
        // ButtonBase.Click Event Handler
        // Routing strategy: Bubbling
        // Delegate: RoutedEventHandler 
        private void ClickButton(object sender, RoutedEventArgs e)
        { events.Add(new TestEvent("Click", sender, e)); }
 
        // UIElement.MouseDown Event Handler
        // Identifier field: MouseDownEvent 
        // Routing strategy: Bubbling
        // Delegate: MouseButtonEventHandler 
        private void MouseDownHandler(object sender, MouseButtonEventArgs e)
        { events.Add(new TestEvent("MouseDown", sender, e)); }
 
        // UIElement.PreviewMouseDown Event Handler
        // Identifier field: PreviewMouseDownEvent 
        // Routing strategy: Tunneling
        // Delegate: MouseButtonEventHandler 
        private void PreviewMouseDownHandler(object sender, MouseButtonEventArgs e)
        {
            // Handle the PreviewMouseDown event of the top-most container (a StackPanel)
            // if the source of the event is the ClearButton button. This halts event 
            // routing for the button.
            if (e.Source is Button &amp;&amp; ((Button)e.Source).Name == "ClearButton")
            {
                events.Clear();
                e.Handled = true;
            }
            else
                events.Add(new TestEvent("PreviewMouseDown", sender, e)); 
        }
 
        // This event handler will never be raised as we already handled 
        // the PreviewMouseDown event when the ClearButton was pressed.
        private void ClearButton_Click(object sender, RoutedEventArgs e)
        { events.Clear(); }    
    }
 
    public class TestEvent
    {
        public string EventName { get; private set; }
        public string Sender { get; private set; }
        public string Source { get; private set; }
        public string OriginalSource { get; private set; }
 
        public TestEvent(string eventName, object sender, RoutedEventArgs e)
        {
            this.EventName = eventName;
            this.Sender = sender.ToString().Replace("System.Windows.", "");
            this.Source = e.Source.ToString().Replace("System.Windows.", "");
            this.OriginalSource = 
                (e.OriginalSource != null ? 
                 e.OriginalSource.ToString()
                     .Replace("System.Windows.", "")
                     .Replace("Microsoft.Windows.", "") : "");
        }
    }
}

The following screenshots show results of user actions:

A user clicked the TextBlock within the Test Button:

A user clicked the Ellipse within the Test Button:

A user clicked the button area of the Test Button:

A user clicked the TextBlock within the Simple Button:

A user clicked the button area of the Simple Button:

Note that the MouseDown event for the buttons does not show up in the list. This is because the button control prevents the MouseDown event from bubbling up the control tree by converting it to the the Click event.

Example: Define and register a ButtonBase.Click event:

public abstract class ButtonBase : ContentControl, ...
{
    // Event definition
    public static readonly RoutedEvent ClickEvent;
 
    // Event registration 
    static ButtonBase()
    {
        ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent(
                                "Click",  // the name of the event
                                RoutingStrategy.Bubble, // the type of routine
                                typeof(RoutedEventHandler), // the delegate that defines
                                                            // the syntax of the event handler
                                typeof(ButtonBase)); // the class that owns the event
        ...
    }
 
    // The traditional event wrapper to make it accessible to all .NET languages
    public event RoutedEventHandler Click
    {
        add
        {
            base.AddHandler(ButtonBase.ClickEvent, value);
        }
 
        remove
        {
            base.RemoveHandler(ButtonBase.ClickEvent, value);
        }
    }
    ...
}

Example: Share a routed event. The definition of a routed event can be shared between classes. For example, the MouseUp event is defined by the System.Windows.Input.Mouse class. The UIElement and ContentElement classes reuse it using the RoutedEvent.AddOwner method:

UIElement.MouseUpEvent = Mouse.MouseUpEvent.AddOwner(typeof(UIElement));

Example: Raise a routed event. The RaiseEvent method is used to raise a routed event. The RaiseEvent method is defined in the UIElement class:

// This code snipped is located in a class that derives from UIElement.
RoutedEventArgs e = new RoutedEventArgs(ButtonBase.ClickEvent, this);
base.RaiseEvent(e);

Example: Add an event handler programmatically:

CustomerLogo.MouseUp += new MouseButtonEventHandler(CustomerLogo_MouseUp);
-or-
// method group conversions
CustomerLogo.MouseUp += CustomerLogo_MouseUp;
-or-
// connect an event directly using UIElement.AddHandler()
CustomerLogo.AddHandler(Image.MouseUpEvent, new MouseButtonEventHandler(CustomerLogo_MouseUp));

Example: Detach an event handler programmatically:

CustomerLogo.MouseUp -= CustomerLogo_MouseUp;
-or-
CustomerLogo.RemoveHandler(Image.MouseUpEvent, new MouseButtonEventHandler(CustomerLogo_MouseUp));

Example: Handle a suppressed event:

There is a way to receive events that are marked as handled by using the AddHandler method and providing true for its third parameter:

btnSubmit.AddHander(UIElement.MouseUpEvent, new MouseButtonEventHandler(btnSubmit_MouseUp), true);

Example: Handle an attched Click event from all buttons located in the StackPanel:

<StackPanel x:Name="StackPanel1" Button.Click="DoSomething">
<Button Name="btn1">1</Button>
<Button Name="btn2">2</Button>
<Button Name="btn3">3</Button>
...
</StackPanel>
private void DoSomething(object sender, RoutedEventArgs e)
{
    if (e.Source == btn1) { ... }
    else if (e.Source == btn2) { ... }
    else if (e.Source == btn3) { ... }
}

Example: Handle an attached Click event from all controls located in the StackPanel that derive from ButtonBase (i.e. Button, RadioButton, CheckBox, etc.):

<StackPanel x:Name="StackPanel1" ButtonBase.Click="DoSomething">
<Button Name="btn1">1</Button>
<Button Name="btn2">2</Button>
<Button Name="btn3">3</Button>
...
</StackPanel>

Example: Wire up an attached event programmatically:

StackPanel1.AddHandler(Button.Click, new RoutedEventHandler(DoSomething));

Transforms

Example: Use RenderTransform and LayoutTransform to scale content of a button. RenderTransform is applied after the layout has been completed i.e. it does not influence the page layout. LayoutTranform affects the layout.

  • The button with content scaled by RenderTransform does not change its size. There is no impact on page layout.
  • The button with content scaled by LayoutTransform is enlarged in order for the content to fit. The page layout changes to accommodate the larger button.

<Window x:Class="QuickTests.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="300" Width="300">
 
    <Window.Resources>
        <Style TargetType="{x:Type Button}">
            <Setter Property="HorizontalAlignment" Value="Center" />
        </Style>        
    </Window.Resources>
 
    <StackPanel Orientation="Vertical">
        <TextBlock>No transform:</TextBlock>
        <Button>
            <TextBlock>Hello</TextBlock>
        </Button>
        <TextBlock HorizontalAlignment="Center">Some text</TextBlock>
 
        <TextBlock><LineBreak/><LineBreak/>RenderTransform:</TextBlock>
        <!-- RenderTransform -->
        <Button>
            <TextBlock>
                <TextBlock.RenderTransform>
                    <ScaleTransform ScaleX="3" ScaleY="3" />
                </TextBlock.RenderTransform>
                Hello
            </TextBlock>
        </Button>
        <TextBlock HorizontalAlignment="Center">Some text</TextBlock>
 
        <TextBlock><LineBreak/><LineBreak/>LayoutTransform:</TextBlock>
        <!-- LayoutTransform -->
        <Button>
            <TextBlock>
                <TextBlock.LayoutTransform>
                    <ScaleTransform ScaleX="3" ScaleY="3" />
                </TextBlock.LayoutTransform>
                Hello
            </TextBlock>
        </Button>
        <TextBlock HorizontalAlignment="Center">Some text</TextBlock>
    </StackPanel>
</Window>

Example: Use RotateTransform to rotate the content of a button. Although RotateTransform is not a scaling transform, the button growns in order to accommodate its content. RotateTransform is actually applied to the bounding box of the content of the button.

<Window x:Class="QuickTests.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="300" Width="300">
 
    <Window.Resources>
        <Style TargetType="{x:Type Button}">
            <Setter Property="HorizontalAlignment" Value="Center" />
        </Style>        
    </Window.Resources>
 
    <StackPanel Orientation="Vertical">
 
        <Grid Margin="3">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
 
            <TextBlock Grid.ColumnSpan="2">Simple RotateTransform</TextBlock>
            <Button Grid.ColumnSpan="2" Grid.Row="1">
                <TextBlock>
                    <TextBlock.LayoutTransform>
                        <RotateTransform Angle="30" />
                    </TextBlock.LayoutTransform>
                    Hello
                </TextBlock>
            </Button>
 
            <TextBlock Grid.Row="2" Grid.ColumnSpan="2">
                <LineBreak/>No transform
            </TextBlock>
            <Button Grid.Row="3">
                <Line Stroke="Blue" Y1="30" X2="100" />
            </Button>
            <Button Grid.Row="3" Grid.Column="1" Margin="3,0,0,0">
                <Border BorderBrush="Black" BorderThickness="1">
                    <Line Stroke="Blue" Y1="30" X2="100" />
                </Border>
            </Button>
 
            <TextBlock Grid.Row="4" Grid.ColumnSpan="2">
                <LineBreak/>RotateTransform
            </TextBlock>
            <Button Grid.Row="5">
                <Line Stroke="Blue" Y1="30" X2="100">
                    <Line.LayoutTransform>
                        <RotateTransform Angle="50" />
                    </Line.LayoutTransform>
                </Line>
            </Button>
            <Button Grid.Row="6" Grid.Column="1" Margin="3,0,0,0">
                <Border BorderBrush="Black" BorderThickness="1">
                    <Border.LayoutTransform>
                        <RotateTransform Angle="50" />
                    </Border.LayoutTransform>
                    <Line Stroke="Blue" Y1="30" X2="100" />
                </Border>
            </Button>
 
        </Grid>
    </StackPanel>
</Window>

Triggers

The triggers classes in WPF:

  • Trigger - The simplest type of a trigger. It watches for a change in a dependency property.
  • MultiTrigger - It combines multiple conditions. All of the conditions must be met before the trigger is invoked.
  • DataTrigger - It watches for changes in bound data.
  • MultiDataTrigger - It combines multiple data triggers.
  • EventTrigger - It applies an animation when an event occurs. This is the most complex trigger.

All the trigger classes derive from the TriggerBase class.

Simple Trigger

  • It can be attached to any dependency property.
  • It watches a dependency property and its value. When the property changes to a given value, the setters stored in the Trigger.Setters collection are applied.
  • The changes applied by a trigger are reverted automatically when a condition stops applying.
  • If there are more than one trigger that modifies the same property, the last trigger in the list takes precedence i.e. the order in which the triggers are listed matters.

Example: Change the background of a button to Navy when the button gets focus:

<Style x:Key="MyButtonStyle">
    <Style.Setters>
        <Setter Property="Control.FontFamily" Value="Arial" />
        <Setter Property="Control.FontSize" Value="6" />
    </Style.Setters>
 
    <Style.Triggers>
        <Trigger Property="Control.IsFocused" Value="True">
            <Setter Property="Control.Background" Value="DarkRed" />
        </Trigger>
    </Style.Triggers>
</Style>

MultiTrigger

  • It is applied when several criteria occured.
  • It provides a Conditions collection.

Example: Apply formatting only when a button has focus and the mouse is over it:

<Style x:Key="MyButtonStyle">
    <Style.Setters>
    ...
    </Style.Setters>
    <Style.Triggers>
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="Control.IsFocused" Value="True">
                <Condition Property="Control.IsMouseOver" Value="True">
            </MultiTrigger.Conditions>
            <MultiTrigger.Setters>
                <Setter Property="Control.Background" Value="Navy" />
            </MultiTrigger.Setters>
        </MultiTrigger>
    </Style.Triggers>
</Style>

EventTrigger

  • It waits for a specific event to be fired.
  • It applies a series of actions (animations) to a control.
  • It has to be reversed (unlike property triggers) if you want the element to return to its original state.

Example: Animate the FontSize property of the button when the MouseEnter event occures:

<Style x:Key="MyButtonStyle">
    <Style.Setters>
    ...
    </Style.Setters>
 
    <Style.Triggers>
        <EventTrigger RoutedEvent="Mouse.MouseEnter">
            <EventTrigger.Actions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="FontSize" To="15" />
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger.Actions>
        </EventTrigger>
 
        <!-- reverse the action performed by the trigger by changing the font size to its original value -->
        <EventTrigger RoutedEvent="Mouse.MouseLeave">
            <EventTrigger.Actions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Duration="0:0:1" Storyboard.TargetProperty="FontSize" />
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger.Actions>
        </EventTrigger>
 
        ...
    </Style.Triggers>
</Style>

Validation

There are two options of validation in WPF:

  • Raising an error in the data object by throwing an exception from a property setter. Although WPF ignores any exceptions thrown when setting a property, it is possible to change that and show a visual indication of an error. An alternative solution would be to implement IDataErrorInfo interface in the data class. This allows for indication of errors without throwing exceptions.
  • Defining validation at the binding level. In this case validation is defined in a separate class that can be reused with multiple bindings that store similar types of data.

In general, validation applies only when a value from the target is used to update the source i.e. when a TwoWay or OneWayToSource binding is used.

IMPORTANT: Validation is always performed before conversion. It means that WPF checks each validation rule in the Binding.ValidationRules collection first. Then, if all of the rules succeed, it calls a converter.

This is WPF standard behaviour when validation fails on an element:

  • The default ErrorTemplate is applied (a red outline around the element).
  • The HasError and Errors properties are set on the element.
  • The Error event is fired. It bubbles up to the parent container.

Validation in the data object

Example: We have an Employee class with two properties: EmployeeName and Salary. We don't want to accept salaries that are less than $0. In order to display a visual indicator (a default red rectangle around a TextBox), we throw an exception when the salary is invalid and we use ExceptionValidationRule in the salary TextBox. The rule tells WPF to report all errors rather than silently ignore them:

<Window x:Class="QuickTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300">
 
    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="12"/>
            <Setter Property="Padding" Value="3"/>
            <Setter Property="Margin" Value="0,10,0,0"/>
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="FontSize" Value="14"/>
            <Setter Property="Padding" Value="3"/>
            <Setter Property="Background" Value="LightGray"/>
        </Style>
 
    </Window.Resources>
 
    <StackPanel x:Name="EmployeeDetails" Margin="10">
        <TextBlock Text="Name:" />
        <TextBox Text="{Binding EmployeeName}" />
        <TextBlock Text="Salary:" />
        <!-- Use ExceptionValidationRule or set Binding.ValidatesOnExceptions="True"  -->
        <TextBox>
            <TextBox.Text>
                <Binding Path="Salary">
                    <Binding.ValidationRules>
                        <ExceptionValidationRule />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    </StackPanel>
</Window>
using System;
using System.ComponentModel;
using System.Windows;
 
namespace QuickTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += new RoutedEventHandler(MainWindow_Loaded);
        }
 
        protected void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            Employee testEmployee = new Employee{ EmployeeName = "George Smith", Salary = 40000M };
            EmployeeDetails.DataContext = testEmployee;
        }
    }
 
    public class Employee : INotifyPropertyChanged
    {
        public string EmployeeName { get; set; }
 
        private decimal salary;
        public decimal Salary
        {
            get
            {
                return salary;
            }
            set
            {
                // Prevent invalid salaries.
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("Salary has to be greater than $0.");
                else
                {
                    OnPropertyChanged(new PropertyChangedEventArgs("Salary"));
                    salary = value;
                }
            }
        }
 
        // Implementation of INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, e);
            }
        }
    }
}

Validation using IDataErrorInfo

The IDataErrorInfo interface requires implementation of the following members:

  • A string property named Error which is a general error information describing the entire object (e.g. “Invalid Data”).
  • A string indexer that returns detailed error information.

Example: Centralize all validation logic in the Error property and the indexer. Use use DataErrorValidationRule in the salary TextBox:

<Window x:Class="QuickTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300">
 
    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="12"/>
            <Setter Property="Padding" Value="3"/>
            <Setter Property="Margin" Value="0,10,0,0"/>
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="FontSize" Value="14"/>
            <Setter Property="Padding" Value="3"/>
            <Setter Property="Background" Value="LightGray"/>
        </Style>
 
    </Window.Resources>
 
    <StackPanel x:Name="EmployeeDetails" Margin="10">
        <TextBlock Text="Name:" />
        <TextBox Text="{Binding EmployeeName}" />
        <TextBlock Text="Salary:" />
        <!-- Use DataErrorValidationRule or set Binding.ValidatesOnDataErrors="True" -->
        <TextBox>
            <TextBox.Text>
                <Binding Path="Salary">
                    <Binding.ValidationRules>
                        <DataErrorValidationRule />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    </StackPanel>
</Window>
using System;
using System.ComponentModel;
using System.Windows;
 
namespace QuickTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += new RoutedEventHandler(MainWindow_Loaded);
        }
 
        protected void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            Employee testEmployee = new Employee{ EmployeeName = "George Smith", Salary = 40000M };
            EmployeeDetails.DataContext = testEmployee;
        }
    }
 
    public class Employee : INotifyPropertyChanged, IDataErrorInfo
    {
        public string EmployeeName { get; set; }
 
        private decimal salary;
        public decimal Salary
        {
            get
            {
                return salary;
            }
            set
            {
                OnPropertyChanged(new PropertyChangedEventArgs("Salary"));
                salary = value;
            }
        }
 
        // Implementation of INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, e);
            }
        }
 
        // Implementation of IDataErrorInfo
        public string this[string propName]
        {
            get
            {
                if (propName == "Salary")
                {
                    if (Salary <= 0)
                        return("Salary has to be greater than $0.");
                }
                return null;
            }
        }
        public string Error
        {
            get
            {
                return "Invalid Data";
            }
        }
    }
}

Custom validation rules

Example: Define a custom validation rule.

Points of interest:

  • Derive our custom validation rule (RangeValidationRule) from System.Windows.Controls.ValidationRule.
  • Override the Validate method.
  • Return the instance of ValidationResult from the Validate method. The isValid parameter indicates if the validation succeeded. The errorContent parameter provides an error message.
  • Add RangeValidationRule to the Binding.ValidationRules collection.
  • Reuse our custom RangeValidationRule and adjust its properties (Min and Max) as needed.
<Window x:Class="QuickTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:QuickTest"
        Title="MainWindow" Height="200" Width="300">
 
    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="12"/>
            <Setter Property="Padding" Value="3"/>
            <Setter Property="Margin" Value="0,10,0,0"/>
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="FontSize" Value="14"/>
            <Setter Property="Padding" Value="3"/>
            <Setter Property="Background" Value="LightGray"/>
        </Style>
 
    </Window.Resources>
 
    <StackPanel x:Name="EmployeeDetails" Margin="10">
        <TextBlock Text="Name:" />
        <TextBox Text="{Binding EmployeeName}" />
        <TextBlock Text="Age:" />
        <!-- Use the custom validation rule -->
        <TextBox>
            <TextBox.Text>
                <Binding Path="Age">
                    <Binding.ValidationRules>
                        <local:RangeValidationRule Min="1" Max="120" />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    </StackPanel>
 
</Window>
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls; // ValidationRule
 
namespace QuickTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += new RoutedEventHandler(MainWindow_Loaded);
        }
 
        protected void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            Employee testEmployee = new Employee{ EmployeeName = "George Smith", Age = 41 };
            EmployeeDetails.DataContext = testEmployee;
        }
    }
 
    public class Employee : INotifyPropertyChanged
    {
        public string EmployeeName { get; set; }
 
        private decimal age;
        public decimal Age
        {
            get
            {
                return age;
            }
            set
            {
                OnPropertyChanged(new PropertyChangedEventArgs("Age"));
                age = value;
            }
        }
 
        // Implementation of INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, e);
            }
        }
    }
 
    // This is our custom validation rule - a class derived from ValidationRule.
    public class RangeValidationRule : ValidationRule
    {
        public int Min { get; set; }
        public int Max { get; set; }
 
        // Override the Validation() method to perform our custom validation.
        public override ValidationResult Validate(object value,
            System.Globalization.CultureInfo cultureInfo)
        {
            int val = 0;
 
            try
            {
                if (((string)value).Length > 0)
                    val = Int32.Parse((string)value);
            }
            catch
            {
                return new ValidationResult(false, "Illegal characters.");
            }
 
            if (val < Min || val > Max)
                return new ValidationResult(false, String.Format("Not in the range {0} to {1}.", Min, Max));
            else
                return new ValidationResult(true, null);
        }
    }
}

The Error event

The Error event is fired when an error is stored or cleared.

We can apply the Error event to the preceding example by following the steps:

1. Replace:

<Binding Path="Age">

with

<Binding Path="Age" NotifyOnValidationError="True">

2. The Error event uses bubbling. It means we can handle this event in the parent container:

Replace:

<StackPanel x:Name="EmployeeDetails" Margin="10">

with

<StackPanel x:Name="EmployeeDetails" Margin="10" Validation.Error="EmployeeDetails_Error">

3. Handle the Error event:

private void EmployeeDetails_Error(object sender, ValidationErrorEventArgs e)
{
    // Check if error is added (not cleared).
    if (e.Action == ValidationErrorEventAction.Added)
    {
        // e.Error is of type ValidationError.
        // If ExceptionValidationRule was used, the e.Error.Exception property would be provided.
        MessageBox.Show(e.Error.ErrorContent.ToString());
    }
}

Other concepts related to validation and error handling

  • To get a list of errors in the current window (or a container in that window) enumerate recursively the elements in the window testing their Validation.HasError property.
  • The standard error templates use the adorner layer which exists above the window content. It allows us to show a different error indicator. All we need is to set the Validation.ErrorTemplate property of the control to a ControlTemplate containing any elements we want. The control itself is represented in the template by AdornerElementPlaceholder.
  • To validate multiple values you can use binding groups by creating a custom validation rule that derives from the ValidationRule class and attaching it to the container that holds all the bound controls. WPF then validates the entire data object. It is known as item-level validation.

Visual and Logical Trees

A logical tree is composed of elements such as StackPanels and Buttons. For example, we can create a Window and then include a StackPanel with two buttons. The window, the stack panel, and the buttons form the logical tree.

A visual tree is an expanded version of the logical tree. It breaks the elements into smaller pieces. For example, the button is broken down into the border (the ButtonChrome class), the container (ContentPresenter), and text (TextBlock). All of these elements are represented by a class that derives from FrameworkElement. Control templates are used to build a visual tree of a control. It is also possible to change just a single element of a visual tree using the Style.TargetName property.

In short:

  • Logical Tree is a representation of UI in XAML.
  • Visual Tree represents all components of all elements on UI arranged hierarchically. It is defined by control templates. Events and resources propagate through the visual tree.

There are two classes that can be used to browse through the logical and visual trees:

  • System.Windows.LogicalTreeHelper
  • System.Windows.Media.VisualTreeHelper

The VisualTreeHelper class contains a few useful methods:

  • GetChildrenCount
  • GetChild
  • GetParent
  • hit testing
  • bounds checking

Example: Find an element recursively by its name in a visual tree of another element. It's an alternative to the method available in MSDN How to: Find DataTemplate-Generated Elements. The searched element has to derive from FrameworkElement as it needs to have the Name property:

private DependencyObject FindElement(DependencyObject obj, string elementName)
{
    DependencyObject foundElement = null;
 
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(obj, i);
 
        if (child is FrameworkElement &&
               ((FrameworkElement)child).Name == elementName)
            return child;
        else
            foundElement = (foundElement ?? FindElement(child, elementName));
    }
 
    return foundElement;
}

A few test cases:

// Find a TextBox 'txtComment' in a list box 'commentList'.
TextBox txtComment = FindElement(commentList, "txtComment") as TextBox;
 
// Find a Popup 'CommentPopup' in a ListBoxItem 'item'.
Popup commentPopup = FindElement(item, "CommentPopup") as Popup;
 
// Find a list box 'Customers' in a Popup 'CustPopup'.
ListBox customerList = FindElement(CustPopup.Child, "CommentList") as ListBox;

Example: Display the visual tree for the current window:

MainWindow.xaml (the visual tree of this window will be displayed):

<Window x:Class="TestApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TestApp"
        Width="300" Height="300">
 
    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBlock x:Name="Text1" Text="Some text here" Padding="5" />
            <TextBlock x:Name="Text2" Text="Another text here"  Padding="5" />
        </StackPanel>
        <Ellipse Stroke="Blue" StrokeThickness="2" Width="100" Height="50" Margin="10" />
 
        <Button Click="ShowVisualTree" Content="Show Visual Tree" Width="150" />
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System.Windows;
 
namespace TestApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
 
        private void ShowVisualTree(object sender, RoutedEventArgs e)
        {
            VisualTreeDisplay treeDisplay = new VisualTreeDisplay();
            treeDisplay.ShowVisualTree(this);
            treeDisplay.Show();
        }
    }
}

VisualTreeDisplay.xaml (this window shows the visual tree of the MainWindow):

<Window x:Class="TestApp.VisualTreeDisplay"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="VisualTreeDisplay" Height="300" Width="300">
    <TreeView Name="treeElements" Margin="10"></TreeView>
</Window>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
 
namespace TestApp
{
    public partial class VisualTreeDisplay : System.Windows.Window
    {
        public VisualTreeDisplay()
        {
            InitializeComponent();
        }
 
        public void ShowVisualTree(DependencyObject element)
        {
            // Clear the tree.
            treeElements.Items.Clear();
 
            // Start processing elements, begin at the root.
            ProcessElement(element, null);
        }
 
        private void ProcessElement(DependencyObject element, TreeViewItem previousItem)
        {
            // Create a TreeViewItem for the current element.
            TreeViewItem item = new TreeViewItem();
            item.Header = element.GetType().Name;
            item.IsExpanded = true;
 
            // Check whether this item should be added to the root of the tree
            // (if it's the first item), or nested under another item.
            if (previousItem == null)
            {
                treeElements.Items.Add(item);
            }
            else
            {
                previousItem.Items.Add(item);
            }
 
            // Check if this element contains other elements.
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
            {
                // Process each contained element recursively.
                ProcessElement(VisualTreeHelper.GetChild(element, i), item);
            }
        }
    }
}

WeakEventManager

The WPF framework offers a class WeakEventManager that leverages a pattern employing weak references. It is useful in WPF which tend not to use disposable objects.

An event creates a strong connection between the publisher and the listener. This can be a problem with garabage collection. For example, if a listener is not directly referenced any more, there's still a reference from the publisher. The garbage collector can't clean up memory from the listener, as the publisher still holds a reference and fires events to the listener.

The two ways of dealing with the strong connection problem:

  • apply the weak event pattern using WeakEventManager as an intermediary between the publisher and listeners
  • unsubscribe from events before the subscribers go out of scope

In the weak event pattern the publisher and the listener(s) are not strongly connected. The listener can be garbage collected when it is not referenced anymore.

In order to implement the weak event pattern, you need to create a weak publisher class that derives from WeakEventManager and a weak listener class that derives from IWeakEventListener.

The connection between the publisher and the listener(s) is made by using the AddListener and RemoveListener methods of the weak publisher class, in this example WeakMessageManager.

using System;
 
// Reference the WindowsBase assembly.
using System.Windows; // WeakEventManager, IWeakEventListener
 
namespace CSharpTest
{
    // WeakMessageManager manages the connection between the publisher
    // and the listener for the MessageReceived event.
    public class WeakMessageManager : WeakEventManager
    {
        // Singleton pattern.
        public static WeakMessageManager Instance
        {
            get
            {
                var manager = GetCurrentManager(typeof(WeakMessageManager)) as WeakMessageManager;
                if (manager == null)
                {
                    manager = new WeakMessageManager();
                    SetCurrentManager(typeof(WeakMessageManager), manager);
                }
                return manager;
            }
        }
 
        // AddListener and RemoveListener are a part of the weak event pattern.
        // The listener is connected and disconnected to the events of the publisher
        // with these methods.
        public static void AddListener(object source, IWeakEventListener listener)
        {
            Instance.ProtectedAddListener(source, listener);
        }
        public static void RemoveListener(object source, IWeakEventListener listener)
        {
            Instance.ProtectedRemoveListener(source, listener);
        }
 
        // StartListening is called when the first listener is added.
        protected override void StartListening(object source)
        {
            var publisher = source as MessageManager;
            if (publisher != null)
            {
                publisher.MessageReceived += MessageManager_MessageReceived;
            }
        }
 
        // StopListening is called when the last listener is removed.
        protected override void StopListening(object source)
        {
            var publisher = source as MessageManager;
            if (publisher != null)
            {
                publisher.MessageReceived -= MessageManager_MessageReceived;
            }
        }
 
        void MessageManager_MessageReceived(object sender, MessageInfoArgs args)
        {
            // Forward the event to the listeners.
            DeliverEvent(sender, args);
        }
    }
 
    // MessageManager is an event publisher. It raises an event
    // when a new message is received. This code is the same as in 
    // the previous example.
    public class MessageManager
    {
        public event EventHandler<MessageInfoArgs> MessageReceived;
 
        public void NewMessage(string message)
        {
            RaiseMessageReceived(message);
        }
 
        protected virtual void RaiseMessageReceived(string message)
        {
            EventHandler<MessageInfoArgs> newMessageReceived = MessageReceived;
 
            // Verify if the MessageReceived delegate (the event handler) is not null.
            if (newMessageReceived != null)
            {
                // The first parameter contains the sender of the event.
                // The second parameter provides information about the event.
                newMessageReceived(this, new MessageInfoArgs(message));
            }
        }
    }
 
    public class MessageInfoArgs : EventArgs
    {
        public string Message { get; private set; }
 
        public MessageInfoArgs(string message)
        {
            this.Message = message;
        }
    }
 
    // The weak event listener subscribes to the MessageReceived event 
    // to be informed when a new message arrives.
    public class MessageClient : IWeakEventListener
    {
        private string clientName;
 
        public MessageClient(string clientName)
        {
            this.clientName = clientName;
        }
 
        public void NewMessageReceived(object sender, MessageInfoArgs args)
        {
            Console.WriteLine("{0}: {1}", clientName, args.Message);
        }
 
        // IWeakEventListener interface implementation.
        bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs args)
        {
            NewMessageReceived(sender, args as MessageInfoArgs);
            return true;
        }
    }
 
    class EntryPoint
    {
        public static void Main()
        {
            MessageManager manager = new MessageManager();
 
            MessageClient client1 = new MessageClient("John");
            WeakMessageManager.AddListener(manager, client1);
 
            manager.NewMessage("First message - welcome!");
 
            MessageClient client2 = new MessageClient("Nick");
            WeakMessageManager.AddListener(manager, client2);
 
            manager.NewMessage("Second message - still there?");
 
            // Unsubscribe John.
            WeakMessageManager.RemoveListener(manager, client1);
 
            manager.NewMessage("Third message - good bye!");
        }
    }
}

Output:

John: First message - welcome!
John: Second message - still there?
Nick: Second message - still there?
Nick: Third message - good bye!

Generic weak event manager

.NET 4.5 introduced a generic implementation of the weak event manager: WeakEventManager<TEventSource, TEventArgs>.

  • It is no longer necessary to implement a custom weak event manager for every event.
  • The listener does not have to implement the IWeakEventListener interface.
using System;
using System.Windows;
 
// Compile against .NET 4.5
 
namespace CSharpTest
{
    // MessageManager is an event publisher. It raises an event
    // when a new message is received. This code is the same as in 
    // the previous example.
    public class MessageManager
    {
        public event EventHandler<MessageInfoArgs> MessageReceived;
 
        public void NewMessage(string message)
        {
            RaiseMessageReceived(message);
        }
 
        protected virtual void RaiseMessageReceived(string message)
        {
            EventHandler<MessageInfoArgs> newMessageReceived = MessageReceived;
 
            // Verify if the MessageReceived delegate (the event handler) is not null.
            if (newMessageReceived != null)
            {
                // The first parameter contains the sender of the event.
                // The second parameter provides information about the event.
                newMessageReceived(this, new MessageInfoArgs(message));
            }
        }
    }
 
    public class MessageInfoArgs : EventArgs
    {
        public string Message { get; private set; }
 
        public MessageInfoArgs(string message)
        {
            this.Message = message;
        }
    }
 
    // The weak event listener subscribes to the MessageReceived event 
    // to be informed when a new message arrives.
    public class MessageClient
    {
        private string clientName;
 
        public MessageClient(string clientName)
        {
            this.clientName = clientName;
        }
 
        public void NewMessageReceived(object sender, MessageInfoArgs args)
        {
            Console.WriteLine("{0}: {1}", clientName, args.Message);
        }
    }
 
    class EntryPoint
    {
        public static void Main()
        {
            MessageManager manager = new MessageManager();
 
            MessageClient client1 = new MessageClient("John");
            WeakEventManager<MessageManager, MessageInfoArgs>.AddHandler(manager, 
                "MessageReceived", client1.NewMessageReceived);
 
            manager.NewMessage("First message - welcome!");
 
            MessageClient client2 = new MessageClient("Nick");
            WeakEventManager<MessageManager, MessageInfoArgs>.AddHandler(manager, 
                "MessageReceived", client2.NewMessageReceived);
 
            manager.NewMessage("Second message - still there?");
 
            // Unsubscribe John.
            WeakEventManager<MessageManager, MessageInfoArgs>.RemoveHandler(manager, 
                "MessageReceived", client1.NewMessageReceived);
 
            manager.NewMessage("Third message - good bye!");
        }
    }
}

The output is the same as in the previous example.

notes/wpf/other_topics.txt · Last modified: 2017/03/23 by leszek