prevent WPF style to be applied to parent during mouseover - wpf

I have the following user control which gets highlighted on mouse over:
<UserControl x:Class="P.WebEnt.Designer.CanvasControls.Container"
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:ig="http://schemas.infragistics.com/xaml"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Border.Style>
<Style>
<Setter Property="Border.Background" Value="White"/>
<Style.Triggers>
<Trigger Property="Border.IsMouseOver" Value="True">
<Setter Property="Border.Background" Value="Beige" />
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel Orientation="Vertical">
<ig:DragDropManager.DropTarget>
<ig:DropTarget IsDropTarget="True">
</ig:DropTarget>
</ig:DragDropManager.DropTarget>
<Label Content="" />
</StackPanel>
</Border>
At run-time, based on our scenario, same control could be added as a child to this control (nesting the same user control for 'n' number of levels).
The requirement is that if the same control is nested inside another, the mouseover on the child should highlight only child and not any of its ancestors. Currently, if I hover the mouse on inner-most control, it highlights all the ancestors.
Any help is appreciated.

here you go, use IsMouseDirectlyOver instead of IsMouseOver
<Trigger Property="Border.IsMouseDirectlyOver"
Value="True">
<Setter Property="Border.Background"
Value="Beige" />
</Trigger>
assuming that the concerned border is the only element nested inside other border, if there is other child elements in the inner most border it may not work as expected. in this case converters or attached behavior may help achieve the same.
EDIT
I attempted to solve the issue in a different way. as you have access to class itself, then we can perhaps do within the class
so change the trigger in style to DataTrigger
<Style>
<Setter Property="Border.Background"
Value="White" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsCurrent,RelativeSource={RelativeSource FindAncestor,AncestorType=UserControl}}"
Value="True">
<Setter Property="Border.Background"
Value="Beige" />
</DataTrigger>
</Style.Triggers>
</Style>
and add the following code in the code behind of Container class
protected override void OnMouseMove(MouseEventArgs e)
{
Debug.Print(e.Source.GetHashCode().ToString());
if (CurrentControl != e.Source)
CurrentControl = e.Source as Container;
e.Handled = true;
}
static Container _CurrentControl;
static Container CurrentControl
{
get
{
return _CurrentControl;
}
set
{
if (_CurrentControl != null)
_CurrentControl.IsCurrent = false;
if (value != null)
value.IsCurrent = true;
_CurrentControl = value;
}
}
public bool IsCurrent
{
get { return (bool)GetValue(IsCurrentProperty); }
set { SetValue(IsCurrentProperty, value); }
}
// Using a DependencyProperty as the backing store for IsCurrent. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsCurrentProperty =
DependencyProperty.Register("IsCurrent", typeof(bool), typeof(Container), new PropertyMetadata(false));
give it a try and see if that is what you are looking for.

Related

Trigger for custom dependency properties in Style

The problem
I defined a reusable control, MyControl, that extends a TextBox.
I want to set a Trigger to one of its dependency properties.
So I added a style to it, with Triggers.
But if I set the TargetType of the Style to MyControl, I get a XAML warning that 'MyControl' TargetType does not match type of element 'TextBlock'.
And if I set it to TextBlock, I get a compilation error that The member "MyDependencyProperty" is not recognized or is not accessible..
How can I define this style with the triggers?
Sample
C# code-behind
namespace UserControls.Local
{
public partial class MyControl : TextBlock
{
#region Trogdor
public static readonly DependencyProperty TrogdorProperty = DependencyProperty.Register(
"Trogdor", typeof (bool), typeof (MyControl), new PropertyMetadata(default(bool)));
public bool Trogdor
{
get { return (bool) GetValue(TrogdorProperty); }
set { SetValue(TrogdorProperty, value); }
}
#endregion
public MyControl()
{
InitializeComponent();
}
}
}
XAML
<TextBlock
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:local="clr-namespace:UserControls.Local"
mc:Ignorable="d"
Text="BOOP!"
x:Class="UserControls.Local.MyControl">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="Blue"/>
<Style.Triggers>
<Trigger Property="Trogdor" Value="True">
<Setter Property="Foreground" Value="DeepPink"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
The solution I found was to "fully qualify" the dependency property on the binding:
<Trigger Property="local:MyControl.Trogdor" Value="True">
Not sure if you are still looking for a solution, but the answer in this thread worked for me, while yours didn't.
It uses a DataTrigger with a binding on the root element, instead of a Trigger:
<DataTrigger Binding="{Binding Path=Highlight, RelativeSource={RelativeSource AncestorType={x:Type Elements:DataElement}}}" Value="True">
<Setter Property="Control.Background" Value="{DynamicResource EntryBoxHighlightBackground}"/>
</DataTrigger>
With your solution, I press the button and the value of the variable changes but the style trigger doesn't apply the changes, like it was not informed of the change in the variable.

Show popup with binding on DataGrid row mouse over

When a user hovers a row in a DataGrid I want to show a popup with some information about this row.
I've stuck how to bind DataTrigger to every row in dynamically populated DataGrid table.
I've found solutions only for tooltips but tooltips don't suit me because I need to have more control on popup (don't hide it immediately when a user moved a mouse cursor to another control, ability to click in popup with mouse etc.)
Here is XAML code where I'm trying to bind popup DataTrigger to every DataGrid row (I've put comments with questions in the code below)
<Window x:Class="VKPN.UI.Windows.TestWindow"
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:local="clr-namespace:VKPN.UI.Windows"
mc:Ignorable="d"
Title="TestWindow" SizeToContent="WidthAndHeight">
<Grid>
<Popup Name="UserPopup" Placement="RelativePoint" HorizontalOffset="-5" VerticalOffset="0"
PlacementTarget="{Binding ElementName=ThisUserControl}">
<Popup.Style>
<Style TargetType="Popup">
<Style.Triggers>
<!--How to specify binding to every DataGridTable row below?-->
<DataTrigger Binding="{Binding ElementName=DataGridTable, Path=???}" Property="IsMouseOver" Value="True">
<Setter Property="IsOpen" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Popup.Style>
<Label>
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<!--How to specify binding to every DataGridTable row below?-->
<DataTrigger Binding="{Binding ElementName=???}" Property="IsMouseOver" Value="True">
<!--DataGrid row has a column "id" which I want to show in the label. Did I do it correct below?-->
<Setter Property="Content" Value="{Binding RelativeSource={RelativeSource Mode=Self}, Path=DataContext.id}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</Popup>
<DataGrid Name="DataGridTable" ItemsSource="{Binding}" IsReadOnly="True" ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto">
</DataGrid>
</Grid>
</Window>
Please help me to figure out how to do it.
Create 2 AttachedProperty called RowPopup and ShowPopup.
RowPopup will hold a reference to Popup control, and ShowPopup will show/hide this Popup based on DataGridRow.IsMouseOver property. These are extremely easy to implement.
Create a Style with TargetType DataGridRow.
Example,
xmlns:local="clr-namespace:MyNamespace"
<Style TargetType="DataGridRow" x:Key="RowStyleKey">
<Setter Property="local:CustomADP.RowPopup" Value="{Binding ElementName=Popup1}"/>
<Setter Property="local:CustomADP.ShowPopup" Value="{Binding IsMouseOver, Mode=OneWay, RelativeSource={RelativeSource Self}}"/>
</Style>
<DataGrid RowStyle="{StaticResource RowStyleKey}" ... />
AttachedProperties Code :
public class CustomADP
{
/********* Set Popup to show ****************/
public static Popup GetRowPopup(DependencyObject obj)
{
return (Popup)obj.GetValue(RowPopupProperty);
}
public static void SetRowPopup(DependencyObject obj, Popup value)
{
obj.SetValue(RowPopupProperty, value);
}
public static readonly DependencyProperty RowPopupProperty =
DependencyProperty.RegisterAttached("RowPopup", typeof(Popup), typeof(CustomADP), new PropertyMetadata(null));
/************* Show Hide using IsOpen property ************/
public static bool GetShowPopup(DependencyObject obj)
{
return (bool) obj.GetValue(ShowPopupProperty);
}
public static void SetShowPopup(DependencyObject obj, bool value)
{
obj.SetValue(ShowPopupProperty, value);
}
// Using a DependencyProperty as the backing store for ShowPopup. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ShowPopupProperty =
DependencyProperty.RegisterAttached("ShowPopup", typeof(bool), typeof(CustomADP), new PropertyMetadata(false, new PropertyChangedCallback(ShowPopupCallback)));
private static void ShowPopupCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is DataGridRow))
return;
if (((DataGridRow)d).IsFocused == true)
{
Popup p = CustomADP.GetRowPopup(d);
p.IsOpen = Convert.ToBoolean(e.NewValue);
}
else
{
Popup p = CustomADP.GetRowPopup(d);
p.IsOpen = Convert.ToBoolean(e.NewValue);
}
}
}

Grid Hide/Show animation

I've got some User Control, which is grid with transparent background, on top of that there's another grid that is suppose to change its width when triggers are fired (ultimately it'll be a in/out animation). Purpose was to simply use something different than Visibility that was instantly hiding entire control without any animations Here's the property it's based on:
public static readonly DependencyProperty IsOpenedProperty = DependencyProperty.Register ("IsOpened", typeof (Boolean), typeof (SidePanel), new PropertyMetadata (false));
public bool IsOpened {
get {
if (GetValue (IsOpenedProperty) != null)
return (bool)GetValue (IsOpenedProperty);
else
return false;
}
set {
SetValue (IsOpenedProperty, value);
Console.WriteLine(value);
}
}
And the Grid itself:
<Grid>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Setter Property="Width" Value="50"/>
<Setter Property="Background" Value="Gray"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsOpened}" Value="True">
<Setter Property="Width" Value="350"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsOpened}" Value="False">
<Setter Property="Width" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<TextBlock Text="{Binding ElementName=Side, Path=Width}"/>
</Grid>
Console gives me corect output but triggers don't fire at all.
Just checked one more thing and when I set trigger for x:Null value then it's fired all the time, how is it possible as Get shouldn't ever return null value here?
Alright I just gave up on dependency property here, created private variable to hold the value and added data context with INotifyPropertyChanged interface implementation and now it works.

Resource 1 works, 2 doesn't

<ToggleButton Command="{Binding Path=Command}" Content="{Binding Path=DisplayName}" Template="{Utilities:BindableResource {Binding Path=TemplateResource}}">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Template" Value="{Utilities:BindableResource {Binding Path=SelectedTemplateResource}}" />
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
</ToggleButton>
The first bindable resource works for "TemplateResource" on the template property of togglebutton however "SelectedTemplateResource" does not work within the tiggers setter. This code is within a resourcedictionary where the actual resource is within a themed resourcedictionary.
I get an error saying key is null for xamlparseexception for the setter value. I've been stairing at this for hours but cannot figure out why it doesn't work... If I take out the style and replace the first binding with the second resource it does display proper however the binding within the style will not work.
Does anybody have any idea why?
EDIT
I just tried this but no luck.
<ToggleButton Command="{Binding Path=Command}" Content="{Binding Path=DisplayName}">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Style.Triggers>
<Trigger Property="IsChecked" Value="False">
<Setter Property="Template" Value="{Utilities:BindableResource {Binding Path=TemplateResource}}" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Template" Value="{Utilities:BindableResource {Binding Path=SelectedTemplateResource}}" />
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
</ToggleButton>
After finding out this really is not possible to do in pure xaml I brought out the c# and create a custom control... this is very basic and can be improved on and I will have change a bit of it but ultimately a custom control solves the issue so that you can hit the click event from within the resource dictionary and change the template on the fly.
public class TabButton : Button
{
public static readonly DependencyProperty SelectedTemplateProperty =
DependencyProperty.Register("SelectedTemplate", typeof(ControlTemplate), typeof(TabButton));
public ControlTemplate SelectedTemplate
{
get { return base.GetValue(SelectedTemplateProperty) as ControlTemplate; }
set { base.SetValue(SelectedTemplateProperty, value); }
}
public TabButton()
{
this.Click += new RoutedEventHandler(TabButton_Click);
}
~TabButton()
{
}
public void TabButton_Click(object sender, RoutedEventArgs e)
{
ControlTemplate template = (ControlTemplate)this.FindResource("Environmental Template Selected");
(sender as TabButton).Template = template;
}
}
Cheers.

How to use IsKeyboardFocusWithin and IsSelected together?

I have a style defined for my ListBoxItems with a trigger to set a background color when IsSelected is True:
<Style x:Key="StepItemStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Name="Border" Padding="0" SnapsToDevicePixels="true">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="#40a0f5ff"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This style maintains the selected item even when the ListBox and ListBoxItem loses focus, which in my case is an absolute must.
The problem is that I also want the ListBoxItem to be selected when one of its TextBox's child gets focused. To achieve this I add a trigger that sets IsSelected to true when IsKeyboardFocusWithin is true:
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True" />
</Trigger>
When I add this trigger the Item is selected when the focus is on a child TextBox, but the first behaviour disappears. Now when I click outside the ListBox, the item is de-selected.
How can I keep both behaviours?
When your listbox looses focus, it will set selected item to null because of your trigger. You can select on focus using some code behind that will not unselect when you loose focus.
XAML:
<Window x:Class="SelectedTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300">
<StackPanel>
<TextBox Text="Loose focus here" />
<ListBox Name="_listBox" ItemsSource="{Binding Path=Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" GotFocus="OnChildGotFocus">
<TextBox Text="{Binding .}" Margin="10" />
<TextBox Text="{Binding .}" Margin="10" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Name="Border" SnapsToDevicePixels="true" Background="Transparent">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="Red"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
</Window>
Code behind:
private void OnChildGotFocus(object sender, RoutedEventArgs e)
{
_listBox.SelectedItem = (sender as StackPanel).DataContext;
}
"When I add this trigger the Item is selected when the focus is on a child TextBox, but the first behaviour disappears. Now when I click outside the ListBox, the item is de-selected."
Actually, I don't think it has lost that original behavior. What I suspect is happening is you're clicking directly in the textbox from somewhere else so the underlying ListBoxItem never actually became selected. If it did however, you'd see the selection would still remain after you left as you want.
You can test this by forcing the ListBoxItem to be selected by clicking directly on it (side-note: you should always give it a background, even if just 'transparent' so it can receive mouse clicks, which it won't if it's null) or even just hitting 'Shift-Tab' to set the focus there, back from the textbox.
However, that doesn't solve your issue, which is that the TextBox gets the focus but doesn't let the underlying ListBoxItem know about it.
The two approaches you can use for that are an event trigger or an attached behavior.
The first is an event trigger on the IsKeyboardFocusWithinChanged event where you set 'IsSelected' to true if the keyboard focus changed to true. (Note: Sheridan's answer does a faux-change-notification but it should not be used in cases where you can multi-select in the list because everything becomes selected.) But even an event trigger causes issues because you lose the multi-select behaviors such as toggling or range-clicking, etc.
The other (and my preferred approach) is to write an attached behavior which you set on the ListBoxItem, either directly, or via a style if you prefer.
Here's the attached behavior. Note: You again would need to handle the multi-select stuff if you want to implement that. Also note that although I'm attaching the behavior to a ListBoxItem, inside I cast to UIElement. This way you can also use it in ComboBoxItem, TreeViewItem, etc. Basically any ContainerItem in a Selector-based control.
public class AutoSelectWhenAnyChildGetsFocus
{
public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached(
"Enabled",
typeof(bool),
typeof(AutoSelectWhenAnyChildGetsFocus),
new UIPropertyMetadata(false, Enabled_Changed));
public static bool GetEnabled(DependencyObject obj){ return (bool)obj.GetValue(EnabledProperty); }
public static void SetEnabled(DependencyObject obj, bool value){ obj.SetValue(EnabledProperty, value); }
private static void Enabled_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var attachEvents = (bool)e.NewValue;
var targetUiElement = (UIElement)sender;
if(attachEvents)
targetUiElement.IsKeyboardFocusWithinChanged += TargetUiElement_IsKeyboardFocusWithinChanged;
else
targetUiElement.IsKeyboardFocusWithinChanged -= TargetUiElement_IsKeyboardFocusWithinChanged;
}
static void TargetUiElement_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var targetUiElement = (UIElement)sender;
if(targetUiElement.IsKeyboardFocusWithin)
Selector.SetIsSelected(targetUiElement, true);
}
}
...and you simply add this as a property setter in your ListBoxItem's style
<Setter Property="behaviors:AutoSelectWhenAnyChildGetsFocus.Enabled" Value="True" />
This of course assumes you've imported an XML namespace called 'behaviors' that points to the namespace where the class is contained. You can put the class itself in a shared 'Helper' library, which is what we do. That way, everywhere we want it, its a simple property set in the XAML and the behavior takes care of everything else.
I figured out that IsKeyboardFocusWithin is not the best solution.
What I did in this case was to set the style on all of the controls used as DataTemplate to send the GotFocus-event to be handled in code behind. Then, in code behind, I searched up the visual tree (using VisualTreeHelper) to find the ListViewItem and set IsSelected to true. This way it does not "touch" the DataContext and works just with the View elements.
<Style TargetType="{x:Type Control}" x:Key="GridCellControlStyle">
...
<EventSetter Event="GotFocus" Handler="SelectListViewItemOnControlGotFocus"/>
...
private void SelectListViewItemOnControlGotFocus(object sender, RoutedEventArgs e)
{
var control = (Control)sender;
FocusParentListViewItem(control);
}
private void FocusParentListViewItem(Control control)
{
var listViewItem = FindVisualParent<ListViewItem>(control);
if (listViewItem != null)
listViewItem.IsSelected = true;
}
public static T FindVisualParent<T>(UIElement element) where T : UIElement
{
UIElement parent = element;
while (parent != null)
{
var correctlyTyped = parent as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}

Resources