【问题标题】:Should MVVM events be routed?是否应该路由 MVVM 事件?
【发布时间】:2010-12-16 18:29:56
【问题描述】:

如果我有视图模型实例的层次结构,我应该路由事件吗?

例如,假设我们有

class A: INotifyPropertyChanged
{
    public B Child ...
}

class B
{
    A _parent

    void OnPropertyChanged (string propertyName)
    {
        if (PropertyChanged != null) PropertyChanged (this, propertyName);
        ///Should I call _parent.OnPropertyChanged (this, propertyName);?////
     }
}

B 是否应该在A 中调用NotifyPropertyChanged

路由的论点是它可以非常方便。特别是,如果 A 拥有 B 的集合而不是一个孩子,那么了解 A 的任何孩子的任何变化都变得非常困难。此外,还有一个发送者第一个参数,为什么不使用它...... 反对的论点是父事件可能变得拥挤。

有什么意见吗?

【问题讨论】:

    标签: c# .net wpf events mvvm


    【解决方案1】:

    如果您的前端绑定实际上是绑定到子对象,例如:

    {Binding B.PropertyName}
    

    ,那么就没有必要像那样把事件冒泡了。如果您的父 ViewModel 确实需要更改其他属性或在该属性更改时对子视图做一些工作,那么这可能是个好主意。

    【讨论】:

    • 如果 A 只有一个孩子,我同意你的看法。但是如果 A 有很多孩子呢?
    • 你指的是A有一个“B”孩子集合的场景吗?或者 A 有其他子模型类型,例如 B、C 和 D?
    • 两者。如果开发人员想知道“A 或它的一个孩子是否发生了什么事”怎么办?我知道可以收听集合的所有事件;也可以听B、C、D,但是比较麻烦。
    【解决方案2】:

    如果您让子对象为其父对象执行属性更改通知,则您将子对象与父对象紧密耦合,并使子对象参与父对象的实现细节。考虑一下:现在,每当您在父类中实现以某种方式依赖于子类状态的新属性时,您都必须修改子类以支持它。

    做到这一点的松散耦合方式(或者,我认为是正确的方式)是让对象不知道彼此的内部细节。让父级监听子级引发的属性更改通知事件,并让其设置其属性并相应地引发其属性更改事件。

    【讨论】:

      【解决方案3】:

      我会反转事件路由。您可以将 A 类(父级)附加到其 B 属性的 PropertyChanged 事件,这样无论何时 B 类引发 PropertyChanged 事件,A 类都会收到 B 发生更改的通知,然后可以引发其 PropertyChanged 事件。

      您还可以利用一个监控类来处理对委托的属性更改的提升。这是一个简单的示例(不适用于生产代码)。

      假设我们有一个 Person 类,它公开了一个 Name 属性,该属性描述了这个人的全名(姓氏、名字和中间名)。每当修改 Name 的子属性之一时,我们希望引发 Name 属性的 PropertyChanged 事件。

      全名类:

      public class FullName : INotifyPropertyChanged, IEquatable<FullName>
      {
          //=======================================================================================================
          //  Constructors
          //=======================================================================================================
          #region FullName()
          public FullName()
          {
      
          }
          #endregion
      
          //=======================================================================================================
          //  Public Properties
          //=======================================================================================================
          #region FirstName
          public string FirstName
          {
              get
              {
                  return _firstName;
              }
      
              set
              {
                  if (!String.Equals(_firstName, value, StringComparison.Ordinal))
                  {
                      _firstName = !String.IsNullOrEmpty(value) ? value.Trim() : String.Empty;
                      this.OnPropertyChanged("FirstName");
                  }
              }
          }
          private string _firstName = String.Empty;
          #endregion
      
          #region LastName
          public string LastName
          {
              get
              {
                  return _lastName;
              }
      
              set
              {
                  if (!String.Equals(_lastName, value, StringComparison.Ordinal))
                  {
                      _lastName = !String.IsNullOrEmpty(value) ? value.Trim() : String.Empty;
                      this.OnPropertyChanged("LastName");
                  }
              }
          }
          private string _lastName = String.Empty;
          #endregion
      
          #region MiddleName
          public string MiddleName
          {
              get
              {
                  return _middleName;
              }
      
              set
              {
                  if (!String.Equals(_middleName, value, StringComparison.Ordinal))
                  {
                      _middleName = !String.IsNullOrEmpty(value) ? value.Trim() : String.Empty;
                      this.OnPropertyChanged("MiddleName");
                  }
              }
          }
          private string _middleName = String.Empty;
          #endregion
      
          //=======================================================================================================
          //  Public Methods
          //=======================================================================================================
          #region Equals(FullName first, FullName second)
          /// <summary>
          /// Determines whether two specified <see cref="FullName"/> objects have the same value.
          /// </summary>
          /// <param name="first">The first role to compare, or <see langword="null"/>.</param>
          /// <param name="second">The second role to compare, or <see langword="null"/>.</param>
          /// <returns>
          /// <see langword="true"/> if the value of <paramref name="first"/> object is the same as the value of <paramref name="second"/> object; otherwise, <see langword="false"/>.
          /// </returns>
          public static bool Equals(FullName first, FullName second)
          {
              if (first == null && second != null)
              {
                  return false;
              }
              else if (first != null && second == null)
              {
                  return false;
              }
              else if (first == null && second == null)
              {
                  return true;
              }
              else
              {
                  return first.Equals(second);
              }
          }
          #endregion
      
          #region ToString()
          /// <summary>
          /// Returns a <see cref="String"/> that represents the current <see cref="FullName"/>.
          /// </summary>
          /// <returns>
          /// A <see cref="String"/> that represents the current <see cref="FullName"/>.
          /// </returns>
          public override string ToString()
          {
              return String.Format(null, "{0}, {1} {2}", this.LastName, this.FirstName, this.MiddleName).Trim();
          }
          #endregion
      
          //=======================================================================================================
          //  IEquatable<FullName> Implementation
          //=======================================================================================================
          #region Equals(FullName other)
          /// <summary>
          /// Indicates whether the current object is equal to another object of the same type.
          /// </summary>
          /// <param name="other">An object to compare with this object.</param>
          /// <returns><see langword="true"/> if the current object is equal to the other parameter; otherwise, <see langword="false"/>.</returns>
          public bool Equals(FullName other)
          {
              if (other == null)
              {
                  return false;
              }
      
              if (!String.Equals(this.FirstName, other.FirstName, StringComparison.Ordinal))
              {
                  return false;
              }
              else if (!String.Equals(this.LastName, other.LastName, StringComparison.Ordinal))
              {
                  return false;
              }
              else if (!String.Equals(this.MiddleName, other.MiddleName, StringComparison.Ordinal))
              {
                  return false;
              }
      
              return true;
          }
          #endregion
      
          #region Equals(object obj)
          /// <summary>
          /// Determines whether the specified <see cref="Object"/> is equal to the current <see cref="Object"/>.
          /// </summary>
          /// <param name="obj">The <see cref="Object"/> to compare with the current <see cref="Object"/>.</param>
          /// <returns>
          /// <see langword="true"/> if the specified <see cref="Object"/> is equal to the current <see cref="Object"/>; otherwise, <see langword="false"/>.
          /// </returns>
          public override bool Equals(object obj)
          {
              return this.Equals(obj as FullName);
          }
          #endregion
      
          #region GetHashCode()
          /// <summary>
          /// Returns the hash code for this instance.
          /// </summary>
          /// <returns>A 32-bit signed integer hash code.</returns>
          /// <a href="http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx"/>
          public override int GetHashCode()
          {
              int firstNameHashCode   = this.FirstName.GetHashCode();
              int lastNameHashCode    = this.LastName.GetHashCode();
              int middleNameHashCode  = this.MiddleName.GetHashCode();
      
              /*
                  * The 23 and 37 are arbitrary numbers which are co-prime.
                  * 
                  * The benefit of the below over the XOR (^) method is that if you have a type 
                  * which has two values which are frequently the same, XORing those values 
                  * will always give the same result (0) whereas the above will 
                  * differentiate between them unless you're very unlucky.
              */
              int hashCode    = 23;
              hashCode        = hashCode * 37 + firstNameHashCode;
              hashCode        = hashCode * 37 + lastNameHashCode;
              hashCode        = hashCode * 37 + middleNameHashCode;
      
              return hashCode;
          }
          #endregion
      
          //=======================================================================================================
          //  INotifyPropertyChanged Implementation
          //=======================================================================================================
          #region PropertyChanged
          /// <summary>
          /// Occurs when a property value changes.
          /// </summary>
          /// <remarks>
          /// The <see cref="PropertyChanged"/> event can indicate all properties on the object have changed 
          /// by using either a <b>null</b> reference (Nothing in Visual Basic) or <see cref="String.Empty"/> 
          /// as the property name in the <see cref="PropertyChangedEventArgs"/>.
          /// </remarks>
          public event PropertyChangedEventHandler PropertyChanged;
          #endregion
      
          #region OnPropertyChanged(string propertyName)
          /// <summary>
          /// Raises the <see cref="PropertyChanged"/> event.
          /// </summary>
          /// <param name="propertyName">The name of the property that changed.</param>
          protected void OnPropertyChanged(string propertyName)
          {
              this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
          }
          #endregion
      
          #region OnPropertyChanged(PropertyChangedEventArgs e)
          /// <summary>
          /// Raises the <see cref="PropertyChanged"/> event.
          /// </summary>
          /// <param name="e">A <see cref="PropertyChangedEventArgs"/> that contains the event data.</param>
          protected void OnPropertyChanged(PropertyChangedEventArgs e)
          {
              PropertyChangedEventHandler handler = this.PropertyChanged;
      
              if (handler != null)
              {
                  handler(this, e);
              }
          }
          #endregion
      }
      

      PropertyChangeMonitor 类:

      public class PropertyChangeMonitor
      {
          //=======================================================================================================
          //  Constructors
          //=======================================================================================================
          #region PropertyChangeMonitor()
          public PropertyChangeMonitor()
          {
      
          }
          #endregion
      
          //=======================================================================================================
          //  Protected Properties
          //=======================================================================================================
          #region Sources
          protected ConcurrentDictionary<INotifyPropertyChanged, Action<string>> Sources
          {
              get
              {
                  return _sources;
              }
          }
          private ConcurrentDictionary<INotifyPropertyChanged, Action<string>> _sources = new ConcurrentDictionary<INotifyPropertyChanged,Action<string>>();
          #endregion
      
          //=======================================================================================================
          //  Public Methods
          //=======================================================================================================
          #region Register(INotifyPropertyChanged source, Action<string> target)
          public void Register(INotifyPropertyChanged source, Action<string> target)
          {
              if(source == null || target == null)
              {
                  return;
              }
      
              if(!this.Sources.ContainsKey(source))
              {
                  if (this.Sources.TryAdd(source, target))
                  {
                      source.PropertyChanged += (o, e) =>
                      {
                          target.Invoke(e.PropertyName);
                      };
                  }
              }
          }
          #endregion
      
          #region Unregister(INotifyPropertyChanged source, Action<string> target)
          public void Unregister(INotifyPropertyChanged source, Action<string> target)
          {
              if (source == null || target == null)
              {
                  return;
              }
      
              if (this.Sources.ContainsKey(source))
              {
                  if (this.Sources.TryRemove(source, out target))
                  {
                      source.PropertyChanged -= (o, e) =>
                      {
                          target.Invoke(e.PropertyName);
                      };
                  }
              }
          }
          #endregion
      }
      

      人物类:

      public class Person : INotifyPropertyChanged
      {
          //=======================================================================================================
          //  Constructors
          //=======================================================================================================
          #region Person()
          public Person()
          {
              this.ChangeMonitor.Register(this.Name, OnPropertyChanged);
          }
          #endregion
      
          //=======================================================================================================
          //  Protected Properties
          //=======================================================================================================
          #region ChangeMonitor
          protected PropertyChangeMonitor ChangeMonitor
          {
              get
              {
                  return _monitor;
              }
          }
          private PropertyChangeMonitor _monitor = new PropertyChangeMonitor();
          #endregion
      
          //=======================================================================================================
          //  Public Properties
          //=======================================================================================================
          #region Name
          public FullName Name
          {
              get
              {
                  return _personName;
              }
      
              set
              {
                  if (!FullName.Equals(_personName, value))
                  {
                      _personName = value;
                      this.OnPropertyChanged("Name");
                  }
              }
          }
          private FullName _personName = new FullName();
          #endregion
      
          //=======================================================================================================
          //  INotifyPropertyChanged Implementation
          //=======================================================================================================
          #region PropertyChanged
          /// <summary>
          /// Occurs when a property value changes.
          /// </summary>
          /// <remarks>
          /// The <see cref="PropertyChanged"/> event can indicate all properties on the object have changed 
          /// by using either a <b>null</b> reference (Nothing in Visual Basic) or <see cref="String.Empty"/> 
          /// as the property name in the <see cref="PropertyChangedEventArgs"/>.
          /// </remarks>
          public event PropertyChangedEventHandler PropertyChanged;
          #endregion
      
          #region OnPropertyChanged(string propertyName)
          /// <summary>
          /// Raises the <see cref="PropertyChanged"/> event.
          /// </summary>
          /// <param name="propertyName">The name of the property that changed.</param>
          protected void OnPropertyChanged(string propertyName)
          {
              this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
          }
          #endregion
      
          #region OnPropertyChanged(PropertyChangedEventArgs e)
          /// <summary>
          /// Raises the <see cref="PropertyChanged"/> event.
          /// </summary>
          /// <param name="e">A <see cref="PropertyChangedEventArgs"/> that contains the event data.</param>
          protected void OnPropertyChanged(PropertyChangedEventArgs e)
          {
              PropertyChangedEventHandler handler = this.PropertyChanged;
      
              if (handler != null)
              {
                  handler(this, e);
              }
          }
          #endregion
      }
      

      请注意,在 Person 的构造函数中,我们向监视器注册了 Name 属性,并指示我们希望在被监视的源引发 PropertyChanged 事件以进行更改时执行的委托方法。如果您希望监控多个属性并为其引发 PropertyChanged 事件,则添加一行代码来处理事件通知的连接会变得很简单。

      每次在 Name 属性设置器中更改 Name 属性时,可能应该修改此实现以向监视器注册和取消注册,但我认为这为您提供了一个要点。

      【讨论】:

      • 这是正确的。但是如果 A 有很多孩子呢?它仍然是可取的吗?
      • 您可以聚合对一个或多个属性的监控,最终结果会为每个被监控的属性提高 PropertyChanged 以进行更改。根据您的描述,我相信这是一个合理的策略基础。
      • 查看我添加到原始答案中的代码示例,希望这能解决您的疑虑和问题。
      猜你喜欢
      • 1970-01-01
      • 2020-04-05
      • 1970-01-01
      • 2016-06-30
      • 2020-05-30
      • 2017-12-06
      • 2012-11-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多