2016. augusztus 31., szerda

RaisePropertyChanged attribútum property-re PostSharppal

MVVM patternt használva WPF fejlesztés során elkerülhetetlen, hogy implementáljuk az INotifyPropertyChanged interfészt. Amikor egy olyan tulajdonság változik a ViewModelben, amire a felületen valami rá van kötve, akkor egy PropertyChanged eseményt kell dobni, hogy értesüljön a UI is. Ehhez tartozóan egy property tipikusan így szokott kinézni:

public class MyViewModel : ViewModelBase
{
    private string demoProp;

    public string DemoProp
    {
        get { return demoProp; }
        set
        {
            if (demoProp == value) return;
            demoProp = value;
            OnPropertyChanged("DemoProp");
        }
    }
}

Ebben a példában a ViewModelBase ősosztály implementálja az interfészt:

public class ViewModelBase : INotifyPropertyChanged
{
    private event PropertyChangedEventHandler propertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        if (!string.IsNullOrWhiteSpace(propertyName) &&
           propertyChanged != null)
 {
       propertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }
    }
}



PostSharp nagy mértékben meg tudja könnyíteni az életet úgy, hogy átláthatóbb, karbantarthatóbb kódot eredményez az aspektus orientáltság révén. A fenti DemoProp kódját többféle módon is képes egyszerűsíteni, ebből két lehetséges:

1. Osztályszintű attribútum és nincs manuális kódolás
Ekkor elegendő egy attribútumot felhelyezni az osztályra és fordításkor minden publikus autoproperty megváltoztatásáról ugyanúgy értesülni fog a rendszer, mintha saját magunk implementáltuk volna az INotifyPropertyChanged interfészt, illetve kötöttük volna be a property-k set ágába az esemény kiváltását. Ennek részletes működéséről itt lehet olvasni.

Ennek a módszernek egyszerre előnye és hátránya is, hogy csak osztályra lehet feltenni ezt az attribútumot. Ebből adódóan nincs lehetőség arra, hogy ki lehessen választani, hogy melyik property-t figyelje és melyiket ne. 

2. Saját készítésű attribútum property-re
Előfordulnak olyan esetek, amikor nem jó az 1. módszer, mivel nem akarjuk az összes property-re ráakasztani. Például ha saját magunk már implementáltuk az INotifyPropertyChanged interfészt, viszont vannak olyan property-k, amikhez nem tartozik extra logika  az érték megváltozásához a felület értesítését leszámítva. Emiatt felesleges is lenne kézzel megcsinálni hozzá a backfieldet és a get/set metódusokat. Egyszerűbb lenne, ha csak egy attribútumot kéne feltenni egy autoproperty-re.

A fenti MyViewModel átírva így nézne ki. A különbség szerintem önmagáért beszél:

public class MyViewModel : ViewModelBase
{
    [RaisePropertyChanged]
    public string DemoProp get; set}
}


A példában használt RaisePropertyChanged PostSharp attribútum pedig alább látható:

using System;
using System.Reflection;
using PostSharp.Aspects;

[AttributeUsage(AttributeTargets.Property)]
[Serializable]
public class RaisePropertyChangedAttribute : OnMethodBoundaryAspect
{
    /// <summary>
    /// Property neve
    /// </summary>
    private string propertyName;
    /// <summary>
    /// történt-e változás, kell-e értesítés
    /// </summary>
    private bool hasChanged;

    /// <summary>
    /// fordítási időben lekérdezett mezőinformáció
    /// </summary>
    private PropertyInfo propertyInfo;

    public override void OnEntry(MethodExecutionArgs args)
    {
        //A CompileTimeValidate biztosítja, hogy ez a kódrészlet
        //csak ViewModelBase-ben lévő property-khez tartozó Sethez legyen hozzáfűzve

        //mivel ez egy Set metódusba való belépés elején fut, 
        //így biztosan van egy paramétere (value)

        object value = propertyInfo.GetValue(args.Instance);
        object param = args.Arguments[0];

        hasChanged = (value != null && param == null) ||
                        (value == null && param != null) ||
                        (value != null && !value.Equals(param));  

        if (!hasChanged)
        {
            args.FlowBehavior = FlowBehavior.Return;
            return;
        }

        base.OnEntry(args);
           
    }

    public override void OnSuccess(MethodExecutionArgs args)
    {
        //siker esetén kell csak dobni az értesítést, 
        //feltéve, hogy történt egyáltalán változás

        if (!hasChanged) return;

        ((ViewModelBase) args.Instance).ReportPropertyChanged(propertyName);
    }

    public override bool CompileTimeValidate(MethodBase method)
    {
        var type = method.ReflectedType;
        if (type == null) return false;

        //ha nem ő maga egy ViewModelBase és ráadásul nem is leszármazottja
        if (type != typeof(ViewModelBase) &&
            !type.IsSubclassOf(typeof(ViewModelBase))) return false;

        //ha nem Set metódus
        if (!IsPropertySetter(method)) return false;
           

        //fordítási időben eltároljuk a property nevét és a leíróját
        propertyName = GetPropertyName(method);
        propertyInfo = type.GetProperty(propertyName);
           
        return true;
    }

    private static string GetPropertyName(MethodBase method)
    {
        return method.Name.Replace("set_", "");
    }

    private static bool IsPropertySetter(MethodBase method)
    {
        return method.Name.StartsWith("set_");
    }
}

Nincsenek megjegyzések:

Megjegyzés küldése