August 4, 2011

Using Dependency Properties to extend existing controls in WPF

So you have a user who requires some behaviour on your controls, but the controls are Microsoft's standard ones, for example the textbox.

You don't want to subclass the control and then have to go around your xaml changing from one to another.

Well this is where dependency properties come into view as they allow you to extend the properties and hence the behaviour of existing controls.




Best Microsoft MCTS Training – Microsoft MCITP Training at Certkingdom.com


Recently someone was asked to change their form so that the enter key moved like the tab key and then they were asked to make the text get selected on focus, thus enabling quick data entry.

So lets start off by creating a simple dependency property to perform the enter as tab.

The easiest way of changing the behaviour of the enter key is to handle the previewkeydown event and override the key code and change from enter to tab. We could do this by changing the form in the code behind, but this would clutter the code.

So the first thing we do is a add a new class that will contain the code handlers etc for the new property.

We need to register our new property and then deal with it's value changing

Public Shared ReadOnly EnterAsTabProperty As DependencyProperty = DependencyProperty.RegisterAttached("EnterAsTab", GetType(Boolean), GetType(SmartText), New UIPropertyMetadata(False, AddressOf EnterAsTabChanged))

this line register a new property EnterAsTab and tells the system that when it's value changes to call the method EnterAsTabChanged

Note when you register a property like this there will have to be 2 methods to handle the property set/get, these must be named SetXXXX,GetXXXX, in our case we will do nothing here other tahn route though to the dependency object where out property will be stored. ( note this means that the property is stored not on our class, but on the target object itself )

So the job is nearly done, we just have to do the tricky wotk in our changed event, ie. attach to the event and make sure we attach to the unloaded event so we can clean up afterwards.

AddHandler ue.Unloaded, AddressOf ue_Unloaded
AddHandler ue.PreviewKeyDown, AddressOf ue_PreviewKeyDown

A quick look at the previewkeydown and you'll see our target code

so a quick build and we can now go to our form xaml
add a namespace reference
xmlns:my="clr-namespace:WpfApplication1DelMe"
and then we can add the new property to the grid control
my:SmartText.EnterAsTab="True"

and there we go start up and the new behaviour is added to the grid and it's contained text boxes.

So now we move onto the autoselect property, we follow the same method as above, but one difference, here we want to be able to add to either a container ( eg grid ) or the text box level itself

But when you try and do this you'd like to iterate the textbox controls in the grid.

How do we do that? here we find that the controls both the the logical tree and visual tree haven't been buitl yet, so the usual mehtod of iteraiting controls fails.

So lets be sneaky and hook the template loaded event after which we know the tree will have been build and then use the same method as above on the iterated tree

For i = 1 To VisualTreeHelper.GetChildrenCount(sender)

Dim dobj As DependencyObject = VisualTreeHelper.GetChild(sender, i - 1)

If TypeOf dobj Is TextBox Then
Dim txt As TextBox = dobj
AddHandler txt.Unloaded, AddressOf ue_AutoSelectUnloaded
AddHandler txt.GotFocus, AddressOf ue_GotFocus
End If

Next

Now are job is done and we can add the new property at the level we want..

No doubt there are a few bits in the code that are wrong, but i think it's a good starting point and explains a simple use of dependency properties


Public Class SmartText

#Region "EnterAsTab"


Public Shared Function GetEnterAsTab(ByVal obj As DependencyObject) As Boolean

GetEnterAsTab = obj.GetValue(EnterAsTabProperty)

End Function

Public Shared Sub SetEnterAsTab(ByVal obj As DependencyObject, ByVal value As Boolean)

obj.SetValue(EnterAsTabProperty, value)

End Sub

Public Shared Sub ue_PreviewKeyDown(ByVal sender As Object, ByVal e As System.Windows.Input.KeyEventArgs)
Dim ue As FrameworkElement

ue = e.OriginalSource
If (e.Key = Key.Enter) Then
e.Handled = True
ue.MoveFocus(New TraversalRequest(FocusNavigationDirection.Next))
End If
End Sub


Private Shared Sub ue_Unloaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim ue As FrameworkElement

ue = sender

If (ue Is Nothing) Then Return

RemoveHandler ue.Unloaded, AddressOf ue_Unloaded
RemoveHandler ue.PreviewKeyDown, AddressOf ue_PreviewKeyDown
End Sub


Public Shared ReadOnly EnterAsTabProperty As DependencyProperty = DependencyProperty.RegisterAttached("EnterAsTab", GetType(Boolean), GetType(SmartText), New UIPropertyMetadata(False, AddressOf EnterAsTabChanged))

Public Shared Sub EnterAsTabChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim ue As FrameworkElement
ue = d
If (ue Is Nothing) Then Return

If (e.NewValue) Then
AddHandler ue.Unloaded, AddressOf ue_Unloaded
AddHandler ue.PreviewKeyDown, AddressOf ue_PreviewKeyDown
Else
RemoveHandler ue.PreviewKeyDown, AddressOf ue_PreviewKeyDown
End If
End Sub

#End Region


#Region "AutoSelect"


Public Shared Function GetAutoSelect(ByVal obj As DependencyObject) As Boolean

GetAutoSelect = obj.GetValue(AutoSelectProperty)

End Function

Public Shared Sub SetAutoSelect(ByVal obj As DependencyObject, ByVal value As Boolean)

obj.SetValue(AutoSelectProperty, value)

End Sub

Public Shared Sub ue_GotFocus(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
Dim ue As TextBox = sender

ue.SelectAll()

End Sub

Private Shared Sub ue_AutoSelectContainerLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)

Dim pa As FrameworkElement = sender
RemoveHandler pa.Loaded, AddressOf ue_AutoSelectContainerLoaded

For i = 1 To VisualTreeHelper.GetChildrenCount(sender)

Dim dobj As DependencyObject = VisualTreeHelper.GetChild(sender, i - 1)

If TypeOf dobj Is TextBox Then
Dim txt As TextBox = dobj
AddHandler txt.Unloaded, AddressOf ue_AutoSelectUnloaded
AddHandler txt.GotFocus, AddressOf ue_GotFocus
End If

Next

End Sub

Private Shared Sub ue_AutoSelectUnloaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim ue As TextBox

ue = sender

If (ue Is Nothing) Then Return

RemoveHandler ue.Unloaded, AddressOf ue_AutoSelectUnloaded
RemoveHandler ue.GotFocus, AddressOf ue_GotFocus
End Sub


Public Shared ReadOnly AutoSelectProperty As DependencyProperty = DependencyProperty.RegisterAttached("AutoSelect", GetType(Boolean), GetType(SmartText), New UIPropertyMetadata(False, AddressOf AutoSelectChanged))

Public Shared Sub AutoSelectChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)

If Not TypeOf d Is TextBox Then

If Not TypeOf d Is FrameworkElement Then Return

Dim pa As FrameworkElement = d

If (e.NewValue) Then
AddHandler pa.Loaded, AddressOf ue_AutoSelectContainerLoaded
Else
RemoveHandler pa.Loaded, AddressOf ue_AutoSelectContainerLoaded
For i = 0 To VisualTreeHelper.GetChildrenCount(pa)

Dim dobj As DependencyObject
dobj = VisualTreeHelper.GetChild(pa, i)

If TypeOf dobj Is TextBox Then
Dim txt As TextBox = dobj
RemoveHandler txt.GotFocus, AddressOf ue_GotFocus
End If

Next
End If

Return

Else

Dim ue As TextBox = d

If (e.NewValue) Then
AddHandler ue.Unloaded, AddressOf ue_AutoSelectUnloaded
AddHandler ue.GotFocus, AddressOf ue_GotFocus
Else
RemoveHandler ue.GotFocus, AddressOf ue_GotFocus
End If
End If

End Sub

#End Region

Private Shared Sub DumpLogicalTree(ByVal parent As Object, ByVal level As Integer)

Dim doParent As DependencyObject
Dim typeName As String = parent.GetType().Name
Dim name As String = Nothing

If (TypeOf parent Is DependencyObject) Then
doParent = parent
name = doParent.GetValue(FrameworkElement.NameProperty)
Trace.WriteLine(String.Format("{0}: {1}", typeName, name))

For Each child In LogicalTreeHelper.GetChildren(doParent)
DumpLogicalTree(child, level + 1)
Next
Else
name = parent.ToString()
Trace.WriteLine(String.Format("{0}: {1}", typeName, name))
Return

End If



End Sub

End Class
Bookmark and Share