Canceling TextBox input on validation error in WPF - wpf

I'm trying to figure out how to cancel user input in a TextBox when a validation error occurs. If the user attempts to enter an invalid character I would like to prevent it from being added to the TextBox.
How can I add to or modify the code below to prevent the TextBox from accepting invalid characters? Is it possible without listening to the TextBox.TextChanged event?
My TextBox looks like:
<TextBox Validation.Error="OnSomeTextBoxValidationError">
<TextBox.Text>
<Binding Path="Value" NotifyOnValidationError="True" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:SomeValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
My custom validation rule looks like:
public class SomeValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string hex_string = value as string;
Match invalid_chars = Regex.Match(hex_string, "[^0-9a-fA-F]");
bool is_valid = (invalid_chars.Success == false);
string error_context = null;
if (is_valid == false)
{
error_context = "Invalid characters";
}
return new ValidationResult(is_valid, error_context);
}
}
I have an error handler... can I do anything with it?
private void OnSomeTextBoxValidationError(object sender, ValidationErrorEventArgs e)
{
// Can I do anything here?
}
Please provide an original answer if possible, rather than referring to a URL. I've read a lot of possible solutions involving event handlers, but I haven't come across anyone discussing the possibility of doing all my validation in the ValidationRule.

After a lot of research it seems that the only way to have full control over the input to a TextBox is to handle several events directly. According to WPF Recipes in C# 2008 (1st ed., p. 169):
Unfortunately, there's no easy way (at present) to combine the useful, high-level data binding feature with the lower-level keyboard handling that would be necessary to prevent the user from typing invalid characters altogether.
Here's what I came up with to create a hexadecimal numeric TextBox which only accepts characters a-f, A-F and 0-9.
SomeClass.xaml
<TextBox
x:Name="SomeTextBox"
LostFocus="TextBoxLostFocus"
PreviewKeyDown="TextBoxPreviewKeyDown"
PreviewTextInput="TextBoxPreviewTextInput" />
SomeClass.xaml.cs
private string mInvalidCharPattern = "[^0-9a-fA-F]";
// In my case SomeClass derives from UserControl
public SomeClass()
{
DataObject.AddPastingHandler(
this.SomeTextBox,
new DataObjectPastingEventHandler(TextBoxPasting));
}
private void TextBoxLostFocus(object sender, RoutedEventArgs e)
{
// You may want to refresh the TextBox's Text here. If the user deletes
// the contents of the TextBox and clicks off of it, then you can restore
// the original value.
}
// Catch the space character, since it doesn't trigger PreviewTextInput
private void TextBoxPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space) { e.Handled = true; }
}
// Do most validation here
private void TextBoxPreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (ValidateTextInput(e.Text) == false) { e.Handled = true; }
}
// Prevent pasting invalid characters
private void TextBoxPasting(object sender, DataObjectPastingEventArgs e)
{
string lPastingText = e.DataObject.GetData(DataFormats.Text) as string;
if (ValidateTextInput(lPastingText) == false) { e.CancelCommand(); }
}
// Do the validation in a separate function which can be reused
private bool ValidateTextInput(string aTextInput)
{
if (aTextInput == null) { return false; }
Match lInvalidMatch = Regex.Match(aTextInput, this.mInvalidCharPattern);
return (lInvalidMatch.Success == false);
}

You've probably seen this already, but it's the simplest solution and has always worked for me. I catch the PreviewKeyDown event and..
<TextBox PreviewKeyDown="TextBox_PreviewKeyDown" Width="150" Height="30"></TextBox>
private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
... validation here, eg. to stop spacebar from being pressed, you'd use:
if (e.Key == Key.Space) e.Handled = true;
}

I'm using this for a Windows Phone Runtime 8.1 app to allow only certain characters:
<TextBox x:Name="TextBoxTitle"
MaxLength="24"
InputScope="AlphanumericHalfWidth"
TextChanged="TextBoxTitle_TextChanged"
KeyUp="TextBoxTitle_KeyUp"
Paste="TextBoxTitle_Paste"/>
using System.Text.RegularExpressions;
bool textBoxTitle_TextPasted = false;
private void TextBoxTitle_Paste(object sender, TextControlPasteEventArgs e)
{
textBoxTitle_TextPasted = true;
}
// only allow characters A-Z, a-z, numbers and spaces
private void TextBoxTitle_TextChanged(object sender, TextChangedEventArgs e)
{
string fileNameCompatibleString = Regex.Replace(TextBoxTitle.Text, "[^a-zA-Z0-9\x20]", String.Empty);
if (TextBoxTitle.Text != fileNameCompatibleString)
{
if (textBoxTitle_TextPasted)
{
TextBoxTitle.Text = fileNameCompatibleString;
TextBoxTitle.SelectionStart = fileNameCompatibleString.Length;
}
else
{
int selectionStartSaved = TextBoxTitle.SelectionStart;
TextBoxTitle.Text = fileNameCompatibleString;
TextBoxTitle.SelectionStart = selectionStartSaved-1;
}
}
textBoxTitle_TextPasted = false;
}
// close SIP keyboard on enter key up
private void TextBoxTitle_KeyUp(object sender, KeyRoutedEventArgs e)
{
if (e.Key == Windows.System.VirtualKey.Enter)
{
Windows.ApplicationModel.Core.CoreApplication.GetCurrentView().CoreWindow.IsInputEnabled = false;
Windows.ApplicationModel.Core.CoreApplication.GetCurrentView().CoreWindow.IsInputEnabled = true;
e.Handled = true;
}
}

Going after the TextBox PreviewKeyUp event worked well. Captured the current text in the text box by casting the sender as a TextBox. Then used a RegEx replace to replace invalid characters. Could also add some tool tip text in here as well, but for now this removes invalid characters and turns the background red for instant user feedback.

Related

grid.Focus, TextBox and Delete key interaction?

I have the following code (a TextBox is focused by default):
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (Keyboard.IsKeyDown(Key.LeftCtrl))
{
if (Keyboard.IsKeyDown(Key.S))
{
grid.Focus();
ModifyPart(sender, e);
}
if (Keyboard.IsKeyDown(Key.Delete))
{
grid.Focus();
DeletePart(sender, e);
}
}
}
The Ctrl + S combination trigers, but Ctrl + Delete won't. Instead it deletes the contents of the TextBox(which I programmatically highlighted by default). I replaced Delete with Enter and it works, so I guess Delete key has some sort of a higher priority over everything else.
If I move grid.Focus(); out and above the if (Keyboard.IsKeyDown(Key.Delete)) method, then it works, but I lose functionalities of Ctrl+C, Ctrl+V, Ctrl+P, etc, because I'm removing focus from the TextBox.
Any idea how to overwrite the Delete priority?
Based on #ASh and #Soleil - Mathieu Prévot answers.
XAML:
PreviewKeyDown="Window_PreviewKeyDown">
CS:
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
this.Close();
}
if (Keyboard.IsKeyDown(Key.LeftCtrl))
{
if (Keyboard.IsKeyDown(Key.S))
{
ModifyPart(sender, e);
e.Handled = true;
}
if (Keyboard.IsKeyDown(Key.Delete))
{
DeletePart(sender, e);
e.Handled = true;
}
}
}

Extended WPF Toolkit DoubleUpDown

I've added an Extended WPF Toolkit DoubleUpDown control.
The behaviour is if you type 5.35 it is fine.
Then say you type 4.3errortext7 and tab it reverts back to 5.35 (as the 4.3errortext7 is not a valid number).
However I'd like it just to go to 4.37 in that case (and ignore the invalid characters.
Is there an elegant way to get my required behaviour?
The best I was able to come up with is to use the PreviewTextInput Event and check for valid input, unfortunatly it will allow still allow a space between numbers, but will prevent all other text from being input.
private void doubleUpDown1_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (!(char.IsNumber(e.Text[0]) || e.Text== "." ))
{
e.Handled = true;
}
}
Maybe a bit late but I had the same problem yesterday and I also did not want to register a handler every time I use the control. I mixed the solution by Mark Hall with an Attached Behavior (inspired by this post: http://www.codeproject.com/Articles/28959/Introduction-to-Attached-Behaviors-in-WPF):
public static class DoubleUpDownBehavior
{
public static readonly DependencyProperty RestrictInputProperty =
DependencyProperty.RegisterAttached("RestrictInput", typeof(bool),
typeof(DoubleUpDownBehavior),
new UIPropertyMetadata(false, OnRestrictInputChanged));
public static bool GetRestrictInput(DoubleUpDown ctrl)
{
return (bool)ctrl.GetValue(RestrictInputProperty);
}
public static void SetRestrictInput(DoubleUpDown ctrl, bool value)
{
ctrl.SetValue(RestrictInputProperty, value);
}
private static void OnRestrictInputChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
DoubleUpDown item = depObj as DoubleUpDown;
if (item == null)
return;
if (e.NewValue is bool == false)
return;
if ((bool)e.NewValue)
item.PreviewTextInput += OnPreviewTextInput;
else
item.PreviewTextInput -= OnPreviewTextInput;
}
private static void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (!(char.IsNumber(e.Text[0]) ||
e.Text == CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator))
{
e.Handled = true;
}
}
}
Then you can simply set the default style for DoubleUpDown like this:
<Style TargetType="xctk:DoubleUpDown">
<Setter Property="behaviors:DoubleUpDownBehavior.RestrictInput" Value="True" />
</Style>
In my case, it was much better to use regex.
private void UpDownBox_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
var upDownBox = (sender as DoubleUpDown);
TextBox textBoxInTemplate = (TextBox)upDownBox.Template.FindName("PART_TextBox", upDownBox);
Regex regex = new Regex("^[.][0-9]+$|^[0-9]*[.]{0,1}[0-9]*$");
e.Handled = !regex.IsMatch(upDownBox.Text.Insert((textBoxInTemplate).SelectionStart, e.Text));
}

Keypad decimal separator on a Wpf TextBox, how to?

I have a Wpf application with some textbox for decimal input.
I would that when I press "dot" key (.) on numeric keypad of pc keyboard it send the correct decimal separator.
For example, on Italian language the decimal separator is "comma" (,)...Is possible set the "dot" key to send the "comma" character when pressed?
Quick and dirty:
private void NumericTextBox_KeyDown(object sender, KeyEventArgs e) {
if (e.Key == Key.Decimal) {
var txb = sender as TextBox;
int caretPos=txb.CaretIndex;
txb.Text = txb.Text.Insert(txb.CaretIndex, System.Globalization.CultureInfo.CurrentCulture.NumberFormat.CurrencyDecimalSeparator);
txb.CaretIndex = caretPos + 1;
e.Handled = true;
}
}
Although you may set the default converter locale in WPF as suggested by Mamta Dalal it is not enough to convert the "decimal" key press to the correct string. This code will display the correct currency symbol and date/time format on data-bound controls
//Will set up correct string formats for data-bound controls,
// but will not replace numpad decimal key press
private void Application_Startup(object sender, StartupEventArgs e)
{
//Among other settings, this code may be used
CultureInfo ci = CultureInfo.CurrentUICulture;
try
{
//Override the default culture with something from app settings
ci = new CultureInfo([insert your preferred settings retrieval method here]);
}
catch { }
Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = ci;
//Here is the important part for databinding default converters
FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(
XmlLanguage.GetLanguage(ci.IetfLanguageTag)));
//Other initialization things
}
I found that handling the previewKeyDown event window-wide is a little cleaner than textbox-specific (it would be better if it could be applied application-wide).
public partial class MainWindow : Window
{
public MainWindow()
{
//Among other code
if (CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator != ".")
{
//Handler attach - will not be done if not needed
PreviewKeyDown += new KeyEventHandler(MainWindow_PreviewKeyDown);
}
}
void MainWindow_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Decimal)
{
e.Handled = true;
if (CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Length > 0)
{
Keyboard.FocusedElement.RaiseEvent(
new TextCompositionEventArgs(
InputManager.Current.PrimaryKeyboardDevice,
new TextComposition(InputManager.Current,
Keyboard.FocusedElement,
CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator)
) { RoutedEvent = TextCompositionManager.TextInputEvent});
}
}
}
}
If anybody could come up with a way to set it application-wide...

What is the "pressed the delete key" event for the WPF Datagrid?

I want to enable the user to highlight a row on the WPF DataGrid and press delete key to delete the row.
the functionality is already built into the UI of the grid, so to the user, the row disappears
I currently handle this on the SelectionChanged event (code below)
I loop through all the "e.RemovedItems" and delete them with LINQ
Problem is: even when you simply select a row and move off of it, selection change is fired and that row is in e.RemovedItems (which is odd, why would simply selecting something put it in a RemovedItems container?).
So I am looking for a DeleteKeyPressed event so I can simply handle it. What is that event called?
I am using the March 2009 toolkit.
XAML:
<Grid DockPanel.Dock="Bottom">
<toolkit:DataGrid x:Name="TheDataGrid"
SelectionChanged="TheDataGrid_SelectionChanged"
AutoGenerateColumns="True"
RowEditEnding="TheDataGrid_RowEditEnding"/>
code-behind:
private void TheDataGrid_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count > 0)
{
Message.Text = "The following were removed: ";
foreach (object obj in e.RemovedItems)
{
Customer customer = obj as Customer;
Message.Text += customer.ContactName + ",";
_db.Order_Details.DeleteAllOnSubmit(
customer.Orders.SelectMany(o => o.Order_Details));
_db.Orders.DeleteAllOnSubmit(customer.Orders);
_db.Customers.DeleteOnSubmit(customer);
}
}
try
{
_db.SubmitChanges();
}
catch (Exception ex)
{
Message.Text = ex.Message;
}
}
ANSWER:
Thanks lnferis, that was exactly what I was looking for, here is my finished delete handling event for the datagrid, note the KeyDown event doesn't fire for some reason.
XAML:
<toolkit:DataGrid x:Name="TheDataGrid"
KeyDown="TheDataGrid_KeyDown"
PreviewKeyDown="TheDataGrid_PreviewKeyDown"
AutoGenerateColumns="True"
RowEditEnding="TheDataGrid_RowEditEnding"/>
code-behind
private void TheDataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Delete)
{
var grid = (DataGrid)sender;
if (grid.SelectedItems.Count > 0)
{
string checkMessage = "The following will be removed: ";
foreach (var row in grid.SelectedItems)
{
Customer customer = row as Customer;
checkMessage += customer.ContactName + ",";
}
checkMessage = Regex.Replace(checkMessage, ",$", "");
var result = MessageBox.Show(checkMessage, "Delete", MessageBoxButton.OKCancel);
if (result == MessageBoxResult.OK)
{
foreach (var row in grid.SelectedItems)
{
Customer customer = row as Customer;
_db.Order_Details.DeleteAllOnSubmit(
customer.Orders.SelectMany(o => o.Order_Details));
_db.Orders.DeleteAllOnSubmit(customer.Orders);
_db.Customers.DeleteOnSubmit(customer);
}
_db.SubmitChanges();
}
else
{
foreach (var row in grid.SelectedItems)
{
Customer customer = row as Customer;
LoadData();
_db.Refresh(System.Data.Linq.RefreshMode.OverwriteCurrentValues, customer); //TODO: this doesn't refresh the datagrid like the other instance in this code
}
}
}
}
}
private void TheDataGrid_KeyDown(object sender, KeyEventArgs e)
{
Console.WriteLine("never gets here for some reason");
}
The RemovedItems items reflects the items removed from the selection, and not from the grid.
Handle the PreviewKeyDown event, and use the SelectedItems property to delete the selected rows there:
private void PreviewKeyDownHandler(object sender, KeyEventArgs e) {
var grid = (DataGrid)sender;
if ( Key.Delete == e.Key ) {
foreach (var row in grid.SelectedItems) {
... // perform linq stuff to delete here
}
}
}
XAML
<DataGrid ItemsSource="{Binding}" CommandManager.PreviewCanExecute="Grid_PreviewCanExecute" />
Code behind
private void Grid_PreviewCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
DataGrid grid = (DataGrid)sender;
if (e.Command == DataGrid.DeleteCommand)
{
if (MessageBox.Show(String.Format("Would you like to delete {0}", (grid.SelectedItem as Person).FirstName), "Confirm Delete", MessageBoxButton.OKCancel) != MessageBoxResult.OK)
e.Handled = true;
}
}
What are you binding your DataGrid to?
Ideally, you should react to CollectionChanged events on the collection you are binding to. That way, your logic (deletion of removed items) will be separated from your UI.
You can build an Observable collection containing your objects and bind it to ItemsSource just for that purpose if the original collection does not have the necessary events.
It might not suit your specific setup, but that's how I usually do it.
Please follow the below code. I have succeeded with the below code.
Please let me know if changes are required.
private void grdEmployee_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Device.Target.GetType().Name == "DataGridCell")
{
if (e.Key == Key.Delete)
{
MessageBoxResult res = MessageBox.Show("Are you sure want to delete?", "Confirmation!", MessageBoxButton.YesNo,MessageBoxImage.Question);
e.Handled = (res == MessageBoxResult.No);
}
}
}
A little late to the party, but to get Inferis answer working:
Dim isEditing = False
AddHandler dg.BeginningEdit, Sub() isEditing = True
AddHandler dg.RowEditEnding, Sub() isEditing = False
AddHandler dg.PreviewKeyDown, Sub(obj, ev)
If e.Key = Key.Delete AndAlso Not isEditing Then ...
This fixes epalms comment: "if you're editing a cell and use the delete key to remove some characters in the cell, you'll end up deleting the whole row"
The cleanest solution is to use PreviewCanExecute like answered by flux, this is a completed solution to make it a bit more clear for anybody that overlooked his answer like I did:
private void Grid_PreviewCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (e.Command == DataGrid.DeleteCommand)
{
if (MessageBox.Show($"Delete something from something else?", "Confirm removal of something", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
// Do what ever needs to be done when someone deletes the row
}
else
{
e.Handled = true;
// Handled means.. no worries, I took care of it.. and it will not delete the row
}
}
}
No need to hook on to CommandManager.Executed after this.
You want to handle the KeyUp or KeyDown event and check the pressed Key for Delete.
private void OnKeyDown(object sender, KeyEventArgs e) {
if ( Key.Delete == e.Key ) {
// Delete pressed
}
}

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