Ok, that probably is a pretty dumb question but I have searched quite a while but could not find a solution for this that works...
I have a Custom control inherited from Control, which shall include code behind automation.
For examble select all text of a controls TextBox when selected, or generate a list of close matches when the content of that TextBox is changed.
public class vokDataGridEdit : Control
{
static vokDataGridEdit()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(vokDataGridEdit), new FrameworkPropertyMetadata(typeof(vokDataGridEdit)));
// Events internal to control (??? found on some how-to's)
EventManager.RegisterClassHandler(typeof(vokDataGridEdit), UIElement.GotKeyboardFocusEvent, new RoutedEventHandler(OnSelectContent), true);
}
// Dependecy Properties ...
// The Event that shall Fire when the TextBox gets Focus / Editing Mode
public static void SelectContent(object sender, RoutedEventArgs e)
{
if (sender is TextBox tb)
{
tb.SelectAll();
}
}
}
And the controls Style Template:
<ResourceDictionary xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ccont = "clr-namespace:App.Controls">
<!-- Default style for the Validation Buttons -->
<Style TargetType="{x:Type ccont:vokDataGridEdit}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ccont:vokDataGridEdit}">
<TextBox Text = "{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=ccont:vokDataGridEdit}}"
BorderThickness = "0"
ContextMenuService.Placement = "Right"
ContextMenuService.PlacementTarget = "{Binding Path=., RelativeSource={RelativeSource Self}}"
GotKeyboardFocus = "SelectContent">
<TextBox.ContextMenu>
<ContextMenu>
<ContextMenu.Template>
<ControlTemplate>
<Border CornerRadius = "5"
Background = "LightGray"
BorderThickness = "1"
BorderBrush = "Gray"
Padding = "2">
<StackPanel Orientation="Vertical">
<!-- Title -->
<TextBlock Text="Test" />
<!-- TODO: List of matches -->
<TextBox Text = "{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=ccont:vokDataGridEdit}}"
BorderThickness = "0" />
</StackPanel>
</Border>
</ControlTemplate>
</ContextMenu.Template>
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Question 1: How can I bind the event SelectContent (to select all TextBox content when it get focus, nb: it is part of a DataGrid for the CellEditingTemplate) to GotKeyboardFocus? Events are normaly fine in the Apps code, but for the Custom Control they do not work as there is no "Code Behind" really for the Style...
Question 2: Assuming I have a dependency Property containing an array of words. Based on the content of the TextBox, I would like to select a few words from the Array in the Dependency Property and pass them to a ListBox in the Custom Control (the Content of the ListBox shall only be managed by the Custom Control, not by anyone using that control. Is there a prefered/canonical MVVM schema on how to implement this?
Usually you should post only one question, not multiple. Regarding first one you can use EventSetter e.g. in implicit Style in UserControl's resources:
<Style TargetType="TextBox">
<EventSetter Event="GotKeyboardFocus" Handler="SelectContent"/>
</Style>
Regarding second question - implement a property, which is subset of your list and do update it accordingly e.g. if dependency property was changed(see property changed callback) or some another values were changed which the subset depends on.
Alternatively you could use a behavior for the TextBox and handle events you need there. See e.g. select all behavior:
public class SelectAllBehavior : Behavior<TextBox>
{
private bool _doSelectAll = false;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.GotFocus += AssociatedObject_GotFocus;
AssociatedObject.PreviewMouseUp += AssociatedObject_MouseUp;
AssociatedObject.PreviewMouseDown += AssociatedObject_MouseDown;
}
private void AssociatedObject_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (_doSelectAll)
{
AssociatedObject.Dispatcher.BeginInvoke((Action) (()=>{ AssociatedObject.SelectAll(); }));
}
_doSelectAll = false;
}
private void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
_doSelectAll = !AssociatedObject.IsFocused;
}
private void AssociatedObject_GotFocus(object sender, System.Windows.RoutedEventArgs e)
{
AssociatedObject.SelectAll();
}
protected override void OnDetaching()
{
AssociatedObject.GotFocus -= AssociatedObject_GotFocus;
AssociatedObject.PreviewMouseUp -= AssociatedObject_MouseUp;
AssociatedObject.PreviewMouseDown -= AssociatedObject_MouseDown;
base.OnDetaching();
}
}
Using this behavior in XAML:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
<TextBox Text="Some text">
<i:Interaction.Behaviors>
<local:SelectAllBehavior/>
</i:Interaction.Behaviors>
</TextBox>
Partial Solution:
Finaly I got event on the direct controls to work (controls in a ContextMenu still don't get EventHandlers...).
Apparently the point was using GetTemplateChild() in order to get the TextBox by name, and then associate the Event handlers:
<ResourceDictionary xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ccont = "clr-namespace:App.Controls">
<!-- Default style for the Validation Buttons -->
<Style TargetType="{x:Type ccont:vokDataGridEdit}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ccont:vokDataGridEdit}">
<TextBox Text = "{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=ccont:vokDataGridEdit}}"
BorderThickness = "0"
ContextMenuService.Placement = "Right"
ContextMenuService.PlacementTarget = "{Binding Path=., RelativeSource={RelativeSource Self}}"
x:Name = "TextBox">
<TextBox.ContextMenu>
<ContextMenu x:Name="Menu">
<ContextMenu.Template>
<ControlTemplate>
<Border CornerRadius = "5"
Background = "LightGray"
BorderThickness = "1"
BorderBrush = "Gray"
Padding = "2">
<StackPanel Orientation="Vertical">
<!-- Title -->
<TextBlock Text="Test" x:Name = "Test" />
<!-- TODO: List of matches -->
<TextBox Text = "{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=ccont:vokDataGridEdit}}"
BorderThickness = "0" />
</StackPanel>
</Border>
</ControlTemplate>
</ContextMenu.Template>
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
And Code (Dependency Properties not shown):
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace App.Controls
{
/// <summary>
/// DataGrid Edit control (see: https://www.c-sharpcorner.com/article/wpf-routed-events/ for RoutedEvents)
/// </summary>
public class vokDataGridEdit : Control
{
static vokDataGridEdit()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(vokDataGridEdit), new FrameworkPropertyMetadata(typeof(vokDataGridEdit)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Demo purpose only, check for previous instances and remove the handler first
if (this.GetTemplateChild("TextBox") is TextBox button)
{
button.PreviewMouseLeftButtonDown += this.SelectContentPreparation;
button.GotKeyboardFocus += this.SelectContent;
button.MouseDoubleClick += this.SelectContent;
//button.GotFocus += this.SelectContent;
}
}
/// <summary>
/// Prepare the Control to ensure it has focus before subsequent event fire
/// </summary>
private void SelectContentPreparation(object sender, MouseButtonEventArgs e)
{
if (sender is TextBox tb)
{
if (!tb.IsKeyboardFocusWithin)
{
e.Handled = true;
tb.Focus();
}
}
}
private void SelectContent(object sender, RoutedEventArgs e)
{
if (sender is TextBox tb)
{
e.Handled = true;
tb.SelectAll();
}
}
}
}
Related
I need to set focus to the content of a ContentPresenter. I can assume the ContentTemplate contains an IInputElement but not anything else about it.
Here is a much simplified example that illustrates the problem:
Main window:
<Window x:Class="FiedControlTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:Esatto.Wpf.CustomControls;assembly=Esatto.Wpf.CustomControls"
xmlns:local="clr-namespace:FiedControlTest">
<Window.Resources>
<Style TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Margin" Value="5"/>
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Background" Value="LightBlue"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Path=Options}" Name="cbOptions" DisplayMemberPath="Description"/>
<Button Content="Set focus" Click="SetFocus"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="TextBox:"/>
<TextBox Name="tbText" Text="A bare text box."/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="ContentPresenter:"/>
<ContentPresenter Content="TextBox in a ContentPresenter" Name="cpText">
<ContentPresenter.ContentTemplate>
<DataTemplate>
<TextBox Text="{Binding Mode=OneWay}" IsReadOnly="True"/>
</DataTemplate>
</ContentPresenter.ContentTemplate>
</ContentPresenter>
</StackPanel>
</StackPanel>
</Window>
Codebehind:
public partial class MainWindow : Window
{
public MainWindow()
{
this.DataContext = this;
Options = new ObservableCollection<Option>(new[]{
new Option(){TargetType=typeof(TextBox), Description="Bare Text Box"},
new Option(){TargetType=typeof(ContentPresenter), Description="Content Presenter"}
});
InitializeComponent();
cbOptions.SelectedIndex = 0;
}
private void SetFocus(object sender, RoutedEventArgs e)
{
var opt = cbOptions.SelectedItem as Option;
if (opt.TargetType == typeof(TextBox))
tbText.Focus();
if (opt.TargetType == typeof(ContentPresenter))
cpText.Focus();
}
public ObservableCollection<Option> Options { get; set; }
public class Option
{
public Type TargetType { get; set; }
public string Description { get; set; }
}
}
There's not much there. The bare TextBox takes focus as expected; the TextBox presented by the ContentPresenter does not.
I have tried adding Focusable="True" to the ContentPresenter but it doesn't have any visible effect. I've tried doing using Keyboard.SetFocus instead of UIElement.Focus but the behavior doesn't change.
How is this done?
In fact what you set focus is the ContentPresenter, not the inner TextBox. So you can use VisualTreeHelper to find the child visual element (the TextBox in this case) and set focus for it. However with IsReadOnly being true, you won't see any caret blinking (which may also be what you want). To show it in readonly mode, we can just set IsReadOnlyCaretVisible to true:
private void SetFocus(object sender, RoutedEventArgs e)
{
var opt = cbOptions.SelectedItem as Option;
if (opt.TargetType == typeof(TextBox))
tbText.Focus();
if (opt.TargetType == typeof(ContentPresenter)) {
var child = VisualTreeHelper.GetChild(cpText, 0) as TextBox;
if(child != null) child.Focus();
}
}
Here the edited XAML code with IsReadOnlyCaretVisible added:
<TextBox Text="{Binding Mode=OneWay}" IsReadOnly="True"
IsReadOnlyCaretVisible="True"/>
Note that the above code can only be applied in your specific case where you use a TextBox as the root visual of ContentTemplate of a ContentPresenter. In general case, you will need some recursive method to find the child visual (based on VisualTreeHelper), you can search more for this, I don't want to include it here because it's a very well-known problem/code in WPF to find visual child in WPF. (the italic phrase can be used as keywords to search for more).
I'm using an attached property to subscrive to the TargetUpdated event from a TextBlock, so I can be notified every time the text changes.
Using the following XAML:
<DataTemplate DataType="{x:Type targetUpdatedApp:Item}">
<targetUpdatedApp:LabelControl Text="{Binding Text, NotifyOnTargetUpdated=True}" Style="{StaticResource LabelTemplateStyle}"/>
</DataTemplate>
</Window.Resources>
<StackPanel>
<ListBox ItemsSource="{Binding Items}">
<!--<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text, NotifyOnTargetUpdated=True}" targetUpdatedApp:DesiredWidth.DesiredMinWidth="120"/>
</DataTemplate>
</ListBox.ItemTemplate>-->
</ListBox>
<Button Click="ButtonBase_OnClick">Button</Button>
</StackPanel>
Here is my AttachedProperty code:
public class DesiredWidth
{
public static readonly DependencyProperty DesiredMinWidthProperty =
DependencyProperty.RegisterAttached(
"DesiredMinWidth", typeof (double),
typeof (TextBlock), new PropertyMetadata(OnDesiredMinWidthChanged));
public static double GetDesiredMinWidth(DependencyObject obj)
{
return (double) obj.GetValue(DesiredMinWidthProperty);
}
public static void SetDesiredMinWidth(DependencyObject obj, double value)
{
obj.SetValue(DesiredMinWidthProperty, value);
}
static void OnDesiredMinWidthChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var textBlock = obj as TextBlock;
if (textBlock == null)
{
return;
}
if (args.NewValue != null)
{
textBlock.TargetUpdated += OnTextBoxTargetUpdated;
}
}
static void OnTextBoxTargetUpdated(object sender, System.Windows.Data.DataTransferEventArgs e)
{
if (e.Property == TextBlock.TextProperty)
{
}
}
}
The Items collection binded to the ListBox is an ObservableCollection and the items on it implement INotifyPropertyChanged.
If I uncomment the code of the ListBox.ItemTemplate and use it instead of the style it works ok, but I use LabelControl (which basically has a Text DependencyProperty) described on the style the TargetUpdated event subscribed on the AttachedProperty never gets fired.
Could someone give me some help on this issue?
Thanks in advance.
PS: Added from comment :
<Style x:Key="LabelTemplateStyle" TargetType="{x:Type argetUpdatedApp:LabelControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type targetUpdatedApp:LabelControl}">
<TextBlock Text="{TemplateBinding Text}" TargetUpdatedApp:DesiredWidth.DesiredMinWidth="120"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
NotifyOnTargetUpdated will only apply to the binding that it's set on, not bindings on elements contained within. You have this set on the LabelControl, but the event handler is attached to the TextBlock inside its ControlTemplate, which isn't notifying. Use:
<ControlTemplate TargetType="{x:Type targetUpdatedApp:LabelControl}">
<TextBlock Text="{TemplateBinding Text, NotifyOnTargetUpdated=True}" targetUpdatedApp:DesiredWidth.DesiredMinWidth="120"/>
</ControlTemplate>
Another way you could do this, which wouldn't require you to edit bindings, is to use another attached property instead of the element's own Text property:
<TextBlock DesiredWidth.Text="{Binding Text}" DesiredWidth.MinWidth="120" />
And in DesiredWidth you could then add a OnTextChanged callback that passes the value on to the TextBlock's Text and does any other handling needed.
So, I'm new to WPF, so maybe this is trivial but I can't figure it out.
I have a textbox.
<TextBox Text="{Binding NewRateAdjustment.Amount, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True,ValidatesOnExceptions=True}" Style="{StaticResource SurchargeAmountTextBox}" AttachedProperties:TextRules.TextRule ="{StaticResource numericRule}">
<i:Interaction.Behaviors>
<gl:NumericTextBoxBehavior DecimalLimit="2" />
</i:Interaction.Behaviors>
</TextBox>
Now, I need to change the DecimalLimit based upon the choice in a drop down on the page, so I created this Style.
<Style x:Key="SurchargeAmountTextBox" TargetType="{x:Type TextBox}" BasedOn="{StaticResource DefaultTextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=NewRateAdjustment.SelectedRateAdjustment.CalculationMethod.Name, UpdateSourceTrigger=PropertyChanged}" Value="Fuel">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=NewRateAdjustment.SelectedRateAdjustment.CalculationMethod.Name, UpdateSourceTrigger=PropertyChanged}" Value="">
<Setter Property="Background" Value="Green"/>
</DataTrigger>
</Style.Triggers>
</Style>
It seems to work for the colors. But how do write the Property Setter for the DecimalLimit???
You can't change a behavior property through a style, but you can try to apply the behavior through a style. The subject has been aborded in other questions, like this, but in your particular case, you want not only to apply the behavior through a style, but apply it with a different configuration depending on the data.
In the following approach I will use a attached property to accomplish that.
First, a dummy behavior similar to the one you are using:
public class NumericTextBoxBehavior : Behavior<TextBox>
{
public double DecimalLimit { get; set; }
protected override void OnAttached()
{
base.OnAttached();
// Dummy action so we can see the change when its applied
this.AssociatedObject.Text = this.DecimalLimit.ToString();
}
}
Now we create an attached property which is gonna be responsible for applying the behavior (you can do this in another class, or in the behavior class if you have access to it):
public static class NumericTextBoxBehaviorExtension
{
public static double? GetDecimalLimit(DependencyObject obj)
{
return (double?)obj.GetValue(DecimalLimitProperty);
}
public static void SetDecimalLimit(DependencyObject obj, double? value)
{
obj.SetValue(DecimalLimitProperty, value);
}
public static readonly DependencyProperty DecimalLimitProperty =
DependencyProperty.RegisterAttached("DecimalLimit", typeof(double?), typeof(NumericTextBoxBehaviorExtension), new PropertyMetadata(null, OnDecimalLimitChanged));
private static void OnDecimalLimitChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var behaviors = Interaction.GetBehaviors(sender);
// Remove the existing behavior instances
foreach (var old in behaviors.OfType<NumericTextBoxBehavior>().ToArray())
behaviors.Remove(old);
if (args.NewValue != null)
{
// Creates a new behavior and attaches to the target
var behavior = new NumericTextBoxBehavior { DecimalLimit = (double)args.NewValue };
// Apply the behavior
behaviors.Add(behavior);
}
}
}
Finally, the following test case will emulate your scenario. We have a TextBox style which is gonna apply a different DecimalLimit depending on the state of the TextBox's DataContext. The xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style x:Key="TextBoxStyle" TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding Name}" Value="Fuel">
<Setter Property="local:NumericTextBoxBehaviorExtension.DecimalLimit" Value="10.0"/>
</DataTrigger>
<DataTrigger Binding="{Binding Name}" Value="">
<Setter Property="local:NumericTextBoxBehaviorExtension.DecimalLimit" Value="1000.0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Button Content="Button" Height="23" HorizontalAlignment="Left" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="81,1,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Style="{StaticResource TextBoxStyle}"/>
</Grid>
In the code behind, we will make the button's action swap the TextBox's DataContext to verify that the style will update the behavior correctly:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
var target = this.textBox1.DataContext as Target;
if (this.textBox1.DataContext == null || string.IsNullOrEmpty(target.Name))
{
this.textBox1.DataContext = new Target() { Name = "Fuel" };
}
else
{
this.textBox1.DataContext = new Target() { Name = "" };
}
}
}
As you can see, the TextBox's Text will change every time we swap the DataContext, which means the style is indeed aplying the correct behavior.
I created an expander style that contains a checkbox in its header. The checkbox state is bound to an attached property:
<Style TargetType="{x:Type Expander}" x:Key="MyCheckboxExpander">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
(...)
<CheckBox x:Name="ExpanderHeaderChk" VerticalAlignment="Center" Margin="4,0,0,2"
IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(my:AP.IsChecked)}" />
(...)
I my view, inside the expander I have a stackpanel with a ComboBox.
Whenever the user checks the expander's checkbox, I wan't that the combobox gets the first item selected, on the oher hand whenever the user unchecks it, I wan't that the selecteditem of the combobox be null.
How can I accomplish this? I'm following the MVVM pattern, but since this is more a matter of the view, I'm open to code-behind suggestions.
Well, I think your design is not optimal. You see, you are trying to change the semantics of the Expander. The real expander doesn't have the semantics with additional checkbox, so the control you are creating is not an Expander any more.
I would suggest that you switch to a user control (or maybe a custom control, look at your semantics), and expose the needed event in your control's class. The XAML for the user control should be perhaps an expander with a checkbox.
Edit: example with UserControl (not tested)
(XAML)
<UserControl x:Class="namespace:MyCheckboxExpander">
<Expander>
...
<Checkbox x:Name="cb"/>
...
</Expander>
</UserControl>
(code-behind)
public class MyCheckboxExpander : UserControl
{
MyCheckboxExpander()
{
InitializeComponent();
cb.Check += OnCheck;
}
void OnCheck(object sender, whatever2 args)
{
if (CheckboxTriggered != null)
CheckboxTriggered(new EventArgs<whatever>);
}
public event EventArgs<whatever> CheckboxTriggered;
}
WPF is so powerfull framework, that you can solve you problem just using next style for Expander:
<Style x:Key="myExpanderStyle" TargetType="{x:Type Expander}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<StackPanel>
<CheckBox x:Name="PART_CheckBox" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
<ComboBox x:Name="PART_ComboBox" ItemsSource="{TemplateBinding Content}" />
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter TargetName="PART_ComboBox" Property="SelectedIndex" Value="0"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
SAMPLE:
<Expander Style="{StaticResource myExpanderStyle}">
<x:Array Type="sys:String">
<sys:String>1</sys:String>
<sys:String>2</sys:String>
<sys:String>3</sys:String>
</x:Array>
</Expander>
Just XAML! I like XAML declarativity.
But from MVVM perspective, this approach has one disadvantage - I can't cover this case with unit tests. So, I would prefer:
create view model with properties: IsChecked(bound to CheckBox),
SelectedItem(bound to ComboBox) and Source(ItemsSource for ComboBox) -
abstration of my real view without any references on controls;
write a logic in view model that set or unset SelectedItem depending
on IsChecked property;
cover that logic with unit test (yep, you can
even start with this point, if you like test first approach).
I followed the suggestion provided by #Baboon and I created a custom control with a routed event named CheckedChanged, this way I can access it through the view's xaml and code-behind:
[TemplatePart(Name = "PART_Expander", Type = typeof(Expander))]
[TemplatePart(Name = "PART_CheckBox", Type = typeof(CheckBox))]
public class MyCustomExpander : Expander
{
static MyCustomExpander()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomExpander), new FrameworkPropertyMetadata(typeof(MyCustomExpander)));
}
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.Register("IsChecked", typeof(bool), typeof(MyCustomExpander),
new UIPropertyMetadata(false));
#region Events
private CheckBox chkExpander = new CheckBox();
public CheckBox ChkExpander { get { return chkExpander; } private set { chkExpander = value; } }
public static readonly RoutedEvent CheckedChangedEvent = EventManager.RegisterRoutedEvent("ExtraButtonClick",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(MyCustomExpander));
public event RoutedEventHandler CheckedChanged
{
add { AddHandler(CheckedChangedEvent, value); }
remove { RemoveHandler(CheckedChangedEvent, value); }
}
void OnCheckedChanged(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(CheckedChangedEvent, this));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
CheckBox chk = base.GetTemplateChild("PART_CheckBox") as CheckBox;
if (chk != null)
{
chk.Checked += new RoutedEventHandler(OnCheckedChanged);
chk.Unchecked += new RoutedEventHandler(OnCheckedChanged);
}
}
#endregion
}
I want to thank to #Baboon and #Vlad for their help.
I have a DataTemplate that will be a templated ListBoxItem, this DataTemplate has a
ComboBox in it which when it has focus I want the ListBoxItem that this template
represents to become selected, this looks right to me. but sadly enough it doesn't work =(
So the real question here is within a DataTemplate is it possible to get or set the value
of the ListBoxItem.IsSelected property via a DataTemplate.Trigger?
<DataTemplate x:Key="myDataTemplate"
DataType="{x:Type local:myTemplateItem}">
<Grid x:Name="_LayoutRoot">
<ComboBox x:Name="testComboBox" />
</Grid>
<DataTemplate.Triggers>
<Trigger Property="IsFocused" value="true" SourceName="testComboBox">
<Setter Property="ListBoxItem.IsSelected" Value="true" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
<ListBox ItemTemplate="{StaticResource myDataTemplate}" />
I found a solution for your problem.
The problem is that when you have a control on your listboxitem, and the control is clicked (like for inputting text or changing the value of a combobox), the ListBoxItem does not get selected.
this should do the job:
public class FocusableListBox : ListBox
{
protected override bool IsItemItsOwnContainerOverride(object item)
{
return (item is FocusableListBoxItem);
}
protected override System.Windows.DependencyObject GetContainerForItemOverride()
{
return new FocusableListBoxItem();
}
}
--> Use this FocusableListBox in stead of the default ListBox of WPF.
And use this ListBoxItem:
public class FocusableListBoxItem : ListBoxItem
{
public FocusableListBoxItem()
{
GotFocus += new RoutedEventHandler(FocusableListBoxItem_GotFocus);
}
void FocusableListBoxItem_GotFocus(object sender, RoutedEventArgs e)
{
object obj = ParentListBox.ItemContainerGenerator.ItemFromContainer(this);
ParentListBox.SelectedItem = obj;
}
private ListBox ParentListBox
{
get
{
return (ItemsControl.ItemsControlFromItemContainer(this) as ListBox);
}
}
}
A Treeview does also have this problem, but this solution does not work for a Treeview, 'cause SelectedItem of Treeview is readonly.
So if you can help me out with the Treeview please ;-)
I found that I preferred to use this:
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True"></Setter>
</Trigger>
</Style.Triggers>
</Style>
Simple and works for all the listboxitems, regardless of what's inside.
No idea why your trigger don't work. To catch the get focus event of the combo box (or any control inside a listbox item) you can use attached routed events. You could put the code also in a derived listbox if you need this behavior in other parts of your application.
XAML:
<Window x:Class="RoutedEventDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Specialized="clr-namespace:System.Collections.Specialized;assembly=System"
xmlns:System="clr-namespace:System;assembly=mscorlib"
Height="300" Width="300">
<Window.Resources>
<DataTemplate x:Key="myDataTemplate">
<Grid>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}" Margin="5,0"/>
<ComboBox Width="50">
<ComboBoxItem>AAA</ComboBoxItem>
<ComboBoxItem>BBB</ComboBoxItem>
</ComboBox>
</StackPanel>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemTemplate="{StaticResource myDataTemplate}">
<ListBox.ItemsSource>
<Specialized:StringCollection>
<System:String>Item 1</System:String>
<System:String>Item 2</System:String>
<System:String>Item 3</System:String>
</Specialized:StringCollection>
</ListBox.ItemsSource>
</ListBox>
</Grid>
</Window>
Code behind hooking up to all got focus events.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace RoutedEventDemo
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
EventManager.RegisterClassHandler(typeof(UIElement),
GotFocusEvent,
new RoutedEventHandler(OnGotFocus));
}
private static void OnGotFocus(object sender, RoutedEventArgs e)
{
// Check if element that got focus is contained by a listboxitem and
// in that case selected the listboxitem.
DependencyObject parent = e.OriginalSource as DependencyObject;
while (parent != null)
{
ListBoxItem clickedOnItem = parent as ListBoxItem;
if (clickedOnItem != null)
{
clickedOnItem.IsSelected = true;
return;
}
parent = VisualTreeHelper.GetParent(parent);
}
}
}
}