如何通过相同viewmodel

时间:2017-04-05 14:34:49

标签: c# wpf events mvvm viewmodel

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

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

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

例如:viewmodel包含Total,BreadQuantity和BreadCost属性。更改BreadQuantity属性后,必须通知用户界面更改BreadQuantity和Total属性以更新相应的绑定。我想反过来只调用BreadQuantity的PropertyChanged事件,因为Total使用该属性来计算总数,那么它的相应绑定也应该更新。

下面我已经包含了我的视图模型继承的类,其中包含事件以及视图模型属性,其中包含有效的示例和我想要做的事情

下面是处理ViewModel事件的类。 OnPropertyChanged(字符串名称)方法用于通知正在更新的属性,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个属性与之关联,则Id必须执行

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

        }
    }

对我来说,这似乎是一种简单的方法来引入错误和大量紧密耦合到程序中。如果以后我想重构代码并将TotalCost重命名为TotalCostOfItems那么我将无法使用视觉工作室&#34; f2&#34;命令这样做,我将不得不寻找这些字符串以更新它们,这就是我想要避免的。

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

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

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

3 个答案:

答案 0 :(得分:1)

我使用公共getter和private / protected setter来计算属性来执行此操作。

我不会更新计算属性的支持字段,而是更新私有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

答案 1 :(得分:1)

我受到鼓舞,想要创造一种更好的方法来解决这个问题:

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中完成任何操作。

答案 2 :(得分:0)

我在这里看到两个选项:

  1. 来自documentation
  2.   

    propertyName参数的空值或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"));
      }
      
    2. 您可以扩展此方法以接受属性名称数组,但您已经有了一个想法。 但这看起来很丑陋,我会先选择。