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_");
    }
}

2016. augusztus 18., csütörtök

JSON-ból osztály

Gyakorta előfordul, hogy egy .NET-ben írt alkalmazásban JSON vagy XML formátumú üzeneteket szeretnénk feldolgozni, amit például egy külső rendszertől kapunk. Ehhez viszont fel kell dolgozni az üzenetet és átültetni egy vagy több, pontosan illeszkedő .NET-es osztályszerkezetre. Többféle módon is elő lehet állítani az ehhez szükséges az osztályokat. A teljes igénye nélkül hármat említek:

1. Manuálisan létrehozva
Önmagáért beszél vagyis kézzel létrehozzuk a szükséges szerkezetet. Ebben az esetben probléma, hogy a fejlesztőnek teljesen meg kell értenie a JSON / XML üzenetet és azt követően előállítania a megfelelő szerkezetet. Bonyolult felépítésnél komoly fejtörést okozhat és időigényessé válhat. 

2. Valamilyen külső eszközzel legenerálva
Erre egy jó példa a http://jsonutils.com oldal, ahol megadva a JSON üzenet csak ki kell választani, hogy milyen nyelvre kérnénk és máris automatikusan előállítja az osztályokat. Előnye, hogy egyszerűen használható és igény szerint DataContract/DataMember attribútumokat is bele tud generálni. Kényelmetlen lehet, hogy a generáláshoz el kell hagyni az IDE-t, még akkor is, hogy ha csak egy weboldal erejéig.

3. Visual Studio-val generálva
Ha már .NET platformra fejlesztünk, akkor szerintem eléggé adja magát, hogy Visual Studio legyen a fejlesztőkörnyezetünk, ami alapjáraton is képes arra, hogy automatikusan legenerálja az osztályokat nekünk. Ehhez nem kell mást tenni, minthogy a vágólapra helyezzük a JSON vagy XML üzenetet, majd egy .cs fájlt megnyitva az Edit / Paste Special menüponton belül kiválasztjuk a Paste JSON As Classes vagy Paste XML As Classes opciót. Előnye, hogy kényelmes, nem kell külső alkalmazáshoz folyamodni. Hátránya, hogy nem tud automatán feltenni attribútumokat.