OnVisualChildrenChanged is called before dependency properties are set - wpf

I have created a custom control (CartesianCanvas) that stores some of the properties of any child element when it is added. To do this I have created a new collection property(ItemsInfo) and overrode OnVisualChildrenChanged so that when a child is added or removed the corresponding properties are added or removed from the collection property.
However, when I add children to the control through XAML, OnVisualChildrenChanged seemes to be called before the properties have been set as all the properties have their default values. This is not the case when a child is added after the window has been loaded. How can I ensure that the child's properties have been set when OnVisualChildrenChanged is called?
Here is my code:
Public ReadOnly Property ItemsInfo As Collection(Of CartesianInfo)
Get
Return DirectCast(GetValue(ItemsInfoProperty), Collection(Of CartesianInfo))
End Get
End Property
Friend Shared ReadOnly ItemsInfoKey As System.Windows.DependencyPropertyKey = System.Windows.DependencyProperty.RegisterReadOnly("ItemsInfo", GetType(Collection(Of CartesianInfo)), GetType(CartesianCanvas), New System.Windows.PropertyMetadata(New Collection(Of CartesianInfo)))
Public Shared ReadOnly ItemsInfoProperty As DependencyProperty = ItemsInfoKey.DependencyProperty
Protected Overrides Sub OnVisualChildrenChanged(visualAdded As DependencyObject, visualRemoved As DependencyObject)
Try
If Not visualAdded Is Nothing Then
If Not GetType(FrameworkElement).IsAssignableFrom(visualAdded.GetType) Then
Me.Children.Remove(visualAdded)
Throw New Exception("The object added:" & visualAdded.ToString & " was not of type or decended from: FrameworkElement and so was removed from CartesianCanvas")
Else
AddItemInfo(visualAdded)
End If
End If
Catch ex As Exception
Console.WriteLine(ex.Message)
End Try
If Not visualRemoved Is Nothing Then
RemoveItemInfo(visualRemoved)
End If
MyBase.OnVisualChildrenChanged(visualAdded, visualRemoved)
End Sub
Private Sub AddItemInfo(ByRef item As FrameworkElement)
Dim itemsInfoCollection As New Collection(Of CartesianInfo)
itemsInfoCollection = ItemsInfo
Dim ItemXCoordinate As Double
Dim ItemYCoordinate As Double
Dim ItemCalculateFromVerticalCenter As Boolean
Dim ItemCalculateFromHorizontalCenter As Boolean
If Double.IsNaN(Canvas.GetLeft(item)) Then
Canvas.SetLeft(item, 0)
End If
If Double.IsNaN(Canvas.GetTop(item)) Then
Canvas.SetTop(item, 0)
End If
ItemXCoordinate = Canvas.GetLeft(item)
ItemYCoordinate = Canvas.GetTop(item)
If item.VerticalAlignment = Windows.VerticalAlignment.Center Then
ItemCalculateFromVerticalCenter = True
End If
If item.HorizontalAlignment = Windows.HorizontalAlignment.Center Then
ItemCalculateFromHorizontalCenter = True
End If
ItemsInfo.Add(New CartesianInfo(item, ItemXCoordinate, ItemYCoordinate, ItemCalculateFromVerticalCenter, ItemCalculateFromHorizontalCenter))
SetValue(ItemsInfoKey, ItemsInfo)
PositionChild(item)
End Sub
Here is my XAML
<local:CartesianCanvas x:Name="MainCanvas">
<Button Width="50" Height="100" Canvas.Top="100" Canvas.Left="20"
Content="test" Click="Button_Click"/>
</local:CartesianCanvas>

WPF is pretty complicated especially when writing custom controls therefore my first suggestion would be - leave it. Take an already existing control or composite few already existing controls into UserControl instead of writing everything from scratch.
Controls in WPF may contain any other control and the control's children are loaded only when needed. That is why you run into your problem.
The solution is when OnVisualChildrenChanged event fires, you should run through all children and subscribe to their Initialized or Loaded event. Once the child is loaded the event will fire and the handler will be called. Furthermore inside the handler you shouldn't forget to unsubscribe to the event.

Related

WPF Can I use a DataTrigger to have the View do something after the View Model changes the value of a Property?

My WPF MVVM VB.NET app loads a list of songs into a ListBox at start. The list contents populate in a BackgroundWorker that is kicked off in the Constructor of the ViewModel. Once this is done, I want to set focus to the first song in the list.
As setting this focus is purely a View operation, I want it in the code-behind of the XAML. It's no business of the ViewModel where focus goes.
I tried doing this on various Window and ListBox events, but they either don't fire, or fire too early. So I'm think what I need is a Boolean Property that the ViewModel sets when it's done loading the songs into the list. That's when I need the View to catch that Property Change, and call the code-behind function that has the logic to maniuplate the View, in the is case, setting focus on the first song in the list.
But this is where my knowledge of WPF is short. I searched and it sounds like DataTrigger could do the trick. But where to put it, and what's the right syntax, and how to have it call my code-behind function?
Or is there an even simpler way that I'm overlooking. This seems like a basic functionality - to trigger some code-behind action in the View when a Property changes a certain way in the ViewModel.
Here's the code-behind function. I can elaborate it once it's successfully getting called at the intended time:
Private Sub FocusSongsList()
' set focus back to the Songs list, selected item (couldn't just set focus to the list, it ran forever and looks like it set focus to every item in turn before releasing the UI)
Dim listBoxItem = CType(LstSongs.ItemContainerGenerator.ContainerFromItem(LstSongs.SelectedItem), ListBoxItem)
If Not listBoxItem Is Nothing Then
listBoxItem.Focus()
End If
End Sub
Here's my ListBox:
<ListBox x:Name="LstSongs" ItemsSource="{Binding FilteredSongs}" DisplayMemberPath="Path"
HorizontalAlignment="Stretch"
SelectionMode="Extended" SelectionChanged="LstSongs_SelectionChanged" Loaded="FocusSongsList"/>
And I would define a new property that can be set from the RunWorkerCompleted part of the BackgroundWorker.
Private _InitialSongLoadCompleted As Boolean
Public Property InitialSongLoadCompleted() As Boolean
Get
Return _InitialSongLoadCompleted
End Get
Set(ByVal value As Boolean)
_InitialSongLoadCompleted = value
RaisePropertyChanged("InitialSongLoadCompleted")
End Set
End Property
A DataTrigger can't execute methods. It can only set properties.
Focus can't be activated by setting a property, therefore a DataTrigger can't solve your problem.
Generally, if you have longrunning initialization routines you should move them to an init routine (which could be async) or use Lazy<T>.
For example, you instantiate your view model class and call Initialize() afterwards. After the method has returned you can continue to initialize the ListBox:
MainWindow.xaml.cs
Partial Class MainWindow
Inherits Window
Private ReadOnly Property MainViewModel As MainViewModel
Public Sub New(ByVal dataContext As TestViewModel, ByVal navigator As INavigator)
InitializeComponent()
Me.MinViewModel = New MainViewMdel()
Me.DataContext = Me.MainViewModel
AddHandler Me.Loaded, AddressOf OnLoaded
End Sub
' Use the Loaded event to call async methods outside the constructor
Private Async Sub OnLoaded(ByVal sender As Object, ByVal e As EventArgs)
Await mainViewModel.InitializeAsync()
' For example handle initial focus
InitializeListBox()
End Sub
End Class
MainViewModel.cs
Class MainViewModel
Inherits INotifyPropertyChanged
Public Async Function InitializeAsync() As Task
Await Task.Run(AddressOf InitializeSongList)
End Function
Private Sub InitializeSongList()
' TODO::Initialize song list
End Sub
End Class
It has been a long long time since I wrote much VB, so I'm afraid this is c# code.
You can handle targetupdated on a binding.
This fires when data transfers from the source ( viewmodel property ) to the target (the ui property and here itemssource)
<ListBox
x:Name="LstSongs"
ItemsSource="{Binding Songs, NotifyOnTargetUpdated=True}"
TargetUpdated="ListBox_TargetUpdated"/>
When you replace your list, that targetupdated will fire.
If you raise property changed then the data will transfer ( obviously ).
private async void ListBox_TargetUpdated(object sender, DataTransferEventArgs e)
{
await Task.Delay(200);
var firstItem = (ListBoxItem)LstSongs.ItemContainerGenerator
.ContainerFromItem(LstSongs.Items[0]);
firstItem.Focus();
Keyboard.Focus(firstItem);
}
As that data transfers, there will initially be no items at all of course so we need a bit of a delay. Hence that Task.Delay which will wait 200ms and should let the UI render. You could make that a bit longer or dispatcher.invokeasync.
It finds the first container and sets focus plus keyboard focus.
It might not be at all obvious that item has focus.
A more elegant approach using dispatcher will effectively schedule this focussing until after the ui has rendered. It might, however, look rather tricky to someone unfamiliar with c#
private void ListBox_TargetUpdated(object sender, DataTransferEventArgs e)
{
Dispatcher.CurrentDispatcher.InvokeAsync(new Action(() =>
{
var firstItem = (ListBoxItem)LstSongs.ItemContainerGenerator
.ContainerFromItem(LstSongs.Items[0]);
firstItem.Focus();
Keyboard.Focus(firstItem);
}), DispatcherPriority.ContextIdle);
}
If you want a blue background then you could select the item.
firstItem.IsSelected = true;
Or you could use some datatrigger and styling working with IsFocused.
https://learn.microsoft.com/en-us/dotnet/api/system.windows.uielement.isfocused?view=windowsdesktop-7.0
( Always distracts me that, one s rather than two and IsFocussed. That'll be US english I gues )
Here's my mainwindowviewmodel
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private List<string> songs = new List<string>();
MainWindowViewModel()
{
Task.Run(() => { SetupSongs(); });
}
private async Task SetupSongs()
{
await Task.Delay(1000);
Songs = new List<string> { "AAA", "BBB", "CCC" };
}
}
I'm using the comunity toolkit mvvm for code generation. Maybe it does vb as well as c#.
You might accomplish your goal by defining a custom event in the viewmodel which is raised when the list processing is complete. The view can subscribe to it and act accordingly.
It would look something like this:
Class MyViewModel
'Custom eventargs shown for completeness, you can use EventHandler if you
'don't need any custom eventargs.
Public Event ListCompleted As EventHandler(Of ListCompletedEventArgs)
'...
Public Sub ProcessSongList()
'Note that if this runs on a background thread, you may need to
'get back on the UI thread to raise an event for the view to handle
RaiseEvent ListCompleted(Me, New ListCompletedEventArgs())
End Sub
End Class
Class MyView
Public Sub New(ByVal vm as MyViewModel)
Me.DataContext = vm
AddHandler vm.ListCompleted, AddressOf OnListCompleted
End Sub
Private Sub OnListCompleted(ByVal sender As Object, ByVal args As ListCompletedEventArgs)
'...
End Sub
'...
End Class
You mentioned doing processing on a background thread. I'm not completely sure which thread the completion event would issue into, but beware that UI stuff can only happen on the UI thread so you might need to use a Dispatcher.Invoke to make sure your code runs on the right thread. I'd do it to run the RaiseEvent so the view doesn't need to know anything about it.

WPF Dependency Property - set from XAML

I am trying to achieve the following in a WPF personal finance app:
In various places I want to display a user control giving details of a asset holding (usually a share, bond etc), the target asset may be changed dynamically by the user in which case the control must be refreshed. Each Asset has a unique identifier, AssetId.
I am using MVVM and I've developed a single window with a View Model that takes AssetID as a parameter (property) and retrieves the relevant details for binding to the View. This work fine. What I'd like to do is make a generic user control with the same functionality so I can basically drop that 'window' inside other windows.
So I pretty much copy-pasted the XAML from that form into a User Control, where I'm struggling is passing in the AssetId from the parent window to the child control.
Google tells me I need a dependency property and here's where I am
Public Class HoldingView
Private _AssetId As Integer
Public AssetIdProperty As DependencyProperty = DependencyProperty.Register("AssetId",
GetType(Integer),
GetType(HoldingView),
New FrameworkPropertyMetadata(New PropertyChangedCallback(AddressOf AssetIDChanged)))
Public Property AssetId As Integer
Get
Return GetValue(AssetIdProperty)
End Get
Set(value As Integer)
SetValue(AssetIdProperty, value)
End Set
End Property
Private Sub AssetIDChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim NewAssetId As Integer
NewAssetId = e.NewValue
Me.DataContext.AssetId = NewAssetId
End Sub
Public Sub New()
' This call is required by the designer.
InitializeComponent()
Me.DataContext = New HoldingViewmodel
End Sub
End Class
Called like this:
<Grid>
<local:HoldingView AssetId="{Binding AssetId}"/>
</Grid>
The code compiles and runs but when I try and load the window that has the user control, the app crashes with this message:
A 'Binding' cannot be set on the 'AssetId' property of type 'HoldingView'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
Which is not that helpful. From my Googling, you can also get this message if the syntax of the DP registration is not spot on, but it looks Ok to my inexperienced eye...
Anybody else had this?
Public AssetIdProperty As DependencyProperty
should be
Public Shared ReadOnly AssetIdProperty As DependencyProperty
Please take a look at Custom Dependency Properties.
Also remove
Me.DataContext = New HoldingViewmodel
because that will effectively break any DataContext-based Bindings like
AssetId="{Binding AssetId}"
where the source property is supposed to be owned by the object in the inherited DataContext, which usually is an object in the application's view model.
Controls should never have their own, "private" view model, but instead handle property changes in code behind. In case of UserControls, there could simply be UI elements in their XAML that would be bound to the UserConrol's own properties.
Hence
Me.DataContext.AssetId = NewAssetId
in the PropertyChangedCallback is pointless and should be removed, as well as
Private _AssetId As Integer
To summarize, it should look like this:
Public Class HoldingView
Public Shared ReadOnly AssetIdProperty As DependencyProperty =
DependencyProperty.Register(
"AssetId",
GetType(Integer),
GetType(HoldingView),
New FrameworkPropertyMetadata(
New PropertyChangedCallback(AddressOf AssetIdPropertyChanged)))
Public Property AssetId As Integer
Get
Return GetValue(AssetIdProperty)
End Get
Set(value As Integer)
SetValue(AssetIdProperty, value)
End Set
End Property
Private Shared Sub AssetIdPropertyChanged(
d As DependencyObject, e As DependencyPropertyChangedEventArgs)
CType(d, HoldingView).AssetIdChanged(e.NewValue)
End Sub
Private Sub AssetIdChanged(id As Integer)
...
End Sub
Public Sub New()
InitializeComponent()
End Sub
End Class

WPF: Design time support for dependency properties with default values

I have written a custom control based on a ListBox. It contains a default ItemTemplate which shows an image given to the ListBox by a custom dependency property. The control also contains a default image, which is used when the user doesn't give an image to the dependency property.
This works so far, but now I've found a little problem and I don't know how to fix that.
When I use my custom control in the XAML designer, it first shows the default image. When I set the image's dependency property to an other image, the new image is immediately shown in the XAML designer.
But when I remove the XAML attribute for the new image again, the XAML designer only shows a white rectangle instead of the default image.
I assume it's because with setting the image's dependency property to some value and then removing it I nulled the value. But even when I check for null in the CoerceCallback and give back the default image when the coerced value is null, doesn't work.
What's the best way to support fallback values for dependency properties?
TestControl.vb
Public Class TestControl
Inherits ListBox
Private Shared _defaultResources As ResourceDictionary
Shared Sub New()
_defaultResources = New ResourceDictionary
_defaultResources.Source = New Uri("...")
End Sub
Public Shared ReadOnly TestProperty As DependencyProperty = DependencyProperty.Register(NameOf(TestControl.Test),
GetType(ImageSource),
GetType(TestControl),
New FrameworkPropertyMetadata(Nothing,
AddressOf TestControl.OnTestChanged,
AddressOf TestControl.OnTestCoerce))
Public Property Test As ImageSource
Get
Return DirectCast(MyBase.GetValue(TestControl.TestProperty), ImageSource)
End Get
Set(value As ImageSource)
MyBase.SetValue(TestControl.TestProperty, value)
End Set
End Property
Private Shared Sub OnTestChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
End Sub
Private Shared Function OnTestCoerce(d As DependencyObject, value As Object) As Object
If (value Is Nothing) Then
Return TryCast(_defaultResources.Item("TestImage"), ImageSource)
End If
Return value
End Function
Public Sub New()
Me.Test = TryCast(_defaultResources.Item("TestImage"), ImageSource)
End Sub
End Class
When I use that control like this
<local:TestControl ItemsSource="{Binding Items}" />
every item shows the default image at design time. When I change the XAML to
<local:TestControl ItemsSource="{Binding Items}"
Test="{StaticResource NewImage}" />
every item shows the new item at design time. But when I remove the Test="{StaticResource NewImage}" again, it doesn't go back to the default image.
Ok, after some testing (using this technique) I have discovered the source of your issue.
First of all, you are not using PropertyMetadata to set your default value, but instead the constructor. I assume you have a good reason to do so, but this essentially means that now you are relying on the coercion callback to set the default value.
However, it is not called (the framework assumes that your "true" default value - Nothing - doesn't need to be validated) after you remove the
Test="{StaticResource TestString}" line. Only the OnTestChanged
is called. This means we can use it to restore the default value:
void OnTestChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is null)
{
((TestControl)d).Test = yourDefaultImage;
return;
}
//Actual "OnChanged" code
}
A clumsy solution indeed, but it works. Depending on your exact situation, you might also want to take a look at Binding's FallbackValue and TargetNullValue properties:
Test="{Binding Source={ }, FallbackValue={ }, TargetNullValue={ }}"

Closing a form from user control using dependency object

I have a navigation control that is displayed in multiple pages. So, I have no way to identify the exact class name during design time. Now, when user navigates to different page, I want to hide the current page. Basically, a typical menubar behaviour. I am able to get the outermost element as a dependency object using the code below.
Private Function GetTopLevelControl(ByVal control As DependencyObject) As DependencyObject
Dim tmp As New DependencyObject
tmp = control
Dim parent As New DependencyObject
parent = Nothing
While Not VisualTreeHelper.GetParent(tmp) Is Nothing
parent = VisualTreeHelper.GetParent(tmp)
End While
Return parent
End Function
Now, on the mouse down event, I am trying to write code to hide this parent object.
Private Sub Menu_Nomination_MouseDown(sender As Object, e As MouseButtonEventArgs)
Dim surveySearchPage As New SurveySearch
surveySearchPage.Show()
Dim parentControl As DependencyObject
parentControl = GetTopLevelControl(Me)
parentControl
End Sub
Problem is parentControl object has no hide or close property at all. So, I am currently stuck in trying to close the page.

Adding an ExceptionValidationRule to a Binding in code

I have been developing an ErrorProvider control that inherits from Decorator. It validates any elements within the control that are bound to something. It loops through every binding and within a FrameworkElement and adds an ExceptionValidationRule as well as a DataErrorValidation to the binding's ValidationRules.
This is the method that does the work:
Private Sub ApplyValidationRulesToBindings()
Dim bindings As Dictionary(Of FrameworkElement, List(Of Binding)) = GetBindings()
For Each felement In bindings.Keys
Dim knownBindings As List(Of Binding) = bindings(felement)
For Each knownBinding As Binding In knownBindings
'Applying Exception and Data Error validation rules'
knownBinding.ValidationRules.Add(New ExceptionValidationRule())
knownBinding.ValidationRules.Add(New DataErrorValidationRule())
Next
Next
End Sub
Apparently the DataErrorValidationRule is applied to the binding, but the ExceptionValidationRule is not.
Does anyone know why this might be the case?
Edit:
Ok, so a little more info on the problem.
I've been reading tons of MSDN documentation on Validation and the Binding class. The Binding.UpdateSourceExceptionFilter Property allows you to specify a function that handles any exceptions that occur on a binding if an ExceptionValidationRule has been associated with the Binding.
I added a method for the UpdateSourceExceptionFilter Property and guess what! It was executed. BUT!! Even though I returned the exception, a ValidationError object was NOT added to Validation.Errors collection of the bound element even though the MSDN documentation said that it would be...
I commented out the code that adds the ExceptionValidationRule dynamically and manually added one to the Binding in XAML like so:
<TextBox HorizontalAlignment="Left" Name="TextBox1" VerticalAlignment="Top"
Width="200">
<TextBox.Text>
<Binding Path="Name">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
The UpdateSourceException method was executed (I didn't change it) and the error was added to the Validation.Errors as the MSDN stated.
What is curious about this whole thing is the fact that the ExceptionValidationRule is in fact added to the binding when done through the VB.NET code (or else the the UpdateSourceException would never have executed); however, the Validate.Errors is not updated with the error.
As I stated earlier, the DataErrorValidationRule is added to the binding and works properly...I'm just having problems with the ExceptionValidationRule.
My Solution:
It turns out that I had to call the BindingOperations.SetBinding Method for the binding and the property to apply the validation rules to the binding. I had no way of retrieving the DependencyProperty that for the element/binding in my ApplyValidationRulesToBindings method so I moved the code that applied rules to the call back method provided my method that retrieves all of the bindings recursively.
Here is my solution:
''' <summary>'
''' Gets and returns a list of all bindings. '
''' </summary>'
''' <returns>A list of all known bindings.</returns>'
Private Function GetBindings() As Dictionary(Of FrameworkElement, List(Of Binding))
If _bindings Is Nothing OrElse _bindings.Count = 0 Then
_bindings = New Dictionary(Of FrameworkElement, List(Of Binding))
FindBindingsRecursively(Me.Parent, AddressOf RetrieveBindings)
End If
Return _bindings
End Function
''' <summary>'
''' Recursively goes through the control tree, looking for bindings on the current data context.'
''' </summary>'
''' <param name="element">The root element to start searching at.</param>'
''' <param name="callbackDelegate">A delegate called when a binding if found.</param>'
Private Sub FindBindingsRecursively(ByVal element As DependencyObject, ByVal callbackDelegate As FoundBindingCallbackDelegate)
' See if we should display the errors on this element'
Dim members As MemberInfo() = element.[GetType]().GetMembers(BindingFlags.[Static] Or BindingFlags.[Public] Or BindingFlags.FlattenHierarchy)
For Each member As MemberInfo In members
Dim dp As DependencyProperty = Nothing
' Check to see if the field or property we were given is a dependency property'
If member.MemberType = MemberTypes.Field Then
Dim field As FieldInfo = DirectCast(member, FieldInfo)
If GetType(DependencyProperty).IsAssignableFrom(field.FieldType) Then
dp = DirectCast(field.GetValue(element), DependencyProperty)
End If
ElseIf member.MemberType = MemberTypes.[Property] Then
Dim prop As PropertyInfo = DirectCast(member, PropertyInfo)
If GetType(DependencyProperty).IsAssignableFrom(prop.PropertyType) Then
dp = DirectCast(prop.GetValue(element, Nothing), DependencyProperty)
End If
End If
If dp IsNot Nothing Then
' we have a dependency property. '
'Checking if it has a binding and if so, checking if it is bound to the property we are interested in'
Dim bb As Binding = BindingOperations.GetBinding(element, dp)
If bb IsNot Nothing Then
' This element has a DependencyProperty that we know of that is bound to the property we are interested in. '
' Passing the information to the call back method so that the caller can handle it.'
If TypeOf element Is FrameworkElement Then
If Me.DataContext IsNot Nothing AndAlso DirectCast(element, FrameworkElement).DataContext IsNot Nothing Then
callbackDelegate(DirectCast(element, FrameworkElement), bb, dp)
End If
End If
End If
End If
Next
'Recursing through any child elements'
If TypeOf element Is FrameworkElement OrElse TypeOf element Is FrameworkContentElement Then
For Each childElement As Object In LogicalTreeHelper.GetChildren(element)
If TypeOf childElement Is DependencyObject Then
FindBindingsRecursively(DirectCast(childElement, DependencyObject), callbackDelegate)
End If
Next
End If
End Sub
''' <summary>'
''' Called when recursively populating the Bindings dictionary with FrameworkElements(key) and their corresponding list of Bindings(value)'
''' </summary>'
''' <param name="element">The element the binding belongs to</param>'
''' <param name="binding">The Binding that belongs to the element</param>'
''' <param name="dp">The DependencyProperty that the binding is bound to</param>'
''' <remarks></remarks>'
Sub RetrieveBindings(ByVal element As FrameworkElement, ByVal binding As Binding, ByVal dp As DependencyProperty)
'Applying an exception validation and data error validation rules to the binding'
'to ensure that validation occurs for the element'
If binding.ValidationRules.ToList.Find(Function(x) GetType(ExceptionValidationRule) Is (x.GetType)) Is Nothing Then
binding.ValidationRules.Add(New ExceptionValidationRule())
binding.ValidationRules.Add(New DataErrorValidationRule())
binding.UpdateSourceExceptionFilter = New UpdateSourceExceptionFilterCallback(AddressOf ReturnExceptionHandler)
'Resetting the binding to include the validation rules just added'
BindingOperations.SetBinding(element, dp, binding)
End If
' Remember this bound element. This is used to display error messages for each property.'
If _bindings.ContainsKey(element) Then
DirectCast(_bindings(element), List(Of Binding)).Add(binding)
Else
_bindings.Add(element, New List(Of Binding)({binding}))
End If
End Sub
Thanks!
-Frinny
I'm not really following how your code works but you can't modify a Binding after it has been used, so you can't add ValidationRules to an existing Binding. I think you'll have to copy the Binding, Property for Property and then add the ValidationRules and then set the new copied Binding with BindingOperations.SetBinding(...).
Another approach may be to create your own subclassed Binding where you add the ValidationRules directly e.g
public class ExBinding : Binding
{
public ExBinding()
{
NotifyOnValidationError = true;
ValidationRules.Add(new ExceptionValidationRule());
ValidationRules.Add(new DataErrorValidationRule());
}
}
Useable like
<TextBox Text="{local:ExBinding Path=MyProperty}"/>
Update
I don't think you understood my answer. You can't modify a Binding once it is in use so what you're trying to do won't work. Here's a C# sample app that shows this. It contains three TextBoxs where
First TextBox adds the Binding with the ExceptionValidationRule in Xaml
Second TextBox adds the Binding in Xaml and adds the ExceptionValidationRule in its Loaded event
Third TextBox adds the Binding with the ExceptionValidationRule in the Loaded event
The ExceptionValidationRule will work for TextBox 1 and 3 but not for 2. Uploaded the sample here: http://www.mediafire.com/?venm09dy66q4rmq
Update 2
You could get this to work if you set the Binding again after you've added the Validation Rule like
BindingExpression bindingExpression = textBox.GetBindingExpression(TextBox.TextProperty);
Binding textBinding = bindingExpression.ParentBinding;
textBinding.ValidationRules.Add(new ExceptionValidationRule());
// Set the Binding again after the `ExceptionValidationRule` has been added
BindingOperations.SetBinding(textBox, TextBox.TextProperty, textBinding);
I'm not sure how your GetBindings method look, but maybe you could add a SetBindings method where you set the Bindings again and call that method after you've added the ExceptionValidationRules

Resources