Issue: Activating other window on closed event - wpf

I have some window, which can have many instances opened in single application.
I track all of them in a static dictionary.
The window must be closed when escape pressed if the window is active. And if the window is closed through escape i need activate other window, stored in a dictionary if there is one. I also need to consider current windows z-order and activate the top most of them, but for now it doesn`t matter.
So, when i have some windows opened and try to close them subsequently pressing escape what i get is that at some moment all of the windows left are closed simultaneously.
Here is the code example:
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private int _count;
private void Button_Click(object sender, RoutedEventArgs e)
{
Window1.Run(this, ++_count);
}
}
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
private static Dictionary<int, Window1> _opened =
new Dictionary<int, Window1>();
private int _key;
public Window1()
{
InitializeComponent();
KeyDown += OnKeyDown;
}
private void OnKeyDown(object sender, KeyEventArgs ea)
{
if (ea.Key == Key.Escape)
{
Close();
}
}
public static void Run(Window owner, int key)
{
Window1 w = null;
if (_opened.TryGetValue(key, out w))
{
w.Activate();
}
else
{
w = new Window1{_key = key};
w.Closed += (s, e) =>
{
var win = s as Window1;
_opened.Remove(win._key);
if (_opened.Count > 0)
{
_opened.First().Value.Activate();
}
};
_opened.Add(key, w);
w.Show();
}
}
}
}
Update#1
Thanks to Potecaru Tudor, he found another solution:
The solution I found was to set e.Handled = true in your KeyDown
handler after calling Close()
Solution Code:
private void OnKeyDown(object sender, KeyEventArgs ea)
{
if (ea.Key == Key.Escape)
{
ea.Handled = true;
Close();
}
}
Update#2
And here is another, not so elegant solution, just for the record
...
_opened.Remove(win._key);
if (_opened.Count > 0)
{
// i suppose here is the error hidden
var w2 = _opened.First().Value;
w2.Dispatcher.BeginInvoke(new Action(() => w2.Activate()));
}
...

I tried to reproduce your issue with no success, the windows are closing just fine one by one. You could try calling the Close() method only if Window.IsActive property is set on true.
EDIT I managed to reproduce your issue.
The solution I found was to set e.Handled = true in your KeyDown handler after calling Close()
I suppose the issue is that the windows become active one after another before the first one gets to be closed, so they also received the KeyDown event firing. The weird thing is it only happens in my sample for the first windows you're opening. After you're closing all of them and start opening others this will not reproduce.

Only way i have found is to delay next window activation, like this:
...
_opened.Remove(win._key);
if (_opened.Count > 0)
{
// i suppose here is the error hidden
var w2 = _opened.First().Value;
w2.Dispatcher.BeginInvoke(new Action(() => w2.Activate()));
}
...
Any other suggestions would be appreciated! Thank you!

Related

WPF: mouse leave event doesn't trigger with mouse down

I'm having an issue with mouse enter / leave events. When mouse button is pressed and hold with cursor inside the control and then cursor is moved out of the control fast enough this events don't trigger.
Could you please advice me why it happens? Is there any way to obtain these events properly?
Please check the sample project to see it in action: https://www.dropbox.com/s/w5ra2vzegjtauso/SampleApp.zip
Update. I have found the same problem here without an answer. Started bounty there.
EDIT: After Sisyphe correctly noted that the behavior did not work for elements with mouse interaction, I have rewritten the code.
The behavior can be attached to a window or any other FrameworkElement. By default, all contained elements will be monitored for MouseLeave while the left mouse button is down, and the handlers executed. The behavior can also be applied just to its associated element by setting MonitorSubControls="False".
What the behavior does, basically (refer to the comments in the code for more detail):
Is only "active" if left mouse button is pressed
Watches for mouse position changes from in- to outside of an element. In this case, executes the event handlers.
Known limitations (could all be resolved with some more effort, I reckon, but don't seem too important to me):
Does not execute handlers for transitions to a contained element ("inner" boundaries)
Does not guarantee correct order of execution of the handlers
Does not resolve that for slow transitions to the outside of the window, e.LeftButton is reported as released (bug?).
I decided not to use the Win32 hook and instead using a timer, who will not fire more than about every 0.15 seconds (despite a smaller interval set, clock drift?). For fast mouse movements, the evaluated points could be too far apart and miss an element that is just flitted across.
This script produces the output below: With the behavior attached to the window, moving inside the orangeBorder (leaves blueBorder by inner boundary with mouse button released: 0), pressing left mousebutton inside the orange border and moving (fast) outside the window executes the leave handlers (1 - 4). Releasing the mouse button outside the window, moving back in over the goldTextBox (5), pressing left mousebutton in the textbox, leaving (fast or slow) outside the window again executes the correct handlers (6 - 9).
Xaml (example):
<Window x:Class="WpfApplication1.MouseLeaveControlWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:beh="clr-namespace:WpfApplication1.Behavior"
Title="MouseLeaveControlWindow" Height="300" Width="300" x:Name="window" MouseLeave="OnMouseLeave">
<i:Interaction.Behaviors>
<beh:MonitorMouseLeaveBehavior />
</i:Interaction.Behaviors>
<Grid x:Name="grid" MouseLeave="OnMouseLeave" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border x:Name="blueBorder" MouseLeave="OnMouseLeave" Background="SteelBlue" Margin="50" Grid.RowSpan="2" />
<Border x:Name="orangeBorder" MouseLeave="OnMouseLeave" Background="DarkOrange" Margin="70, 70, 70, 20" />
<TextBox x:Name="goldTextBox" MouseLeave="OnMouseLeave" Background="Gold" Margin="70, 20, 70, 70" Grid.Row="1" Text="I'm a TextBox" />
</Grid>
</Window>
Code behind (just for debug purpose):
public partial class MouseLeaveControlWindow : Window
{
public MouseLeaveControlWindow()
{
InitializeComponent();
}
private int i = 0;
private void OnMouseLeave(object sender, MouseEventArgs e)
{
FrameworkElement fe = (FrameworkElement)sender;
if (e.LeftButton == MouseButtonState.Pressed)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0}: Left {1}.", i, fe.Name)); i++;
}
else
{
System.Diagnostics.Debug.WriteLine(string.Format("{0}: Left {1} (Released).", i, fe.Name)); i++;
}
}
}
MonitorMouseLeaveBehavior:
using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Interop;
using System.ComponentModel;
using System.Windows.Media;
using WpfApplication1.Helpers;
namespace WpfApplication1.Behavior
{
public class MonitorMouseLeaveBehavior : Behavior<FrameworkElement>
{
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetCursorPos(ref Win32Point pt);
[StructLayout(LayoutKind.Sequential)]
internal struct Win32Point
{
public Int32 X;
public Int32 Y;
};
[DllImport("user32.dll")]
public static extern short GetAsyncKeyState(UInt16 virtualKeyCode);
private enum VK
{
LBUTTON = 0x01
}
private bool _tracking;
private const int _interval = 1;
private Timer _checkPosTimer = new Timer(_interval);
private Dictionary<FrameworkElement, RoutedEventHandlerInfo[]> _leaveHandlersForElement = new Dictionary<FrameworkElement, RoutedEventHandlerInfo[]>();
private Window _window;
private Dictionary<FrameworkElement, Rect> _boundsByElement = new Dictionary<FrameworkElement, Rect>();
private Dictionary<FrameworkElement, bool> _wasInside = new Dictionary<FrameworkElement, bool>();
private List<FrameworkElement> _elements = new List<FrameworkElement>();
/// <summary>
/// If true, all subcontrols are monitored for the mouseleave event when left mousebutton is down.
/// True by default.
/// </summary>
public bool MonitorSubControls { get { return (bool)GetValue(MonitorSubControlsProperty); } set { SetValue(MonitorSubControlsProperty, value); } }
public static readonly DependencyProperty MonitorSubControlsProperty = DependencyProperty.Register("MonitorSubControls", typeof(bool), typeof(MonitorMouseLeaveBehavior), new PropertyMetadata(true, OnMonitorSubControlsChanged));
private static void OnMonitorSubControlsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MonitorMouseLeaveBehavior beh = (MonitorMouseLeaveBehavior)d;
beh.AddOrRemoveLogicalChildren((bool)e.NewValue);
}
/// <summary>
/// Initial actions
/// </summary>
protected override void OnAttached()
{
_window = this.AssociatedObject is Window ? (Window)this.AssociatedObject : Window.GetWindow(this.AssociatedObject); // get window
_window.SourceInitialized += (s, e) =>
{
this.AddOrRemoveLogicalChildren(this.MonitorSubControls); // get all monitored elements
this.AttachHandlers(true); // attach mousedown and sizechanged handlers
this.GetAllBounds(); // determine bounds of all elements
_checkPosTimer.Elapsed += (s1, e1) => Dispatcher.BeginInvoke((Action)(() => { CheckPosition(); }));
};
base.OnAttached();
}
protected override void OnDetaching()
{
this.AttachHandlers(false);
base.OnDetaching();
}
/// <summary>
/// Starts or stops monitoring of the AssociatedObject's logical children.
/// </summary>
/// <param name="add"></param>
private void AddOrRemoveLogicalChildren(bool add)
{
if (_window != null && _window.IsInitialized)
{
AddOrRemoveSizeChangedHandlers(false);
_elements.Clear();
if (add)
_elements.AddRange(VisualHelper.FindLogicalChildren<FrameworkElement>(this.AssociatedObject));
_elements.Add(this.AssociatedObject);
AddOrRemoveSizeChangedHandlers(true);
}
}
/// <summary>
/// Attaches/detaches size changed handlers to the monitored elements
/// </summary>
/// <param name="add"></param>
private void AddOrRemoveSizeChangedHandlers(bool add)
{
foreach (var element in _elements)
{
element.SizeChanged -= element_SizeChanged;
if (add) element.SizeChanged += element_SizeChanged;
}
}
/// <summary>
/// Adjusts the stored bounds to the changed size
/// </summary>
void element_SizeChanged(object sender, SizeChangedEventArgs e)
{
FrameworkElement fe = sender as FrameworkElement;
if (fe != null)
GetBounds(fe);
}
/// <summary>
/// Attaches/Detaches MouseLeftButtonDown and SizeChanged handlers
/// </summary>
/// <param name="attach">true: attach, false: detach</param>
private void AttachHandlers(bool attach)
{
AddOrRemoveSizeChangedHandlers(attach);
if (attach)
_window.PreviewMouseLeftButtonDown += window_PreviewMouseLeftButtonDown;
else // detach
_window.PreviewMouseLeftButtonDown -= window_PreviewMouseLeftButtonDown;
}
/// <summary>
/// Gets the bounds for all monitored elements
/// </summary>
private void GetAllBounds()
{
_boundsByElement.Clear();
foreach (var element in _elements)
GetBounds(element);
}
/// <summary>
/// Gets the bounds of the control, which are used to check if the mouse position
/// is located within. Note that this only covers rectangular control shapes.
/// </summary>
private void GetBounds(FrameworkElement element)
{
Point p1 = new Point(0, 0);
Point p2 = new Point(element.ActualWidth, element.ActualHeight);
p1 = element.TransformToVisual(_window).Transform(p1);
p2 = element.TransformToVisual(_window).Transform(p2);
if (element == _window) // window bounds need to account for the border
{
var titleHeight = SystemParameters.WindowCaptionHeight + 2 * SystemParameters.ResizeFrameHorizontalBorderHeight; // not sure about that one
var verticalBorderWidth = SystemParameters.ResizeFrameVerticalBorderWidth;
p1.Offset(-verticalBorderWidth, -titleHeight);
p2.Offset(-verticalBorderWidth, -titleHeight);
}
Rect bounds = new Rect(p1, p2);
if (_boundsByElement.ContainsKey(element))
_boundsByElement[element] = bounds;
else
_boundsByElement.Add(element, bounds);
}
/// <summary>
/// For all monitored elements, detach the MouseLeave event handlers and store them locally,
/// to be executed manually.
/// </summary>
private void RerouteLeaveHandlers()
{
foreach (var element in _elements)
{
if (!_leaveHandlersForElement.ContainsKey(element))
{
var handlers = ReflectionHelper.GetRoutedEventHandlers(element, UIElement.MouseLeaveEvent);
if (handlers != null)
{
_leaveHandlersForElement.Add(element, handlers);
foreach (var handler in handlers)
element.MouseLeave -= (MouseEventHandler)handler.Handler; // detach handlers
}
}
}
}
/// <summary>
/// Reattach all leave handlers that were detached in window_PreviewMouseLeftButtonDown.
/// </summary>
private void ReattachLeaveHandlers()
{
foreach (var kvp in _leaveHandlersForElement)
{
FrameworkElement fe = kvp.Key;
foreach (var handler in kvp.Value)
{
if (handler.Handler is MouseEventHandler)
fe.MouseLeave += (MouseEventHandler)handler.Handler;
}
}
_leaveHandlersForElement.Clear();
}
/// <summary>
/// Checks if the mouse position is inside the bounds of the elements
/// If there is a transition from inside to outside, the leave event handlers are executed
/// </summary>
private void DetermineIsInside()
{
Point p = _window.PointFromScreen(GetMousePosition());
foreach (var element in _elements)
{
if (_boundsByElement.ContainsKey(element))
{
bool isInside = _boundsByElement[element].Contains(p);
bool wasInside = _wasInside.ContainsKey(element) && _wasInside[element];
if (wasInside && !isInside)
ExecuteLeaveHandlers(element);
if (_wasInside.ContainsKey(element))
_wasInside[element] = isInside;
else
_wasInside.Add(element, isInside);
}
}
}
/// <summary>
/// Gets the mouse position relative to the screen
/// </summary>
public static Point GetMousePosition()
{
Win32Point w32Mouse = new Win32Point();
GetCursorPos(ref w32Mouse);
return new Point(w32Mouse.X, w32Mouse.Y);
}
/// <summary>
/// Gets the mouse button state. MouseEventArgs.LeftButton is notoriously unreliable.
/// </summary>
private bool IsMouseLeftButtonPressed()
{
short leftMouseKeyState = GetAsyncKeyState((ushort)VK.LBUTTON);
bool ispressed = leftMouseKeyState < 0;
return ispressed;
}
/// <summary>
/// Executes the leave handlers that were attached to the controls.
/// They have been detached previously by this behavior (see window_PreviewMouseLeftButtonDown), to prevent double execution.
/// After mouseup, they are reattached (see CheckPosition)
/// </summary>
private void ExecuteLeaveHandlers(FrameworkElement fe)
{
MouseDevice mouseDev = InputManager.Current.PrimaryMouseDevice;
MouseEventArgs mouseEvent = new MouseEventArgs(mouseDev, 0) { RoutedEvent = Control.MouseLeaveEvent };
if (_leaveHandlersForElement.ContainsKey(fe))
{
foreach (var handler in _leaveHandlersForElement[fe])
{
if (handler.Handler is MouseEventHandler)
((MouseEventHandler)handler.Handler).Invoke(fe, mouseEvent);
}
}
}
/// <summary>
/// Sets the mouse capture (events outside the window are still directed to it),
/// and tells the behavior to watch out for a missed leave event
/// </summary>
private void window_PreviewMouseLeftButtonDown(object sender, MouseEventArgs e)
{
System.Diagnostics.Debug.WriteLine("--- left mousebutton down ---"); // todo remove
this.RerouteLeaveHandlers();
_tracking = true;
_checkPosTimer.Start();
}
/// <summary>
/// Uses the _tracking field as well as left mouse button state to determine if either
/// leave event handlers should be executed, or monitoring should be stopped.
/// </summary>
private void CheckPosition()
{
if (_tracking)
{
if (IsMouseLeftButtonPressed())
{
this.DetermineIsInside();
}
else
{
_wasInside.Clear();
_tracking = false;
_checkPosTimer.Stop();
System.Diagnostics.Debug.WriteLine("--- left mousebutton up ---"); // todo remove
// invoking ReattachLeaveHandlers() immediately would rethrow MouseLeave for top grid/window
// if both a) mouse is outside window and b) mouse moves. Wait with reattach until mouse is inside window again and moves.
_window.MouseMove += ReattachHandler;
}
}
}
/// <summary>
/// Handles the first _window.MouseMove event after left mouse button was released,
/// and reattaches the MouseLeaveHandlers. Detaches itself to be executed only once.
/// </summary>
private void ReattachHandler(object sender, MouseEventArgs e)
{
ReattachLeaveHandlers();
_window.MouseMove -= ReattachHandler; // only once
}
}
}
VisualHelper.FindLogicalChildren, ReflectionHelper.GetRoutedEventHandlers:
public static List<T> FindLogicalChildren<T>(DependencyObject obj) where T : DependencyObject
{
List<T> children = new List<T>();
foreach (var child in LogicalTreeHelper.GetChildren(obj))
{
if (child != null)
{
if (child is T)
children.Add((T)child);
if (child is DependencyObject)
children.AddRange(FindLogicalChildren<T>((DependencyObject)child)); // recursive
}
}
return children;
}
/// <summary>
/// Gets the list of routed event handlers subscribed to the specified routed event.
/// </summary>
/// <param name="element">The UI element on which the event is defined.</param>
/// <param name="routedEvent">The routed event for which to retrieve the event handlers.</param>
/// <returns>The list of subscribed routed event handlers.</returns>
public static RoutedEventHandlerInfo[] GetRoutedEventHandlers(UIElement element, RoutedEvent routedEvent)
{
var routedEventHandlers = default(RoutedEventHandlerInfo[]);
// Get the EventHandlersStore instance which holds event handlers for the specified element.
// The EventHandlersStore class is declared as internal.
var eventHandlersStoreProperty = typeof(UIElement).GetProperty("EventHandlersStore", BindingFlags.Instance | BindingFlags.NonPublic);
object eventHandlersStore = eventHandlersStoreProperty.GetValue(element, null);
if (eventHandlersStore != null)
{
// Invoke the GetRoutedEventHandlers method on the EventHandlersStore instance
// for getting an array of the subscribed event handlers.
var getRoutedEventHandlers = eventHandlersStore.GetType().GetMethod("GetRoutedEventHandlers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
routedEventHandlers = (RoutedEventHandlerInfo[])getRoutedEventHandlers.Invoke(eventHandlersStore, new object[] { routedEvent });
}
return routedEventHandlers;
}
Approach #1 - is still a valid one (as a pure managed solution) if you work out the specifics.
(capture could be given to a specific control to avoid issues, but I haven't tried)
This should help you get the events ('fixed' events).
Key is to track the mouse move when outside window (and only when mouse is down).
For that you'd need to do the capture (but slightly different than suggested as that won't work - on down/up instead).
private void Window_MouseDown(object sender, MouseEventArgs e)
{
this.CaptureMouse();
}
private void Window_MouseUp(object sender, MouseEventArgs e)
{
this.ReleaseMouseCapture();
}
private void Window_MouseLeave(object sender, MouseEventArgs e)
{
test1.Content = "Mouse left";
}
private void Window_MouseEnter(object sender, MouseEventArgs e)
{
test1.Content = "Mouse entered";
}
private void Window_MouseMove(object sender, MouseEventArgs e)
{
if (Mouse.Captured == this)
{
if (!this.IsMouseInBounds(e))
Window_MouseLeave(sender, e);
else
Window_MouseEnter(sender, e);
}
test2.Content = e.GetPosition(this).ToString();
}
private bool IsMouseInBounds(MouseEventArgs e)
{
var client = ((FrameworkElement)this.Content);
Rect bounds = new Rect(0, 0, client.ActualWidth, client.ActualHeight);
return bounds.Contains(e.GetPosition(this));
}
private Point GetRealPosition(Point mousePoint)
{
return Application.Current.MainWindow.PointFromScreen(mousePoint);
}
Note:
You'd need to finish this according to your situation. I have just 'dummy wired' the mouse move to Enter and Leave and w/o any smart algorithm there (i.e. generated enter/leave will keep on firing). I.e. add some flag to actually save the state of the enter/leave properly.
Also I'm measuring whether mouse is within the 'client bounds' of the Window. You'd need to adjust that if you need that in respect of borders etc.
Also I forgot to add the obvious - wire up the new events MouseDown="Window_MouseDown" MouseUp="Window_MouseUp"
That's "normal" behaviour. Capture the mouse inside MouseEnter handler.
Mouse.Capture(yourUIElement);
and later release it in MouseLeave,
Mouse.Capture(null);
Edited: More explanation.
WPF does not track mouse movement precisely. You can deduce that from the fact that if you capture MouseMove event, you can see that the it reports you event every 20milliseconds intervals and not by pixel precision.. more like 8 pixels per event.
Now this is not that horrible, but WPF also does not report mouse movement outside the window, if you happen to move your mouse. This is default behaviour. You can change it throuh Mouse.Capture as said.
Now, you can imagine why this problem happens. If you can move your mouse outside the window faster than mouse move report happens, then WPF still thinks that it's inside the application.
EDIT
In case you need it - I edited in a simplified wrapper for ease of use (just add commands in your
view-model)
Approach #2 - using Global Mouse Hook to track mouse move - the rest is similar to #1.
Actually, this is more of an example on how to do a global hook from C#.
In XAML you can hook up all 3 or just one, two events
my:Hooks.EnterCommand="{Binding EnterCommand}"
my:Hooks.LeaveCommand="{Binding LeaveCommand}"
my:Hooks.MouseMoveCommand="{Binding MoveCommand}"
In your view-model define commands
RelayCommand _enterCommand;
public RelayCommand EnterCommand
{
get
{
return _enterCommand ?? (_enterCommand = new RelayCommand(param =>
{
var point = (Point)param;
test1.Content = "Mouse entered";
// test2.Content = point.ToString();
},
param => true));
}
}
And the attached properties (the 'nice' wrapper)...
public static class Hooks
{
private static Dictionary<ContentControl, Action> _hash = new Dictionary<ContentControl, Action>();
#region MouseMoveCommand
public static ICommand GetMouseMoveCommand(ContentControl control) { return (ICommand)control.GetValue(MouseMoveCommandProperty); }
public static void SetMouseMoveCommand(ContentControl control, ICommand value) { control.SetValue(MouseMoveCommandProperty, value); }
public static readonly DependencyProperty MouseMoveCommandProperty =
DependencyProperty.RegisterAttached("MouseMoveCommand", typeof(ICommand), typeof(Hooks), new UIPropertyMetadata(null, OnMouseMoveCommandChanged));
static void OnMouseMoveCommandChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
ContentControl control = depObj as ContentControl;
if (control != null && e.NewValue is ICommand)
SetupMouseMove(control);
}
static void Instance_MouseMoveLL(object sender, WinHook.MouseLLMessageArgs e)
{
}
static void OnAutoGeneratingColumn(ICommand command, object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (command.CanExecute(e)) command.Execute(e);
}
#endregion
#region EnterCommand
public static ICommand GetEnterCommand(ContentControl control) { return (ICommand)control.GetValue(EnterCommandProperty); }
public static void SetEnterCommand(ContentControl control, ICommand value) { control.SetValue(EnterCommandProperty, value); }
public static readonly DependencyProperty EnterCommandProperty =
DependencyProperty.RegisterAttached("EnterCommand", typeof(ICommand), typeof(Hooks), new UIPropertyMetadata(null, OnEnterCommandChanged));
static void OnEnterCommandChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
ContentControl control = depObj as ContentControl;
if (control != null && e.NewValue is ICommand)
SetupMouseMove(control);
}
#endregion
#region LeaveCommand
public static ICommand GetLeaveCommand(ContentControl control) { return (ICommand)control.GetValue(LeaveCommandProperty); }
public static void SetLeaveCommand(ContentControl control, ICommand value) { control.SetValue(LeaveCommandProperty, value); }
public static readonly DependencyProperty LeaveCommandProperty =
DependencyProperty.RegisterAttached("LeaveCommand", typeof(ICommand), typeof(Hooks), new UIPropertyMetadata(null, OnLeaveCommandChanged));
static void OnLeaveCommandChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
ContentControl control = depObj as ContentControl;
if (control != null && e.NewValue is ICommand)
SetupMouseMove(control);
}
#endregion
static void SetupMouseMove(ContentControl control)
{
Action onmove;
if (_hash.TryGetValue(control, out onmove) == false)
{
onmove = () =>
{
var entered = false;
var moveCommand = control.GetValue(Hooks.MouseMoveCommandProperty) as ICommand;
var enterCommand = control.GetValue(Hooks.EnterCommandProperty) as ICommand;
var leaveCommand = control.GetValue(Hooks.LeaveCommandProperty) as ICommand;
// hook is invoked on the 'caller thread' (i.e. your GUI one) so it's safe
// don't forget to unhook and dispose / release it, handle unsubscribe for events
WinHook.Instance.MouseMoveLL += (s, e) =>
{
Point point = control.PointFromScreen(new Point(e.Message.Pt.X, e.Message.Pt.Y));
if (moveCommand != null && moveCommand.CanExecute(point))
moveCommand.Execute(point);
var newEntered = control.IsMouseInBounds(point); // don't use 'IsMouseOver'
if (newEntered != entered)
{
entered = newEntered;
if (entered)
{
if (enterCommand != null && enterCommand.CanExecute(point))
enterCommand.Execute(point);
}
else
{
if (leaveCommand != null && leaveCommand.CanExecute(point))
leaveCommand.Execute(point);
}
}
};
};
control.Loaded += (s, e) => onmove();
_hash[control] = onmove;
}
}
private static bool IsMouseInBounds(this ContentControl control, Point point)
{
var client = ((FrameworkElement)control.Content);
Rect bounds = new Rect(0, 0, client.ActualWidth, client.ActualHeight);
return bounds.Contains(point);
}
}
And you could use HookManager from the article.
Or the minimal hook code (mind that proper IDisoposable is required, exception handling etc.):
public sealed class WinHook : IDisposable
{
public static readonly WinHook Instance = new WinHook();
public const int WH_MOUSE_LL = 14;
public const uint WM_MOUSEMOVE = 0x0200;
public delegate void MouseLLMessageHandler(object sender, MouseLLMessageArgs e);
public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
public static extern int GetCurrentThreadId();
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern bool UnhookWindowsHookEx(int idHook);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern int CallNextHookEx(int idHook, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
public class MouseLLHookStruct
{
public POINT Pt;
public uint mouseData;
public uint flags;
public uint time;
public uint dwExtraInfo;
}
public class MouseLLMessageArgs : EventArgs
{
public bool IsProcessed { get; set; }
public MouseLLHookStruct Message { get; private set; }
public MouseLLMessageArgs(MouseLLHookStruct message) { this.Message = message; }
}
static IntPtr GetModuleHandle()
{
using (Process process = Process.GetCurrentProcess())
using (ProcessModule module = process.MainModule)
return GetModuleHandle(module.ModuleName);
}
public event MouseLLMessageHandler MouseMoveLL;
int _hLLMouseHook = 0;
HookProc LLMouseHook;
private WinHook()
{
IntPtr hModule = GetModuleHandle();
LLMouseHook = LowLevelMouseProc;
_hLLMouseHook = SetWindowsHookEx(WH_MOUSE_LL, LLMouseHook, hModule, 0);
if (_hLLMouseHook == 0) { } // "failed w/ an error code: {0}", new Win32Exception(Marshal.GetLastWin32Error()).Message
}
public void Release()
{
if (_hLLMouseHook == 0) return;
int hhook = _hLLMouseHook;
_hLLMouseHook = 0;
bool ret = UnhookWindowsHookEx(hhook);
if (ret == false) { } // "failed w/ an error code: {0}", new Win32Exception(Marshal.GetLastWin32Error()).Message
}
public int LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && lParam.ToInt32() > 0
&& wParam.ToInt32() == (int)WM_MOUSEMOVE)
{
MouseLLHookStruct msg = (MouseLLHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseLLHookStruct));
MouseLLMessageArgs args = new MouseLLMessageArgs(msg);
if (MouseMoveLL != null)
MouseMoveLL(this, args);
if (args.IsProcessed)
return -1; // return 1;
}
return CallNextHookEx(_hLLMouseHook, nCode, wParam, lParam);
}
// implement IDisposable properly and call `Release` for unmanaged resources / hook
public void Dispose() { }
}
Note: global mouse hooks are notorious for performance issues. And you cannot use the local one (recommended but most of the time useless) - as it doesn't get outside mouse moves.
Also avoid putting anything 'heavy' inside the event - or anything that 'spawns off' from it. There is a limit actually on the time you can spend processing the event - or your hook will get removed, i.e. stop working. If you need to do some processing from the event, pop up a new thread and Invoke back.
My favorite solution is actually to give the hook its own thread and then events need to be invoked - but that's out of scope and a bit more complex (you need a 'pump' in there etc.).
As to 'why' all this is necessary:
I don't like speculating, but seems that events are throttled - and the critical 'one' is missed when 'crossing the border', something like that. Anyway, it's all about the mouse moves no matter how you look at it.

Close a Window from Another In Wpf

If two Window is opened mainly A and B, how to close Window A using code that written on Window B.
Your best bet would be to create a property on Window B that you pass the creating Window to. Something like this. I have a Window named MainWindow and a second Window named Window2.
Main Window
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
Window2 secondForm;
public MainWindow()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
secondForm = new Window2();
secondForm.setCreatingForm =this;
secondForm.Show();
}
}
}
Window2
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window2.xaml
/// </summary>
public partial class Window2 : Window
{
Window creatingForm;
public Window2()
{
InitializeComponent();
}
public Window setCreatingForm
{
get { return creatingForm; }
set { creatingForm = value; }
}
private void button1_Click(object sender, RoutedEventArgs e)
{
if (creatingForm != null)
creatingForm.Close();
}
}
}
In respose to your comment, closing a window that was created by another form is as easy as calling the Close Method of the created Form:
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
Window2 secondForm;
public MainWindow()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
if (secondForm == null)
{
secondForm = new Window2();
secondForm.Show();
}
else
secondForm.Activate();
}
private void button2_Click(object sender, RoutedEventArgs e)
{
if (secondForm != null)
{
secondForm.Close();
secondForm = new Window2();
//How ever you are passing information to the secondWindow
secondForm.Show();
}
}
}
}
Here is a way to close any window from any other window. You can modify it to work with multiple instances by giving your windows some unique identifier and then just searching for that in the foreach loop.
public static class Helper
{
public static void CloseWindowOfWhichThereIsOnlyOne<T>()
{
Assembly currentAssembly = Assembly.GetExecutingAssembly();
foreach (Window w in Application.Current.Windows)
{
if (w.GetType().Assembly == currentAssembly && w is T)
{
w.Close();
break;
}
}
}
}
Or with a unique identifier "fudge":
public static void CloseWIndowUsingIdentifier(string windowTag)
{
Assembly currentAssembly = Assembly.GetExecutingAssembly();
foreach (Window w in Application.Current.Windows)
{
if (w.GetType().Assembly == currentAssembly && w.Tag.Equals(windowTag))
{
w.Close();
break;
}
}
}
I like this better than the suggested solution because you don't need to mess with your windows, other than to give them unique tags. I've only been using this for small projects where there is no risk of things not being unique, I'm not going to lose track of 10-12 windows!
The other suggested solution is a little silly (I don't have 50 karma to comment on it) as you could just call win.close() if you already had a reference to the object...
it is very simple make one public class and method like this
class Helper
{
public static void CloseWindow(Window x)
{
Assembly currentAssembly = Assembly.GetExecutingAssembly();
// int count = Application.Current.Windows;
foreach (Window w in Application.Current.Windows)
{
//Form f = Application.OpenForms[i];
if (w.GetType().Assembly == currentAssembly && w==x)
{
w.Close();
}
}
}
}
now call this function from where you want close window like this .
Helper.CloseWindow(win);//win is object of window which you want to close.
hope this helps.
foreach (Window w in Application.Current.Windows)
{
if (w.Name != "Main_Window_wind" )
{
w.Visibility = System.Windows.Visibility.Hidden;
}
}
//name is the x:Name="Main_Window_wind" in xaml
You can close now as hiden all windows without closing the named Main_Window_wind , you can add another window to be not closed with this in if: w.Name != "Main_Window_wind" && w.Name != "AnyOther_Window_wind" &&...
a quicker way is this:
for (int intCounter = App.Current.Windows.Count - 1; intCounter > -1; intCounter--)
{
if (App.Current.Windows[intCounter].Name != "Main_Window_wind")
App.Current.Windows[intCounter].Visibility = System.Windows.Visibility.Hidden;
}

WPF memory leak after creating window in another thread

I am creating window in another thread. After closing the thread some of the resources window is not released from the memory. Because of this growing counter GDI Objects and User Objects in windows task manager. Graphics that not released are font and region. I haven't idea what is going on...
public class WaitingWindowManager
{
private Thread thread;
private bool canAbortThread = false;
private Window waitingWindow;
public void BeginWaiting()
{
this.thread = new Thread(this.RunThread);
this.thread.IsBackground = true;
this.thread.SetApartmentState(ApartmentState.STA);
this.thread.Start();
}
public void EndWaiting()
{
if (this.waitingWindow != null)
{
this.waitingWindow.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)(() => { this.waitingWindow.Close(); }));
while (!this.canAbortThread) { };
}
this.thread.Abort();
}
public void RunThread()
{
this.waitingWindow = new Window();
this.waitingWindow.Closed += new EventHandler(waitingWindow_Closed);
this.waitingWindow.ShowDialog();
}
void waitingWindow_Closed(object sender, EventArgs e)
{
this.canAbortThread = true;
}
}
And call :
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
WaitingWindowManager waitingWindowManager = new WaitingWindowManager();
waitingWindowManager.BeginWaiting();
Application.Current.Dispatcher.Thread.Join(5000);
waitingWindowManager.EndWaiting();
}
}
Remove your Closed eventhandler in your waitingWindow_Closed Event. It is causing your window to not be disposed. If you manually add events you need to make sure you remove them when finished.
I also noticed another Stackoverflow question that was pertaining to memory leaks in wpf. It referenced this article maybe this will help you.
Add Dispatcher.CurrentDispatcher.InvokeShutdown(); in your closing code. That should take care of any leaking memory.

unit test an attached behavior wpf

I am still grokking attached behaviors in general, and am at a loss to see how to write a unit test for one.
I pasted some code below from Sacha Barber's Cinch framework that allows a window to be closed via attached behavior. Can somewone show me an example unit test for it?
Thanks!
Berryl
#region Close
/// <summary>Dependency property which holds the ICommand for the Close event</summary>
public static readonly DependencyProperty CloseProperty =
DependencyProperty.RegisterAttached("Close",
typeof(ICommand), typeof(Lifetime),
new UIPropertyMetadata(null, OnCloseEventInfoChanged));
/// <summary>Attached Property getter to retrieve the CloseProperty ICommand</summary>
public static ICommand GetClose(DependencyObject source)
{
return (ICommand)source.GetValue(CloseProperty);
}
/// <summary>Attached Property setter to change the CloseProperty ICommand</summary>
public static void SetClose(DependencyObject source, ICommand command)
{
source.SetValue(CloseProperty, command);
}
/// <summary>This is the property changed handler for the Close property.</summary>
private static void OnCloseEventInfoChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var win = sender as Window;
if (win == null) return;
win.Closing -= OnWindowClosing;
win.Closed -= OnWindowClosed;
if (e.NewValue == null) return;
win.Closing += OnWindowClosing;
win.Closed += OnWindowClosed;
}
/// <summary>
/// This method is invoked when the Window.Closing event is raised.
/// It checks with the ICommand.CanExecute handler
/// and cancels the event if the handler returns false.
/// </summary>
private static void OnWindowClosing(object sender, CancelEventArgs e)
{
var dpo = (DependencyObject)sender;
var ic = GetClose(dpo);
if (ic == null) return;
e.Cancel = !ic.CanExecute(GetCommandParameter(dpo));
}
/// <summary>
/// This method is invoked when the Window.Closed event is raised.
/// It executes the ICommand.Execute handler.
/// </summary>
static void OnWindowClosed(object sender, EventArgs e)
{
var dpo = (DependencyObject)sender;
var ic = GetClose(dpo);
if (ic == null) return;
ic.Execute(GetCommandParameter(dpo));
}
#endregion
You would likely use a lambda in your ICommand using a DelegateCommand or a RelayCommand. Multiple implementations of these exists all over the place and Cinch may have something similar. Really simple version (as an example, not meant for production use):
public class DelegateCommand : ICommand {
private Action _execute = null;
public void Execute( object parameter ) {
_execute();
}
public DelegateCommand( Action execute ) {
_execute = execute;
}
#region stuff that doesn't affect functionality
public bool CanExecute( object parameter ) {
return true;
}
public event EventHandler CanExecuteChanged {
add { }
remove { }
}
#endregion
}
Then your test body might look something like this:
bool wascalled = false;
var execute = new DelegateCommand(
() => {
wascalled = true;
} );
var window = new Window();
SomeClass.SetClose( window, execute );
// does the window need to be shown for Close() to work? Nope.
window.Close();
AssertIsTrue( wascalled );
This is an over-simplified example. There are of course other tests you'll want to perform, in which case you should create or find a fuller implementation of DelegateCommand that also properly implements CanExecute, among other things.
DependencyProperty changing and value coercion on their own looks like 'Impossible Dependencies' for me. Having reference to Window there makes things even trickier. I think I'd go with Humble Object pattern here...

WPF: Slider with an event that triggers after a user drags

I am currently making an MP3 player in WPF, and I want to make a slider that will allow the user to seek to a particular position in an MP3 by sliding the slider to the left or right.
I have tried using the ValueChanged event but that triggers every time it's value is changed, so if you drag it across, the event will fire multiple times, I want the event to only fire when the user has finished dragging the slider and Then get the new value.
How can I achieve this?
[Update]
I have found this post on MSDN which basically discusses the same thing, and they came up with two "solutions"; either subclassing the Slider or invoking a DispatcherTimer in the ValueChanged event that invokes the action after a timespan.
Can you come up with anything better then the two mentioned above?
Besides using the Thumb.DragCompleted event you can also use both ValueChanged and Thumb.DragStarted, this way you don’t lose functionality when the user modifies the value by pressing the arrow keys or by clicking on the slider bar.
Xaml:
<Slider ValueChanged="Slider_ValueChanged"
Thumb.DragStarted="Slider_DragStarted"
Thumb.DragCompleted="Slider_DragCompleted"/>
Code behind:
private bool dragStarted = false;
private void Slider_DragCompleted(object sender, DragCompletedEventArgs e)
{
DoWork(((Slider)sender).Value);
this.dragStarted = false;
}
private void Slider_DragStarted(object sender, DragStartedEventArgs e)
{
this.dragStarted = true;
}
private void Slider_ValueChanged(
object sender,
RoutedPropertyChangedEventArgs<double> e)
{
if (!dragStarted)
DoWork(e.NewValue);
}
You can use the thumb's 'DragCompleted' event for this. Unfortunately, this is only fired when dragging, so you'll need to handle other clicks and key presses separately. If you only want it to be draggable, you could disable these means of moving the slider by setting LargeChange to 0 and Focusable to false.
Example:
<Slider Thumb.DragCompleted="MySlider_DragCompleted" />
<Slider PreviewMouseUp="MySlider_DragCompleted" />
works for me.
The value you want is the value after a mousup event, either on clicks on the side or after a drag of the handle.
Since MouseUp doesn't tunnel down (it is handeled before it can), you have to use PreviewMouseUp.
Another MVVM-friendly solution (I was not happy with answers)
View:
<Slider Maximum="100" Value="{Binding SomeValue}"/>
ViewModel:
public class SomeViewModel : INotifyPropertyChanged
{
private readonly object _someValueLock = new object();
private int _someValue;
public int SomeValue
{
get { return _someValue; }
set
{
_someValue = value;
OnPropertyChanged();
lock (_someValueLock)
Monitor.PulseAll(_someValueLock);
Task.Run(() =>
{
lock (_someValueLock)
if (!Monitor.Wait(_someValueLock, 1000))
{
// do something here
}
});
}
}
}
It's delayed (by 1000 ms in given example) operation. New task is created for every change done by slider (either by mouse or keyboard). Before starting task it signals (by using Monitor.PulseAll, perhaps even Monitor.Pulse would be enough) to running already tasks (if any) to stop. Do something part will only occurs when Monitor.Wait don't get signal within timeout.
Why this solution? I don't like spawning behavior or having unnecessary event handling in the View. All code is in one place, no extra events needed, ViewModel has choice to either react on each value change or at the end of user operation (which adds tons of flexibility, especially when using binding).
My implementation is based on #Alan's and #SandRock's answer:
public class SliderValueChangeByDragBehavior : Behavior<Slider>
{
private bool hasDragStarted;
/// <summary>
/// On behavior attached.
/// </summary>
protected override void OnAttached()
{
AssociatedObject.AddHandler(Thumb.DragStartedEvent, (DragStartedEventHandler)Slider_DragStarted);
AssociatedObject.AddHandler(Thumb.DragCompletedEvent, (DragCompletedEventHandler)Slider_DragCompleted);
AssociatedObject.ValueChanged += Slider_ValueChanged;
base.OnAttached();
}
/// <summary>
/// On behavior detaching.
/// </summary>
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.RemoveHandler(Thumb.DragStartedEvent, (DragStartedEventHandler)Slider_DragStarted);
AssociatedObject.RemoveHandler(Thumb.DragCompletedEvent, (DragCompletedEventHandler)Slider_DragCompleted);
AssociatedObject.ValueChanged -= Slider_ValueChanged;
}
private void updateValueBindingSource()
=> BindingOperations.GetBindingExpression(AssociatedObject, RangeBase.ValueProperty)?.UpdateSource();
private void Slider_DragStarted(object sender, DragStartedEventArgs e)
=> hasDragStarted = true;
private void Slider_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
hasDragStarted = false;
updateValueBindingSource();
}
private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (!hasDragStarted)
updateValueBindingSource();
}
}
You can apply it in that way:
...
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:myWhateverNamespace="clr-namespace:My.Whatever.Namespace;assembly=My.Whatever.Assembly"
...
<Slider
x:Name="srUserInterfaceScale"
VerticalAlignment="Center"
DockPanel.Dock="Bottom"
IsMoveToPointEnabled="True"
Maximum="{x:Static localLibraries:Library.MAX_USER_INTERFACE_SCALE}"
Minimum="{x:Static localLibraries:Library.MIN_USER_INTERFACE_SCALE}"
Value="{Binding Source={x:Static localProperties:Settings.Default}, Path=UserInterfaceScale, UpdateSourceTrigger=Explicit}">
<i:Interaction.Behaviors>
<myWhateverNamespace:SliderValueChangeByDragBehavior />
</i:Interaction.Behaviors>
</Slider>
I've set the UpdateSourceTrigger to explicit, as the behaviour does it. And you are in need of the nuget package Microsoft.Xaml.Behaviors(.Wpf/.Uwp.Managed).
Here is a behavior that handles this problem plus the same thing with the keyboard. https://gist.github.com/4326429
It exposes a Command and Value properties. The value is passed as the parameter of the command. You can databind to the value property (and use it in the viewmodel). You may add an event handler for a code-behind approach.
<Slider>
<i:Interaction.Behaviors>
<b:SliderValueChangedBehavior Command="{Binding ValueChangedCommand}"
Value="{Binding MyValue}" />
</i:Interaction.Behaviors>
</Slider>
My solution is basically Santo's solution with a few more flags. For me, the slider is being updated from either reading the stream or the user manipulation (either from a mouse drag or using the arrow keys etc)
First, I had wrote the code to update the slider value from reading the stream:
delegate void UpdateSliderPositionDelegate();
void UpdateSliderPosition()
{
if (Thread.CurrentThread != Dispatcher.Thread)
{
UpdateSliderPositionDelegate function = new UpdateSliderPositionDelegate(UpdateSliderPosition);
Dispatcher.Invoke(function, new object[] { });
}
else
{
double percentage = 0; //calculate percentage
percentage *= 100;
slider.Value = percentage; //this triggers the slider.ValueChanged event
}
}
I then added my code that captured when the user was manipulating the slider with a mouse drag:
<Slider Name="slider"
Maximum="100" TickFrequency="10"
ValueChanged="slider_ValueChanged"
Thumb.DragStarted="slider_DragStarted"
Thumb.DragCompleted="slider_DragCompleted">
</Slider>
And added the code behind:
/// <summary>
/// True when the user is dragging the slider with the mouse
/// </summary>
bool sliderThumbDragging = false;
private void slider_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
{
sliderThumbDragging = true;
}
private void slider_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
sliderThumbDragging = false;
}
When the user updates the slider's value with a mouse drag, the value will still change due to the stream being read and calling UpdateSliderPosition(). To prevent conflicts, UpdateSliderPosition() had to be changed:
delegate void UpdateSliderPositionDelegate();
void UpdateSliderPosition()
{
if (Thread.CurrentThread != Dispatcher.Thread)
{
UpdateSliderPositionDelegate function = new UpdateSliderPositionDelegate(UpdateSliderPosition);
Dispatcher.Invoke(function, new object[] { });
}
else
{
if (sliderThumbDragging == false) //ensure user isn't updating the slider
{
double percentage = 0; //calculate percentage
percentage *= 100;
slider.Value = percentage; //this triggers the slider.ValueChanged event
}
}
}
While this will prevent conflicts, we are still unable to determine whether the value is being updated by the user or by a call to UpdateSliderPosition(). This is fixed by yet another flag, this time set from within UpdateSliderPosition().
/// <summary>
/// A value of true indicates that the slider value is being updated due to the stream being read (not by user manipulation).
/// </summary>
bool updatingSliderPosition = false;
delegate void UpdateSliderPositionDelegate();
void UpdateSliderPosition()
{
if (Thread.CurrentThread != Dispatcher.Thread)
{
UpdateSliderPositionDelegate function = new UpdateSliderPositionDelegate(UpdateSliderPosition);
Dispatcher.Invoke(function, new object[] { });
}
else
{
if (sliderThumbDragging == false) //ensure user isn't updating the slider
{
updatingSliderPosition = true;
double percentage = 0; //calculate percentage
percentage *= 100;
slider.Value = percentage; //this triggers the slider.ValueChanged event
updatingSliderPosition = false;
}
}
}
Finally, we're able to detect whether the slider is being updated by the user or by the call to UpdateSliderPosition():
private void slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (updatingSliderPosition == false)
{
//user is manipulating the slider value (either by keyboard or mouse)
}
else
{
//slider value is being updated by a call to UpdateSliderPosition()
}
}
Hope that helps someone!
If you want to get the manipulation ended information even if the user is not using the thumb to change the value (ie clicking somewhere in the track bar), you can attach an event handler to your slider for the pointer pressed and capture lost events. You can do the same thing for the keyboard events
var pointerPressedHandler = new PointerEventHandler(OnSliderPointerPressed);
slider.AddHandler(Control.PointerPressedEvent, pointerPressedHandler, true);
var pointerCaptureLostHandler = new PointerEventHandler(OnSliderCaptureLost);
slider.AddHandler(Control.PointerCaptureLostEvent, pointerCaptureLostHandler, true);
var keyDownEventHandler = new KeyEventHandler(OnSliderKeyDown);
slider.AddHandler(Control.KeyDownEvent, keyDownEventHandler, true);
var keyUpEventHandler = new KeyEventHandler(OnSliderKeyUp);
slider.AddHandler(Control.KeyUpEvent, keyUpEventHandler, true);
The "magic" here is the AddHandler with the true parameter at the end which allows us to get the slider "internal" events.
The event handlers :
private void OnKeyDown(object sender, KeyRoutedEventArgs args)
{
m_bIsPressed = true;
}
private void OnKeyUp(object sender, KeyRoutedEventArgs args)
{
Debug.WriteLine("VALUE AFTER KEY CHANGE {0}", slider.Value);
m_bIsPressed = false;
}
private void OnSliderCaptureLost(object sender, PointerRoutedEventArgs e)
{
Debug.WriteLine("VALUE AFTER CHANGE {0}", slider.Value);
m_bIsPressed = false;
}
private void OnSliderPointerPressed(object sender, PointerRoutedEventArgs e)
{
m_bIsPressed = true;
}
The m_bIsPressed member will be true when the user is currently manipulating the slider (click, drag or keyboard). It will be reset to false once done .
private void OnValueChanged(object sender, object e)
{
if(!m_bIsPressed) { // do something }
}
This subclassed version of the Slider wokrs as you want:
public class NonRealtimeSlider : Slider
{
static NonRealtimeSlider()
{
var defaultMetadata = ValueProperty.GetMetadata(typeof(TextBox));
ValueProperty.OverrideMetadata(typeof(NonRealtimeSlider), new FrameworkPropertyMetadata(
defaultMetadata.DefaultValue,
FrameworkPropertyMetadataOptions.Journal | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
defaultMetadata.PropertyChangedCallback,
defaultMetadata.CoerceValueCallback,
true,
UpdateSourceTrigger.Explicit));
}
protected override void OnThumbDragCompleted(DragCompletedEventArgs e)
{
base.OnThumbDragCompleted(e);
GetBindingExpression(ValueProperty)?.UpdateSource();
}
}
I liked Answer by #sinatr.
My Solution Based on Answer Above:
This solution cleans up the code a lot and encapsulates the mechanism.
public class SingleExecuteAction
{
private readonly object _someValueLock = new object();
private readonly int TimeOut;
public SingleExecuteAction(int timeOut = 1000)
{
TimeOut = timeOut;
}
public void Execute(Action action)
{
lock (_someValueLock)
Monitor.PulseAll(_someValueLock);
Task.Run(() =>
{
lock (_someValueLock)
if (!Monitor.Wait(_someValueLock, TimeOut))
{
action();
}
});
}
}
Use it in Your class as:
public class YourClass
{
SingleExecuteAction Action = new SingleExecuteAction(1000);
private int _someProperty;
public int SomeProperty
{
get => _someProperty;
set
{
_someProperty = value;
Action.Execute(() => DoSomething());
}
}
public void DoSomething()
{
// Only gets executed once after delay of 1000
}
}
My solution for the WinUI3 v1.2.2 follows here:
Xaml file:
<Slider Margin="10, 0" MinWidth="200" LargeChange="0.5"
TickPlacement="BottomRight" TickFrequency="10"
SnapsTo="StepValues" StepFrequency="5"
Maximum="719"
Value="{x:Bind Path=XamlViewModel.XamlSliderToDateInt, Mode=TwoWay}">
</Slider>
To-Date slider property:
private int _sliderToDateInt;
public int XamlSliderToDateInt
{
get { return _sliderToDateInt; }
set
{
SetProperty(ref _sliderToDateInt, value);
_myDebounceTimer.Debounce(() =>
{
this.XamlSelectedTimeChangedTo = TimeSpan.FromMinutes(value);
// time-expensive methods:
this.XamlLCModel = _myOxyPlotModel.UpdatePlotModel(_myLCPowerRecList, XamlSliderFromDateInt, XamlSliderToDateInt, _myOxyPlotPageOptions);
this.XamlTRModel = _myOxyPlotModel.UpdatePlotModel(_myTRPowerRecList, XamlSliderFromDateInt, XamlSliderToDateInt, _myOxyPlotPageOptions);
},
TimeSpan.FromSeconds(0.6));
}
}
Timer declaration:
private DispatcherQueueTimer _myDebounceTimer;
Timer initialization in constructor:
_myDebounceTimer = _dispatcherQueue.CreateTimer();
The method _myOxyPlotModel.UpdatePlotModel() will be called not faster than every 0.6sec, even when the XamlSliderToDateInt property is updated much faster by dragging the slider.
It feels like drag to a position then stop dragging with/without releasing the mouse button and just after the stop the timer counts to 0.6sec and calls my oxyplot-methods.
The Debounce() method belongs to the namespace CommunityToolkit.WinUI.UI.
<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>
PositionSlider.LostMouseCapture += new MouseEventHandler(Position_LostMouseCapture);
PositionSlider.AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(Position_DragCompleted));

Resources