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

how to pass events through properties of the same viewmodel

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

ViewModels 有几个属性,其中一些属性是基于其他 属性 值的计算属性。

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

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

下面我包括了我的视图模型继承的 class,其中包含事件以及视图模型属性,其中包含有效的示例和我正在尝试执行的操作

下面是处理 ViewModel 事件的 class。 OnPropertyChanged(string name) 方法用于通知 属性 正在更新,NewOnPropertyChanged 是一个新方法,它做同样的事情但缩短了 viewmodel 中的代码并且还使用属性来接收属性名称帮助防止拼写错误导致正确的事件不触发。

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

下面是按预期工作的继承 ViewModel 的属性

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 个关联的属性,那么我必须这样做

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

        }
    }

在我看来,这似乎是一种将错误和大量紧密耦合引入程序的简单方法。如果稍后我想重构代码并将 TotalCost 重命名为 TotalCostOfItems 那么我将无法使用 visual studios "f2" 命令来执行此操作,我将不得不寻找这些字符串来更新它们,这就是我我正在努力避免。

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

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

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

我使用 public 吸气剂和 private/protected setters 计算属性来执行此操作。

我没有更新计算属性的支持字段,而是更新了私有 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);
    }

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

SetValue 是基础 class 上的一种方法,它设置支持字段并仅在值实际更改时自动引发 PropertyChanged。它 returns true 仅当值改变时。

更改 Duration 将级联到 StopTimeFromTo

我在这里看到两个选项:

  1. 来自 documentation

An Empty value or null for the propertyName parameter indicates that all of the properties have changed.

因此只需调用 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"));
    }
    

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

我受到启发,想创造一种更好的方法来处理这个问题:

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 函数来添加任意数量的级联。

我可能会更新它以使用自定义属性,这样就不必在构造函数中做任何事情了。