WPF中针对链接只读属性的INotifyPropertyChanged

14

我正试图理解如果有一个只读的属性依赖于另一个属性,如何更新UI,以使对一个属性的更改更新两个UI元素(在本例中为文本框和只读文本框)。例如:

public class raz : INotifyPropertyChanged
{

  int _foo;
  public int foo
  {
    get
    {
      return _foo;
    }
    set
    {
      _foo = value;
      onPropertyChanged(this, "foo");
    }
  }

  public int bar
  {
    get
    {
      return foo*foo;
    }
  }

  public raz()
  {

  }

  public event PropertyChangedEventHandler PropertyChanged;
  private void onPropertyChanged(object sender, string propertyName)
  {
    if(this.PropertyChanged != null)
    {
      PropertyChanged(sender, new PropertyChangedEventArgs(propertyName));
    }
  }
}

我的理解是当修改foo时,bar不会自动更新UI。那么正确的方式是什么?

7个回答

15

我知道这是一个老问题,但这是“NotifyPropertyChanged of linked properties”在谷歌中的第一条搜索结果,所以我认为添加这个答案是合适的,以便提供一些具体的代码。

我使用了Robert Rossney的建议,并创建了一个自定义属性,然后在基视图模型的PropertyChanged事件中使用它。

属性类:

[AttributeUsage(AttributeTargets.Property,AllowMultiple = true)]
public class DependsOnPropertyAttribute : Attribute
{
    public readonly string Dependence;

    public DependsOnPropertyAttribute(string otherProperty)
    {
        Dependence = otherProperty;
    }
}

而在我的基本视图模型中(所有其他 WPF 视图模型都继承自此):

public abstract class BaseViewModel : INotifyPropertyChanged
{
    protected Dictionary<string, List<string>> DependencyMap;

    protected BaseViewModel()
    {
        DependencyMap = new Dictionary<string, List<string>>();

        foreach (var property in GetType().GetProperties())
        {
            var attributes = property.GetCustomAttributes<DependsOnPropertyAttribute>();
            foreach (var dependsAttr in attributes)
            {
                if (dependsAttr == null)
                    continue;

                var dependence = dependsAttr.Dependence;
                if (!DependencyMap.ContainsKey(dependence))
                    DependencyMap.Add(dependence, new List<string>());
                DependencyMap[dependence].Add(property.Name);
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler == null)
            return;

        handler(this, new PropertyChangedEventArgs(propertyName));

        if (!DependencyMap.ContainsKey(propertyName))
            return;

        foreach (var dependentProperty in DependencyMap[propertyName])
        {
            handler(this, new PropertyChangedEventArgs(dependentProperty));
        }
    }
}

现在我可以轻松地标记属性,就像这样:

public int NormalProperty
{
    get {return _model.modelProperty; }
    set 
    {
        _model.modelProperty = value;
        OnPropertyChanged();
    }
}

[DependsOnProperty(nameof(NormalProperty))]
public int CalculatedProperty
{
    get { return _model.modelProperty + 1; }
}

如果在A类中有一个名为AnyState的属性,它基本上是从另外两个类中的各自一个属性获取而来。B类和C类都有一个名为IsRunning的属性。prop A get { return B.IsRunning || C.IsRunning;} - Luishg
@Luishg 如果这些属性在不同的类中,您将需要一个不同的解决方案。这个属性只用于单个类的计算属性。 - Mage Xy

11

表明 bar 已经发生变化的一种方法是在 foo 的 setter 方法中添加一个调用 onPropertyChanged(this, "bar")。我知道这看起来很丑陋,但这就是实现方式。

如果 foo 是在祖先类中定义的,或者你没有访问该 setter 方法的实现,则可以订阅 PropertyChanged 事件,以便当你看到 "foo" 更改时,也可以触发 "bar" 的更改通知。在自己的对象实例上订阅事件同样不太美观,但可完成任务。


3
我不明白为什么你认为在onPropertyChanged(this, "bar")上触发事件是特别丑陋的?如果.bar依赖于3个属性并且你在这三个setter中都调用了它,那我可以理解...但是然后他可以将计算从.bar的属性getter中移出,并放入一个调用更新方法的计算方法中。 - Goblin
10
由于您在属性A的设置器中放置了用于属性B正确行为所需的逻辑,因此代码看起来很丑陋。可能有许多属性依赖于属性A的值。必须修改属性A以满足使用属性A的其他属性的需求,这种做法不够明显且不易扩展。 - dthorpe
1
一个更加简洁的解决方案是有一种方式来指示B依赖于A,当A发生变化时,B也应该发出值改变的信号。上面的第二个选项,监听自己的PropertyChanged事件,就是这种风格,但是监听自己的事件也有点奇怪。 - dthorpe
很有可能。我的背景是构建API和应用程序框架,每个人都可以看到代码。在应用程序中,我们可以做很多在框架或API中不允许的事情。;> - dthorpe
1
我认为foo不应该负责引发barPropertyChanged事件。虽然这样做可以实现,但如果这些属性在另一个类中怎么办?那么你就必须用另一种方式来实现它。我已经在另一个问题中回答了这个问题,在同一个类或另一个类中的属性都是相同的解决方案。https://dev59.com/i6Hia4cB1Zd3GeqPQkPb - Jogge
显示剩余2条评论

8

如果这是一个严重的问题(所谓的“严重”,是指您有相当数量的依赖只读属性),您可以创建一个属性依赖映射,例如:

private static Dictionary<string, string[]> _DependencyMap = 
    new Dictionary<string, string[]>
{
   {"Foo", new[] { "Bar", "Baz" } },
};

然后在OnPropertyChanged中引用它:

PropertyChanged(this, new PropertyChangedEventArgs(propertyName))
if (_DependencyMap.ContainsKey(propertyName))
{
   foreach (string p in _DependencyMap[propertyName])
   {
      PropertyChanged(this, new PropertyChangedEventArgs(p))
   }
}

这与在 Foo setter 中多次调用 OnPropertyChanged 并没有本质区别,因为您必须为每个新的依赖属性更新依赖关系映射。

但是这使得随后实现 PropertyChangeDependsOnAttribute 并使用反射扫描类型并构建依赖关系映射成为可能。 这样,您的属性将如下所示:

[PropertyChangeDependsOn("Foo")]
public int Bar { get { return Foo * Foo; } }

你能展示一下你实现的 PropertyChangeDependsOn 系统吗? - Shimmy Weitzhandler
唉,我只是断言它是可行的。实际上,我并不需要去实现它。 - Robert Rossney
谢谢。我自己实现了它,但是在避免使用反射重新加载关系方面遇到了一些困难。 - Shimmy Weitzhandler
可能需要对此进行递归处理,以便从另一个计算属性等计算出的属性得到正确的通知。-- 是否已经有开源库可以实现这一点? - BrainSlugs83

7
您可以简单地调用:
OnPropertyChanged(this, "bar");

从这个类的任何地方都可以……你甚至可以像这样做:

    public raz()
    {
        this.PropertyChanged += new PropertyChangedEventHandler(raz_PropertyChanged);
    }

    void raz_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if(e.PropertyName == "foo")
        {
             onPropertyChanged(this, "bar");
        }
    }

1
当我需要重复一堆属性时,这似乎是一个更好的可扩展解决方案。 - tbischel
1
订阅PropertyChanged事件并触发bar更改通知正是PRISM在其Commanding Quickstart中所做的。他们的示例更好- 如果(propertyName ==“Price”|| propertyName ==“Quantity”|| propertyName ==“Shipping”) { this.NotifyPropertyChanged(“Total”); } - RichardOD

3

如果您仅将bar用于UI目的,则完全可以从模型中删除它。 您可以将UI元素绑定到foo属性,并使用自定义值转换器将结果从foo更改为foo * foo。

在WPF中,通常有很多实现相同功能的方法。 往往没有正确的方法,只有个人偏好。


在我的情况下,用户需要看到它,程序也需要访问它,但这听起来很有趣。 - tbischel

0
我相信这一定可以以声明式的方式实现,但是我第一次尝试解决这个问题失败了。我想要实现的解决方案是使用Lambda表达式来定义它。由于表达式可以被解析,所以应该可以解析这些表达式并将其附加到所有NotifyPropertyChanged事件上,以便在依赖数据发生更改时得到通知。
在ContinousLinq中,这对集合非常有效。
private SelfUpdatingExpression<int> m_fooExp = new SelfUpdatingExpression<int>(this, ()=> Foo * Foo);

public int Foo
{
    get
    { 
        return  m_fooExp.Value;   
    }
}

但不幸的是,我在表达式和Linq的基础知识方面有所欠缺 :(


WPF能否直接绑定自更新表达式?如果可以,为什么不直接返回该类型而不是计算出的int类型呢? - BrainSlugs83

0
根据计算的开销和预期使用频率,将其作为私有设置属性并在设置foo时计算值可能是有益的,而不是在调用bar get时即时计算。这基本上是一种缓存解决方案,然后您可以将属性更改通知作为bar私有setter的一部分进行。我通常更喜欢这种方法,主要是因为我使用AOP(通过Postsharp)来实现实际的INotifyPropertyChanged样板文件。
-Dan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接