如何通过同一视图模型的属性传递事件



我目前正在编写一个 C# WPF 应用程序,该应用程序在我的 ViewModel 中实现 INotifyPropertyChanged 接口

ViewModel 具有多个属性,其中一些属性是基于其他属性值的计算属性。

我正在尝试做的是简化现有代码,以允许 propertyChanged 事件遍历属性,以便 xaml 中的相应绑定全部更新。

例如:视图模型包含"总计"、"面包数量"和"面包成本"属性。更改 BreadQuantity 属性时,它必须通知用户界面对 BreadQuantity 和 Total 属性的更改,以便更新相应的绑定。相反,我只想调用面包数量的 PropertyChanged 事件,并且由于 Total 使用该属性来计算总数,因此它的相应绑定也应该更新。

下面我包含了我的视图模型继承的类,其中包含事件以及视图模型属性,并举例说明了什么有效以及我正在尝试做什么

下面是处理视图模型事件的类。OnPropertyChanged(字符串名称)方法用于通知该属性正在更新,NewOnPropertyChanged 是一个新方法,它执行相同的操作,但缩短了视图模型中的代码,并且还使用属性来接收属性名称,以帮助防止拼写错误导致正确的事件不触发。

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));
}
}

下面是继承视图模型中按预期工作

的属性
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");
}
}

但是我想找到一种方法来避免OnPropertyChanged("TotalCost");对于绑定到视图的每个属性。

此应用程序只是用于学习这些术语的测试应用程序,但完整的应用程序将执行相同的操作,但将具有与属性关联的多个计算属性,并将创建大量冗余样板代码和拼写错误的可能性

例如,如果还有 3 个属性与之关联,那么 Id 必须做

public int BreadQuantity
{
get
{
return _bread_quantity;
}
set
{
NewOnPropertyChanged<int>(ref _bread_quantity, value);
OnPropertyChanged("TotalCost");
OnPropertyChanged("Inventory");
OnPropertyChanged("CartItems");
}
}

对我来说,这似乎是在程序中引入错误和大量紧密耦合的简单方法。如果以后我想重构代码并将 TotalCost 重命名为 TotalCostOfItems,那么我将无法使用可视化工作室"f2"命令来执行此操作,我将不得不寻找这些字符串来更新它们,这就是我试图避免的。

提前非常感谢所有花时间阅读我的问题并想出解决方案的人

@@@@@@@@ 编辑 @@@@@@@@

发现在 C# 6.0 中,您可以使用nameof(Property)从属性中获取字符串,并且还允许您安全地重构应用程序

我使用公共 getter 和私有/受保护的 setter 来计算属性来执行此操作。

我没有更新计算属性的支持字段,而是更新私有资源库,这会为该属性引发PropertyChanged

这要求存储计算属性,而不是动态计算。

这是我当前项目的片段:

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);
}

它来自存储事件相关信息的类。 有StartTimeStopTimeDuration属性,计算字符串显示名为FromTo的友好显示值。

SetValue是基类上的一种方法,它设置支持字段,并且仅在值实际更改时才自动引发PropertyChanged。 仅当值更改时,它才返回true

更改Duration将级联到StopTimeFromTo.

我受到启发,创造了一种更好的方法来解决这个问题:

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看起来像:

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;
}
}

它可以像这样使用:

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}";
}

这将导致对Name的任何更改自动级联到DoubleNameTripleName。 您可以通过链接AddCascade函数来添加任意数量的级联。

我可以更新它以使用自定义属性,这样就不必在 cosntructor 中执行任何操作。

我在这里看到两个选项:

  1. 从文档

属性名称参数的空值或 null 指示所有属性都已更改。

因此,只需调用OnPropertyChanged();OnPropertyChanged(null);,它就会更新您的所有属性。

  1. 您必须手动调用为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"));
    }
    

您可以扩展此方法以接受属性名称数组,但您有一个想法。 但这看起来很丑,我会选择第一个选项。

相关内容

  • 没有找到相关文章