Setting a local style for an adorner - wpf

I have an adorner which should be placed beside it's adorned element. Depening on the value of the custom Position dependency property the adorner appears at the left or right side of the element.
I want to use a style to set the value of the Position property. But I can only do this if I add the style to the resources of the top-level control. If I place the style inside the resources of any child element it shows no effect.
Is there a way that I can set the adorner style on a per-element basis like in the following example?
<Window x:Class="StyledAdorner.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StyledAdorner">
<Window.Resources>
<Style TargetType="local:MyAdorner">
<Setter Property="Position" Value="Right" />
</Style>
<Style TargetType="Button">
<Setter Property="Content" Value="Adorn me!" />
<Setter Property="Margin" Value="15" />
<EventSetter Event="Click" Handler="AddAdorner" />
</Style>
</Window.Resources>
<StackPanel>
<Button />
<Button>
<Button.Resources>
<Style TargetType="local:MyAdorner">
<!-- This setter has no effect! -->
<Setter Property="Position" Value="Left" />
</Style>
</Button.Resources>
</Button>
</StackPanel>
</Window>
The only solution I can image is to scan the adorned element's resources for an adorner style. If there is one then check if there is a setter for the Position property and use this value. But that looks like a really dirty hack...
Code for AddAdorner handler that creates the adorner:
private void AddAdorner(object sender, RoutedEventArgs e)
{
new MyAdorner((UIElement)sender);
}
Constructor for MyAdorner
private Path _indicator = new Path { /* details omitted */ };
public MyAdorner(UIElement adornedElement) : base(adornedElement)
{
AdornerLayer.GetAdornerLayer(AdornedElement).Add(this);
AddVisualChild(_indicator);
InvalidateMeasure();
InvalidateArrange();
}

I could solve my problem by turning the Position property into an attached property:
public static readonly DependencyProperty PositionProperty = DependencyProperty.RegisterAttached(
"Position",
typeof(AdornerPosition),
typeof(MyAdorner),
new FrameworkPropertyMetadata(AdornerPosition.Right, UpdateAdornerLayerLayout));
Now, I just have to set the desired value on the adorned element:
<Button local:MyAdorner.Position="Left">
The property is evaluated in adorner`s ArrangeOverride method when the position for the adorner is calculated.
Note that the UpdateAdornerLayerLayout property changed callback is neccessary to force a layout update for the adorner layer when the property changes:
private static void UpdateAdornerLayerLayout(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is UIElement element)
{
var layer = AdornerLayer.GetAdornerLayer(element);
layer?.Update();
}
}

Related

Making a button content change within user control on click

I realise that WPF is still a bucket of magic to me. The problem seems to be simple. I have a user control with a button. I would like to change the button content (text) on click.
If I open the form with user control without initialising the button value and then say in
void Button_Click(object sender, RoutedEventArgs e)
{
Button.Content = "New Value";
}
it works.
If I initialise the button value dynamically in the control constructor by Button.Content = "Init Value", the second bit Button.Content = "New Value"
never happens (it happens, but button text does not show the change ever again, at least that what it seems).
So I decided to use a binding. Declared ButtonText property in MyUserControl (+ the corresponding DependencyProperty with getter and setter) and tried to do ButtonText = "Init Value"; in constructor and ButtonText = "New Value"; in Button_Click(). The first one works, the second one still does not. I assume because of the wrong data context in Button_Click()?
In MyUserControl I tried a few things including
<Button x:Name="Button"
Content="{Binding Path=ButtonText, RelativeSource={RelativeSource AncestorType=UserControl}}"
Click="Button_Click" />
<Button x:Name="Button"
Content="{Binding Path=ButtonText, Element=MyUserControl}"
Click="Button_Click" />
and nothing seems to work.
What is the easiest way to achieve what I need, i.e. both dynamic initialisation and dynamic change? With an explanation, if possible, please, why my first (direct) approach does not work and what the binding approach is missing.
Thanks a lot!
EDIT:
An alternative would be to use triggers.
This example works:
<Style x:Key="ButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Content" Value="Init Value"/>
<Style.Triggers>
<Trigger Property="IsClicked" Value="True">
<Setter Property="Content" Value="New Value" />
</Trigger>
</Style.Triggers>
</Style>
My problem is that I need a DataTrigger, like this
<Style x:Key="ButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Content" Value="Init Value"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=MyUserControlProperty, ElementName=MyUserControl}" Value="True">
<Setter Property="Content" Value="New Value" />
</DataTrigger>
</Style.Triggers>
</Style>
How do I make MyUserControlProperty value change propagate properly?
The funniest thing is if I open the control as a new form the initialisation of Button.Content = "Init Value" does not screw things up and everything just works. What the? Why is this simple task so hard and why so many behaviours?
Don't set Button.Content directly, when you have a binding set on it. If you set Button.Content in the Constructor you effectively remove the binding.
This works:
<Window x:Class="StackOverflow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<Button Content="{Binding Path=ButtonText1, RelativeSource={RelativeSource AncestorType=Window}}" Click="Button1_OnClick"></Button>
</StackPanel>
</Window>
using System.Windows;
namespace StackOverflow
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
ButtonText1 = "Paul";
}
private void Button1_OnClick(object sender, RoutedEventArgs e)
{
ButtonText1 = "Maria";
}
public string ButtonText1
{
get => (string)GetValue(ButtonText1Property);
set => SetValue(ButtonText1Property, value);
}
// Using a DependencyProperty as the backing store for ButtonText1. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ButtonText1Property =
DependencyProperty.Register("ButtonText1", typeof(string), typeof(MainWindow), new PropertyMetadata("Peter"));
}
}

Height setter ignored on custom Window

I added a Custom Control to my project and changed the parent type to Window. I show it on a button click.
I have style setters for height and width, but only the one defined first in the xaml has effect. The other shows larger than styled.
Anyone know what's happening here?
In generic.xaml:
<Style TargetType="{x:Type local:ChildWindow}">
<Setter Property="Width" Value="300"/>
<Setter Property="Height" Value="300"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ChildWindow}">
<TextBlock Background="White">Child window</TextBlock>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
ChildWindow.cs is default except the parent is now "Window":
public class ChildWindow : Window
{
static ChildWindow()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ChildWindow), new FrameworkPropertyMetadata(typeof(ChildWindow)));
}
}
MainWindow.xaml.cs shows the ChildWindow:
private void Button_Click(object sender, RoutedEventArgs e)
{
var childWindow = new ChildWindow();
childWindow.Show();
}
Workaround:
I haven't found why it behaves like that, or how to make the style setters work, but a workaround is to get rid of the style setters and set the default values for the Width and Height dependency properties. I did this in the static constructor:
static ChildWindow()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ChildWindow), new FrameworkPropertyMetadata(typeof(ChildWindow)));
// Get rid of the style setters and add this:
WidthProperty.OverrideMetadata(typeof(ChildWindow), new FrameworkPropertyMetadata(300.0));
HeightProperty.OverrideMetadata(typeof(ChildWindow), new FrameworkPropertyMetadata(300.0));
}

How to style child of a UserControl in XAML in Silverlight?

I have a UserControl MyParentControl which has another control inside (TreeView). I expose this control as a dep property say TreeView MyChildControl.
Then in XAML which uses MyParentConrol I want to access all the TreeView properties, for example Style.
I want to write something like:
<my:MyParentControl>
<my:MyParentControl.MyChildControl.Style>
<Style />
</my:MyParentControl.MyChildControl.Style>
</my:MyParentControl>
Is there a way to achieve that?
By exposing the DependencyProperty for your inner control you have solved half of the problem - ie you can set individual properties in xaml.
The next step is to have those property setters affect the child control.
There are two options to achieve that.
In your control template, define your child control and use Bindings on each property you want to set.
Define a container element in your parent control template and set it's content to your child whenever the dependency property changes.
Although both of these methods could work, you may find that the solution involving the least amount of code, and the greatest amount of flexibility, is to expose a Style property for your child control and apply that in the control template.
public class ParentControl : Control
{
public Style ChildControlStyle
{
get { return (Style)GetValue(ChildControlStyleProperty); }
set { SetValue(ChildControlStyleProperty, value); }
}
public static readonly DependencyProperty ChildControlStyleProperty =
DependencyProperty.Register("ChildControlStyle",
typeof(Style),
typeof(ParentControl),
new PropertyMetadata(null));
}
<Style TargetType="ParentControl">
<Setter Property="ChildControlStyle">
<Setter.Value>
<Style TargetType="ChildControl">
<!-- setters -->
</Style>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ParentControl">
<Grid>
<ChildControl Style="{TemplateBinding ChildControlStyle}" />
<!-- other stuff -->
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
You would get that effect by writing XAML like this:
<my:MyParentControl>
<my:MyParentControl.Resources>
<Style TargetType="my:MyChildControl">
<Setter Property="Background" Value="Red"/>
</Style>
</my:MyParentControl.Resources>
<my:MyParentControl>
In this example, this XAML creates a MyParentControl in which all the children of type MyChildControl have red backgrounds.

WPF setting keyboard-focus without code behind at runtime

I searched a lot on that topic but couldnt really find a solution for this using no code behind. I know some would say using code-behind for this view related things is totally ok, but nevertheless i would like to avoid it.
I have a usercontrol which shows a "dialog" with a single textbox and an OK button. That dialog is a simple usercontrol that is placed on top of all others. By default the usercontrols visibility is set to collapsed. I would like to set the keyboardfocus to the textbox on the dialog usercontrol if the usercontrol gets visible. Is there any way to do this completely in xaml? Since my dialog-control is not visible at the time when the control is loaded, simply setting
FocusManager.FocusedElement="{Binding ElementName=tbID}"
will not work. I tried to use some kind of visibility trigger:
<TextBox Grid.Column="3"
Grid.Row="5"
Name="tbID"
VerticalAlignment="Center">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Visibility" Value="Visible">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=tbID}" />
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
but this doesnt work either. The trigger gets fired but the textbox doesnt get the focus. I would really appreciate any suggestions on that. Thanks in advance!
You could try using an attached behavior to set the focus. Here's some sample code:
public static class Focus
{
public static readonly DependencyProperty ShouldFocusWhenVisibleProperty =
DependencyProperty.RegisterAttached("ShouldFocusWhenVisible", typeof (bool), typeof (Focus), new PropertyMetadata(default(bool), ShouldFocusWhenVisibleChanged));
private static void ShouldFocusWhenVisibleChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var uiElement = sender as UIElement;
if (uiElement == null) return;
var shouldFocus = GetShouldFocusWhenVisible(uiElement);
if (shouldFocus)
{
UpdateFocus(uiElement);
uiElement.IsVisibleChanged += UiElementOnIsVisibleChanged;
}
else
uiElement.IsVisibleChanged -= UiElementOnIsVisibleChanged;
}
private static void UiElementOnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var uiElement = sender as UIElement;
if (uiElement == null) return;
UpdateFocus(uiElement);
}
private static void UpdateFocus(UIElement uiElement)
{
if (!uiElement.IsVisible) return;
Keyboard.PrimaryDevice.Focus(uiElement);
}
public static void SetShouldFocusWhenVisible(UIElement uiElement, bool value)
{
uiElement.SetValue(ShouldFocusWhenVisibleProperty, value);
}
public static bool GetShouldFocusWhenVisible(UIElement uiElement)
{
return (bool)uiElement.GetValue(ShouldFocusWhenVisibleProperty);
}
}
Then, you apply the following code to the TextBox in your dialog: <TextBox local:Focus.ShouldFocusWhenVisible="True" />. Note that local: will need to be a reference to the namespace of the Focus class above.
I think you want to bind to the UserControl Visibility property not the TextBox
Example
<UserControl x:Class="WpfApplication7.IconButton"
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"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="200" Name="_this">
<Grid>
<TextBox Name="tbID" Margin="0,12,0,0" VerticalAlignment="Top">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=_this, Path=Visibility}" Value="Visible">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=tbID}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</Grid>
</UserControl>

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