Update Binding on Composite Property when Child Property Changes - wpf

Let's say I have a property called Greeting that is principally composed of two bound properties: LastName and FirstName. Can I subscribe to updates on first and last name so I can force a refresh with OnPropertyChanged() for my Greeting property. Here's a simple example:
View
<TextBox Text="{Binding FirstName}" />
<TextBox Text="{Binding LastName}" />
<TextBlock Text="{Binding Greeting}" />
ViewModel
Public Property FirstName() As String
Get
Return _firstName
End Get
Set(ByVal value As String)
_firstName = value
OnPropertyChanged("FirstName")
End Set
End Property
'... Omitting LastName for brevity ...
Public ReadOnly Property Greeting() As String
Get
Return String.Format("Hello {0} {1}", Firstname, LastName)
End Get
End Property
The way this is currently set up, nothing will ever update the Greeting binding. I could put OnPropertyChanged("Greeting") in the setter for FirstName and LastName, but this feels wrong. In a more complex example, I'd rather each object just take care of refreshing itself when something changes.
Q:) Can I force an update for a ReadOnly Property when one of the Properties it's composed of changes?

You can call PropertyChange of Greetings from setter of FirstName and LastName
Public Property FirstName() As String
Get
Return _firstName
End Get
Set(ByVal value As String)
_firstName = value
OnPropertyChanged("FirstName")
OnPropertyChanged("Greeting")
End Set
End Property
OR
You can subscribe to PropertyChanged of your ViewModel in itself
AddHandler this.PropertyChanged, AddressOf PropertyChanged
and in PropertyChanged you can check which property is changed depending on that you can RaisePropertyChanged for Greeting

Borrowing from nit's answer, just to round it out a little. Here's what I did to fire an update on the Greeting property when FirstName or LastName changes:
Private Sub UpdateGreeting(ByVal sender As Object, ByVal e As PropertyChangedEventArgs) _
Handles Me.PropertyChanged
If e.PropertyName = "FirstName" OrElse e.PropertyName="LastName" Then
OnPropertyChanged("Greeting")
End If
End Sub
It handles the PropertyChanged event that is already implemented as part of the INotifyPropertyChanged interface on the ViewModel.
Then it checks if the PropertyName value of the event argument is equal to "FirstName" or "LastName".
If so, it manually raises the OnPropertyChanged() method for the Greeting property.

Related

Detect, whether an exception occured when using "ValidatesOnExceptions"

Imagine a WPF project with an MVVM approach. So, there is a view and a view model.
In the view model I have a property, that might throw an exception in the setter.
Public Property DateValue As Nullable(Of Date)
Get
Return _dateValue
End Get
Set(value As Nullable(Of Date))
If value.HasValue Then
If value.Value < Date.Today Then
Throw New Exception("Error Message")
End If
End If
_dateValue = value
'skipped NotifyPropertyChanged in this example for the sake of simplicity
End Set
End Property
In the view there is a control bound to this property. And since I like to see my exceptions I switched on ValidatesOnExceptions in the binding and add an ErrorTemplate.
...
<DatePicker SelectedDate="{Binding DateValue, ValidatesOnExceptions=True}"
SelectedDateFormat="Short"
Validation.ErrorTemplate="{StaticResource ErrorTemplate}" />
...
Since you can't reset the value of a DatePicker once you picked one (at least I don't know how to do that) I added a small reset button right next to the DatePicker which is bound to a command which sets the property DateValue of the view model to Nothing. And since I don't want to see this button all the time I bound its Visibility to DateValue.HasValue, so this button only shows, when there's a value to reset.
So far, so good.
But now I have a problem when I pick an invalid date in the DatePicker (one that throws an exception in the properties' setter).
My reset button doesn't show up, since there's no value in the bound property, and I can't reset the DatePicker any other way (at least not that I know of). I'd first have to pick a proper date before I can reset the whole thing.
So, is there any way to determine, whether my property setter threw an exception. There must be a way, since this very error is shown to the user.
Or do I have to manually remember, that I threw an exception in another variable, to be able to access this information when needed?
And how do I "clear" the DatePicker. Since there's no value in the property, setting the property to Nothing will not change anything in the view. How would I get rid of the error?
Class ViewModel
Inherits INotifyPropertyChanged
Private selectedDate As DateTime
Public Property SelectedDate As DateTime
Get
Return Me.selectedDate
End Get
Set(ByVal value As DateTime)
If value < DateTime.Today Then
Throw New ArgumentException("Date can't be in the past.")
End If
Me.selectedDate = value
OnPropertyChanged()
End Set
End Property
Public ReadOnly Property ResetDateCommand As ICommand
Get
Return New RelayCommand(AddressOf ExecuteResetDate)
End Get
End Property
Public Sub New()
Me.SelectedDate = DateTime.Today
End Sub
Public Sub ExecuteResetDate(ByVal commandParameter As Object)
Return CSharpImpl.__Assign(Me.SelectedDate, DateTime.Today)
End Sub
End Class
MainWindow.xaml
<Window>
<Window.Resources>
<ViewModel />
</Window.Resources>
<StackPanel x:Name="RootPanel" viewModels:Item.IsMarkedAsRead="True">
<Button Content="Reset Date"
Visibility="{Binding ElementName=DatePicker, Path=(Validation.HasError), Converter={StaticResource BooleanToVisibilityConverter}}"
Command="{Binding ResetDateCommand}" />
<DatePicker x:Name="DatePicker"
SelectedDate="{Binding SelectedDate, ValidatesOnExceptions=True}"
SelectedDateFormat="Short" />
</StackPanel>
</Window>
Remarks
It's best practice in UI design that you always prevent wrong input. If possible you shouldn't allow invalid input e.g. by disabling buttons, hiding invalid options, limiting ranges etc.
Don't allow the user to select invalid options. This leads to a frustrating user experience. One way to prevent wrong input is to use specialized controls. Instead of forcing the user to type a date you offer a DatePicker. This eliminates typos. But it can also eliminate wrong selections by providing only valid dates.
If you want to disallow selecting dates of the past you can narrow down the selectable range:
<!-- Only show dates from today -->
<DatePicker DisplayDateStart="{x:Static system:DateTime.Today}" />
If you have additional illegal dates e.g. holidays you can define a collection of those dates by setting the DatePicker.BlackoutDates property:
DatePickerHelper.cs
Class DatePickerHelper
Inherits DependencyObject
Public Shared ReadOnly BlackedDaysProperty As DependencyProperty = DependencyProperty.RegisterAttached("BlackedDays", GetType(IEnumerable(Of CalendarDateRange)), GetType(DatePickerHelper), New PropertyMetadata(Nothing, AddressOf DatePickerHelper.OnBlackDatesChanged))
Public Shared Sub SetBlackedDays(ByVal attachingElement As DependencyObject, ByVal value As IEnumerable(Of CalendarDateRange))
Return attachingElement.SetValue(DatePickerHelper.BlackedDaysProperty, value)
End Sub
Public Shared Function GetBlackedDays(ByVal attachingElement As DependencyObject) As IEnumerable(Of CalendarDateRange)
Return CType(attachingElement.GetValue(DatePickerHelper.BlackedDaysProperty), IEnumerable(Of CalendarDateRange))
End Function
Private Shared ReadOnly Property DatePickerTable As Dictionary(Of INotifyCollectionChanged, DatePicker)
Private Shared Sub New()
DatePickerHelper.DatePickerTable = New Dictionary(Of INotifyCollectionChanged, DatePicker)()
End Sub
Private Shared Sub OnBlackDatesChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim attachedDatePicker = TryCast(d, DatePicker)
Dim oldObservableCollection As INotifyCollectionChanged = Nothing
If CSharpImpl.__Assign(oldObservableCollection, TryCast(e.OldValue, INotifyCollectionChanged)) IsNot Nothing Then
oldObservableCollection.CollectionChanged -= AddressOf UpdateDatePickerBlockedDates
DatePickerHelper.DatePickerTable.Remove(oldObservableCollection)
End If
Dim newObservableCollection As INotifyCollectionChanged = Nothing
If CSharpImpl.__Assign(newObservableCollection, TryCast(e.NewValue, INotifyCollectionChanged)) IsNot Nothing Then
newObservableCollection.CollectionChanged += AddressOf UpdateDatePickerBlockedDates
DatePickerHelper.DatePickerTable.Add(newObservableCollection, attachedDatePicker)
End If
attachedDatePicker.BlackoutDates.AddRange(TryCast(e.NewValue, IEnumerable(Of CalendarDateRange)))
End Sub
Private Shared Sub UpdateDatePickerBlockedDates(ByVal sender As Object, ByVal e As NotifyCollectionChangedEventArgs)
Dim attachedDatePicker As DatePicker = Nothing
If Not DatePickerHelper.DatePickerTable.TryGetValue(TryCast(sender, INotifyCollectionChanged), attachedDatePicker) Then
Return
End If
Select Case e.Action
Case NotifyCollectionChangedAction.Add
attachedDatePicker.BlackoutDates.AddRange(e.NewItems.OfType(Of CalendarDateRange)())
Case NotifyCollectionChangedAction.Remove, NotifyCollectionChangedAction.Replace
e.OldItems.OfType(Of CalendarDateRange)().ToList().ForEach(Function(removedItem) attachedDatePicker.BlackoutDates.Remove(removedItem))
Case NotifyCollectionChangedAction.Move
Case NotifyCollectionChangedAction.Reset
attachedDatePicker.BlackoutDates.Clear()
Case Else
End Select
End Sub
End Class
ViewModel.cs
Class ViewModel
Public Property BlockedDates As ObservableCollection(Of CalendarDateRange)
// Block Christmas holidays and all days in the past
Public Sub New()
Return CSharpImpl.__Assign(Me.BlockedDates, New ObservableCollection(Of CalendarDateRange) From {
New CalendarDateRange(New DateTime(2020, 12, 24), New DateTime(2020, 12, 26)),
New CalendarDateRange(DateTime.MinValue, DateTime.Today.Subtract(TimeSpan.FromDays(1)))
})
End Sub
End Class
MainWindow.xaml
<!-- Only show dates from today -->
<DatePicker DatePickerHelper.BlackedDays="{Binding BlockedDates}" />

Converting normal property to dependency property

I have a control that I am using for my new application. This control has a regular property as such.
Public Property Value() As String
Get
If AutoCompleteTextBox.SearchText Is Nothing Then
Return String.Empty
Else
Return AutoCompleteTextBox.SearchText.ToString.Trim
End If
End Get
Set(value As String)
AutoCompleteTextBox.SearchText = value
End Set
End Property
Edit:
So, after multiple tries, I am finally at this stage.
Public Shared ValueProperty As DependencyProperty = DependencyProperty.Register("Value", GetType(String), GetType(AutoCompleteBox))
Public Property Value() As String
Get
Return Me.GetValue(ValueProperty).ToString
End Get
Set(value As String)
Me.SetValue(ValueProperty, value)
End Set
End Property
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
This is the dependency property. This property is still not binding. No errors are shown in output window for binding.
Text="{Binding RelativeSource={RelativeSource Self}, Path=Value, Mode=TwoWay}"
This is my binding method. I have no idea what else I can do. At least if there was an error, I could have figured out something. Without any error, I am just a headless chicken here.
Please refer to the following url for all the dependency fundamentals
http://www.wpftutorial.net/dependencyproperties.html
Basically, you can get a property changed event of dependency property by providing a FrameworkPropertyMetadata.
new FrameworkPropertyMetadata( [Default Value],
OnCurrentTimePropertyChanged);
And you can get back the target control (DependencyObject) at the event handler and implement your logic over there
private static void OnCurrentTimePropertyChanged(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
AutoCompleteTextBox control = source as AutoCompleteTextBox;
string time = (string)e.NewValue;
// Put some update logic here...
}
Declaring a dependency property in a control is a good thing.
You could make some binding in the xaml (sorry I don't have your XAML - I imagine).
Something like :
<TextBox x:Name="AutoCompleteTextBox"
Text="{Binding RelativeSource={RelativeSource=Self},Path=Value}"/>
Regards
TextBox has a property called Text. When you access Text property it will give you text entered in TextBox. Same is your case. Now why you want to convert it into a DP ? A DP would be useful if you want o bind this DP to some other control.
Extend this control itself. Make a new control and introduce this new DP.
While a DP is used where you want to bind this property to some control. This property then gets updated from control or control gets updated from this DP depending upon binding mode set.
How to do binding :
<TextBox x:Name="UserInput" />
<uc:MyAutoCompleteTextBox ValueDP="{Binding Text, ElementName=UserInput, Mode=OneWay}" />
MyAutoCompleteTextBox is new control which extends(inherits) from your old AutoComplete control.
If you want to apply some filtering logic or anything else, you can apply it in your DP itself like this :
Get
someVariable = TryCast(Me.GetValue(ValueProperty), String)
' apply somg logic to someVariable
' use your old Value property from here
Return someVariable
End Get
There are many WPF Binding tutorials on net.
I recommend :
http://blog.scottlogic.com/2012/04/05/everything-you-wanted-to-know-about-databinding-in-wpf-silverlight-and-wp7-part-one.html
Just change your code with following code and you should be good
your code
Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("Value", GetType(String), GetType(AutoCompleteBox))
Public Property Value() As String
Get
Return TryCast(Me.GetValue(ValueProperty), String)
End Get
Set(value As String)
Me.SetValue(ValueProperty, value)
End Set
End Property
New code
Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("Value", GetType(String), GetType(AutoCompleteBox))
Public Property Value() As String
Get
If AutoCompleteTextBox.SearchText Is Nothing Then
Return String.Empty
Else
Return AutoCompleteTextBox.SearchText.ToString.Trim
End If
End Get
Set(value As String)
AutoCompleteTextBox.SearchText = value
End Set
End Property
This DP will do what your Older property was doing. But just think about your requirement there can be a better way of writing the things.
Thanks

WPF Two controls bound to single property not updating each other

I am developing a WPF application using the MVVM design pattern.
I have a ViewModel which implements INotifyPropertyChanged via it's base class. The ViewModel contains a property which is then bound to two text boxes. Relevant code below.
ViewModelBase
Public MustInherit Class ViewModelBase : Implements INotifyPropertyChanged
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
Protected Sub onPropertyChanged(propertyName As String)
If Not propertyName Is Nothing Then RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
ViewModel
Public Class TemplateEditVM : Inherits ViewModelBase
Public Property Name As String
Get
Return _Template.Name
End Get
Set(value As String)
If Not _Template.Name = value Then
_Template.Name = value
onPropertyChanged("TemplateEditName")
End If
End Set
End Property
End Class
View
<TextBox Text="{Binding Path=Name}" />
<TextBox Text="{Binding Path=Name}" />
The value of the property Name correctly populates both text boxes when the View is first loaded. The problem is that when I change the text in one of the text boxes the text of the other doesn't change, even though the underlying property has. If I step through it I can see that the PropertyChanged event is being fired.
Can anybody tell me why? Many thanks for any help.
Set your binding mode to Twoway
<TextBox Text="{Binding Path=Name, Mode="TwoWay"}" />
And your on propertyChanged method should math the exact name you bind to

Silverlight 5 INotify OnPropertyChanged not triggered?

If I bind a TextBox to an object's (Merchandise is the object) property like:
Text="{Binding Path=Merchandise.Quantity, Mode=TwoWay}
<TextBox x:Name="QuantityTextBox" Grid.Column="1" Grid.Row="5" Width="70" Text="{Binding Path=Merchandise.Quantity, Mode=TwoWay, TargetNullValue='0', FallbackValue='0', ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"/>
The DataContext is set to my ViewModel which contains (among other properties) the declared property of type Merchandise.
Implements INotifyPropertyChanged, IDataErrorInfo
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs)
If PropertyChangedEvent IsNot Nothing Then
RaiseEvent PropertyChanged(Me, e)
Select Case e.PropertyName
Case "Merchandise"
DoSomething()
Case Else
End Select
End If
End Sub
Private _Merchandise As DC.SL.Services.WebServiceMerchandise.Merchandise
Public Property Merchandise() As DC.SL.Services.WebServiceMerchandise.Merchandise
Get
Return _Merchandise
End Get
Set(ByVal value As DC.SL.Services.WebServiceMerchandise.Merchandise)
_Merchandise = value
OnPropertyChanged(New PropertyChangedEventArgs("Merchandise"))
End Set
End Property
When I input changes in the TextBox the PropertyChanged event is NOT triggered. The only way I can work around this is by creating duplicate properties in my ViewModel ... i.e. Quantity and then later when Ok button is clicked I assigned the ViewModel properties to my Merchandise object. But this is definitely not efficient seems to defeat the purpose of being able to Bind using Path=SomeObject.Property.
Any hints on how to solve this?
Thanks, Rob.
You need to raise a PropertyChanged event on the properties of your Merchandise class. So that class needs to implement the INotifyPropertyChanged interface as well as your view model class. Otherwise, the UI doesn't know that anything's changed.

RaisePropertyChanged doesn't update binding

I've come across some strange behavior. I have a control that binds to a property like so:
<HyperlinkButton x:Name="ProjectBeheerLink"
Visibility="{Binding IsBeheerder, Converter={StaticResource VisibilityConverter}}"/>
In my viewmodel I have the property implemented like this:
Public ReadOnly Property IsBeheerder As Boolean
Get
Return iwtApp.AllMyFunctieRollen.Any(Function(x) x.Rol.Equals(Constants.RoleBeheerder))
End Get
End Property
Then when I raise my PropertyChanged event in my callback method
Private Sub GetMyPersonCompleted(ByVal lo As LoadOperation(Of FunctieRol))
'Init FunctieRollen ect. ...
RaisePropertyChanged(Function() Me.IsBeheerder)
End Sub
my binding does not update.
However! If I implement a backend field like this:
_isBeheerder = iwtApp.AllMyFunctieRollen.Any(Function(x) x.Rol.Equals(Constants.RoleBeheerder))
RaisePropertyChanged(Function() Me.IsBeheerder)
And change my property like this:
Public ReadOnly Property IsBeheerder As Boolean
Get
Return _isBeheerder
End Get
End Property
Everything works fine... Can someone explain me this behavior?
Just curious as to why..
The best way to implement a bound property is as follows:
Dim _isBeheerder As Boolean
Public ReadOnly Property IsBeheerder As Boolean
Get
Return _isBeheerder
End Get
Set
If value <> _isBeheerder Then
_isBeheerder = value
RaisePropertyChanged(Function() Me.IsBeheerder)
End If
End Get
End Property
then in your service callback you just set this property:
Private Sub GetMyPersonCompleted(ByVal lo As LoadOperation(Of FunctieRol))
IsBeheerder = myNewValue
End Sub

Resources