Избегайте вызова RaisePropertyChanged в каждом сеттере


Я хочу избавиться от занимающих много места и повторяющихся RaisePropertyChanged-свойств в моих классах моделей. Мне нужен мой класс моделей...

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; }
}

Может ли это быть достигнуто с помощью какого-то прокси-класса?

Я хочу избежать написания прокси для каждого отдельного класса модели.

6 3

6 ответов:

Я не знаю простого и поддерживаемого подхода к этому в "vanilla" C#, но вы можете достичь этого с помощью аспекты. Я использовал PostSharp для этого, который имеет недостаток в том, чтобы быть платным продуктом 3-й партии, но имеет бесплатную версию, где вы можете сделать это также. PostSharp использует преимущества атрибуты например, определение цели, наследование и т. д. и распространяет их на аспекты.

Затем можно определить LocationInterceptionAspect, который переопределяет метод OnSetValue для вызова делегата RaisePropertyChanged. Затем вы можете использовать автоматически созданные свойства, украшенные вашим атрибутом аспекта.

Платная версия PostSharp позволяет сделать это на уровне класса, поэтому вам понадобится только один атрибут (или ни одного, если вы украсили свой базовый класс и определили атрибут как наследуемый). Это и есть описал на сайте PostSharp в качестве примера использования InstanceLevelAspect

Я шел по 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), но все равно не нужно засорять классы всем этим. Он вызывает уведомления для свойств только для чтения, которые зависят от свойства.

Конечно, это немного фальшиво, так как это просто автоматизирует написание и ничего не меняет в лежащей в основе концепции вообще. Но, возможно, это компромисс...

Вот дополнительная информация: Ссылка

Мы можем избежать повторяющегося кода записи RaisePropertyChanged на каждом свойстве setter в WPF.

Используйте бесплатную версию 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);                    
        }
    }
}
}

Это уже старый материал, просто никто не упоминал:

Https://kindofmagic.codeplex.com/

Вы можете включить автоматическое уведомление для каждого свойства в вашем ViewModel с атрибутом 1 в классе.

Я нашел этот класс в пространстве имен System.Dynamic... Он позволяет перехватывать фактические вызовы DataBinding, выполняемые DependencyObject на вашей цели привязки, к Property на вашем источнике привязки.

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 и переопределяет оба метода TryGetMember и TrySetMember.

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);

В Поле Зрения.xaml:

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

В итоге получается вот что:

Также проверьте эту статью в code project, которая обеспечивает еще больше информация...

Приходя с другой стороны (и если у вас нет модного расширения), вы можете "автоспецифицировать" измененные свойства с помощью методов расширения, описанных в моем ответе здесь: прокси-сервер службы WCF не устанавливает свойство "FieldSpecified"

В частности, вы можете использовать " метод без отражения "для настройки определенной обработки" 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 синтаксического анализа?