【问题标题】:Avoid calling RaisePropertyChanged in every setter避免在每个 setter 中调用 RaisePropertyChanged
【发布时间】:2012-11-19 18:22:55
【问题描述】:

我想摆脱模型类中占用空间和重复的 RaisePropertyChanged-Properties。我想要我的模型课...

public class ProductWorkItem : NotificationObject
{
    private string name;
    public string Name
    {
        get { return name; }
        set { 
            if (value == name) return; 
            name = value; RaisePropertyChanged(() => Name); 
        }
    }
    private string description;
    public string Description
    {
        get { return description; }
        set { 
            if (value == description) return; 
            description = value; RaisePropertyChanged(() => Description); 
        }
    }
    private string brand;
    public string Brand
    {
        get { return brand; }
        set { 
            if (value == brand) return; 
            brand = value; RaisePropertyChanged(() => Brand); 
        }
    }
}

...再次看起来像这样简单:(但在属性更改时通知视图)

public class ProductWorkItem
{
    public string Name{ get; set; }
    public string Description{ get; set; }
    public string Brand{ get; set; }
}

这可以通过某种代理类来实现吗?

我想避免为每个模型类编写代理。

【问题讨论】:

  • NotificationObjectPrism 4.0 框架的一部分

标签: c# mvvm inotifypropertychanged proxy-classes


【解决方案1】:

这已经是老东西了,只是没人提:

https://marketplace.visualstudio.com/items?itemName=AlexeyLavnikov.KindOfMagic

您可以使用类上的 1 个属性为 ViewModel 中的每个属性打开自动通知。

【讨论】:

  • @JRlawhorne 谢谢,我已经替换了链接。
【解决方案2】:

我使用了 NotifyPropertyWeaver 扩展,从那时起就定期使用它。它是一个 Visual Studio 扩展,在编译代码之前,它为您实现了始终相同的 INPC 内容。你什么都没有注意到。

您需要安装扩展,然后您的模型需要如下所示:

public class ProductWorkItem : INotifyPropertyChanged
{
    public string Name{ get; set; }
    public string Description{ get; set; }
    public string Brand{ get; set; }

    public event PropertyChangedEventHandler PropertyChanged;
}

该扩展程序会为您添加所有其余部分。我喜欢这种方法的地方在于,您的类仍然“正式”实现 INPC 接口,并且您也可以在非 WPF 上下文中使用它(因为 INPC 根本不只是 WPF 的东西),但仍然没有用所有这些东西在你的课堂上乱扔垃圾。它会针对依赖于属性的只读属性发出通知。

当然,这有点假,因为它只是自动化了写作,根本没有改变基本概念的任何内容。但也许这是一种妥协……

这里有更多信息:Link

【讨论】:

  • 您实际上不需要安装 VSIX 即可使用它。它只是帮助您配置 MSBuild 任务。配置后,它将在其他没有 VSIX 的机器上工作
【解决方案3】:

来自另一边(如果您没有花哨的扩展),您可以使用我在此处的回答中概述的扩展方法“自动指定”更改的属性: WCF service proxy not setting "FieldSpecified" property

具体来说,您可以使用“非反射方法”为每个类配置特定的“OnPropertyChanged”处理,以执行其他操作,而不仅仅是设置指定的字段。

public static class PropertySpecifiedExtensions2
{
    /// <summary>
    /// Bind the <see cref="INotifyPropertyChanged.PropertyChanged"/> handler to automatically call each class's <see cref="IAutoNotifyPropertyChanged.Autonotify"/> method on the property name.
    /// </summary>
    /// <param name="entity">the entity to bind the autospecify event to</param>
    public static void Autonotify(this IAutoNotifyPropertyChanged entity)
    {
        entity.PropertyChanged += (me, e) => ((IAutoNotifyPropertyChanged)me).WhenPropertyChanges(e.PropertyName);
    }

    /// <summary>
    /// Create a new entity and <see cref="Autonotify"/> it's properties when changed
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T Create<T>() where T : IAutoNotifyPropertyChanged, new()
    {
        var ret = new T();
        ret.Autonotify();
        return ret;
    }
}

/// <summary>
/// Used by <see cref="PropertySpecifiedExtensions.Autonotify"/> to standardize implementation behavior
/// </summary>
public interface IAutoNotifyPropertyChanged : INotifyPropertyChanged
{
    void WhenPropertyChanges(string propertyName);
}

然后每个类自己定义行为:

public partial class MyRandomClass: IAutoNotifyPropertyChanged
{

    /// <summary>
    /// Create a new empty instance and <see cref="PropertySpecifiedExtensions.Autospecify"/> its properties when changed
    /// </summary>
    /// <returns></returns>
    public static MyRandomClass Create()
    {
        return PropertySpecifiedExtensions2.Create<MyRandomClass>();
    }

    public void WhenPropertyChanges(string propertyName)
    {
        switch (propertyName)
        {
            case "field1": this.field1Specified = true; return;
            // etc
        }

        // etc
        if(propertyName.StartsWith(...)) { /* do other stuff */ }
    }
}

当然,这样做的缺点是属性名称的魔术字符串使重构变得困难,您可以通过 Expression 解析来解决?

【讨论】:

    【解决方案4】:

    我们可以避免在 WPF 中的每个属性设置器上编写 RaisePropertyChanged 的​​重复代码。

    使用免费版本的 Postsharp。

    通过使用下面的代码,我们可以只绑定虚拟属性来查看。

    namespace Test
    {
    [Serializable]
    [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = true)]
    public sealed class RaisePropertyChangedAttribute : MethodInterceptionAspect
    {
        private string propertyName;
    
        /// <summary>
        /// Compiles the time validate.
        /// </summary>
        /// <param name="method">The method.</param>
        public override bool CompileTimeValidate(MethodBase method)
        {
            return IsPropertySetter(method) && !method.IsAbstract && IsVirtualProperty(method);
        }
    
        /// <summary>
        /// Method invoked at build time to initialize the instance fields of the current aspect. This method is invoked
        /// before any other build-time method.
        /// </summary>
        /// <param name="method">Method to which the current aspect is applied</param>
        /// <param name="aspectInfo">Reserved for future usage.</param>
        public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
        {
            base.CompileTimeInitialize(method, aspectInfo);
            propertyName = GetPropertyName(method);
        }
    
        /// <summary>
        /// Determines whether [is virtual property] [the specified method].
        /// </summary>
        /// <param name="method">The method.</param>
        /// <returns>
        ///   <c>true</c> if [is virtual property] [the specified method]; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsVirtualProperty(MethodBase method)
        {
            if (method.IsVirtual)
            {
                return true;
            }
    
            var getMethodName = method.Name.Replace("set_", "get_");
            var getMethod = method.DeclaringType.GetMethod(getMethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
    
            return getMethod != null && getMethod.IsVirtual;
        }
    
        private static string GetPropertyName(MethodBase method)
        {
            return method.Name.Replace("set_", string.Empty);
        }
    
        /// <summary>
        /// Determines whether [is property setter] [the specified method].
        /// </summary>
        /// <param name="method">The method.</param>
        /// <returns>
        /// <c>true</c> if [is property setter] [the specified method]; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsPropertySetter(MethodBase method)
        {
            return method.Name.StartsWith("set_", StringComparison.OrdinalIgnoreCase);
        }
    
        /// <summary>
        /// Method invoked <i>instead</i> of the method to which the aspect has been applied.
        /// </summary>
        /// <param name="args">Advice arguments.</param>
        public override void OnInvoke(MethodInterceptionArgs args)
        {
            var arg = args as MethodInterceptionArgsImpl;
    
            if ((arg != null) && (arg.TypedBinding == null))
            {
                return;
            }
    
            // Note ViewModelBase is base class for ViewModel
            var target = args.Instance as ViewModelBase;
    
            args.Proceed();
    
            if (target != null)
            {
                target.OnPropertyChanged(propertyName);                    
            }
        }
    }
    }
    

    【讨论】:

      【解决方案5】:

      我在System.Dynamic 命名空间中找到了this class...它可以让您拦截绑定目标上的DependencyObject 对绑定源上的Property 进行的实际DataBinding 调用。

      http://i.msdn.microsoft.com/en-us/library/system.windows.data.binding.DataBindingMostBasic(v=vs.110).png?appId=Dev11IDEF1&l=EN-US&k=k(System.Windows.Data.Binding)%3bk(VS.XamlEditor)%3bk(TargetFrameworkMoniker-.NETFramework

      所以现在可以做的是实现一个类(我们称之为DynamicNpcProxy),它实现INotifyPropertyChanged,派生自DynamicObject,并覆盖TryGetMemberTrySetMember方法。

      public class DynamicNpcProxy : DynamicObject, INotifyPropertyChanged
      {
          public DynamicNpcProxy(object proxiedObject)
          {
              ProxiedObject = proxiedObject;
          }
      
          //...
      
          public object ProxiedObject { get; set; }
      
          public override bool TrySetMember(SetMemberBinder binder, object value)
          {
              SetMember(binder.Name, value);
              return true;
          }
      
          protected virtual void SetMember(string propertyName, object value)
          {
              GetPropertyInfo(propertyName).SetValue(ProxiedObject, value, null);
              if (PropertyChanged != null) 
                  PropertyChanged(ProxiedObject, new PropertyChangedEventArgs(propertyName));
          }
      
          protected PropertyInfo GetPropertyInfo(string propertyName)
          {
              return ProxiedObject.GetType().GetProperty(propertyName);
          }
      
          // override bool TryGetMember(...)
      }
      

      要让它工作,将代理包裹在你当前的绑定源周围,替换它们,然后让DynamicObject 完成剩下的工作。

      在 ViewModel.cs 中:

      IList<ProductWorkItem> items;
      //... assign items
      var proxies = items.Select(p => new DynamicNpcProxy(p)).ToList();
      ICollectionView Products = CollectionViewSource.GetDefaultView(proxies);
      

      在 View.xaml 中:

      <TextBox Text="{Binding Products.CurrentItem.Name}" /> 
      <TextBox Text="{Binding Products.CurrentItem.Description}" /> 
      

      你最终得到的是这样的:

      还可以在code project 上查看this article,它提供了更多信息...

      【讨论】:

      • 如果模型上的属性仅通过代理(在本例中为 GUI)更改,则此方法有效。 “在幕后”调用的更改不会引发通知。这是否是一个解决方案取决于实际情况。
      【解决方案6】:

      我知道在“vanilla”C# 中没有简单且可维护的方法,但您可以通过 aspects 实现此目的。我为此使用了 PostSharp,它的缺点是作为付费的 3rd 方产品,但有一个免费版本,您也可以在其中执行此操作。 PostSharp 利用 attributes 的优势,如目标指定、继承等,并将它们扩展到方面。

      然后您可以定义一个LocationInterceptionAspect,它会覆盖OnSetValue 方法来调用您的RaisePropertyChanged 委托。然后你可以使用用你的 aspect 属性装饰的自动生成的属性。

      PostSharp 的付费版本允许您在类级别执行此操作,因此您只需要一个属性(或者不需要,如果您装饰您的基类并将该属性定义为可继承)。这是 PostSharp 网站上的 described,作为 InstanceLevelAspect

      的用例

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2023-04-02
        • 2023-03-22
        • 2023-03-22
        • 2011-05-31
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多