WPF ToggleButton incorrect render behavior - wpf

What is going on here, and how to do a workaround?
Press MyToggleButton. Now it looks like instantly pressed (checked).
At the same time MyToggleButton_Checked handler disables the MyToggleButton.
Now press non-toggle Button 'Enable'. What we see? The MyToggleButton looks like it is enabled. OK. But wait, now it looks like non-pressed! Why?!
XAML:
<StackPanel>
<ToggleButton x:Name="MyToggleButton" Content="MyToggleButton" Checked="MyToggleButton_Checked"/>
<TextBlock Text="{Binding IsChecked, ElementName=MyToggleButton}" Margin="0,4"/>
<Button Name="EnableButton" Content="Enable" Click="EnableButton_Click"/>
<Button Name="DisableButton" Content="Disable" Click="DisableButton_Click"/>
</StackPanel>
Code-behind:
void MyToggleButton_Checked(object sender, RoutedEventArgs e)
{
MyToggleButton.IsEnabled = false;
}
void EnableButton_Click(object sender, RoutedEventArgs e)
{
MyToggleButton.IsEnabled = true;
}
void DisableButton_Click(object sender, RoutedEventArgs e)
{
MyToggleButton.IsEnabled = false;
}
UPD:
The only possible workaround on the moment is:
void EnableButton_Click(object sender, RoutedEventArgs e)
{
MyToggleButton.IsEnabled = true;
var controlTemplate = MyToggleButton.Template;
var buttonChrome = (Microsoft.Windows.Themes.ButtonChrome)controlTemplate.FindName("Chrome", MyToggleButton);
buttonChrome.RenderPressed = false;
buttonChrome.RenderPressed = true;
}
Are there any others?
UPD2:
Another workaround is:
void EnableButton_Click(object sender, RoutedEventArgs e)
{
MyToggleButton.IsEnabled = true;
var controlTemplate = MyToggleButton.Template;
MyToggleButton.Template = null;
MyToggleButton.Template = controlTemplate;
}
But the control is flickering at the moment of template substitution.

That's my solution for the problem, here is the video, the usage:
<StackPanel>
<ToggleButton x:Name="MyToggleButton" Content="MyToggleButton" Checked="MyToggleButton_Checked"
l:CorrectToggleButtonCheckedEnableBehavior.IsActive="True"/>
<Button Name="EnableButton" Content="Enable" Click="EnableButton_Click"/>
</StackPanel>
and the attached behavior source code:
public static class CorrectToggleButtonCheckedEnableBehavior
{
public static bool GetIsActive(ToggleButton toggleButton)
{
return (bool)toggleButton.GetValue(IsActiveProperty);
}
public static void SetIsActive(ToggleButton toggleButton, bool value)
{
toggleButton.SetValue(IsActiveProperty, value);
}
public static readonly DependencyProperty IsActiveProperty =
DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(CorrectToggleButtonCheckedEnableBehavior),
new UIPropertyMetadata(false, OnIsActiveChanged));
static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var toggleButton = (ToggleButton)d;
if (GetIsActive(toggleButton))
{
toggleButton.IsEnabledChanged += new DependencyPropertyChangedEventHandler(ToggleButton_IsEnabledChanged);
}
else
{
toggleButton.IsEnabledChanged -= new DependencyPropertyChangedEventHandler(ToggleButton_IsEnabledChanged);
}
}
static void ToggleButton_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var toggleButton = (ToggleButton)sender;
if (toggleButton.IsEnabled && (toggleButton.IsChecked ?? false))
{
Invalidate(toggleButton);
}
}
static void Invalidate(ToggleButton toggleButton)
{
var controlTemplate = toggleButton.Template;
toggleButton.Template = null;
toggleButton.Template = controlTemplate;
}
}

Related

How to mute SelectionChanged when the ItemsSource changes

I have a ComboBox with the ItemsSource data bound. This ComboBox also listens to the SelectionChanged event.
However, when the ItemsSource changes, the SelectionChanged event is raised. This happens only the when ItemsSource is a view.
Is there a way to have the SelectionChanged raised only when the user does it, not when the ItemsSource property changes?
If you do your data binding in code behind you can unsubscribe to the SelectionChanged while ItemsSource is being changed. See below sample code:
XAML:
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel DataContextChanged="OnDataContextChanged">
<Button Content="Change items" Click="OnClick" />
<ComboBox Name="_cb" />
</StackPanel>
</Window>
Code behind:
public partial class Window1
{
public Window1()
{
InitializeComponent();
_cb.SelectionChanged += OnSelectionChanged;
DataContext = new VM();
}
private void OnClick(object sender, RoutedEventArgs e)
{
(DataContext as VM).UpdateItems();
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
VM vm = DataContext as VM;
if (vm != null)
{
_cb.ItemsSource = vm.Items;
vm.PropertyChanged += OnVMPropertyChanged;
}
else
{
_cb.ItemsSource = null;
}
}
void OnVMPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Items")
{
_cb.SelectionChanged -= OnSelectionChanged;
_cb.ItemsSource = (DataContext as VM).Items;
_cb.SelectionChanged += OnSelectionChanged;
}
}
}
public class VM : INotifyPropertyChanged
{
public VM()
{
UpdateItems();
}
public event PropertyChangedEventHandler PropertyChanged;
private List<string> _items = new List<string>();
public List<string> Items
{
get { return _items; }
set
{
_items = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Items"));
}
}
}
public void UpdateItems()
{
List<string> items = new List<string>();
for (int i = 0; i < 10; i++)
{
items.Add(_random.Next().ToString());
}
Items = items;
}
private static Random _random = new Random();
}
I found that method :
private void comboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(!ComboBox.IsDropDownOpen)
{
return;
}
///your code
}

How programatically collapse Expander in Silverlight?

I need to be able to collapse expander on clicking anywhere outside the expander area. I am wondering what technique can be used. Any advice is highly appreciated.
Expander XAML - set binding to isExpanded:
<toolkit:Expander Header="Tasks" IsExpanded="{Binding IsExpanded}">
Code behind:
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value == _isExpanded)
return;
_isExpanded = value;
OnPropertyChanged("IsExpanded");
}
}
I assume you are using code behind.
Can you please try this in the Expander's MouseLeave & MouseEnter handlers
private void expander1_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
Application.Current.RootVisual.MouseLeftButtonDown += RootVisual_MouseLeftButtonDown;
}
void RootVisual_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
this.expander1.IsExpanded = false;
}
private void expander1_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
Application.Current.RootVisual.MouseLeftButtonDown -= RootVisual_MouseLeftButtonDown;
}

Can't get simple WPF drag and drop to work

For a simple test I want to drag a Button to a TextBox. I can start dragging the Button, but the Drop event is not raised. What am I missing?
Xaml:
<Window x:Class="DayPlanner.View.DnDTest"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DnDTest" Height="200" Width="200">
<StackPanel>
<Button Name="button"
Content="OK"
PreviewMouseLeftButtonDown="button_PreviewMouseLeftButtonDown"
PreviewMouseMove="button_PreviewMouseMove"/>
<TextBox Name="textBox"
AllowDrop="True"
DragEnter="textBox_DragEnter"
Drop="textBox_Drop"/>
</StackPanel>
</Window>
Code:
public partial class DnDTest : Window
{
public DnDTest()
{
InitializeComponent();
}
private Point dragStartPoint;
private void button_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
dragStartPoint = e.GetPosition(null);
}
private static bool IsDragging(Point dragStartPoint, MouseEventArgs e)
{
var diff = e.GetPosition(null) - dragStartPoint;
return
e.LeftButton == MouseButtonState.Pressed &&
(Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance);
}
private void button_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (IsDragging(dragStartPoint, e))
{
DragDrop.DoDragDrop(button, new DataObject("Button", button), DragDropEffects.Move);
e.Handled = true;
}
}
private void textBox_DragEnter(object sender, DragEventArgs e)
{
e.Handled = true;
}
private void textBox_Drop(object sender, DragEventArgs e)
{
var button = (Button)e.Data.GetData("Button");
textBox.Text = string.Format("[0]", button.Content.ToString());
e.Handled = true;
}
}
This might be some strange case, but to fix it, I needed to handle or dragging events, including the Preview versions.
Here's how to make it work.
Xaml:
<Window x:Class="DayPlanner.View.DnDTestBasic"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DnDTestBasic" Height="200" Width="200">
<StackPanel>
<Button Name="button"
Content="OK"
PreviewMouseLeftButtonDown="button_PreviewMouseLeftButtonDown"
PreviewMouseMove="button_PreviewMouseMove"/>
<TextBox Name="textBox"
AllowDrop="True"
PreviewDragEnter="textBox_Dragging"
DragEnter="textBox_Dragging"
PreviewDragOver="textBox_Dragging"
DragOver="textBox_Dragging"
Drop="textBox_Drop"/>
<TextBlock Name="status"
Text="No dragging"/>
</StackPanel>
</Window>
Code:
public partial class DnDTestBasic : Window
{
public DnDTestBasic()
{
InitializeComponent();
}
private Point dragStartPoint;
private void button_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
dragStartPoint = e.GetPosition(null);
status.Text = "New drag start position";
}
private static bool IsDragging(Point dragStartPoint, MouseEventArgs e)
{
var diff = e.GetPosition(null) - dragStartPoint;
return
e.LeftButton == MouseButtonState.Pressed &&
(Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance);
}
private void button_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (IsDragging(dragStartPoint, e))
{
status.Text = "Starting drag...";
DragDrop.DoDragDrop(button, new DataObject("Button", button), DragDropEffects.Copy);
status.Text = "Dragging done.";
e.Handled = true;
}
}
private void textBox_Dragging(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent("Button"))
e.Effects = DragDropEffects.Copy;
else
e.Effects = DragDropEffects.None;
e.Handled = true;
}
private void textBox_Drop(object sender, DragEventArgs e)
{
var button = (Button)e.Data.GetData("Button");
textBox.Text = string.Format("[{0}]", button.Content.ToString());
e.Handled = true;
}
}
I believe it has to do with the fact that when you start the drag event, the button control is capturing mouse input. Any mouse movements you do after that are registered to the button instead of to the application
I actually had a similar problem and ended up using MouseEnter/Leave events instead of the built in WPF drag/drop framework.

WPF DialogResult declaratively?

In WinForms we could specify DialogResult for buttons. In WPF we can declare in XAML only Cancel button:
<Button Content="Cancel" IsCancel="True" />
For others we need to catch ButtonClick and write code like that:
private void Button_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = true;
}
I am using MVVM, so I have only XAML code for windows. But for modal windows I need to write such code and I don't like this. Is there a more elegant way to do such things in WPF?
You can do this with an attached behavior to keep your MVVM clean. The C# code for your attached behavior might look something like so:
public static class DialogBehaviors
{
private static void OnClick(object sender, RoutedEventArgs e)
{
var button = (Button)sender;
var parent = VisualTreeHelper.GetParent(button);
while (parent != null && !(parent is Window))
{
parent = VisualTreeHelper.GetParent(parent);
}
if (parent != null)
{
((Window)parent).DialogResult = true;
}
}
private static void IsAcceptChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var button = (Button)obj;
var enabled = (bool)e.NewValue;
if (button != null)
{
if (enabled)
{
button.Click += OnClick;
}
else
{
button.Click -= OnClick;
}
}
}
public static readonly DependencyProperty IsAcceptProperty =
DependencyProperty.RegisterAttached(
name: "IsAccept",
propertyType: typeof(bool),
ownerType: typeof(Button),
defaultMetadata: new UIPropertyMetadata(
defaultValue: false,
propertyChangedCallback: IsAcceptChanged));
public static bool GetIsAccept(DependencyObject obj)
{
return (bool)obj.GetValue(IsAcceptProperty);
}
public static void SetIsAccept(DependencyObject obj, bool value)
{
obj.SetValue(IsAcceptProperty, value);
}
}
You can use the property in XAML with the code below:
<Button local:IsAccept="True">OK</Button>
An alternative way is to use Popup Control
Try this tutorial.

Interpret enter as tab WPF

I want to interpret Enter key as Tab key in whole my WPF application, that is, everywhere in my application when user press Enter I want to focus the next focusable control,except when button is focused. Is there any way to do that in application life circle? Can anyone give me an example?
Thanks a lot!
You can use my EnterKeyTraversal attached property code if you like. Add it to the top-level container on a WPF window and everything inside will treat enter as tab:
<StackPanel my:EnterKeyTraversal.IsEnabled="True">
...
</StackPanel>
Based on Richard Aguirre's answer, which is better than the selected answer for ease of use, imho, you can make this more generic by simply changing the Grid to a UIElement.
To change it in whole project you need to do this
In App.xaml.cs:
protected override void OnStartup(StartupEventArgs e)
{
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyDownEvent, new KeyEventHandler(Grid_PreviewKeyDown));
base.OnStartup(e);
}
private void Grid_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
var uie = e.OriginalSource as UIElement;
if (e.Key == Key.Enter)
{
e.Handled = true;
uie.MoveFocus(
new TraversalRequest(
FocusNavigationDirection.Next));
}
}
Compile.
And done it. Now you can use enter like tab.
Note: This work for elements in the grid
I got around woodyiii's issue by adding a FrameworkElement.Tag (whose value is IgnoreEnterKeyTraversal) to certain elements (buttons, comboboxes, or anything I want to ignore the enter key traversal) in my XAML. I then looked for this tag & value in the attached property. Like so:
if (e.Key == Key.Enter)
{
if (ue.Tag != null && ue.Tag.ToString() == "IgnoreEnterKeyTraversal")
{
//ignore
}
else
{
e.Handled = true;
ue.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
woodyiii, There is a function in the UIElement called PredictFocus() which by its name know its function, then you can check if that element is enabled or not so as to move the focus to it or not...
Here is Matt Hamilton's code, if anyone is wondering since his site is down apparently:
public class EnterKeyTraversal
{
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
static void ue_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
var ue = e.OriginalSource as FrameworkElement;
if (e.Key == Key.Enter)
{
e.Handled = true;
ue.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
private static void ue_Unloaded(object sender, RoutedEventArgs e)
{
var ue = sender as FrameworkElement;
if (ue == null) return;
ue.Unloaded -= ue_Unloaded;
ue.PreviewKeyDown -= ue_PreviewKeyDown;
}
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool),
typeof(EnterKeyTraversal), new UIPropertyMetadata(false, IsEnabledChanged));
static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ue = d as FrameworkElement;
if (ue == null) return;
if ((bool)e.NewValue)
{
ue.Unloaded += ue_Unloaded;
ue.PreviewKeyDown += ue_PreviewKeyDown;
}
else
{
ue.PreviewKeyDown -= ue_PreviewKeyDown;
}
}
}
Another, a more on/off implementation approach would be to use behaviors:
public class TextBoxEnterFocusesNextBehavior :
Behavior<TextBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewKeyDown += AssociatedObjectOnPreviewKeyDown;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewKeyDown -= AssociatedObjectOnPreviewKeyDown;
base.OnDetaching();
}
private void AssociatedObjectOnPreviewKeyDown(object sender, KeyEventArgs args)
{
if (args.Key != Key.Enter) { return; }
args.Handled = true;
AssociatedObject.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
Usage example:
<UserControl xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:behaviors="clr-namespace:Your.Namespace.To.Behaviors"
...>
<DockPanel>
<TextBox x:Name="TextBoxWithBehavior"
DockPanel.Dock="Top">
<b:Interaction.Behaviors>
<behaviors:TextBoxEnterFocusesNextBehavior />
</b:Interaction.Behaviors>
</TextBox>
<TextBox x:Name="TextBoxWithoutBehavior"
DockPanel.Dock="Top" />
<TextBox x:Name="AnotherTextBoxWithBehavior"
DockPanel.Dock="Top">
<b:Interaction.Behaviors>
<behaviors:TextBoxEnterFocusesNextBehavior />
</b:Interaction.Behaviors>
</TextBox>
</DockPanel>
</UserControl>
My solution:
public class MoveToNext : TriggerAction<DependencyObject>
{
protected override void Invoke(object parameter)
{
if (parameter is RoutedEventArgs routedEventArgs && routedEventArgs.OriginalSource is FrameworkElement element)
{
routedEventArgs.Handled = true;
element.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
}
Usage:
<StackPanel>
<i:Interaction.Triggers>
<i:KeyTrigger Key="Return">
<util:MoveToNext/>
</i:KeyTrigger>
</i:Interaction.Triggers>
<!-- put your controls here -->
</StackPanel>
If you want the behavior to be attached to only one control instead of all controls within a layouter, simply add the <i:Interaction.Triggers block to that specific control.

Resources