How to Handle validation within a custom TextBox - wpf

I've got a custom TextBox to handle measurements (feet, inches, mm) that has a couple Dependency Properties that determine what the formatting of the box should be when the box loses focus. I got all the conversions happening in the OnLostFocus function because conversions mid-input would not work. In OnLostFocus, I convert the value to a number depending on some other DP properties and set a Measurement property. All this works great.
My question is, how do I handle validation? When someone inputs an invalid value, I want the textbox to go red, much like you could do with a Binding that has ValidatesOnExceptions=true. I tried something like below in the catch block of OnLostfocus
protected override void OnLostFocus(RoutedEventArgs e)
{
try
{
if (string.IsNullOrWhiteSpace(Text))
{
Text = "0";
}
if (IsMetric)
{
var measurement = convertStuffHere();
Text = measurement.Text;
Measurement = measurement.Value;
}
else
{
var measurement = convertOtherStuffHere();
// convert and formatting stuff here...
Text = measurement.Text;
Measurement = measurement.Value;
}
var binding = this.GetBindingExpression(TextBox.TextProperty);
if (binding != null)
Validation.ClearInvalid(this.GetBindingExpression(TextBox.TextProperty));
}
catch (Exception)
{
var rule = new DataErrorValidationRule();
var binding = this.GetBindingExpression(TextBox.TextProperty);
if (binding != null)
{
ValidationError validationError = new ValidationError(rule, this.GetBindingExpression(TextBox.TextProperty));
validationError.ErrorContent = "This is not a valid input";
Validation.MarkInvalid(this.GetBindingExpression(TextBox.TextProperty), validationError);
}
}
finally
{
base.OnLostFocus(e);
}
}
This almost works, but the validation error shows up late. I have to lose focus, get focus, and lose focus again before a red box shows around the textbox.
I'm using it like <myns:MeasurementTextBox Text="{Binding MeasurementText1, ValidatesOnExceptions=True}" Margin="10" IsMetric="True"></myns:MeasurementTextBox>

You can use TextBoxBase.TextChanged event instead of UIElement.LostFocus.

Related

Cancelling selection in combobox in wpf using MVVM

I have a combobox of type List. I have the ItemsSource and the ItemSelected bound through the datacontext. If the selected item has been changed then I show a pop up message confirming the users action. On clicking of 'Ok' the selection gets changed. But on clicking of cancel, the selection should be cancelled and previous item should be retained. Below is the property that is bound to SelectedItem of the combobox.
Public SomeClass Sel
{
get
{
return _sel;
}
set
{
if (_sel != value)
{
var sview = _sel;
if (Compare())
{
_sel = value;
if (Sel != null)
IsDefault = Sel.IsDefault;
OnPropertyChanged(() => Sel);
}
else
{
MessageBoxResult result = MessageBox.Show("Message.", "Owb Message", MessageBoxButton.OKCancel);
if (result == MessageBoxResult.OK)
{
_sel = value;
if (Sel != null)
IsDefault = Sel.IsDefault;
OnPropertyChanged(() => Sel);
}
else
{
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
_sel = sview;
OnPropertyChanged("Sel");
}), DispatcherPriority.Send, null);
return;
}
}
}
}
}
The combo box is in a pop window. So would Dispatcher object work in that case?
I'm guessing the selected value is retained, but the View doesn't update correctly.
Have a look at this article: http://www.codeproject.com/Articles/407550/The-Perils-of-Canceling-WPF-ComboBox-Selection. Basically, the few workarounds that did exist in .Net 3.5 no longer work in .Net 4.0..
As a general rule, if you've got visual controls leaking into your viewmodel, you're going down a path you don't want to go down.
Create a Behavior that intercepts the OnChanged event of the ComboBox and launches a message box. Here's a tutorial on using behaviours
This keeps all the UI logic in the UI and leaves your viewmodel to manage data and validation.
It works like magic now! I missed out setting the value before calling dispatcher.
_sel = sview

TextBox resets text when tabbing out ot other controls

I have a form with several controls. There a situtions where 'textBoxOtherRelationship' is disable and the text is set to string.empty. But when I then got to another control and tab out the data appears again,while the control remains disabled.
textBoxOtherRelationship.DataBindings.Add(new Binding("Text", _binder, "RelationshipNotes"));
private void ComboBoxRelationShipSelectedValueChanged(object sender, EventArgs e)
{
if ((Constants.Relationship)comboBoxRelationShip.SelectedItem.DataValue == Constants.Relationship.Other)
{
textBoxOtherRelationship.Enabled = true;
if (_formMode != ActionMode.ReadOnly)
{
textBoxFirstName.BackColor = Color.White;
}
}
else
{
textBoxOtherRelationship.Enabled = false;
_model.RelationshipNotes = null;
textBoxOtherRelationship.Text = string.Empty;
if (_formMode != ActionMode.ReadOnly)
{
textBoxFirstName.BackColor = Color.LightYellow;
}
}
}
Hmm.. so I see this line here:
textBoxOtherRelationship.DataBindings.Add(
new Binding("Text", _binder, "RelationshipNotes"));
which tells me that you've got binding set up between the Text property on the textBoxOtherRelationship and a property called "RelationshipNotes" on the datasource _binder.
Great.
So, I'm assuming that the two-way binding works just fine and that when you type something into the textBoxOtherRelationship and that control loses focus the underlying RelationshipNotes property is getting updated as well, right?
Now, looking at your code there, I don't think the underlying datasource is being updated when you set the Text property to string.Empty because that usually doesn't happen until the textbox loses focus and you've disabled the control.
If you add:
textBoxOtherRelationship.DataBindings[0].WriteValue();
after you set the value to string.Empty that string.Empty value will get stored back to the datasource because the databinding will know there is something to update. Programmatically, it doesn't.
I see you have this line:
textBoxOtherRelationship.Enabled = false;
_model.RelationshipNotes = null; <<<----------------------
textBoxOtherRelationship.Text = string.Empty;
Is _model.RelationshipNotes what is ultimately supposed to be bound to that textbox?
The SelectedIndexChanged event doesn't commit the databindings until after the control loses focus, so the quick fix is to write the value first in your event:
private void ComboBoxRelationShipSelectedValueChanged(object sender, EventArgs e)
{
if (comboBoxRelationShip.DataBindings.Count > 0) {
comboBoxRelationShip.DataBindings[0].WriteValue();
if ((Constants.Relationship)comboBoxRelationShip.SelectedItem.DataValue ==
Constants.Relationship.Other) {
textBoxOtherRelationship.Enabled = true;
if (_formMode != ActionMode.ReadOnly) {
textBoxFirstName.BackColor = Color.White;
}
} else {
textBoxOtherRelationship.Enabled = false;
_model.RelationshipNotes = null;
textBoxOtherRelationship.Text = string.Empty;
if (_formMode != ActionMode.ReadOnly) {
textBoxFirstName.BackColor = Color.LightYellow;
}
}
}
}

How to prevent unwanted tooltip pop-up in Infragistics ComboBoxTool on ribbon control (winforms)

I have a ComboBoxTool on an UltraToolbarsManager implementing a ribbon control. No matter what I set the ToolTipText to it always displays a tooltip:
[e.g. mousing over the gdg combo show this]
I have tried setting all the other tooltip related attributes (ToolTipTextFormatted, ToolTipTitle) to null but this doesn't help.
If a non-zero length tooltip text is specified then this shows as expected
The ribbon child controls are all added programatically
The other controls on the ribbon do not have this issue
I have also tried setting-up a very simple ribbon on a dummy project and that does not exhibit this strange behaviour. So it is something else that is effecting this.
It looks like it may be a bug. You should probably submit it to Infragistics.
If you don't want any tool tips displaying for the entire ribbon group, you can set the RibbonGroup.Settings.ShowToolTips value to False. In fact, if you want to turn off tool tips on a wider scale, you can set one of the following properties to False instead:
RibbonTab.GroupSettings.ShowToolTips
ContextualTabGroup.GroupSettings.ShowToolTips
Ribbon.GroupSettings.ShowToolTips
UltraToolbarsManager.ShowToolTips
Each property will turn off tool tips for all tool instances within the associated container.
But if you only want to turn of tool tips for this one tool, you can use a tool that derives from ComboBoxTool. In your derived tool, you can override ShouldDisplayToolTip and you can return False.
Infragistics supplied an answer:
Add your own CreationFilter to the ToolbarsManager
ultraToolbarsManager1.CreationFilter = new MyCreation();
Catch the tool creation and replace the tooltip with your own implementation
public class MyCreation : IUIElementCreationFilter {
private readonly int max;
public MyCreation()
{
}
public MyCreation(int toolTipMaxWidth)
{
max = toolTipMaxWidth;
}
public void AfterCreateChildElements(UIElement parent)
{
parent.ToolTipItem = new MyToolTipItem(max);
}
public bool BeforeCreateChildElements(UIElement parent)
{
return false;
}
}
public class MyToolTipItem : IToolTipItem {
private readonly int max;
public MyToolTipItem(int maxWidth)
{
max = maxWidth;
}
public MyToolTipItem()
{
}
public ToolTipInfo GetToolTipInfo(Point mousePosition, UIElement element, UIElement previousToolTipElement,
ToolTipInfo toolTipInfoDefault)
{
// set tooltip info for ribbon ApplicationMenuButton
var app = element as ApplicationMenuButtonUIElement;
if (app != null)
{
var appmenu = ((UltraToolbarsDockAreaUIElement) ((app.Parent).Parent)).ToolbarsManager.Ribbon.ApplicationMenu;
if (max > 0)
toolTipInfoDefault.MaxWidth = max;
toolTipInfoDefault.Title = appmenu.ToolTipTitle;
string tooltiptex = appmenu.ToolTipText;
if (!string.IsNullOrEmpty(appmenu.ToolTipTextFormatted))
{
toolTipInfoDefault.ToolTipTextStyle = ToolTipTextStyle.Formatted;
tooltiptex = appmenu.ToolTipTextFormatted;
}
toolTipInfoDefault.ToolTipText = tooltiptex;
}
// set tooltip info for tools
if (element.ToolTipItem != null && UIElement.IsContextOfType(element.GetContext(), typeof (ToolBase)))
{
var tool = (ToolBase) element.GetContext(typeof (ToolBase));
var loc = tool.ToolbarsManager.DockWithinContainer.PointToScreen(new Point(0, 0));
loc.Offset(tool.UIElement.Rect.Location.X, 185);
if (max > 0)
toolTipInfoDefault.MaxWidth = max;
toolTipInfoDefault.Title = tool.SharedProps.ToolTipTitle;
string tooltiptex = tool.SharedProps.ToolTipText;
if (!string.IsNullOrEmpty(tool.SharedProps.ToolTipTextFormatted))
{
toolTipInfoDefault.ToolTipTextStyle = ToolTipTextStyle.Formatted;
tooltiptex = tool.SharedProps.ToolTipTextFormatted;
}
toolTipInfoDefault.ToolTipText = tooltiptex;
toolTipInfoDefault.DisplayStyle = Infragistics.Win.ToolTipDisplayStyle.Office2007;
toolTipInfoDefault.Location = loc;
}
return toolTipInfoDefault;
}
Required a bit of tweaking to get the tooltip in the right place and pick-up the tooltip text from TooltipTextResolved.

WPF MVVM: Add item not present in combobox

I'm using a MVVM approach with WPF to let the user select one item in a combobox. The model contains a set of possible options, the combobox is bound to this set, the current selection is again bound to a property of my model. This part works fine.
Now I'd like to allow the user to enter an arbitrary text into the combobox. If the text doesn't correspond to an existing item the program should ask him if he wants to add a new item. He should also be allowed to cancel the action and select another item.
How would I do that within the MVVM pattern?
You would check the "already existing" status of the text from your ViewModel's bound property setter. At that point, you need a mechanism to raise an event and decide what to do based on what happens.
An example:
enum Outcome { Add, Cancel }
class BlahEventArgs : EventArgs
{
Outcome Outcome { get; set; }
}
class ViewModel
{
private string name;
public EventHandler<BlahEventArgs> NotExistingNameSet;
public Name
{
get { return this.name; }
set
{
if (/* value is existing */) {
this.name = value;
return;
}
var handler = this.NotExistingNameSet;
if (handler == null) {
// you can't just return here, because the UI
// will desync from the data model.
throw new ArgumentOutOfRangeException("value");
}
var e = new BlahEventArgs { Outcome = Outcome.Add };
handler(this, e);
switch (e.Outcome) {
case Outcome.Add:
// Add the new data
this.name = value;
break;
case Outcome.Cancel:
throw new Exception("Cancelled property set");
}
}
}
}
Your View would add an event handler to NotExistingNameSet to present appropriate UI and set the value of e.Outcome accordingly.

WPF Validation Manually Adding Errors into Validation.Errors Collection

Is there any way to manually/dynamically add errors to the Validation.Errors collection?
from http://www.wpftutorial.net/ValidationErrorByCode.html
ValidationError validationError = new ValidationError(regexValidationRule,
textBox.GetBindingExpression(TextBox.TextProperty));
validationError.ErrorContent = "This is not a valid e-mail address";
Validation.MarkInvalid(
textBox.GetBindingExpression(TextBox.TextProperty),
validationError);
jrwren's answer guided me in the right direction, but wasn't very clear as to what regexValidationRule was nor how to clear the validation error. Here's the end result I came up with.
I chose to use Tag since I was using this manual validation in a situation where I wasn't actually using a viewmodel or bindings. This gave something I could bind to without worrying about affecting the view.
Adding a binding in code behind:
private void AddValidationAbility(FrameworkElement uiElement)
{
var binding = new Binding("TagProperty");
binding.Source = this;
uiElement.SetBinding(FrameworkElement.TagProperty, binding);
}
and trigging a validation error on it without using IDataError:
using System.Windows;
using System.Windows.Controls;
private void UpdateValidation(FrameworkElement control, string error)
{
var bindingExpression = control.GetBindingExpression(FrameworkElement.TagProperty);
if (error == null)
{
Validation.ClearInvalid(bindingExpression);
}
else
{
var validationError = new ValidationError(new DataErrorValidationRule(), bindingExpression);
validationError.ErrorContent = error;
Validation.MarkInvalid(bindingExpression, validationError);
}
}

Resources