Tuesday, 1 January 2013

WPF Restricting Text Entry to Numeric Characters, the MVVM Way

I needed to Limit several TextBoxes in my WPF MVVM application to only allow Numeric Characters.

I stumbles upon a very useful post by Jermey Likeness which described a method using Attached Behaviours, which worked well for me;

http://csharperimage.jeremylikness.com/2009/10/silverlight-behaviors-and-triggers_07.html

I modified this code slightly to allow for Decimal point control also.

The VB.net Code is as follows;

Imports System.Collections.ObjectModel
 
Namespace Behaviours
 
    Public Class clsTextBoxFilters
 
#Region "Variables"
 
        Private Shared ReadOnly _controlKeys As List(Of Key) = New List(Of Key) From {Key.Back,
                                                                                           Key.CapsLock,
                                                                                            Key.LeftCtrl,
                                                                                            Key.RightCtrl,
                                                                                            Key.Down,
                                                                                            Key.[End],
                                                                                            Key.Enter,
                                                                                            Key.Escape,
                                                                                            Key.Home,
                                                                                            Key.Insert,
                                                                                            Key.Left,
                                                                                            Key.PageDown,
                                                                                            Key.PageUp,
                                                                                            Key.Right,
                                                                                            Key.LeftShift,
                                                                                            Key.RightShift,
                                                                                            Key.Tab,
                                                                                            Key.Up,
                                                                                            Key.Decimal,
                                                                                            Key.OemPeriod}
 
#End Region
 
#Region "Helper Functions"
 
        ''' <summary>
        ''' Check if the key is a Numeric
        ''' </summary>
        ''' <param name="key__1"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Private Shared Function _IsDigit(key__1 As Key) As Boolean
            Dim shiftKey As Boolean = (Keyboard.Modifiers And ModifierKeys.Shift) <> 0
            Dim retVal As Boolean
            If key__1 >= Key.D0 AndAlso key__1 <= Key.D9 AndAlso Not shiftKey Then
                retVal = True
            Else
                retVal = key__1 >= Key.NumPad0 AndAlso key__1 <= Key.NumPad9
            End If
            Return retVal
        End Function
 
#End Region
 
#Region "Getters and Setters"
 
        ''' <summary>
        ''' Get the value of the Numeric Filter Dependancy Property
        ''' </summary>
        ''' <param name="src"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Shared Function GetIsPositiveNumericFilter(src As DependencyObject) As Boolean
            Return CBool(src.GetValue(IsPositiveNumericFilterProperty))
        End Function
 
        ''' <summary>
        ''' Set the Value of the Numeric Filter Dependancy Property
        ''' </summary>
        ''' <param name="src"></param>
        ''' <param name="value"></param>
        ''' <remarks></remarks>
        Public Shared Sub SetIsPositiveNumericFilter(src As DependencyObject, value As Boolean)
            src.SetValue(IsPositiveNumericFilterProperty, value)
        End Sub
 
        ''' <summary>
        ''' Get the Value of the IsIntegerOnly Dependancy Property
        ''' </summary>
        ''' <param name="src"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Shared Function GetIsIntegerOnly(src As DependencyObject) As Boolean
            Return CBool(src.GetValue(IsIntegerOnlyProperty))
        End Function
 
        ''' <summary>
        ''' Sets the Value of the IsIntegerOnly Dependancy Property
        ''' </summary>
        ''' <param name="src"></param>
        ''' <param name="value"></param>
        ''' <remarks></remarks>
        Public Shared Sub SetIsIntegerOnly(src As DependencyObject, value As Boolean)
            src.SetValue(IsIntegerOnlyProperty, value)
        End Sub
 
#End Region
 
#Region "Dependancy Properties"
 
        Public Shared IsPositiveNumericFilterProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsPositiveNumericFilter", GetType(Boolean), GetType(clsTextBoxFilters), New PropertyMetadata(False, AddressOf IsPositiveNumericFilterChanged))
        Public Shared IsIntegerOnlyProperty As DependencyProperty = DependencyProperty.Register("IsIntegerOnly", GetType(Boolean), GetType(clsTextBoxFilters), Nothing)
 
#End Region
 
#Region "Dependancy Property Change Handlers"
 
        ''' <summary>
        ''' Determines if the Behaviour is active, adds the KeyDown event Handler
        ''' </summary>
        ''' <param name="src"></param>
        ''' <param name="args"></param>
        ''' <remarks></remarks>
        Public Shared Sub IsPositiveNumericFilterChanged(src As DependencyObject, args As DependencyPropertyChangedEventArgs)
            If src IsNot Nothing AndAlso TypeOf src Is TextBox Then
                Dim textBox As TextBox = TryCast(src, TextBox)
 
                If CBool(args.NewValue) Then
                    AddHandler textBox.KeyDown, AddressOf _TextBoxPositiveNumericKeyDown
                End If
            End If
        End Sub
 
#End Region
 
#Region "Event Handlers"
 
        ''' <summary>
        ''' KeyDown event... Check if the Key is valid... If so allow, if not then ignore the key
        ''' </summary>
        ''' <param name="sender"></param>
        ''' <param name="e"></param>
        ''' <remarks></remarks>
        Private Shared Sub _TextBoxPositiveNumericKeyDown(sender As Object, e As KeyEventArgs)
            Dim handled As Boolean = True
 
            Dim txtSender As TextBox = DirectCast(sender, TextBox)
 
            If e.Key = Key.OemPeriod Then                   'Is the current key a Decimal Point?
 
                If txtSender.GetValue(IsIntegerOnlyProperty) = True Then                    'Is this an Integer Only TextBox?
 
                    handled = True
 
                ElseIf InStr(txtSender.Text, ".") = 0 And Len(txtSender.Text) > 0 Then      'We only want 1 decimal point, and it can't be the first digit!
 
                    handled = False
 
                Else                                                                        'Otherwise ignore the keypress
 
                    handled = True
 
                End If
 
            ElseIf _controlKeys.Contains(e.Key) OrElse _IsDigit(e.Key) Then                 'Is the Key a numeric or an allowed key?
 
                handled = False
 
            End If
 
            e.Handled = handled
 
        End Sub
 
#End Region
 
    End Class
 
End Namespace

The XAML to add to the UserControl Header Namespaces is;



xmlns:Behaviors="clr-namespace:CriticalPath_WPF_MVVM_EF.Behaviours"

The XAML to add to the TextBox Is;



Behaviors:clsTextBoxFilters.IsPositiveNumericFilter="True" Behaviors:clsTextBoxFilters.IsIntegerOnly="True"

Very handy!

Setting Control Focus… The MVVM Way…

I needed to be able to set the Focus of a control, using MVVM. This means of course, that we can’t interact directly with the View, but instead must rely on Binding.

I found a very helpful post by Khalid Rafique which allows exactly this;

http://codenicely.blogspot.co.uk/2012/01/how-to-set-textbox-focus-in-silverlight.html

By making use of attached Interactivity Behaviours, we can Bind an IsFocused Property, allowing the focus of the selected control to be set..Very handy indeed.

The VB.net code is shown below;

Behaviour Class:

Imports System.Windows.Interactivity
 
Namespace Behaviours
 
    Public Class clsFocusBehavior
        Inherits Behavior(Of Control)
 
#Region "Properties"
 
        ''' <summary>
        ''' The IsFocused Property, which is a backing for the IsFocusedProperty Dependancy Property
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property IsFocused() As Boolean
            Get
                Return CBool(GetValue(IsFocusedProperty))
            End Get
            Set(value As Boolean)
                SetValue(IsFocusedProperty, value)
            End Set
        End Property
 
        ''' <summary>
        ''' The Has Initial Focus Property, which is a backing for the HasInitialFocusProperty Dependancy Property
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property HasInitialFocus() As Boolean
            Get
                Return CBool(GetValue(HasInitialFocusProperty))
            End Get
            Set(value As Boolean)
                SetValue(HasInitialFocusProperty, value)
            End Set
        End Property
 
#End Region
 
#Region "Dependancy Properties"
 
        ''' <summary>
        ''' Expose an IsFocused Dependancy Property to allow Binding
        ''' </summary>
        ''' <remarks></remarks>
        Public Shared ReadOnly IsFocusedProperty As DependencyProperty = DependencyProperty.Register("IsFocused", GetType(Boolean), GetType(clsFocusBehavior), New PropertyMetadata(False, Sub(d, e)
                                                                                                                                                                                               If CBool(e.NewValue) Then
                                                                                                                                                                                                   DirectCast(d, clsFocusBehavior).AssociatedObject.Focus()
                                                                                                                                                                                               End If
                                                                                                                                                                                           End Sub))
 
        ''' <summary>
        ''' Expose a HasInitialFocus Property, which allows Binaind dicating if the Control should be focuses Initially
        ''' </summary>
        ''' <remarks></remarks>
        Public Shared ReadOnly HasInitialFocusProperty As DependencyProperty = DependencyProperty.Register("HasInitialFocus", GetType(Boolean), GetType(clsFocusBehavior), New PropertyMetadata(False, Nothing))
 
#End Region
 
#Region "Helper Subs"
 
        ''' <summary>
        ''' Override the OnAttached Sub... Add Event Handlers
        ''' </summary>
        ''' <remarks></remarks>
        Protected Overrides Sub OnAttached()
 
            AddHandler AssociatedObject.GotFocus, Function(sender, args) InlineAssignHelper(IsFocused, True)
            AddHandler AssociatedObject.LostFocus, Function(sender, a) InlineAssignHelper(IsFocused, False)
            AddHandler AssociatedObject.Loaded, Sub(o, a)
                                                    If HasInitialFocus OrElse IsFocused Then
                                                        AssociatedObject.Focus()
                                                    End If
                                                End Sub
            MyBase.OnAttached()
        End Sub
 
        ''' <summary>
        ''' Used to simplify assigning the Target Vaues of Control Events
        ''' </summary>
        ''' <typeparam name="T"></typeparam>
        ''' <param name="target"></param>
        ''' <param name="value"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Private Shared Function InlineAssignHelper(Of T)(ByRef target As T, value As T) As T
            target = value
            Return value
        End Function
 
#End Region
 
    End Class
 
End Namespace

XAML to add to View Header Namepaces:



xmlns:Behaviors="clr-namespace:CriticalPath_WPF_MVVM_EF.Behaviours"
             

XAML to add to control:




<i:Interaction.Behaviors>
    <Behaviors:clsFocusBehavior HasInitialFocus="False" IsFocused="{Binding DataContext.OrderNumberFocus, Mode=TwoWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}"/>
</i:Interaction.Behaviors>

Code to add to ViewModel:



Private _OrderNumberFocus As Boolean
 
''' <summary>
''' The Order Number Focus
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property OrderNumberFocus As Boolean
    Get
        Return _OrderNumberFocus
    End Get
    Set(value As Boolean)
        _OrderNumberFocus = value
        OnPropertyChanged("OrderNumberFocus")
    End Set
End Property


Set the Focus using;



OrderNumberFocus = True

Saturday, 22 December 2012

WPF Validation Adorners in Tab Control Disappear when Tabs Changed

I found recently than the Validation Error Adorners on my WPF Project when used within a Tab Control, would disappear if I changed tabs.

After a little searching, I found this helpful post;

http://karlshifflett.wordpress.com/2008/02/19/wpf-validation-errors-disappear-inside-tabcontrol-when-switching-tabitems/

You basically need to wrap your Tab Control Tab Content within a Adorner Decorator as follows;

<Border>      
    <AdornerDecorator>            
        <StackPanel>                  
            ...           
        </StackPanel>      
    </AdornerDecorator>
</Border>

This “bug” is due to the fact that the Error Adorners are painted on the Adorner Layer, which is discarded when changing tabs. The code above preserves this layer, preventing the Adorners from being lost.