There are three types of routed events in WPF:
Event routing allows an event to be raised by one element but be handled in another one.
Related therminology:
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 && ((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));
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.
<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>
The triggers classes in WPF:
All the trigger classes derive from the TriggerBase class.
Simple Trigger
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
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
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>
There are two options of validation in WPF:
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:
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:
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:
<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
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:
There are two classes that can be used to browse through the logical and visual trees:
The VisualTreeHelper class contains a few useful methods:
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); } } } }
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:
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>.
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.