My scenario, simplified: I have a ListView containing rows of Employees, and in each Employee row, there are buttons "Increase" and "Decrease" adjusting his salary.
Pretend that in my program, double-clicking an Employee row means "fire this person".
The problem is that while I'm clicking "Increase" rapidly, this triggers a double click event on the ListViewItem. Naturally, I don't want to fire people when I'm just increasing their salary.
According to how all other events work, I expect to be able to solve this by setting Handled=true on the event. This, however, doesn't work. It appears to me that WPF generates two separate, completely unlinked, double click events.
The following is a minimal example to reproduce my issue. The visible components:
<ListView>
<ListViewItem MouseDoubleClick="ListViewItem_MouseDoubleClick">
<Button MouseDoubleClick="Button_MouseDoubleClick"/>
</ListViewItem>
</ListView>
And the handler code:
private void Button_MouseDoubleClick(object s, MouseButtonEventArgs e) {
if (!e.Handled) MessageBox.Show("Button got unhandled doubleclick.");
e.Handled = true;
}
private void ListViewItem_MouseDoubleClick(object s, MouseButtonEventArgs e) {
if (!e.Handled) MessageBox.Show("ListViewItem got unhandled doubleclick.");
e.Handled = true;
}
After firing up this program and double-clicking the listed button, both messageboxes show up in sequence. (Also, the button is stuck in the down position after this.)
As a "fix" I can, on the ListViewItem handler, inspect the visual tree attached to the event and check that "there is a button there somewhere" and thus discard the event, but this is a last resort. I want to at least understand the issue before coding such a kludge.
Does anyone know why WPF does this, and an elegant idiomatic way to avoid the problem?
I think you'll find that the MouseDoubleClick event is an abstraction on top of the MouseDown event. That is, if two MouseDown events occur in quick enough succession, the MouseDoubleClick event will also be raised. Both the Button and ListViewItem appear to have this logic, so that explains why you're seeing two distinct MouseDoubleClick events.
As per MSDN:
Although this routed event seems to
follow a bubbling route through an
element tree, it actually is a direct
routed event that is raised along the
element tree by each UIElement. If you
set the Handled property to true in a
MouseDoubleClick event handler,
subsequent MouseDoubleClick events
along the route will occur with
Handled set to false.
You could try handling MouseDown on the Button and setting that to handled so that it doesn't propagate to the ListViewItem.
Wish I could verify this myself but I'm .NET-less at the moment.
The MSDN documentation for the MouseDoubleClick does give a suggestion on how to keep the MouseDoubleClick event from bubbling up:
Control authors who want to handle
mouse double clicks should use the
MouseLeftButtonDown event when
ClickCount is equal to two. This will
cause the state of Handled to
propagate appropriately in the case
where another element in the element
tree handles the event.
So you could hanlde the MouseLeftButtonDown event and set hanged to true if ClickCount is two. But this fails on Buttons because they already handle the MouseLeftButtonDown and don't raise that event.
But there is still the PreviewMouseLeftButtonDown event. Use that on your buttons to set handled to true when ClickCount equals two as below:
private void Button_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
if (e.ClickCount == 2)
e.Handled = true;
}
Since there have been no definite answers to this question, this is the workaround I ended up using:
protected override void ListViewItem_MouseDoubleClick(MouseButtonEventArgs e) {
var originalSource = e.OriginalSource as System.Windows.Media.Visual;
if (originalSource.IsDescendantOf(this)) {
// Test for IsDescendantOf because other event handlers can have changed
// the visual tree such that the actually clicked original source
// component is no longer in the tree.
// You may want to handle the "not" case differently, but for my
// application's UI, this makes sense.
for (System.Windows.DependencyObject depObj = originalSource;
depObj != this;
depObj = System.Windows.Media.VisualTreeHelper.GetParent(depObj))
{
if (depObj is System.Windows.Controls.Primitives.ButtonBase) return;
}
}
MessageBox.Show("ListViewItem doubleclicked.");
}
Class names are here unnecessarily typed with full namespaces for documentation purposes.
Well it may not be elegant or idiomatic, but you might like it better than your current workaround:
int handledTimestamp = 0;
private void ListViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (e.Timestamp != handledTimestamp)
{
System.Diagnostics.Debug.WriteLine("ListView at " + e.Timestamp);
handledTimestamp = e.Timestamp;
}
e.Handled = true;
}
private void Button_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (e.Timestamp != handledTimestamp)
{
System.Diagnostics.Debug.WriteLine("Button at " + e.Timestamp);
handledTimestamp = e.Timestamp;
}
e.Handled = true;
}
The weird thing is this doesn't work if you don't set e.Handled = true. If you don't set e.Handled and put a breakpoint or a Sleep into the button's handler, you will see the delay in the ListView's handler. (Even without an explicit delay there will still be some small delay, enough to break it.) But once you set e.Handled it doesn't matter how long of a delay there is, they will have the same timestamp. I'm not sure why this is, and I'm not sure if this is documented behavior that you can rely on.
Control.MouseDoubleClick is not a bubble event but a direct event.
Since checking this question with Snoop, which is a tool for browsing visual trees and routed events, I see that Control.MouseDoubleClick events of 'ListView' and 'ListBoxItem' are fired at one time. You could check with this Snoop tool.
First, to find an answer, it is needed to check that both event arguments of the MouseDoublClick are same objects. You would expect they are same objects. If it is true, it is very strange as your question, but they are not same instances. We can check it with following codes.
RoutedEventArgs _eventArg;
private void Button_MouseDoubleClick(object s, RoutedEventArgs e)
{
if (!e.Handled) Debug.WriteLine("Button got unhandled doubleclick.");
//e.Handled = true;
_eventArg = e;
}
private void ListViewItem_MouseDoubleClick(object s, RoutedEventArgs e)
{
if (!e.Handled) Debug.WriteLine("ListViewItem got unhandled doubleclick.");
e.Handled = true;
if (_eventArg != null)
{
var result = _eventArg.Equals(e);
Debug.WriteLine(result);
}
}
It means that the event argument of the MouseDoublClick is created newly at somewhere, but I don't understand deeply why it is.
To be clearer, let's check for the event argument of the BottonBase.Click. It will be return the true about checking same instances.
<ListView>
<ListViewItem ButtonBase.Click="ListViewItem_MouseDoubleClick">
<Button Click="Button_MouseDoubleClick" Content="click"/>
</ListViewItem>
</ListView>
If you only focus on the execution as you mentioned there'll be lots of solutions. As above, I think that using the flag(_eventArg) is also good choice.
I've just had this same problem. There is a simple but non-obvious solution.
Here is how double click is raised by Control ....
private static void HandleDoubleClick(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount == 2)
{
Control control = (Control)sender;
MouseButtonEventArgs mouseButtonEventArgs = new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, e.ChangedButton, e.StylusDevice);
if (e.RoutedEvent == UIElement.PreviewMouseLeftButtonDownEvent || e.RoutedEvent == UIElement.PreviewMouseRightButtonDownEvent)
{
mouseButtonEventArgs.RoutedEvent = Control.PreviewMouseDoubleClickEvent;
mouseButtonEventArgs.Source = e.OriginalSource;
mouseButtonEventArgs.OverrideSource(e.Source);
control.OnPreviewMouseDoubleClick(mouseButtonEventArgs);
}
else
{
mouseButtonEventArgs.RoutedEvent = Control.MouseDoubleClickEvent;
mouseButtonEventArgs.Source = e.OriginalSource;
mouseButtonEventArgs.OverrideSource(e.Source);
control.OnMouseDoubleClick(mouseButtonEventArgs);
}
if (mouseButtonEventArgs.Handled)
{
e.Handled = true;
}
}
}
So if you handle PreviewMouseDoubleClick setting e.Handled = true on the child control MouseDoubleClick won't fire on the parent control.
You cannot easily change the way double clicking events get fired because they are dependent on user settings and that delay is customized in control panel.
You should checkout RepeatButton that allows you to press's button and while it is pressed it generates multiple click events in regular sequence.
In case if you want to customize event bubbling then you should search for Preview events that allows you to block propogation of events. What are WPF Preview Events?
Related
I add a function that adds text to FlowDocument when the mouse clicks.
There is no Click event in FlowDocument, so I listen to FlowDocument.MouseLeftButtonDown and MouseLeftButtonUp and check whether the mouse moves between down and up. When I click the mouse left button, the text successfully adds. However, I can't select any text in the FlowDocument.
I tried PreviewMouseLeftButtonDown and PreviewMouseLeftButtonUp. The behavior is the same. Isn't there a PostMouseLeftButtonDown?
My Code:
Point mouseDownPoint;
private void doc_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
mouseDownPoint = Mouse.GetPosition(doc);
e.Handled = true;
}
private void doc_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
var mouseUpPoint = Mouse.GetPosition(doc);
if ((mouseUpPoint - mouseDownPoint).Length < 8) /* add text */;
}
The control handles the event internally.
If you register the event handler programmatically like this, your doc_MouseLeftButtonUp event handler should get invoked (note that last handledEventsToo parameter):
doc.AddHandler(ContentElement.MouseLeftButtonUpEvent,
(MouseButtonEventHandler)doc_MouseLeftButtonUp, true);
Note that you may also have to take care of the MouseLeftButtonUp that is raised by the control itself.
I found the solution. Listen to FlowDocument.MouseLeftButtonDown and do not use e.Handled=true and listen to FlowDocumentScrollViewer.PreviewMouseLeftButtonUp will get text selection and add text behavior at the same time.
I have a slider control and both the app and user will be making adjustments. The slider will trigger the ValueChanged event when the value changes, but I only want to trigger that if it was from the user ie, mouse or keyboard input changed it. I guess I could do it the hard way and check mouse and keyboard events and set a bool but I'd rather handle it all in one event if possible.
How do I check who changed the value?
private void slider1_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) {
if(wasUser) {
...
}
}
The quick and dirty way is to take advantage of the fact that event-handlers are called synchronously via property-setters - so set a field (accessible by all the handlers) to selectively ignore events if the current event was indirectly raised by a previous event-handler instead of direct user action.
Consider a textbox that can have its text set by the program, or typed-in by the user:
class MyWindow : Window {
private Boolean isIndirectEvent = false;
void Timer_Tick() {
this.isIndirectEvent = true;
this.TextBox.Text = "foo"; // TextBox_TextChanged will be invoked inside this property's setter
this.isIndirectEvent = false;
}
void TextBox_TextChanged(Object sender, EventArgs e) {
if( this.isIndirectEvent ) return;
this.InvokeTurboEncabulator();
}
}
(Of course, the "best" approach would be to use MVVM, Dependency Properties and Data Bindings which completely obviates this entire class of problems, but that requires making significant changes to your program, fwiw).
I am trying to change my mouse cursor at certain point when I'm dragging my mouse around in a wpf listview. However, when I set my mouse, it quickly gets overridden by something else, and get changed back to the drag cursor.
I am not sure where the cursor change comes from, it is certainly not from my code, so it has to be system. If it is system, then I have to intercept the event for cursor change, and handle the event in order for the cursor to show what I want right?
So is there a WPF equivalent of this Control.CursorChanged event? Or perhaps there's some other way to approach this problem?
Edit:
here's part of my code
private void SetDragCursor()
{
if (_badDragLoc)
{
Mouse.OverrideCursor = Cursors.No;
}
else
{
Mouse.OverrideCursor = Cursors.Arrow;
}
}
private void listView_DragOver(object sender, DragEventArgs e)
{
if (at a bad drag location)
{
_badDragLoc = true;
SetDragCursor();
}
}
I also have a drag leave event handler, in which I also have the SetDragCursor() method as well. When I step by step go through each line of code in debugger, the mouse turned into the drag cursor from the no cursor right after it enters the drag leave handler. Which is why I think it has to be the system.
If it indeed is the system, then if I can capture the event firing, I can then handle those event myself and not let it bubble through.
Thank you!
Just does not work like that, the way to set the cursor during a DragOver event is the following:
void listView__DragOver(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent("Images"))
{
e.Effects = DragDropEffects.None;
e.Handled = true;
}
}
depending on the value of DragDropEffects enum you assign to e.Effects the mouse will change cursor.
do not call Mouse.OverrideCursor because is not the right way.
I have a tabbed GUI with each tab containing a Frame. In one of these Frames there is a DataGrid. When the user selects this tab, I need my datagrid sorted, so I'm using the TabControl SelectionChanged event to trigger the sort. However, this event triggers every time an item is selected from the DataGrid, even though the tabs themselves remain untouched.
I've tried number of different events:
GotFocus for a TabItem
RequestBringIntoView for a TabItem
but they all seem to suffer from this problem. What is causing this?
The TabControl.SelectionChanged is the same event as a ComboBox.SelectionChanged
It originates from Selector.SelectionChanged.
So, if you do not mark your event as handled in your event handler, it will bubble up the tree, and eventually arrive at your TabControl, which is causing this "firing too often" issue.
Mark your event as handled in your SelectionChanged of your ComboBox/ListBox/ListView/any other Selector you use in your DataGrid like so:
private void MyComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
e.Handled = true;
}
And this inconvenience will go away ;).
private void tabControlName_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.Source is TabControl) //if this event fired from TabControl then enter
{
if (tabItemName.IsSelected)
{
//Do your job here
}
}
}
If you have added a handler with AddHandler in a parent element, all selection changes will fire the SelectionChanged-event. In this case, you can give your TabControl a name and then check in the EventHandler if the name of the OriginalSource is the name of your TabControl.
Another good approch is adding a handler to the tabControl.Items.SelectionChanged:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ItemCollection view = tabControl.Items;
view.CurrentChanged += new EventHandler(view_CurrentChanged);
}
void view_CurrentChanged(object sender, EventArgs e)
{
throw new NotImplementedException();
}
Maybe is not the xamly way, but is less pain as it only fires when an item is changed.
I have a ComboBox that is a simple drop down style. I wanted to open a new window when the user right clicks on an item in the list, but am having trouble getting it to detect a right click has occurred.
My code:
private void cmbCardList_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right && cmbCardList.SelectedIndex != -1)
{
frmViewCard vc = new frmViewCard();
vc.updateCardDisplay(cmbCardList.SelectedItem);
vc.Show();
}
}
If I change e.Button == MouseButtons.Left the whole thing fires off just fine. Any way I can get this working as I intend?
I'm afraid that will not be posible unless you do some serious hacking.
This article will explain.
Quoted for you:
Individual Controls
The following controls do not conform to the standard mouse click event behavior:
Button, CheckBox, ComboBox, and RadioButton controls
Left click: Click, MouseClick
Right click: No click events raised
Left double-click: Click, MouseClick;
Click, MouseClick
Right double-click: No click events
raised
As an epitaph to this question, you can make this work using normal .NET functionality; you just have to go a little deeper into the event call stack. Instead of handling the MouseClick event, handle the MouseDown event. I had to do something similar recently, and I simply overrode the OnMouseDown method instead of attaching a handler. But, a handler should work too. Here's the code:
protected override void OnMouseDown(MouseEventArgs e)
{
if (e.Button == MouseButtons.Right && !HandlingRightClick)
{
HandlingRightClick = true;
if (!cmsRightClickMenu.Visible)
cmsRightClickMenu.Show(this, e.Location);
else cmsRightClickMenu.Hide();
}
base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
HandlingRightClick = false;
base.OnMouseUp(e);
}
private bool HandlingRightClick { get; set; }
The HandlingRightClick property is to prevent multiple triggers of the OnMouseDown logic; the UI will send multiple MouseDown messages, which can interfere with hiding the right-click menu. To prevent this, I only perform the logic once on the first MouseDown trigger (the logic's simple enough that I don't care if two invocations happen to race, but you might), then ignore any other MouseDown triggers until a MouseUp occurs. It's not perfect, but this'll do what you need it to.
You can use the Opening event of ContextMenuStrip to handle right click event.
var chk = new CheckBox();
chk.ContextMenuStrip = cmsNone;
private void cmsNone_Opening(object sender, CancelEventArgs e)
{
e.Cancel = true;
var cms = (ContextMenuStrip)sender;
var chk = cms.SourceControl;
//do your stuff
}