WPF EventHandler fired on the wrong Element - wpf

I am puzzled by this:
I have made a very simple example:
MainWindow.xaml:
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="RichTextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RichTextBox">
<Grid Height="100" Width="200">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Label Background="Blue" Grid.Row="0">Label</Label>
<Border PreviewMouseDown="Border_PreviewMouseDown" Background="Red" Grid.Row="1">
<ScrollViewer x:Name="PART_ContentHost" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<RichTextBox>
<FlowDocument>
<FlowDocument.Blocks>
<Paragraph>
oaizeropiazuerpoaizeurpoaizeurpaozieurpaozieru
</Paragraph>
</FlowDocument.Blocks>
</FlowDocument>
</RichTextBox>
</Grid>
</Window>
MainWindow.xaml.cs:
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
namespace Test
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Border_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Debug.WriteLine("Click !");
}
}
}
now, as I explicitly put the PreviewMouseDown EventHandler on the Border and not on the label in my template, I expect that it will fire when I click on the (red) border of the control, but not when I click on the (blue) label.
However, the event is fired when I click on the (red) Border AND when I click on the (blue) label.
so why does the Label call an EventHandler that I explicitly attached to an other part of the controlTemplate (i.e.: the border)?
I've checked: If I remove the PreviewMouseDown="Border_PreviewMouseDown" code from the border's properties, the event is not fired on the label any more.
what am I missing here?
and what would be the right way to do? How can I design my controlTemplate so that the PreviewMouseDown Event is fired only by a sub-part of the templated control?
thanks in advance
Edit: following Snowbear's answer, I checked the originalSource of the event when I click on the Label. It is indeed the border. Why is this so? in what way is the border encapsulating the label in the template above? I specifically set them on different grid rows to avoid this, so how come?
Edit2 Just for the fun, I created a handler that only prints the sender/source/originalSource of the event, and I attached it in the template to The grid, the border and the scrollviewer.
Here Is what I get when I click ONCE (and only once) on the vertical scrollbar for instance:
Clic -- Sender: System.Windows.Controls.Grid -- OriginalSource: Microsoft.Windows.Themes.ScrollChrome -- Source: MyRichTextBox
Clic -- Sender: System.Windows.Controls.Border -- OriginalSource: Microsoft.Windows.Themes.ScrollChrome -- Source: MyRichTextBox
Clic -- Sender: System.Windows.Controls.ScrollViewer -- OriginalSource: Microsoft.Windows.Themes.ScrollChrome -- Source: MyRichTextBox
Clic -- Sender: System.Windows.Controls.Grid -- OriginalSource: Microsoft.Windows.Themes.ScrollChrome -- Source: System.Windows.Controls.ScrollViewer
Clic -- Sender: System.Windows.Controls.Border -- OriginalSource: Microsoft.Windows.Themes.ScrollChrome -- Source: System.Windows.Controls.ScrollViewer
Clic -- Sender: System.Windows.Controls.ScrollViewer -- OriginalSource: Microsoft.Windows.Themes.ScrollChrome -- Source: System.Windows.Controls.ScrollViewer
this clearly settles the matter: The event is indeed tunnelled twice, for some reason, First with the TemplatedParent (i.e.: the RichtextBox) as Source, and Then with the contentPresenter (i.e.: the ScrollViewer) as Source.
By Merlin's most baggy pants, I really wonder what went through the head of the MS Dev that programmed this...

Gee, you've uncovered a very strange behavior indeed. It appears that the mouse handling for all the elements that are part of a TextBoxBase control template can be revectored to the text content element and then tunnel/bubble from there. As a result, including a label in a rich text box control template means it will participate in mouse events on the rich text as though it were part of the rich text itself.
To work around this problem, you can use a ContentControl that includes the associated elements and then forwards its Content property to a "normal" TextBoxBase derived element. Here is a simplified XAML example. The first section reproduces the strange behavior in a simpler example and the second section shows how to use a ContentControl to work around the problem.
<Grid>
<StackPanel>
<TextBox Text="Some text">
<TextBox.Template>
<ControlTemplate TargetType="TextBox">
<StackPanel>
<Rectangle Fill="Green" Width="200" Height="50"/>
<Border x:Name="RedBorder" PreviewMouseDown="Border_PreviewMouseDown" Background="Red">
<AdornerDecorator x:Name="PART_ContentHost" />
</Border>
</StackPanel>
</ControlTemplate>
</TextBox.Template>
</TextBox>
<ContentControl Content="Some text">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<StackPanel>
<Rectangle Fill="Green" Width="200" Height="50"/>
<Border x:Name="RedBorder" PreviewMouseDown="Border_PreviewMouseDown" Background="Red">
<TextBlock Text="{TemplateBinding Content}"/>
</Border>
</StackPanel>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
</StackPanel>
</Grid>

You're missing bubbling events here. http://msdn.microsoft.com/en-us/library/ms742806.aspx#routing_strategies
You can check RoutedEventArgs.OriginalSource to determine which border was clicked.
UPDATE: Ok, it looks like I missed, but not completely. I've played for some time with your sample, it looks like RichTextBox (or probably TextBoxBase) does something with PART_ContentHost which forces tunnelling events to go towards this part. Your sample behaves well out of RichTextBox so I assume it does something with own template. Though I do not how to investigate it further without going into .Net sources.

Related

WPF Focus, can't get both logical and keyboard focus

I have been through several other postings about WPF and focus, and the best I can figure there's something I'm missing in my code. I'm working on an app using strict MVVM so I'm trying to avoid any code-behind in the view files (using attached behaviors when code-behind is necessary), but at this point even putting the focus code in the code-behind of the view it's not working.
I have an application with a main window and I'm trying to make a search window popup on a hot key. I'd like as soon as the user hits the hot key, the keyboard focus to be on the search text so it's just hotkey and then type your search term. Everything but the logical focus is working at this point, even though keyboard claims to have focus on the element.
I can't seem to get it to take both keyboard and logical focus at the same time from the code. However, if I hit Tab as soon as the search box appears, I'm put right into the text box.
Main window code:
<ribbon:RibbonWindow x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ribbon="clr-namespace:Microsoft.Windows.Controls.Ribbon;assembly=RibbonControlsLibrary"
xmlns:attached="clr-namespace:UserInterface.Attached"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF45"
xmlns:viewModels="clr-namespace:UserInterface.ViewModels"
xmlns:views="clr-namespace:UserInterface.Views"
xmlns:layout="clr-namespace:UserInterface.ViewModels.Layout"
xmlns:layout1="clr-namespace:UserInterface.Views.Layout"
MinHeight="560"
MinWidth="950"
WindowStartupLocation="CenterScreen"
Icon="{Binding Layout.IconPath}"
DataContext="{Binding Main, Source={StaticResource Locator}}"
FocusManager.FocusedElement="{Binding ElementName=LayoutControl}"
Title="{Binding Layout.Title}">
<!-- Ribbon menu shortcuts -->
<Window.InputBindings>
<KeyBinding Modifiers="Control" Key="T" Command="{Binding Layout.Commands[GlobalObjectSearch]}" />
</Window.InputBindings>
<Grid>
<ContentPresenter Content="{Binding Layout}" x:Name="LayoutControl">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type layout:MainViewModel}">
<layout1:MainView/>
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
</Grid>
</ribbon:RibbonWindow>
Code to make search window appear:
public SelfClosingDialogView ShowSelfClosingDialog(IWindowDialogViewModel dataContext)
{
dataContext.CheckWhetherArgumentIsNull(nameof(dataContext));
var view = new SelfClosingDialogView
{
DataContext = dataContext,
Owner = Application.Current?.MainWindow
};
view.Show();
return view;
}
Search window code (Reused, so generic):
<Window x:Class="UserInterface.Views.DialogViews.SelfClosingDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:attached="clr-namespace:UserInterface.Attached"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
SizeToContent="WidthAndHeight"
WindowStyle="None"
WindowStartupLocation="CenterOwner">
<!-- Allow view models to cause the window to close -->
<Window.Style>
<Style TargetType="{x:Type Window}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsClosed}" Value="true">
<!-- Executes close -->
<Setter Property="attached:WindowCloseBehavior.Close" Value="true" />
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Style>
<!-- Displays the passed-in view model -->
<Grid>
<ContentPresenter x:Name="DialogPresenter" Content="{Binding}" Margin="0" />
</Grid>
</Window>
Code for my search view:
<UserControl x:Class="UserInterface.Views.DialogViews.ObjectSearchView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dialogViewModels="clr-namespace:UserInterface.ViewModels.DialogViewModels"
xmlns:utils="clr-namespace:WPF.Utils"
xmlns:attached="clr-namespace:UserInterface.Attached"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance dialogViewModels:ObjectSearchViewModel}"
MinWidth="250"
Focusable="True"
FocusManager.IsFocusScope="True">
<UserControl.InputBindings>
<KeyBinding Key="Enter" Command="{Binding BrowseToObjectCommand}" />
<KeyBinding Key="Escape" Command="{Binding CloseWindowCommand}" />
</UserControl.InputBindings>
<UserControl.Resources>
<Style BasedOn="{StaticResource FormTextBlockStyle}" TargetType="TextBlock" />
</UserControl.Resources>
<StackPanel>
<TextBox Name="SearchText"
Focusable="True"
Text="{utils:ValidatingLiveBinding SearchText}"
attached:NavigatingListBoxBehavior.LinkedListBox="{Binding ElementName=SearchResults}">
</TextBox>
<ScrollViewer HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Auto"
MaxHeight="400">
<ListBox Name="SearchResults"
ItemsSource="{Binding SearchResults}"
SelectedItem="{Binding SelectedSearchItem}"
Visibility="{Binding HasSearchResults, Converter={StaticResource BooleanToVisibilityConverter}}"
attached:ItemsControlProperties.DoubleClickCommand="{Binding BrowseToObjectCommand}"
KeyboardNavigation.IsTabStop="False"
IsSynchronizedWithCurrentItem="True" />
</ScrollViewer>
</StackPanel>
</UserControl>
And finally, the code-behind hack I'm trying to attempt to get focus (plus debugging code so that I don't lose the focus switching back and forth to Visual Studio):
public partial class ObjectSearchView : UserControl
{
public ObjectSearchView()
{
InitializeComponent();
this.Loaded += this.OnLoad;
}
private void OnLoad(object sender, RoutedEventArgs e)
{
this.PrintFocusInfo();
FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), this.SearchText);
this.PrintFocusInfo();
this.SearchText.Focus();
this.PrintFocusInfo();
Keyboard.Focus(this.SearchText);
this.PrintFocusInfo();
}
[Conditional("DEBUG")]
private void PrintFocusInfo()
{
var logicalElement = FocusManager.GetFocusedElement(FocusManager.GetFocusScope(this.SearchText));
Debug.WriteLine("Current logical focus is on '{0}', of type '{1}' ({2})".FormatInvariantCulture((logicalElement as FrameworkElement)?.Name, logicalElement?.GetType().Name, logicalElement));
var focusedElement = Keyboard.FocusedElement;
Debug.WriteLine(
"Current Keyboard Focus is on '{0}', of type '{1}' ({2})".FormatInvariantCulture(
(focusedElement as FrameworkElement)?.Name,
focusedElement.GetType().Name,
focusedElement));
}
}
Output window contents:
Current logical focus is on '', of type '' ()
Current Keyboard Focus is on '', of type 'MainWindow' (UserInterface.Views.MainWindow)
Current logical focus is on '', of type '' ()
Current Keyboard Focus is on '', of type 'MainWindow' (UserInterface.Views.MainWindow)
Current logical focus is on '', of type '' ()
Current Keyboard Focus is on 'SearchText', of type 'TextBox' (System.Windows.Controls.TextBox)
Current logical focus is on '', of type '' ()
Current Keyboard Focus is on 'SearchText', of type 'TextBox' (System.Windows.Controls.TextBox)
I've tried to include everything I can think of, but I cannot get logical focus to show anything except null.
Here is the behavior I ultimately created which fixed this for me. There's still a lot I don't know about WHY this works... but if you're having a problem getting Focus to cooperate it looks like the key is catching it when IsVisible is set to true, and having the Dispatcher set the focus for you. I linked this event to the IsVisibleChanged element on the textbox (through an attached behavior).
private void SetFocusOnVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
this.Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(() => this.AssociatedObject.Focus()));
}
}

Behavior<T> AssociatedObject does not work correctly

I have a class implemnting Behavior<FrameworkElement> with
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.AllowDrop = true;
AssociatedObject.DragEnter += AssociatedObject_DragEnter;
AssociatedObject.DragOver += AssociatedObject_DragOver;
AssociatedObject.Drop += AssociatedObject_Drop;
}
And in the xaml I have
<Border Background="Turquoise">
<Grid Height="800" AllowDrop="True">
<i:Interaction.Behaviors>
<behaviors1:FrameworkElementDropBehavior></behaviors1:FrameworkElementDropBehavior>
</i:Interaction.Behaviors>
...
</Grid>
</Border>
I've defined the FrameworkElementDropBehavior in the Grid and I expect I can drop the same object on this Grid because the AssociatedObject should be the whole Grid. But what happens is I am only allowed to drop on part of the Grid where there's element defined, such as the blue, white or value part. I do used prism to inject the whole green Grid into the TabControl. Any ideas why I can only drop partly?
Just set the Grid's background property, for example, to Transparent.
<Grid Height="800" AllowDrop="True" Background="Transparent">
<i:Interaction.Behaviors>
<behaviors1:FrameworkElementDropBehavior/>
</i:Interaction.Behaviors>
</Grid>
Doing so, you enable the hit testing on the whole grid area including any empty regions without child controls.
I fixed the problem by setting the FrameworkElementDropBehavior to the Border instead .
<Border Background="Turquoise">
<i:Interaction.Behaviors>
<behaviors1:FrameworkElementDropBehavior></behaviors1:FrameworkElementDropBehavior>
</i:Interaction.Behaviors>
<Grid Height="800" AllowDrop="True">
...
</Grid>
</Border>

Style Setter Freezing Brush

I seem to have run into some behavior regarding WPF ResourceDictionaries, Brushes, and Styles (at least that's what I've noticed so far) that is counter to my understanding of how these things should work. Basically, if I reference a Brush from a Setter withing a Style in a ResourceDictionary it causes the Brush to become frozen. The example below illustrates this, as I get an InvalidOperationException when I try to change the Color on the shared Brush within my button's Click event handler. It should cause both Rectangle's color to change, as they both use the same shared Brush, but I get the exception instead.
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<SolidColorBrush x:Key="TestBrush" Color="Red" />
<Style TargetType="Rectangle">
<Setter Property="Fill" Value="{StaticResource TestBrush}" />
</Style>
</Window.Resources>
<StackPanel>
<Button Name="Button1" Content="Change Color" Click="Button1_Click" />
<Rectangle Height="20" />
<Rectangle Height="20" />
</StackPanel>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button1_Click(object sender, RoutedEventArgs e)
{
var brush = (SolidColorBrush)FindResource("TestBrush");
// InvalidOperationException Here. Brush is Frozen/Read-Only
brush.Color = Colors.Blue;
}
}
If I simply remove the Style (more specifically the Setter) and reference the Brush (still from the ResourceDictionary) directly from each Rectangle, I get the expected behavior of the Rectangles' colors changing in tandem from the button click event. See code below (button click event hanlder remains the same).
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<SolidColorBrush x:Key="TestBrush" Color="Red" />
</Window.Resources>
<StackPanel>
<Button Name="Button1" Content="Change Color" Click="Button1_Click" />
<Rectangle Height="20" Fill="{StaticResource TestBrush}" />
<Rectangle Height="20" Fill="{StaticResource TestBrush}" />
</StackPanel>
</Window>
I only see the Brush becoming frozen when it is referenced as a StaticResource from a Style's Setter. I can actaully reference the same Brush from other locations within the ResourceDictionary without it becoming frozen; i.e. the contents of ControlTemplates.
Can anyone please explain what is going on with this strange behavior and if it's by-design or a bug?
Thanks,
Brandon
...once a style has been applied, it is sealed and cannot be changed.
If you want to dynamically change a style that has already been
applied, you must create a new style to replace the existing one. For
more information, see the IsSealed property.
See http://msdn.microsoft.com/en-us/library/ms745683.aspx

Please teach me how to use ControlTemplate properly?

Ok, I think I am pretty noob on using ControlTemplate lol...
All I want is to template my every UserControl so every UserControl will have Delete, Save, Cancel buttons.
So now I am writting some test......
Try to make a border around the everything...
In App.xaml
<Application.Resources>
<ControlTemplate x:Key="DeleteSaveCancelTemplate">
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<Border BorderThickness="5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Black">
<ContentPresenter/>
</Border>
</Grid>
</ControlTemplate>
</Application.Resources>
Then in UserControl:
<UserControl ... Height="150" Width="300" Template="{StaticResource DeleteSaveCancelTemplate}" >
But all I see is just ...black... what did I do wrong? I thought I should use ContentPresenter to shows the content?
For most controls you use ContentPresenter. ItemsPresenter only applies to elements that extend ItemsControl.
So... I found the problem lol From this page
The ControlPresenter doens't bind to Content by default...
I need to write
<ContentPresenter Content="{TemplateBinding Property=ContentControl.Content}" />
Unless you have specify the TargetType, then you it will bind by default.

How to add scrolling buttons to a data bound, horiztonal list

So I have a List, which contains items. Right now they are thumbnails of pictures. I wanted this list to be bound to a changing list in code behind, so I used a Listbox. However, I needed this box to flow horizontally. So it is styled as a StackPanel. Lastly, I want buttons to control the scrolling, not scrollbars. That's the part that does not work Here's a code sample :
<UserControl x:Class="TestBench.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<Style x:Key="StackHorz" TargetType="ListBox">
<Style.Setters>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" Background="AliceBlue" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<ScrollViewer BorderBrush="DarkGreen" BorderThickness="2" VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Disabled">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<Button x:Name="_Next" Content="NEXT" Height="20" Width="40" VerticalAlignment="Bottom" HorizontalAlignment="Right"/>
<Button x:Name="_Prev" Content="PREV" Height="20" Width="40" VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<ListBox x:Name="TestList" Height="100" Width="800" VerticalAlignment="Top">
...Insert ListItems...
</ListBox>
</Grid>
In this example the listbox is not bound, but I do need to be able to set ItemsSource={Binding Content}. Code behind I tried was :
namespace TestBench
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
TestList.Style = this.Resources["StackHorzTop"] as Style;
_Next.Click += new RoutedEventHandler(_Next_Click);
_Prev.Click += new RoutedEventHandler(_Prev_Click);
}
void _Prev_Click(object sender, RoutedEventArgs e)
{
TestList.ScrollIntoView(TestList.Items[0]);
}
void _Next_Click(object sender, RoutedEventArgs e)
{
TestList.ScrollIntoView(TestList.Items[TestList.Items.Count - 1]);
}
}
}
But the ScrollIntoView does nothing. I also tried getting the ScrollViewer as a VisualTreeHelper.GetChild() of the list box, but scrolling there using ScrollToHorizontalOffset() does nothing either.
I know it's a weird way to set things up, but I need all 3 functionalities (Binding, Horizontal orientation, No scroll bars with button scrolling). Anyone know where I am going wrong on this one?
Thanks in advance,
Chart.
Maybe you can try putting your listbox in a ScrollViewer and style the ScrollViewer so that the scroll bars are not visible (only the buttons).
More info on the ScrollViewer's templatable parts can be found here.

Resources