2016. szeptember 11., vasárnap

Nem publikus metódusok/osztályok elérése külön Unit test projektben

Jó ötlet és helyes dolog, ha a unit testek egy (vagy esetleg több) külön projektbe kerülnek, ezzel szeparálva az éles kódtól. Másfelől nézve viszont egy külső projektből hivatkozva alapjáraton csak a publikus dolgokat tudjuk elérni, így könnyen támadhatna az az ötlet (hibásan), hogy fájó szívvel megszegjük elveinket és publikussá tegyünk bizonyos belső adatszerkezeteket, metódusokat, változókat pusztán azért, hogy tesztelhessük azokat. 

A .NET C#-ban a láthatósági szintek között szerepel egy olyan, hogy internal,  ami azt jelenti, hogy csak az adott assembly-n belül lehetséges hivatkozni rá, csak azon belül látható. Amennyiben mégis megpróbálnánk kívülről elérni (például egy szeparált unit test projektből), akkor fordítási idejű hibát dobna, mondván, hogy nem fér hozzá.

Rossz megoldás: mindent publikussá
Egyik lehetséges megoldás erre, hogy ha a kérdéses osztályt, metódust, stb., publikussá tesszük. Ez maximum csak első látszatra tűnhet megoldásnak, mivel hamar előjön, hogy szinte mindent publikussá kéne tennünk, amivel pedig éppen csak az osztály egyik célját, a hozzáférés-szabályozást veszítjük el. Létezik ennél egy hatékonyabb megoldás is.

Helyes megoldás: assembly-k összebarátkoztatása (Friend Assemblies)
Egy barátnak jelölt assembly ugyanúgy hozzá tud férni egy másik assembly-ben lévő internal típusaihoz és egyéb tagjaihoz, mintha azok publikusak lennének. Az InternalsVisibleTo attribútum használatával lehet megjelölni egy vagy akár több assembly-t barátként.

Ezt az attribútumot annak a projektnek az AssemblyInfo.cs fájljába kell tenni, amelyiket szeretnénk elérhetővé tenni:

[assembly: InternalsVisibleTo("UnitTestProject")]

Ennek hatására projektszinten minden, ami internal láthatóságú, elérhetővé válik a barát számára.

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.


2016. június 28., kedd

NULLIF kontra ISNULL valamint a nullával való osztás kezelése

A NULLIF és az ISNULL ránézésre hasonlónak tűnhetnek, viszont meglehetősen eltérő módon viselkednek.


NULLIF

Szintaxis: NULLIF(expression, expression)

Ha a két kifejezés értéke különbözik, akkor visszaadja az első kifejezést. Ha viszont megegyeznek, akkor NULL-t ad vissza. Egyszerűbb SQL utasítás érhető el vele, mintha a CASE-t használnánk.

NULLIF és CASE összehasonlítására példa az Books Online-ból (BOL):
USE AdventureWorks2012; 
GO 
SELECT ProductID, MakeFlag, FinishedGoodsFlag,  
   NULLIF(MakeFlag,FinishedGoodsFlag)AS 'Null if Equal' 
FROM Production.Product 
WHERE ProductID < 10; 
GO 
 
SELECT ProductID, MakeFlag, FinishedGoodsFlag,'Null if Equal' = 
   CASE 
       WHEN MakeFlag = FinishedGoodsFlag THEN NULL 
       ELSE MakeFlag 
   END 
FROM Production.Product 
WHERE ProductID < 10; 
GO 


ISNULL

Szintaxis: ISNULL(check_expression, replacement_value)

Ha a vizsgálandó első paraméter nem NULL, akkor azt adja vissza, ellenkező esetben a helyettesítő értéket. Kiválóan alkalmas olyan esetekben például, amikor valamilyen alapértelmezett értéket szeretnénk használni, ha egyébként NULL lenne. A COALESCE utasítás egy speciális esetének is felfogható, amikor csak két paramétert kapott és abból kell visszaadnia az első nem NULL értéket.

Jó példa az aggregáló műveletekre szintén az MSDN-ről:
USE AdventureWorks2012; 
GO 
SELECT AVG(ISNULL(Weight, 50)) 
FROM Production.Product; 
GO 

Az aggregáló függvényeknél erősen ajánlott végiggondolni, hogy kellene-e használni, mert ezeknél a függvényeknél, ha legalább egy elem NULL, akkor az eredmény is NULL lesz.


Közös példa: nullával való osztás 
A kettő kombinálására egy jó példa a nullával való osztás kezelése. Először a NULLIF segítségével kezeljük, hogy ha nullával osztanánk, akkor ne dobjon hibát, ekkor ugyanis NULL lesz az eredmény.
SELECT @osztando / NULLIF( @oszto, 0 ) AS value

Majd erre hívjuk meg az ISNULL-t, hogy ilyenkor nullát adjon vissza és kész is:
SELECT ISNULL( @osztando / NULLIF( @oszto, 0 ), 0) AS value

Entity Frameworkből UDT paraméterű tárolt eljárás futtatása

Az Entity Framework alaphangon nem támogatja a saját SQL típust, vagyis a User Defined Type-ot. Ha egy olyan tárolt eljárást szeretnénk importálni az EF-fel, ami UDT típusú paramétert vár, akkor ugyan nem fog hibát dobni, de nem is fogja legenerálni a hozzátartozó kódot.

Ennek áthidalására egy jó módszer az EntityFrameworkExtras nevű NuGettel is elérhető csomag, aminek segítségével típusosan lehet ilyen tárolt eljárást futtatni. A bekötéséhez az alábbi néhány lépés szükséges.

1. UDT létrehozása MS SQL Server adatbázisban

CREATE TYPE [dbo].[TEMP_IDTABLE] AS TABLE(
       [ID] [int] NULL
)
GO

2. Ezt a típust paraméterként használó tárolt eljárás létrehozása

CREATE PROCEDURE [dbo].[DummyStoredProcedure]
       @myValues [Temp_IDTABLE] READONLY
AS
BEGIN
       -- értelmes logika
       SELECT * FROM @myValues
END

3. A használt EF verziónak megfelelő EntityFrameworkExtras hozzáadása a projekthez
  • EF 5: EntityFrameworkExtras.EF5
  • EF 6: EntityFrameworkExtras.EF6

4. Létre kell hozni egy osztályt, ami majd az 1. lépésben elkészült UDT-t fogja reprezentálni

[UserDefinedTableType("TEMP_IDTABLE")]
public class TempIdTable
{
    [UserDefinedTableTypeColumn(1, Name = "ID")]
    public int? ID { get; set; }
}

Természetesen az osztály és a mezők neve bármi lehet, mivel az attribútumokkal lesz beállítva, hogy az adatbázisban mire kell majd leképezni.

5. Az előbbihez hasonlóan a tárolt eljáráshoz kell egy osztály

[StoredProcedure("DummyStoredProcedure")]
public class DummyStoredProcedure
{
    [StoredProcedureParameter(SqlDbType.Udt, ParameterName = "myValues")]
    public List<TempIdTable> MyValues { get; set; }
}

6. Ezek után már csak meg kell hívni az eljárást. Ehhez az EntityFrameworkExtras tartalmaz Extended Methodokat, amik a DbObjectre illetve az ObjectContextre akadnak rá, és olyan objektumokat várnak, amik el vannak látva a StoredProcedure attribútummal.

ObjectContext oc = new ObjectContext("ConnectionString");

var sp = new DummyStoredProcedure
{
    MyValues = new List<TempIdTable>
    {
        new TempIdTable { ID = 1 },
        new TempIdTable { ID = 2 }
    }
};

IEnumerable<int> results = oc.ExecuteStoredProcedure<int>(sp);

2016. június 5., vasárnap

SSMS IntelliSense Cache frissítése

Időnként előfordul, hogy miután létrehoztam, módosítottam esetleg töröltem valamilyen objektumot, az SSMS hibát jelez olyan SQL utasításokban, amik az érintett objektumra hivatkoznak. Annak ellenére, hogy az SQL script sikeresen lefutna, eléggé zavaró, amikor bemutatásnál vagy megbeszélésen piros hibajelzések tarkítják a kódot. Ezt az okozza, hogy az SSMS-ben lévő IntelliSense Cache még nem frissült a változtatás óta.

Szerencsére többféleképpen is ki lehet kényszeríteni, hogy frissüljön:
  • Gyorsgombok segítségével: CRTL+SHIFT+R
  • Menüben kikeresve: Edit / IntelliSense / Refresh Local Cache

HTML FORM-ban egy darab inputbox

A HTML FORM tagnek van egy olyan tulajdonsága, hogy ha csak egy darab, egy soros beviteli mezőt tartalmaz (<input type="text"/>), akkor az ENTER lenyomására mindig lefut a SUBMIT, függetlenül attól, hogy van-e rá beállítva alapértelmezett gomb.

Célszerű az alábbiakat ellenőrizni, ha az ENTERre olyankor is újratölti az oldalt, amikor nem kellene:
  • csak egy darab beviteli mezőt tartalmaz a FORM
  • van submit típusú gomb (<input type="submit"/>)
  • van BUTTON tag és annak milyen értékű a type attribútuma, mert a submit az alapértelmezett
  • van valami JavaScript függvény beregisztrálva a FORM-ra, a beviteli mezőre vagy esetleg az egyik gombra a FORM-hoz kapcsolódóan

2016. április 3., vasárnap

Visual Studio 2010 eltávolítása

Elérkezett a pillanat, hogy megválok a rég nem használt VS2010-től. Gondoltam, egyszerűen fogom az uninstallert és ráeresztem, de nincs ilyen. Mivel nem volt kéznél a telepítője, így jó ötletnek tűnt, hogy ezt követően a Programok eltávolítása menüpontban keressek ki és töröljek le minden VS2010-re utaló alkalmazást. Meglepetten fogadtam, hogy a VS mappája továbbra is ott vigyorgott.

Létezik egy Visual Studio 2010 Uninstall Utility alkalmazás, ami pont erre való, és az alábbi üzemmódokban képes működik:

Default (VS_2010_Uninstall-RTM.ENU.exe)
Minden "top level" terméket töröl, ami a 2010-es verzióhoz tartozik és az ezeket támogató komponenseket. Nem bántja a korábbi verziókkal megosztott komponenseket, illetve a rendszerszintű frissítéseket, pl a .NET 4.0-t.

Full (VS2010_Uninstall-RTM.ENU.exe /full)
Ebben a módban már a korábbi verziókkal megosztott komponenseket is eltávolítja, ami gondot okozhat  a telepített korábbi VS verziókban. A .NET 4.0-t ez sem bántja.

Complete (VS2010_Uninstall-RTM.ENU.exe /full /netfx)
Minden VS2010-hez tartozó alkalmazást eltávolít, beleértve a .NET4.0-t is.

A leírás alapján a default módot választottam, ami immáron ténylegesen le is törölte a VS maradványait.


Nem sokkal később az újabb VS verzióm puffogott, a .NET4-es projektjeimet nem tudta betölteni és az alábbi opciókat ajánlotta:
  • .NET4.5-re áttérés
  • .NET4 letöltése és telepítése
  • projekt kihagyása
A VS-ben nem lehetett tovább kiválasztani a .NET4 egyik verzióját sem. Ám legyen, gondoltam én, feltelepítem újra, viszont a telepítő azt mondta, hogy az operációs rendszeremnek eleve része, valamint ez vagy egy újabb verzió már telepítve van, ami miatt nem hajlandó települni. Ezek részben igazak is, mert a .NET 4.5 valóban megvan, és elvileg része volt az OS-nek is, most pedig már nem az.

A VS2013 "javítás" funkciója megoldotta a helyzetet és nem kellett újratenni a teljes Windows-t.

Tanulság az egészben, hogy ha egy Microsoft termék mellett nem található uninstall.exe, akkor célszerű a telepítőjével próbálkozni, mivel abban lesz benne. Másfelől, ha nincs kéznél a telepítő, akkor nagy valószínűséggel külön mégiscsak beszerezhető egy kisméretű uninstaller.


2016. április 1., péntek

SSRS-ben #Error a számított érték helyén

Megesett, hogy egy riporton szerettem volna megjeleníteni egy kifejezéssel előállított értéket, de a riporton csak egy #Error jelent meg a helyén. A naplófájlban lévő hibaüzenet sem volt túl beszédes:

ERROR: Throwing Microsoft.ReportingServices.ReportProcessing.ReportProcessingException: , Microsoft.ReportingServices.ReportProcessing.ReportProcessingException: The specified operation is not valid. ;

Ilyen hibánál általában az a baja, hogy eltérő típusú adatokat szeretnénk aggregálni. Esetemben egy Sum(IIF( exp, érték1, érték2)) okozta a gondot, ahol bizonyos csillagállás mellett nem egyezett meg a két érték típusa, mert két különböző számtípusú volt.