How can I avoid recursion when updating my ViewModel properties? - c#

In my View I have a slider and a combobox.
When I change the slider, I want the combobox to change.
When I change the combobox, I want the slider to change.
I can udpate one or the other, but if I try to update both I get a StackOverflow error since one property keeps updating the other in an infinite loop.
I've tried putting in a Recalculate() where the updating is done in one place, but still run into the recursion problem.
How can I have each control update the other without going into recursion?
in View:
<ComboBox
ItemsSource="{Binding Customers}"
ItemTemplate="{StaticResource CustomerComboBoxTemplate}"
Margin="20"
HorizontalAlignment="Left"
SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}"/>
<Slider Minimum="0"
Maximum="{Binding HighestCustomerIndex, Mode=TwoWay}"
Value="{Binding SelectedCustomerIndex, Mode=TwoWay}"/>
in ViewModel:
#region ViewModelProperty: SelectedCustomer
private Customer _selectedCustomer;
public Customer SelectedCustomer
{
get
{
return _selectedCustomer;
}
set
{
_selectedCustomer = value;
OnPropertyChanged("SelectedCustomer");
SelectedCustomerIndex = _customers.IndexOf(_selectedCustomer);
}
}
#endregion
#region ViewModelProperty: SelectedCustomerIndex
private int _selectedCustomerIndex;
public int SelectedCustomerIndex
{
get
{
return _selectedCustomerIndex;
}
set
{
_selectedCustomerIndex = value;
OnPropertyChanged("SelectedCustomerIndex");
SelectedCustomer = _customers[_selectedCustomerIndex];
}
}
#endregion

try in the set functions something like:
public int SelectedCustomerIndex
{
get
{
return _selectedCustomerIndex;
}
set
{
if (value != _selectedCustomerIndex)
{
_selectedCustomerIndex = value;
OnPropertyChanged("SelectedCustomerIndex");
SelectedCustomer = _customers[_selectedCustomerIndex];
}
}
}
to fire the events only when there is an actual change in value. This way, a second call to the set property with the same value does not cause another change event.
You have to do that for the other property as well of course.

Both properties are called from each other, hence the recursion. Not related to binding at all. Proper way is to change each other and fire change notification for both properties when either property changes:
public Customer SelectedCustomer
{
get
{
return _selectedCustomerIndex;
}
set
{
_selectedCustomer = value;
_selectedCustomerIndex = _customers.IndexOf(value);
OnPropertyChanged("SelectedCustomer");
OnPropertyChanged("SelectedCustomerIndex");
}
}
public int SelectedCustomerIndex
{
get
{
return _selectedCustomerIndex;
}
set
{
_selectedCustomerIndex = value;
_selectedCustomer = _customers[_selectedCustomerIndex];
OnPropertyChanged("SelectedCustomer");
OnPropertyChanged("SelectedCustomerIndex");
}
}

Related

Calculated field not updating until edited in UI

I'm trying to test out data binding with XAML and C# as a novice programmer. I have two sliders that are bound to properties and I want to update a TextBox with the sum of the two values of the properties set by the sliders.
I'm using INotifyPropertyChanged and tried changing every property I could find but I can't get the textbox to update until I edit the textbox, at which point, the textbox updates to the correct value. Using UpdateSourceTrigger=PropertyChanged only updates the textbox as soon as I edit the textbox instead of when I select another element. I've tried writing a separate event handler that doesn't use [CallerNameMember] and uses a specified property but it didn't seem to change anything.
<Grid>
<Grid.RowDefinitions>
</Grid.RowDefinitions>
<TextBox Grid.Row="0"
Text="{Binding BoundNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
FontSize="20"
FontWeight="Bold"
AllowDrop="False" />
<Slider Grid.Row="1"
Value="{Binding BoundNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Maximum="100"
Minimum="10"
IsSnapToTickEnabled="True"
TickFrequency="10" />
<TextBox Grid.Row="2"
Text="{Binding BoundNumber2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
AllowDrop="False" />
<Slider Grid.Row="3"
Value="{Binding BoundNumber2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Maximum="100"
Minimum="10"
IsSnapToTickEnabled="True"
TickFrequency="10" />
<TextBox Grid.Row="4"
Name="MathBox"
Text="{Binding QuickMath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}">
</TextBox>
</Grid>
public partial class OrderScreen : INotifyPropertyChanged
{
public OrderScreen()
{
DataContext = this;
InitializeComponent();
}
private int quickMath;
public int QuickMath
{
get { return _boundNumber + _boundNumber2; }
set
{
if (value != quickMath)
{
quickMath = value;
OnPropertyChanged();
}
}
}
private int _boundNumber;
public int BoundNumber
{
get { return _boundNumber; }
set
{
if (_boundNumber != value)
{
_boundNumber = value;
// MathBox.Text = quickMath.ToString();
OnPropertyChanged();
}
}
}
private int _boundNumber2;
public int BoundNumber2
{
get { return _boundNumber2; }
set
{
if (_boundNumber2 != value)
{
_boundNumber2 = value;
MathBox.Text = quickMath.ToString();
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
I can get it to work with the commented out MathBox.Text = quickMath.ToString(); but I was hoping there was a better way to do this with data binding. Thanks in anticipation!
Binding mechanism subscribes to the PropertyChanged event of DataSource object, so there is no need to "initialize" the event along with the INPC implementation, but as you might have noticed, PropertyChanged event for the QuickMath property is indeed never triggered when BoundNumber or BoundNumber2 are changed.
You can fix it in different ways, e.g. explicitly call OnPropertyChanged for all affected properties:
private int _boundNumber;
public int BoundNumber
{
get { return _boundNumber; }
set
{
if (_boundNumber != value)
{
_boundNumber = value;
OnPropertyChanged();
OnPropertyChanged(nameof(QuickMath));
}
}
}
Note that this way you can keep QuickMath property a read-only. This approach works nicely in other situations, like with time-related properties, say if your data source property formats a string like "Edited 2 minutes ago" based on a recorded timestamp and current time and you call PropertyChanged as a timed task.
public int QuickMath => _boundNumber + _boundNumber2;
Alternatively, you can update QuickMath along with modifying BoundNumber and BoundNumber2 to trigger OnPropertyChanged() call inside QuickMath setter:
private int _boundNumber2;
public int BoundNumber2
{
get { return _boundNumber2; }
set
{
if (_boundNumber2 != value)
{
_boundNumber2 = value;
OnPropertyChanged();
QuickMath = BoundNumber + BoundNumber2;
}
}
}
This makes sense if the logic in QuickMath wouldn't allow making it a read-only property. In this case you have to adjust the getter accordingly and use private or protected setter there to avoid data inconsistency and unexpected behavior.
private int _quickMath;
public int QuickMath
{
get { return _quickMath; }
private set
{
if (value != _quickMath)
{
_quickMath = value;
OnPropertyChanged();
}
}
}
In both cases there is no need for two-way binding to QuickMath:
<TextBlock Grid.Row="4" Text="{Binding QuickMath, Mode=OneWay}"/>
On a side-note and looking at the rest of the code, it really worth mentioning that binding mechanism is expected to segregate UI from the data, where XAML knows about data source object properties (names and types) but not about it's internal implementation, while data source object can have no knowledge about XAML at all. So
there should be no calls from data object to FrameworkElements like MathBox.Text
it's considered a good design to have data object class completely separate from the page or control class.
Hope this helps.
You haven't initialized your PropertyChanged event anywhere, so it will never be called. Declare and initialize it like so:
public event PropertyChangedEventHandler PropertyChanged = delegate { };
A TextBox bound to the calculated property QuickMath should receive PropertyChanged event from it in order to update the text in the field.
Despite your OrderScreen implementing the INotifyPropertyChanged interface, it will not raise the event when QuickMath is changed because its setter (where the raising of the event is located) is never called. You can fix it, for example, by calling the QuickMath setter from the independent properties setters as suggested in other answers or delegate that work to DependenciesTracking lib:
public class OrderScreen : INotifyPropertyChanged
{
private readonly IDependenciesMap<OrderScreen> _dependenciesMap =
new DependenciesMap<OrderScreen>()
.AddDependency(i => i.QuickMath, i => i.BoundNumber + i.BoundNumber2, i => i.BoundNumber, i => i.BoundNumber2);
public OrderScreen() => _dependenciesMap.StartTracking(this);
private int _boundNumber2;
private int _boundNumber;
private int _quickMath;
public int QuickMath
{
get => _quickMath;
private set
{
if (value != _quickMath)
{
_quickMath = value;
OnPropertyChanged();
}
}
}
public int BoundNumber
{
get => _boundNumber;
set
{
if (_boundNumber != value)
{
_boundNumber = value;
OnPropertyChanged();
}
}
}
public int BoundNumber2
{
get => _boundNumber2;
set
{
if (_boundNumber2 != value)
{
_boundNumber2 = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public class Tests_SO_56623403
{
[Test]
public void Test_SO_56623403()
{
var sut = new OrderScreen();
var raisedEventsCount = 0;
sut.PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(OrderScreen.QuickMath))
++raisedEventsCount;
};
Assert.Multiple(() =>
{
Assert.That(sut.QuickMath, Is.EqualTo(0));
Assert.That(raisedEventsCount, Is.EqualTo(0));
});
sut.BoundNumber = 12;
Assert.Multiple(() =>
{
Assert.That(sut.QuickMath, Is.EqualTo(12));
Assert.That(raisedEventsCount, Is.EqualTo(1));
});
sut.BoundNumber2 = 40;
Assert.Multiple(() =>
{
Assert.That(sut.QuickMath, Is.EqualTo(52));
Assert.That(raisedEventsCount, Is.EqualTo(2));
});
}
}

Assigning SelectedValue of ComboBox to a string property of viewmodel wpf

So I have very simple combobox containing list of values. I am supposed to bind the selectedvalue to a viewmodel property and store it in DB. Below is my current approach:
SampleViewModel.cs
public class SampleViewModel: BindableBase
{
public SampleViewModel()
{
MyDetails = new ObservableCollection<DetailItems>(){
new DetailItems{Name="Detail1"},
new DetailItems{Name = "Detail2"},
new DetailItems{Name= "Detail3"},
new DetailItems{Name="Detail4"}
};
}
private ObservableCollection<DetailItems> _myDetails;
private string _myDetail;
public ObservableCollection<DetailItems> MyDetails { get { return _myDetails; } set { SetProperty(ref _myDetails, value); } }
public string MyDetail { get { return _myDetail; } set { SetProperty(ref _myDetail, value); } }
}
public class DetailItems: BindableBase
{
private string _name;
public string Name { get { return _name; } set { SetProperty(ref _name, value); } }
}
and my ComboBox in View is as follows
<ComboBox x:Name="cbDetails"
ItemsSource="{Binding MyDetails}"
DisplayMemberPath="Name"
SelectedValuePath="{Binding Path=Name}"
SelectedValue="{Binding MyDetail}"/>
But whenever I get data in backend, the string MyDetail will have an instance of DetailItems converted to string. Could anyone let me know how I can change this to bind appropriate value to MyDetail?
The reason is quite simple: SelectedValuePath expects path to the property of object, not binding (just like DisplayMemberPath does). So a fix would be:
<ComboBox x:Name="cbDetails"
ItemsSource="{Binding MyDetails}"
DisplayMemberPath="Name"
SelectedValuePath="Name"
SelectedValue="{Binding MyDetail}"/>
Obviously, SelectedValue always holds the currently selected item only. So you have to change the type from string to DetailItems as follow,
private DetailItems_myDetail;
public DetailItems MyDetail { get { return _myDetail; } set { SetProperty(ref _myDetail, value); } }
}
if you want the selected name call MyDetail.Name it will return your string

c# wpf Combox value in editable field must be different that displaymemberpath

I have a Combobox with a concate string in displaymemberpath ('DescriptionComplete' in code-behind below), and that is what i get in the editable field BUT this is only the IdEchantillon that i want into the editable field...
How must i do please ?
XAML:
<ComboBox x:Name="cbEchantillon" SelectedValue="{Binding CurrentEchantillon.IdEchantillon}" ItemsSource="{Binding OcEchantillon}" DisplayMemberPath="DescriptionComplete" SelectedValuePath="IdEchantillon" SelectedItem="{Binding CurrentEchantillon}" Text="{Binding IdEchantillon}" IsEditable="True" Width="355" FontSize="14" FontFamily="Courier New" SelectionChanged="cbEchantillon_SelectionChanged"></ComboBox>
Code behind:
public class Echantillon : ViewModelBase
{
private string _IdEchantillon;
private string _Description;
private DateTime _dateechan;
private string _descriptionComplete;
}
public string IdEchantillon
{
get { return _IdEchantillon; }
set { _IdEchantillon = value; RaisePropertyChanged("IdEchantillon"); }
}
public string Description
{
get { return _Description; }
set { _Description = value; RaisePropertyChanged("Description"); }
}
public DateTime Dateechan
{
get { return _dateechan; }
set { _dateechan = value; RaisePropertyChanged("Dateechan"); }
}
public string DescriptionComplete
{
get { return string.Format("{0} {1} {2}", IdEchantillon.PadRight(20), Dateechan.ToShortDateString(), Description); }
set { _descriptionComplete = value; }
}
}
At first, please, cleanup your ComboBox property bindings. You should not create binding for SelectedValue, because you already have binding for SelectedItem property. The SelectedValue is determined by extracting the value by SelectedValuePath from the SelectedItem.
In case of binding to ViewModel, you should not also bind Text property. Use ItemsSource as you do, and set DisplayMemberPath property to what you want to show as the text representation of the every item in the bound collection.

Managing the IsEnabled property of a button

I have a xaml window in my program that has a button called "Save", and a textBox. I also have a ViewModel for this window. Inside the ViewModel I have a string property for the textBox, and a bool property for IsEnabled on the button. I would like the button to only be enabled when there is text inside the textBox.
xaml:
<Button IsEnabled="{Binding SaveEnabled}" ... />
<TextBox Text="{Binding Name}" ... />
ViewModel properties:
//Property for Name
public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChange(() => Name);
if (value == null)
{
_saveEnabled = false;
NotifyPropertyChange(() => SaveEnabled);
}
else
{
_saveEnabled = true;
NotifyPropertyChange(() => SaveEnabled);
}
}
}
//Prop for Save Button -- IsEnabled
public bool SaveEnabled
{
get { return _saveEnabled; }
set
{
_saveEnabled = value;
NotifyPropertyChange(() => SaveEnabled);
}
}
I think my main question here is, where do I put the code concerning this problem? As you can see above, I've tried to put it into the setter of the Name property, but it comes back with no success.
You can just do:
public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChanged(() => Name);
NotifyPropertyChanged(() => SaveEnabled);
}
}
public bool SaveEnabled
{
get { return !string.IsNullOrEmpty(_name); }
}
EDIT: Add this to your xaml:
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}">...</TextBox>
Use ICommands that are used in MVVM:
private ICommand _commandSave;
public ICommand CommandSave
{
get { return _commandSave ?? (_commandSave = new SimpleCommand<object, object>(CanSave, ExecuteSave)); }
}
private bool CanSave(object param)
{
return !string.IsNullOrEmpty(Name);
}
private void ExecuteSave(object param)
{
}
And then use the following in the XAML Code
<TextBox Command="{Binding CommandSave}" ... />
Depending on the Framework that you use the command class works differen. For a generic implementation I suggest Relay Command.

WPF MVVM Editable Combobox new value is null

Tried all the solutions for similar issues around here, still no go. I have a ComboBox that should work for selection of existing items and/or for adding new ones. Only the selected item part works. Category is just an object with a Name and Id.
Thanks in advance!
XAML
<ComboBox Name="CbCategory" ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory.Name, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding NewCategory.Name}" DisplayMemberPath="Name"
IsEditable="True"/>
Code behind
private Category _selectedCategory;
public Category SelectedCategory
{
get { return _selectedCategory; }
set
{
if (Equals(_selectedCategory, value)) return;
_selectedCategory = value;
SendPropertyChanged("SelectedCategory");
}
}
private Category _newCategory;
public Category NewCategory
{
get { return _newCategory; }
set
{
if (Equals(_newCategory, value)) return;
_newCategory = value;
SendPropertyChanged("NewCategory");
}
}
Your Text Binding doesn't work because you're binding against a null Category property. Instantiate it instead.
public Category NewCategory
{
get { return _newCategory ?? (_newCategory = new Category()); }
set
{
if (Equals(_newCategory, value)) return;
_newCategory = value;
SendPropertyChanged("NewCategory");
}
}
Edit: Elaborating as per your comment:
Your ComboBox.Text binding is set to "{Binding NewCategory.Name}", so no matter what the value of SelectedCategory is, the Text property will always reflect the name of the NewCategory.
When NewCategory is null, the Text property has nothing to bind to, and therefore the 2-way binding cannot be executed (that is, the value of the Text property cannot be passed back to NewCategory.Name, because that will cause an NullReferenceException (because the NewCategory is null).
This does not affect the case of the SelectedItem, because that's binding directly to the SelectedCategory property, and not a sub-property of that.
Create new varible to keep text of combobox. If the selectedItem having null value get the text of combobox as new Item,
Code :
<ComboBox Name="CbCategory" ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory.Name, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding Name}" DisplayMemberPath="Name"
IsEditable="True"/>
private String _name;
public Category Name
{
get { return _name; }
set
{
_name = value
SendPropertyChanged("Name");
}
}
public ICommand ItemChange
{
get
{
`return new RelayCommand(() =>`{
try{string item = this.SelectedCategory.Code;}
catch(Exception ex){string item = this.Name;}
}, () => { return true; });
}
}

Categories