【问题标题】:Dependecy properties that depend on other properties依赖于其他属性的依赖属性
【发布时间】:2012-02-20 13:07:37
【问题描述】:

C 类实现 INotifyPropertyChanged。

假设 C 具有 Length、Width 和 Area 属性,其中 Area = Length * Width。任一可能的变化都会导致面积发生变化。这三个都是绑定的,即 UI 期望这三个都通知它们值的变化。

当 Length 或 Width 改变时,它们的 setter 调用 NotifyPropertyChanged。

我应该如何处理计算的 Area 属性?目前我能想到的模式是在 NotifyPropertyChanged 中检测更改的属性是 Length 还是 Width,如果是这种情况,则为 Area 启动附加的 PropertyChanged 通知。然而,这需要我在 NotifyPropertyChanged 内部维护依赖关系图,我认为这是一种反模式。

所以,我的问题是:我应该如何编写依赖于其他依赖属性的依赖属性?

edit:这里的人建议 Length 和 Width 也为 Area 调用 NotifyPropertyChanged。同样,我认为这是一种反模式。属性(恕我直言)不应该知道谁依赖它,NotifyPropertyChanged 也不应该知道。只有属性应该知道它依赖于谁。

【问题讨论】:

  • 不要将 dependency properties 与实现 INotifyPropertyChanged 的​​类的属性混淆。这不是一回事。
  • 如果你真的不喜欢它。将您的视图模型注册到它自己的 PropertyChanged 事件,监听 Width 和 Length 的属性更改,然后再次引发 Area 的更改。但同样,提出多个属性是完全有效的。事实上, raise 永远不会调用属性的 setter,只会调用 getter,所以它是安全的。
  • 我同意你的看法,@Avi。 LengthWidth 不应该负责为 Area 提升 PropertyChanged - 如果是,如果这些属性在另一个类中,你会怎么做?我在另一个问题中回答了这个问题:stackoverflow.com/questions/43653750/…

标签: wpf dependency-properties


【解决方案1】:

这个问题一直困扰着我,所以我重新打开它。

首先,我要为任何个人接受我的“反模式”评论的人道歉。这里提供的解决方案确实是在 WPF 中完成的。但是,恕我直言,它们是不良做法造成的,是框架中的缺陷。

我的主张是 information hiding 指南规定,当 B 依赖于 A 时,A 不应该知道 B。例如,当 B A 派生时,A 不应该有代码说:“如果我的运行时类型真的是 B,那么做这个那个”。类似地,当 B 使用 A 时,A 不应该有代码说:“如果调用此方法的对象是 B,那么……”

因此,如果属性 B 依赖于属性 A,则 A 不应该是负责直接警告 B 的人。

相反,在 NotifyPropertyChanged 中维护(如我目前所做的)依赖关系图也是一种反模式。该方法应该是轻量级的,并且按照它的名称声明,而不是维护属性之间的依赖关系。

所以,我认为所需的解决方案是通过aspect oriented programming:Peroperty B 应该使用“I-depend-on(Property A)”属性,并且一些代码重写器应该创建依赖关系图并透明地修改 NotifyPropertyChanged。

今天,我是一个开发单一产品的程序员,所以我不能再证明研究这个是合理的,但我觉得这是正确的解决方案。

【讨论】:

  • 不,你错了。信息隐藏在同一个数据结构(同一个类,没有继承)中毫无意义。此外,INotifyPropertyChanged 是一个带有一个事件的接口:PropertyChanged。谁实现了这个接口,只有知道何时引发 PropertyChanged 的​​人。不需要 AOP,那是完全不同的话题。
【解决方案2】:

这里有一篇文章描述了如何创建一个自定义属性,该属性根据另一个属性自动调用 PropertyChanged:http://www.redmountainsw.com/wordpress/2012/01/17/a-nicer-way-to-handle-dependent-values-on-propertychanged/

代码将如下所示:

[DependsOn("A")]
[DependsOn("B")]
public int Total
{
  get { return A + B; }
}

public int A 
{
  get { return m_A; }
  set { m_A = value; RaisePropertyChanged("A"); }
}

public int B
{
  get { return m_B: }
  set { m_B = value; RaisePropertyChanged("B"); }
}

我自己没有尝试过,但我喜欢这个主意

【讨论】:

    【解决方案3】:

    LengthWidth 属性发生更改时,您会为Area 触发PropertyChanged 此外LengthWidth 触发它。

    这是一个非常简单的实现,它基于支持字段和方法OnPropertyChanged 来触发PropertyChanged 事件:

    public Double Length {
      get { return this.length; }
      set {
        this.length = value;
        OnPropertyChanged("Length");
        OnPropertyChanged("Area");
      }
    }
    
    public Double Width {
      get { return this.width; }
      set {
        this.width = value;
        OnPropertyChanged("Width");
        OnPropertyChanged("Area");
      }
    }
    
    public Double Area {
      get { return this.length*this.width; }
    }
    

    这样做肯定不是反模式。这正是这样做的模式。作为该类的实现者,您知道当 Length 发生更改时,Area 也会发生更改,您可以通过引发适当的事件对其进行编码。

    【讨论】:

    • 但是如果WidthLength 在另一个班级,你会如何为Area 提高PropertyChanged?检查这个问题的答案:stackoverflow.com/questions/43653750/…
    • 虽然我也是这样做的,但是当你有一个中等规模的班级时,这很快就会失控。您开始寻找将 OnPropertyChanged() 调用用于更改的属性的位置。当您编辑属性 A 时,您不应开始在包含数百行甚至数千行代码的类文件中查找 OnPropertyChanged("A") 调用。
    【解决方案4】:

    那么你应该在 Length 和 Width 属性设置器中加注两次。一个用于实际属性,一个用于区域属性。

    例如:

    private int _width;
    public int Width
    {
        get { return _width; }
        set
        {
            if (_width == value) return;
            _width = value;
            NotifyPropertyChanged("Width");
            NotifyPropertyChanged("Area");
        }
    }
    

    这里的人建议 Length 和 Width 也调用 区域的 NotifyPropertyChanged。再次,我认为这是一个 反模式。财产(恕我直言)不应该知道谁依赖于 它,不应该 NotifyPropertyChanged。只有财产应该是 知道它取决于谁。

    这不是反模式。实际上,你的数据封装在这个类中,所以这个类知道什么时候发生了什么变化。你不应该知道这个类之外的区域取决于宽度和长度。所以通知听众关于 Area 最合乎逻辑的地方是 Width 和 Length 设置器。

    属性(恕我直言)不应该知道谁依赖它,因为 不应该 NotifyPropertyChanged。

    它不会破坏封装,因为你在同一个类中,在同一个数据结构中。

    一个额外的信息是,knockout.js(一个javascript mvvm库)有一个访问这个问题的概念:Computed Observables。所以我相信这是绝对可以接受的。

    【讨论】:

    • 在 Ember.js 框架中,计算属性表示相同的功能,但它只使计算属性声明依赖的属性名称,而不是像此解决方案中暗示的相反。跨度>
    • 虽然看起来很相似,但 emberjs 计算属性是另一回事。使用 INotifyPropertyChanged 以及您在此处看到的内容,您甚至不需要声明依赖项。 NotifyPropertyChanged 不是像 function(){}.property('foo', 'bar') 这样的依赖声明,它只是触发一个事件。
    • 这不是重点,而是您在依赖属性中引发事件,隐式声明它。似乎不对。如果您提出的事件也相互依赖怎么办。如果 B 依赖于 A 并且 C 依赖于 B 并且 D 依赖于 C。尽管 D 只是模糊地关心 A 并且可能在语义上不相关,但 A 属性将为所有人调用 Notify。例如,为什么 Age 字段应该关心在 12 月 4 日创建的与客户相关的订单...
    • 我理解您的推理,并且总体上说的完全有道理,但您需要了解的是 INotifyPropertyChanged 与依赖属性不同。这里没有自动或声明性的依赖跟踪,因此您需要手动进行。如果您需要表示可能跨越边界的更复杂的依赖关系,那么您可能需要其他东西。 INotifyPropertyChanged 只是一个简单的界面,它与例如无关。 emberjs 计算属性概念。
    • “如果您需要表示可能跨越边界的更复杂的依赖关系,那么您可能需要其他东西。” - 这就是人们说感觉不对的原因。它不可扩展。谁不想要一个可扩展的解决方案?一切从简单开始,然后需要进步。
    【解决方案5】:

    这是一个属性的可能实现:

    public class DependentPropertiesAttribute : Attribute
    {
        private readonly string[] properties;
    
        public DependentPropertiesAttribute(params string[] dp)
        {
            properties = dp;
        }
    
        public string[] Properties
        {
            get
            {
                return properties;
            }
        }
    }
    

    然后在Base View Model中,我们处理调用属性依赖的机制:

    public class ViewModelBase : INotifyPropertyChanged
    {
        public ViewModelBase()
        {
            DetectPropertiesDependencies();
        }
    
        private readonly Dictionary<string, List<string>> _dependencies = new Dictionary<string, List<string>>();
    
        private void DetectPropertiesDependencies()
        {
            var propertyInfoWithDependencies = GetType().GetProperties().Where(
            prop => Attribute.IsDefined(prop, typeof(DependentPropertiesAttribute))).ToArray();
    
            foreach (PropertyInfo propertyInfo in propertyInfoWithDependencies)
            {
                var ca = propertyInfo.GetCustomAttributes(false).OfType<DependentPropertiesAttribute>().Single();
                if (ca.Properties != null)
                {
                    foreach (string prop in ca.Properties)
                    {
                        if (!_dependencies.ContainsKey(prop))
                        {
                            _dependencies.Add(prop, new List<string>());
                        }
    
                        _dependencies[prop].Add(propertyInfo.Name);
                    }
                }
            }
        }
    
        protected void OnPropertyChanged(params Expression<Func<object>>[] expressions)
        {
            expressions.Select(expr => ReflectionHelper.GetPropertyName(expr)).ToList().ForEach(p => {
                RaisePropertyChanged(p);
                RaiseDependentProperties(p, new List<string>() { p });
            });
    
        }
    
        public event PropertyChangedEventHandler PropertyChanged = delegate { };
    
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    
        protected void RaiseDependentProperties(string propertyName, List<string> calledProperties = null)
        {
            if (!_dependencies.Any() || !_dependencies.ContainsKey(propertyName))
                return;
    
            if (calledProperties == null)
                calledProperties = new List<string>();
    
            List<string> dependentProperties = _dependencies[propertyName];
    
            foreach (var dependentProperty in dependentProperties)
            {
                if (!calledProperties.Contains(dependentProperty))
                {
                    RaisePropertyChanged(dependentProperty);
                    RaiseDependentProperties(dependentProperty, calledProperties);
                }
            }
        }
    }
    

    最后我们在 ViewModel 中定义依赖关系

    [DependentProperties("Prop1", "Prop2")]
    public bool SomeCalculatedProperty
    {
        get
        {
            return Prop1 + Prop2;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2017-12-16
      • 2016-07-08
      • 1970-01-01
      • 2011-09-09
      • 1970-01-01
      • 2011-08-20
      • 2018-03-26
      • 1970-01-01
      • 2011-11-07
      相关资源
      最近更新 更多