Callback before updating DependencyProperty - c#

I have a ListBox in WPB bound to an ObservableCollection
public static readonly DependencyProperty ProgramsProperty =
DependencyProperty.Register("Programs",
typeof(ObservableCollection<ProgramData>), typeof(ProgramView),
new PropertyMetadata(default(ObservableCollection<ProgramData>)));
public ObservableCollection<ProgramData> Programs
{
get { return (ObservableCollection<ProgramData>)GetValue(ProgramsProperty); }
set { SetValue(ProgramsProperty, value); }
}
and the selected element of the ListBox bound to one element
public static readonly DependencyProperty SelectedProgramProperty =
DependencyProperty.Register("SelectedProgram",
typeof(ProgramData), typeof(ProgramView),
new PropertyMetadata(default(ProgramData)));
public DispenserInfo SelectedProgram
{
get { return (ProgramData)GetValue(SelectedProgramProperty); }
set { SetValue(SelectedProgramProperty, value); }
}
When the user changes the selected element in the ListBox I would like to check the status of the "old" selected element - maybe we need to save the old element - and react in some way.
So I would like to something like this
public static bool UpdateCallback(ProgramData oldVal, ProgramData newVal)
{
if (oldVal.DataChanged == false)
return true;
var res = MessageBox.Show("Save or discard changes", "Question",
MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
switch (res)
{
case MessageBoxResult.Yes:
oldVal.Save();
return true;
case MessageBoxResult.No:
oldVal.Discard();
return true;
case MessageBoxResult.Cancel:
return false;
}
return true;
}
I there a way to do this with validation/coercing callbacks?
Update:
As Oliver and Sheridan suggested I tried the changed as well as the coerce callback and this is working quite well to do some tasks in between clicking and refreshing the UI. But when I try to cancel the update in the coerceCallback like this
private static object CoerceValueCallback(DependencyObject dependencyObject, object baseValue)
{
var #this = (ProgramView)dependencyObject;
var res = MessageBox.Show("Save or discard changes", "Question", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
switch (res)
{
case MessageBoxResult.Cancel:
return #this.SelectedProgram;
}
return baseValue;
}
the UI stays at the old values as expected but the ListBox highlights the wrong line - the one the user has clicked and not the one from the binding when canceled in the coerceCallback.
Do I need to update the binding manually? Any ideas

You can add a PropertyChanged handler to your DependencyProperty:
public static readonly DependencyProperty ProgramsProperty =
DependencyProperty.Register("Programs",
typeof(ObservableCollection<ProgramData>), typeof(ProgramView),
new UIPropertyMetadata(default(ObservableCollection<ProgramData>,
(d, e) => ((ProgramView)d).OnProgramsChanged(d, e))));
private void OnProgramsChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
// Do something with e.OldValue and e.NewValue here
}

Sure there is. Look at the last parameter of DependencyProperty.Register !
public static readonly DependencyProperty MyPropertyProperty =
System.Windows.DependencyProperty.Register("MyProperty", typeof(string), typeof(MyClass), new PropertyMetadata(default(string), (
sender, args) =>
{
//changed callback
var #this = (MyClass)sender;
var newval = (string)args.NewValue;
var oldval = (string)args.NewValue;
// do whatever you like
},
(s, e) =>
{
// coerce callback
if (e == null)
return " - empty string -";
return e;
}));
For most WPF dependency properties, there is even more: Properties metadata are an instance of FrameworkPropertyMetadata instead of simple good old PropertyMetadata. It has more "hidden" options. You may use it as well instead of PropertyMetadata.

Related

WPF: Creating a custom button with automatic bindings from c# code

I have a large number of buttons that have all the same binding schema based on a key string. I thought I could save some code duplication by making a custom control that takes that string and sets all the bindings accordingly. I came up with the following code:
public class StateTransitionButton : Button
{
public string StateTransition
{
get { return (string)this.GetValue(StateTransitionProperty); }
set { this.SetValue(StateTransitionProperty, value); }
}
public static readonly DependencyProperty StateTransitionProperty =
DependencyProperty.Register("MyProperty", typeof(string), typeof(StateTransitionButton), new PropertyMetadata(null, OnTransitionChanged));
private static void OnTransitionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is StateTransitionButton button && e.NewValue is string key)
{
button.CommandParameter = key;
Binding commandBinding = new("ButtonClicked");
commandBinding.Source = button.DataContext;
_ = button.SetBinding(CommandProperty, commandBinding);
button.CommandParameter = key;
Binding visibilityBinding = new("CurrentState")
{
Converter = new UIStateToVisibilityConverter(),
ConverterParameter = key
};
visibilityBinding.Source = button.DataContext;
_ = button.SetBinding(VisibilityProperty, visibilityBinding);
Binding tooltipBinding = new("CurrentState")
{
Converter = new UIStateToTooltipConverter(),
ConverterParameter = key
};
tooltipBinding.Source = button.DataContext;
_ = button.SetBinding(ToolTipProperty, tooltipBinding);
}
}
}
The converters are already used in the old code and work as intended. In the code above the bindings appear not to be set correctly. When I check them in snoop the command binding has an error (i never figured out how to get usable error text from snoop), visibility still has the default value and not a binding and the tooltip property is not inspectable.
Update: I figured this out as I was finishing up writing it. I will post it anyway and answer it myself because I couldn't find any nice clear examples of how to set up bindings inside a custom control and other might find my solution useful. Please mark this as a duplicate if there is a better one I missed.
So it turns out the StateTransition property was getting set before the datacontext. This means all the bindings were binding onto null. The fix is to listen to the DataContextChanged event and rebind. This is more correct anyway even if the original code had worked because otherwise it would not handle datacontext changes.
Working example:
public class StateTransitionButton : Button
{
public StateTransitionButton()
{
this.DataContextChanged += this.OnDataContextChanged;
}
public string StateTransition
{
get { return (string)this.GetValue(StateTransitionProperty); }
set { this.SetValue(StateTransitionProperty, value); }
}
public static readonly DependencyProperty StateTransitionProperty =
DependencyProperty.Register("MyProperty", typeof(string), typeof(StateTransitionButton), new PropertyMetadata(null, OnTransitionChanged));
private static void OnTransitionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is StateTransitionButton button && e.NewValue is string key)
{
button.SetBindings(key);
}
}
public void SetBindings(string key)
{
this.CommandParameter = key;
Binding commandBinding = new("ButtonClicked");
commandBinding.Source = this.DataContext;
_ = this.SetBinding(CommandProperty, commandBinding);
this.CommandParameter = key;
Binding visibilityBinding = new("CurrentState")
{
Converter = new UIStateToVisibilityConverter(),
ConverterParameter = key
};
visibilityBinding.Source = this.DataContext;
_ = this.SetBinding(VisibilityProperty, visibilityBinding);
Binding tooltipBinding = new("CurrentState")
{
Converter = new UIStateToTooltipConverter(),
ConverterParameter = key
};
tooltipBinding.Source = this.DataContext;
_ = this.SetBinding(ToolTipProperty, tooltipBinding);
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
this.SetBindings(this.StateTransition);
}
}

Binding Failure in WPF using MVVM

I have created a custom TextEditor control that inherits from AvalonEdit. I have done this to facilitate the use of MVVM and Caliburn Micro using this editor control. The [cut down for display purposes] MvvTextEditor class is
public class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public MvvmTextEditor()
{
TextArea.SelectionChanged += TextArea_SelectionChanged;
}
void TextArea_SelectionChanged(object sender, EventArgs e)
{
this.SelectionStart = SelectionStart;
this.SelectionLength = SelectionLength;
}
public static readonly DependencyProperty SelectionLengthProperty =
DependencyProperty.Register("SelectionLength", typeof(int), typeof(MvvmTextEditor),
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.SelectionLength = (int)args.NewValue;
}));
public new int SelectionLength
{
get { return base.SelectionLength; }
set { SetValue(SelectionLengthProperty, value); }
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] string caller = null)
{
var handler = PropertyChanged;
if (handler != null)
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
}
Now, in the view that holds this control, I have the following XAML:
<Controls:MvvmTextEditor
Caliburn:Message.Attach="[Event TextChanged] = [Action DocumentChanged()]"
TextLocation="{Binding TextLocation, Mode=TwoWay}"
SyntaxHighlighting="{Binding HighlightingDefinition}"
SelectionLength="{Binding SelectionLength,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
Document="{Binding Document, Mode=TwoWay}"/>
My issue is SelectionLength (and SelectionStart but let us just consider the length for now as the problem is the same). If I selected something with the mouse, the binding from the View to my View Model works great. Now, I have written a find and replace utility and I want to set the SelectionLength (which has get and set available in the TextEditor control) from the code behind. In my View Model I am simply setting SelectionLength = 50, I implement this in the View Model like
private int selectionLength;
public int SelectionLength
{
get { return selectionLength; }
set
{
if (selectionLength == value)
return;
selectionLength = value;
Console.WriteLine(String.Format("Selection Length = {0}", selectionLength));
NotifyOfPropertyChange(() => SelectionLength);
}
}
when I set SelectionLength = 50, the DependencyProperty SelectionLengthProperty does not get updated in the MvvmTextEditor class, it is like the TwoWay binding to my control is failing but using Snoop there is no sign of this. I thought this would just work via the binding, but this does not seem to be the case.
Is there something simple I am missing, or will I have to set up and event handler in the MvvmTextEditor class which listens for changes in my View Model and updated the DP itself [which presents it's own problems]?
Thanks for your time.
This is because the Getter and Setter from a DependencyProperty is only a .NET Wrapper. The Framework will use the GetValue and SetValue itself.
What you can try is to access the PropertyChangedCallback from your DependencyProperty and there set the correct Value.
public int SelectionLength
{
get { return (int)GetValue(SelectionLengthProperty); }
set { SetValue(SelectionLengthProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectionLength. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectionLengthProperty =
DependencyProperty.Register("SelectionLength", typeof(int), typeof(MvvmTextEditor), new PropertyMetadata(0,SelectionLengthPropertyChanged));
private static void SelectionLengthPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var textEditor = obj as MvvmTextEditor;
textEditor.SelectionLength = e.NewValue;
}
Here is another answer if you are still open. Since SelectionLength is already defined as a dependency property on the base class, rather than create a derived class (or add an already existing property to the derived class), I would use an attached property to achieve the same functionality.
The key is to use System.ComponentModel.DependencyPropertyDescriptor to subscribe to the change event of the already existing SelectionLength dependency property and then take your desired action in the event handler.
Sample code below:
public class SomeBehavior
{
public static readonly DependencyProperty IsEnabledProperty
= DependencyProperty.RegisterAttached("IsEnabled",
typeof(bool), typeof(SomeBehavior), new PropertyMetadata(OnIsEnabledChanged));
public static void SetIsEnabled(DependencyObject dpo, bool value)
{
dpo.SetValue(IsEnabledProperty, value);
}
public static bool GetIsEnabled(DependencyObject dpo)
{
return (bool)dpo.GetValue(IsEnabledProperty);
}
private static void OnIsEnabledChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs args)
{
var editor = dpo as TextEditor;
if (editor == null)
return;
var dpDescriptor = System.ComponentModel.DependencyPropertyDescriptor.FromProperty(TextEditor.SelectionLengthProperty,editor.GetType());
dpDescriptor.AddValueChanged(editor, OnSelectionLengthChanged);
}
private static void OnSelectionLengthChanged(object sender, EventArgs e)
{
var editor = (TextEditor)sender;
editor.Select(editor.SelectionStart, editor.SelectionLength);
}
}
Xaml below:
<Controls:TextEditor Behaviors:SomeBehavior.IsEnabled="True">
</Controls:TextEditor>
This is how I did this...
public static readonly DependencyProperty SelectionLengthProperty =
DependencyProperty.Register("SelectionLength", typeof(int), typeof(MvvmTextEditor),
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
if (target.SelectionLength != (int)args.NewValue)
{
target.SelectionLength = (int)args.NewValue;
target.Select(target.SelectionStart, (int)args.NewValue);
}
}));
public new int SelectionLength
{
get { return base.SelectionLength; }
//get { return (int)GetValue(SelectionLengthProperty); }
set { SetValue(SelectionLengthProperty, value); }
}
Sorry for any time wasted. I hope this helps someone else...

How can I update the source from a custom control with dependency properties in silverlight?

I've created a custom control that extends the RichTextBox so that I can create a binding for the xaml property. It all works well as long as I just update the property from the viewmodel but when I try to edit in the richtextbox the property is not updated back.
I have the following code in the extended version of the richtextbox.
public static readonly DependencyProperty TextProperty = DependencyProperty.Register ("Text", typeof(string), typeof(BindableRichTextBox), new PropertyMetadata(OnTextPropertyChanged));
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var rtb = d as BindableRichTextBox;
if (rtb == null)
return;
string xaml = null;
if (e.NewValue != null)
{
xaml = e.NewValue as string;
if (xaml == null)
return;
}
rtb.Xaml = xaml ?? string.Empty;
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
In the view I've set the binding like such
<Controls:BindableRichTextBox Text="{Binding XamlText, Mode=TwoWay}"/>
In the viewmodel I've created the XamlText as a normal property with the NotifyPropertyChanged event being called on updates.
I want the bound XamlText to be updated when the user enters texts in the RichTextBox either on lostfocus or directly during edit, it doesn't really matter.
How can I change the code to make this happen?
You will need to listen to changes to the Xaml-property of the BindableRichTextBox and set the Text-property accordingly. There is an answer available here describing how that could be achieved. Using the approach described in that would the result in the following code (untested):
public BindableRichTextBox()
{
this.RegisterForNotification("Xaml", this, (d,e) => ((BindableRichTextBox)d).Text = e.NewValue);
}
public void RegisterForNotification(string propertyName, FrameworkElement element, PropertyChangedCallback callback)
{
var binding = new Binding(propertyName) { Source = element };
var property = DependencyProperty.RegisterAttached(
"ListenAttached" + propertyName,
typeof(object),
typeof(UserControl),
new PropertyMetadata(callback));
element.SetBinding(property, binding);
}

wpf datagrid : create a DatagridNumericColumn in wpf

I have a requirement that I want to make a datagridcolumn which only accepts numeric values(integer) ,when the user enter something other than numbers handle the textbox .
I tried a lot of webpages ,Iam tired of these ,I greately appreciate anybody have the helping mind.
Based on #nit suggestion, you can create your own class derived from DataGridTextColumn like this:
public class DataGridNumericColumn : DataGridTextColumn
{
protected override object PrepareCellForEdit(System.Windows.FrameworkElement editingElement, System.Windows.RoutedEventArgs editingEventArgs)
{
TextBox edit = editingElement as TextBox;
edit.PreviewTextInput += OnPreviewTextInput;
return base.PrepareCellForEdit(editingElement, editingEventArgs);
}
void OnPreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
{
try
{
Convert.ToInt32(e.Text);
}
catch
{
// Show some kind of error message if you want
// Set handled to true
e.Handled = true;
}
}
}
In the PrepareCellForEdit method you register the OnPreviewTextInput method to the editing TextBox PreviewTextInput event, where you validate for numeric values.
In xaml, you simply use it:
<DataGrid ItemsSource="{Binding SomeCollection}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding NonNumericProperty}"/>
<local:DataGridNumericColumn Binding="{Binding NumericProperty}"/>
</DataGrid.Columns>
</DataGrid>
Hope this helps
Use TryParse instead, this helps to restrict input values to integer numbers only.
/// <summary>
/// This class help to create data grid cell which only support interger numbers.
/// </summary>
public class DataGridNumericColumn : DataGridTextColumn
{
protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
{
TextBox edit = editingElement as TextBox;
if (edit != null) edit.PreviewTextInput += OnPreviewTextInput;
return base.PrepareCellForEdit(editingElement, editingEventArgs);
}
private void OnPreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
{
int value;
if (!int.TryParse(e.Text, out value))
e.Handled = true;
}
}
If you dont want to show any validation errors and just want to block any non-numeral value then you can create the DataGridTemplateColumn and in CellEditingTemplate use the TextBox.
<DataGridTemplateColumn Width="100*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=NumericProperty}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox PreviewTextInput="TextBox_PreviewTextInput" Text="{Binding Path=NumericProperty}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
and in PreviewTextInput of the TextBox set e.Handled = true if value is other than integer:
private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
try
{
Convert.ToInt32(e.Text);
}
catch
{
e.Handled = true;
}
}
I got here looking for a solution to the same problem: constraining the input into cells on a DataGrid to be numeric. But the accepted answer did not work for me. The following did:
For the DataGrid add an event handler for PreparingForCellEdit.
In that event handler, cast the EditingElement to a TextBox and add an event handler for PreviewTextInput to the TextBox.
In the PreviewTextInput event handler set e.Handled to true, if the input should not be allowed.
The above steps work if the user clicks the cell to edit. However, if the cell is not in edit mode, the PreparingForCellEdit event will not be called. To perform validation in that case:
Add an event handler to the DataGrid for PreviewTextInput.
In that event handler, safely cast e.OriginalSource to a DataGridCell (exiting, if it is not a DataGridCell), check the DataGridCell's IsEditing property, and if the cell is not editing set e.Handled to true.
The effect of the above is that the user will have to click into the cell in order to edit its contents and, as such, the PreparingForCellEdit / PreviewTextInput combination above will be invoked for all changes to the cell's contents.
Just to extend #Omribitan's answer, Here is the solution with a data Paste guard added:
public class NumericTextColumn : DataGridTextColumn
{
protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
{
var edit = editingElement as TextBox;
edit.PreviewTextInput += Edit_PreviewTextInput;
DataObject.AddPastingHandler(edit, OnPaste);
return base.PrepareCellForEdit(editingElement, editingEventArgs);
}
private void OnPaste(object sender, DataObjectPastingEventArgs e)
{
var data = e.SourceDataObject.GetData(DataFormats.Text);
if (!IsDataValid(data)) e.CancelCommand();
}
private void Edit_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = !IsDataValid(e.Text);
}
bool IsDataValid(object data)
{
try
{
Convert.ToInt32(data);
return true;
}
catch
{
return false;
}
}
}
For whatever it's worth, here's how I solved it. This solution allows you to specify a variety of options when validating input, allows string formatting to be used (e.g. '$15.00' in the data grid) and more.
The null value and string formatting provided by the Binding class itself do not suffice as neither act correctly when the cell is editable so this class covers it. What this does is it uses another class that I've been using for a long time already: TextBoxInputBehavior, it has been an invaluable asset for me and it originally came from WPF – TextBox Input Behavior blog post albeit the version here seems much older (but well tested). So what I did I just transferred this existing functionality I already had on my TextBoxes to the my custom column and thus I have the same behaviour in both. Isn't that neat?
Here's the code of the custom column:
public class DataGridNumberColumn : DataGridTextColumn
{
private TextBoxInputBehavior _behavior;
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
var element = base.GenerateElement(cell, dataItem);
// A clever workaround the StringFormat issue with the Binding set to the 'Binding' property. If you use StringFormat it
// will only work in edit mode if you changed the value, otherwise it will retain formatting when you enter editing.
if (!string.IsNullOrEmpty(StringFormat))
{
BindingOperations.ClearBinding(element, TextBlock.TextProperty);
BindingOperations.SetBinding(element, FrameworkElement.TagProperty, Binding);
BindingOperations.SetBinding(element,
TextBlock.TextProperty,
new Binding
{
Source = element,
Path = new PropertyPath("Tag"),
StringFormat = StringFormat
});
}
return element;
}
protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
{
if (!(editingElement is TextBox textBox))
return null;
var originalText = textBox.Text;
_behavior = new TextBoxInputBehavior
{
IsNumeric = true,
EmptyValue = EmptyValue,
IsInteger = IsInteger
};
_behavior.Attach(textBox);
textBox.Focus();
if (editingEventArgs is TextCompositionEventArgs compositionArgs) // User has activated editing by already typing something
{
if (compositionArgs.Text == "\b") // Backspace, it should 'clear' the cell
{
textBox.Text = EmptyValue;
textBox.SelectAll();
return originalText;
}
if (_behavior.ValidateText(compositionArgs.Text))
{
textBox.Text = compositionArgs.Text;
textBox.Select(textBox.Text.Length, 0);
return originalText;
}
}
if (!(editingEventArgs is MouseButtonEventArgs) || !PlaceCaretOnTextBox(textBox, Mouse.GetPosition(textBox)))
textBox.SelectAll();
return originalText;
}
private static bool PlaceCaretOnTextBox(TextBox textBox, Point position)
{
int characterIndexFromPoint = textBox.GetCharacterIndexFromPoint(position, false);
if (characterIndexFromPoint < 0)
return false;
textBox.Select(characterIndexFromPoint, 0);
return true;
}
protected override void CancelCellEdit(FrameworkElement editingElement, object uneditedValue)
{
UnwireTextBox();
base.CancelCellEdit(editingElement, uneditedValue);
}
protected override bool CommitCellEdit(FrameworkElement editingElement)
{
UnwireTextBox();
return base.CommitCellEdit(editingElement);
}
private void UnwireTextBox() => _behavior.Detach();
public static readonly DependencyProperty EmptyValueProperty = DependencyProperty.Register(
nameof(EmptyValue),
typeof(string),
typeof(DataGridNumberColumn));
public string EmptyValue
{
get => (string)GetValue(EmptyValueProperty);
set => SetValue(EmptyValueProperty, value);
}
public static readonly DependencyProperty IsIntegerProperty = DependencyProperty.Register(
nameof(IsInteger),
typeof(bool),
typeof(DataGridNumberColumn));
public bool IsInteger
{
get => (bool)GetValue(IsIntegerProperty);
set => SetValue(IsIntegerProperty, value);
}
public static readonly DependencyProperty StringFormatProperty = DependencyProperty.Register(
nameof(StringFormat),
typeof(string),
typeof(DataGridNumberColumn));
public string StringFormat
{
get => (string) GetValue(StringFormatProperty);
set => SetValue(StringFormatProperty, value);
}
}
What I did is I peeked into the source code of DataGridTextColumn and handled the TextBox creation in almost the same way plus I attached the custom behaviour to the TextBox.
Here's the code of the behavior I attached (this is a behavior you can use on any TextBox):
public class TextBoxInputBehavior : Behavior<TextBox>
{
#region DependencyProperties
public static readonly DependencyProperty RegularExpressionProperty = DependencyProperty.Register(
nameof(RegularExpression),
typeof(string),
typeof(TextBoxInputBehavior),
new FrameworkPropertyMetadata(".*"));
public string RegularExpression
{
get
{
if (IsInteger)
return #"^[0-9\-]+$";
if (IsNumeric)
return #"^[0-9.\-]+$";
return (string)GetValue(RegularExpressionProperty);
}
set { SetValue(RegularExpressionProperty, value); }
}
public static readonly DependencyProperty MaxLengthProperty = DependencyProperty.Register(
nameof(MaxLength),
typeof(int),
typeof(TextBoxInputBehavior),
new FrameworkPropertyMetadata(int.MinValue));
public int MaxLength
{
get { return (int)GetValue(MaxLengthProperty); }
set { SetValue(MaxLengthProperty, value); }
}
public static readonly DependencyProperty EmptyValueProperty = DependencyProperty.Register(
nameof(EmptyValue),
typeof(string),
typeof(TextBoxInputBehavior));
public string EmptyValue
{
get { return (string)GetValue(EmptyValueProperty); }
set { SetValue(EmptyValueProperty, value); }
}
public static readonly DependencyProperty IsNumericProperty = DependencyProperty.Register(
nameof(IsNumeric),
typeof(bool),
typeof(TextBoxInputBehavior));
public bool IsNumeric
{
get { return (bool)GetValue(IsNumericProperty); }
set { SetValue(IsNumericProperty, value); }
}
public static readonly DependencyProperty IsIntegerProperty = DependencyProperty.Register(
nameof(IsInteger),
typeof(bool),
typeof(TextBoxInputBehavior));
public bool IsInteger
{
get { return (bool)GetValue(IsIntegerProperty); }
set
{
if (value)
SetValue(IsNumericProperty, true);
SetValue(IsIntegerProperty, value);
}
}
public static readonly DependencyProperty AllowSpaceProperty = DependencyProperty.Register(
nameof(AllowSpace),
typeof (bool),
typeof (TextBoxInputBehavior));
public bool AllowSpace
{
get { return (bool) GetValue(AllowSpaceProperty); }
set { SetValue(AllowSpaceProperty, value); }
}
#endregion
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewTextInput += PreviewTextInputHandler;
AssociatedObject.PreviewKeyDown += PreviewKeyDownHandler;
DataObject.AddPastingHandler(AssociatedObject, PastingHandler);
}
protected override void OnDetaching()
{
base.OnDetaching();
if (AssociatedObject == null)
return;
AssociatedObject.PreviewTextInput -= PreviewTextInputHandler;
AssociatedObject.PreviewKeyDown -= PreviewKeyDownHandler;
DataObject.RemovePastingHandler(AssociatedObject, PastingHandler);
}
private void PreviewTextInputHandler(object sender, TextCompositionEventArgs e)
{
string text;
if (AssociatedObject.Text.Length < AssociatedObject.CaretIndex)
text = AssociatedObject.Text;
else
text = TreatSelectedText(out var remainingTextAfterRemoveSelection)
? remainingTextAfterRemoveSelection.Insert(AssociatedObject.SelectionStart, e.Text)
: AssociatedObject.Text.Insert(AssociatedObject.CaretIndex, e.Text);
e.Handled = !ValidateText(text);
}
private void PreviewKeyDownHandler(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
e.Handled = !AllowSpace;
if (string.IsNullOrEmpty(EmptyValue))
return;
string text = null;
// Handle the Backspace key
if (e.Key == Key.Back)
{
if (!TreatSelectedText(out text))
{
if (AssociatedObject.SelectionStart > 0)
text = AssociatedObject.Text.Remove(AssociatedObject.SelectionStart - 1, 1);
}
}
// Handle the Delete key
else if (e.Key == Key.Delete)
{
// If text was selected, delete it
if (!TreatSelectedText(out text) && AssociatedObject.Text.Length > AssociatedObject.SelectionStart)
{
// Otherwise delete next symbol
text = AssociatedObject.Text.Remove(AssociatedObject.SelectionStart, 1);
}
}
if (text == string.Empty)
{
AssociatedObject.Text = EmptyValue;
if (e.Key == Key.Back)
AssociatedObject.SelectionStart++;
e.Handled = true;
}
}
private void PastingHandler(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(DataFormats.Text))
{
var text = Convert.ToString(e.DataObject.GetData(DataFormats.Text));
if (!ValidateText(text))
e.CancelCommand();
}
else
e.CancelCommand();
}
public bool ValidateText(string text)
{
return new Regex(RegularExpression, RegexOptions.IgnoreCase).IsMatch(text) && (MaxLength == int.MinValue || text.Length <= MaxLength);
}
/// <summary>
/// Handle text selection.
/// </summary>
/// <returns>true if the character was successfully removed; otherwise, false.</returns>
private bool TreatSelectedText(out string text)
{
text = null;
if (AssociatedObject.SelectionLength <= 0)
return false;
var length = AssociatedObject.Text.Length;
if (AssociatedObject.SelectionStart >= length)
return true;
if (AssociatedObject.SelectionStart + AssociatedObject.SelectionLength >= length)
AssociatedObject.SelectionLength = length - AssociatedObject.SelectionStart;
text = AssociatedObject.Text.Remove(AssociatedObject.SelectionStart, AssociatedObject.SelectionLength);
return true;
}
}
All the good credit for above Behaviour class goes to blindmeis, I merely tweaked it over time. After checking his Blog I see he has a newer version of it so you may check it out. I was very happy to find out I could use his behaviour on DataGrid as well!
This solution worked really well, you can edit the cell properly via mouse/keyboard, paste the contents properly, use any binding source update triggers, use any string formatting etc. - it just works.
Here's an example of how to use it:
<local:DataGridNumberColumn Header="Nullable Int Currency" IsInteger="True" Binding="{Binding IntegerNullable, TargetNullValue=''}" StringFormat="{}{0:C}" />
Hope this helps someone.
I went on from Omris approach
however i wanted to be able to delete the cell value after it has input in case they wanted to clear it
The way i did this was overriding the CommitCellEdit method and making the string null instead of blank. Im also using decimal? in my case
public class DataGridNumericColumn : DataGridTextColumn
{
protected override object PrepareCellForEdit(System.Windows.FrameworkElement editingElement, System.Windows.RoutedEventArgs editingEventArgs)
{
TextBox edit = editingElement as TextBox;
edit.PreviewTextInput += OnPreviewTextInput;
return base.PrepareCellForEdit(editingElement, editingEventArgs);
}
protected override bool CommitCellEdit(System.Windows.FrameworkElement editingElement)
{
TextBox tb = editingElement as TextBox;
if (string.IsNullOrEmpty(tb.Text))
tb.Text = null;
return base.CommitCellEdit(editingElement);
}
void OnPreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
{
try
{
Convert.ToDecimal(e.Text);
}
catch
{
// Show some kind of error message if you want
// Set handled to true
e.Handled = true;
}
}
}

setting Dependency property from two different sources: want different postprocessing

I have the following WPF code. You can see in the comments there that I have a problem with my OnValueChanged handler. I need the code there to differentiate between a Value set from the UI (through various bindings) and one set from the manager class. I had hoped that DependencyPropertyChangedEventArgs would have some kind of source that I could use to differentiate this, but I don't see anything like that. Ideas? Is there some way to set a WPF DependencyProperty without triggering its PropertyChanged handler? Thanks for your time.
public class GaugeBaseControl : UserControl
{
protected readonly AssetModelManager Manager;
public GaugeBaseControl(AssetModelManager mgr)
{
Manager = mgr;
if(mgr != null)
mgr.TelemetryValueChanged += MgrOnTelemetryValueChanged; // coming on background thread
}
private void MgrOnTelemetryValueChanged(KeyValuePair<string, object> keyValuePair)
{
if(_localTelemetryId != keyValuePair.Key)
return;
Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() =>
{
if (!Equals(Value, keyValuePair.Value))
Value = keyValuePair.Value;
}));
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gbc = (GaugeBaseControl) d;
var id = gbc.TelemetryId;
if (!string.IsNullOrEmpty(id))
{
// this is the problem:
// I need to always set gbc.Manager[id] if this event was triggered from the UI (even when equal)
// however, if it was triggered by TelemetryValueChanged then we don't want to go around in circles
if (!Equals(gbc.Manager[id], e.NewValue))
gbc.Manager[id] = e.NewValue;
}
}
private string _localTelemetryId; // to save us a cross-thread check
private static void OnTelemetryIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gbc = (GaugeBaseControl)d;
var tid = gbc.TelemetryId;
gbc._localTelemetryId = tid;
gbc.Value = string.IsNullOrEmpty(tid) ? null : gbc.Manager[tid];
}
public static readonly DependencyProperty TelmetryIdProperty = DependencyProperty.Register("TelemetryId", typeof(string), typeof(GaugeBaseControl), new PropertyMetadata(OnTelemetryIdChanged));
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(GaugeBaseControl), new PropertyMetadata(OnValueChanged));
public object Value
{
get { return GetValue(ValueProperty); }
set { SetValue(ValueProperty, value);}
}
public string TelemetryId
{
get { return (string)GetValue(TelmetryIdProperty); }
set { SetValue(TelmetryIdProperty, value); }
}
}
It seems a bit hackish, but it is the best shot i could come up with without changing the architecture. You could stop listening on the TelemetryValueChanged event while doing your internal update to stop the roundtrip like so:
internal void SetManagerIdInternal(string id, object value)
{
if(mgr != null)
{
mgr.TelemetryValueChanged -= MgrOnTelemetryValueChanged;
mgr[id] = value;
mgr.TelemetryValueChanged += MgrOnTelemetryValueChanged;
}
}
And use it like this:
if (!Equals(gbc.Manager[id], e.NewValue))
SetManagerIdInternal(id, e.NewValue);
You could also use a private field to just skip doing work without unregistering/reregistering the event in MgrOnTelemetryValueChanged wich might be better performance wise, but i haven't tested it.

Categories