Reputation: 573
I am currently writing a C# WPF application that implements the INotifyPropertyChanged interface in my ViewModels
The ViewModels have several properties and some of those properties are computed properties based on the other property values.
What I am trying to do is simplify existing code to allow the propertyChanged event to travel through the properties so the corresponding bindings in the xaml all update.
For Example: The viewmodel contains a Total, BreadQuantity, and BreadCost properties. When the BreadQuantity property is changed it has to notify the user interface of a change to the BreadQuantity and Total properties to have the corresponding bindings update. I would like instead to only invoke the PropertyChanged event for the BreadQuantity and since Total uses that property to calculate the total then it's corresponding binding should update as well.
Below I have Included the class that my view model inherits that contains the event as well as the view model properties with examples of what works and what I am trying to do
Below is the class that handles the events for the ViewModels. The OnPropertyChanged(string name) method is used to notify of that property being update, The NewOnPropertyChanged is a new one that does the same thing but shortens the code in the viewmodel and also uses an attribute to recieve the properties name to help prevent a typo from causing the correct event to not fire.
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public void NewOnPropertyChanged<T>(ref T variable, T value, [CallerMemberName] string propertyName = "")
{
variable = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Below is the Properties from the Inheriting ViewModel that works as expected
public decimal TotalCost
{
get
{
decimal[] quant_list = new decimal[3] { BreadQuantity, MilkQuantity, CerealQuantity };
decimal[] price_list = new decimal[3] { BreadPrice, MilkPrice, CerealPrice };
return calc_cost.CalculateCostArray(quant_list, price_list);
}
set
{
NewOnPropertyChanged<decimal>(ref _total_cost, value);
}
}
public decimal BreadPrice
{
get
{
return _bread_price;
}
set
{
NewOnPropertyChanged<decimal>(ref _bread_price, value);
OnPropertyChanged("TotalCost");
}
}
However I would like to find a way where I can avoid OnPropertyChanged("TotalCost");
for each property that is binded to the view.
This application is only a test application for learning these terms but the full application will do the same thing but will have several computed properties associated with properties and will create a lot of redundant boilerplate code and possibilities for typos
For example if there were 3 more properties associate with it then Id have to do
public int BreadQuantity
{
get
{
return _bread_quantity;
}
set
{
NewOnPropertyChanged<int>(ref _bread_quantity, value);
OnPropertyChanged("TotalCost");
OnPropertyChanged("Inventory");
OnPropertyChanged("CartItems");
}
}
Which to me looks like an easy way to introduce errors and lots of tight coupling into the program. If later on I wanted to refactor the code and rename TotalCost to TotalCostOfItems then I would not be able to use visual studios "f2" command to do that, I would have to hunt down these strings to update them and this is what I am trying to avoid.
Thank you very much in advance for all of those who took the time to read my problem and think of a solution
@@@@@@@@ EDIT @@@@@@@@
Found out that in C# 6.0 that you can use nameof(Property)
to acquire the string from the property, and also allowing you to refactor the application safely
Upvotes: 2
Views: 1593
Reputation: 17001
I was inspired to create a better way to handle this:
public class PropertyChangeCascade<T> where T : ObservableObject
{
public PropertyChangeCascade(ObservableObject target)
{
Target = target;
Target.PropertyChanged += PropertyChangedHandler;
_cascadeInfo = new Dictionary<string, List<string>>();
}
public ObservableObject Target { get; }
public bool PreventLoops { get; set; } = false;
private Dictionary<string, List<string>> _cascadeInfo;
public PropertyChangeCascade<T> AddCascade(string sourceProperty,
List<string> targetProperties)
{
List<string> cascadeList = null;
if (!_cascadeInfo.TryGetValue(sourceProperty, out cascadeList))
{
cascadeList = new List<string>();
_cascadeInfo.Add(sourceProperty, cascadeList);
}
cascadeList.AddRange(targetProperties);
return this;
}
public PropertyChangeCascade<T> AddCascade(Expression<Func<T, object>> sourceProperty,
Expression<Func<T, object>> targetProperties)
{
string sourceName = null;
var lambda = (LambdaExpression)sourceProperty;
if (lambda.Body is MemberExpression expressionS)
{
sourceName = expressionS.Member.Name;
}
else if (lambda.Body is UnaryExpression unaryExpression)
{
sourceName = ((MemberExpression)unaryExpression.Operand).Member.Name;
}
else
{
throw new ArgumentException("sourceProperty must be a single property", nameof(sourceProperty));
}
var targetNames = new List<string>();
lambda = (LambdaExpression)targetProperties;
if (lambda.Body is MemberExpression expression)
{
targetNames.Add(expression.Member.Name);
}
else if (lambda.Body is UnaryExpression unaryExpression)
{
targetNames.Add(((MemberExpression)unaryExpression.Operand).Member.Name);
}
else if (lambda.Body.NodeType == ExpressionType.New)
{
var newExp = (NewExpression)lambda.Body;
foreach (var exp in newExp.Arguments.Select(argument => argument as MemberExpression))
{
if (exp != null)
{
var mExp = exp;
targetNames.Add(mExp.Member.Name);
}
else
{
throw new ArgumentException("Syntax Error: targetProperties has to be an expression " +
"that returns a new object containing a list of " +
"properties, e.g.: s => new { s.Property1, s.Property2 }");
}
}
}
else
{
throw new ArgumentException("Syntax Error: targetProperties has to be an expression " +
"that returns a new object containing a list of " +
"properties, e.g.: s => new { s.Property1, s.Property2 }");
}
return AddCascade(sourceName, targetNames);
}
public void Detach()
{
Target.PropertyChanged -= PropertyChangedHandler;
}
private void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
List<string> cascadeList = null;
if (_cascadeInfo.TryGetValue(e.PropertyName, out cascadeList))
{
if (PreventLoops)
{
var cascaded = new HashSet<string>();
cascadeList.ForEach(cascadeTo =>
{
if (!cascaded.Contains(cascadeTo))
{
cascaded.Add(cascadeTo);
Target.RaisePropertyChanged(cascadeTo);
}
});
}
else
{
cascadeList.ForEach(cascadeTo =>
{
Target.RaisePropertyChanged(cascadeTo);
});
}
}
}
}
ObservableObject
looks like:
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
internal void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetValue<T>(ref T backingField, T newValue, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(backingField, newValue))
{
return false;
}
backingField = newValue;
RaisePropertyChanged(propertyName);
return true;
}
}
It can be used like this:
class CascadingPropertyVM : ObservableObject
{
public CascadingPropertyVM()
{
new PropertyChangeCascade<CascadingPropertyVM>(this)
.AddCascade(s => s.Name,
t => new { t.DoubleName, t.TripleName });
}
private string _name;
public string Name
{
get => _name;
set => SetValue(ref _name, value);
}
public string DoubleName => $"{Name} {Name}";
public string TripleName => $"{Name} {Name} {Name}";
}
This will cause any changes to Name
to automatically cascade to DoubleName
, and TripleName
. You can add as many cascades as you want by chaining the AddCascade
function.
I may update this to use custom attributes so that nothing as has to be done in the cosntructor.
Upvotes: 1
Reputation: 17001
I use public getters and private/protected setters for computed properties to do this.
Instead of updating the backing field for computed properties, I update the private setter, which raises PropertyChanged
for that property.
This requires that the computed property is stored, instead of computed on the fly.
Here is a snippet from my current project:
private TimeSpan _duration;
public TimeSpan Duration
{
get { return _duration; }
set
{
if (SetValue(ref _duration, value))
{
StopTime = StartTime + _duration;
FromTo = CalculateFromTo();
}
}
}
private string CalculateFromTo()
{
return $"{StartTime:t} - {StopTime:t}";
}
private string _fromTo;
public string FromTo
{
get => _fromTo;
private set => SetValue(ref _fromTo, value);
}
This is from a class that stores information about events. There are StartTime
, StopTime
, and Duration
properties, with a computed string showing a friendly display value for them named FromTo
.
SetValue
is a method on a base class that sets the backing field and automatically raises PropertyChanged
only if the value actually changed. It returns true
only if the value changed.
Changing Duration
will cascade to StopTime
and FromTo
.
Upvotes: 1
Reputation: 184
I see two options here:
An Empty value or null for the propertyName parameter indicates that all of the properties have changed.
So just invoke OnPropertyChanged();
or OnPropertyChanged(null);
and it's going to update all of your properties.
You'll have to manually invoke property changed for TotalCost
public void NewOnPropertyChanged<T>(ref T variable, T value, [CallerMemberName] string propertyName = "")
{
variable = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("TotalCost"));
}
You can extend this method to accept the array of property names, but you've got an idea. But this looks ugly, and I would go with first option.
Upvotes: 0