Archive for the ‘.NET’ Category

WCF perf

Thursday, February 14th, 2013

Ma sikerült 700 MBits/seccel adatokat átvinni titkosítva, tömörítve WCF-fel egy erős pc és egy szerver között. :)
Ehhez egyszerre 3 szerverre uploadoltunk fájlokat párhuzamosan, netTcpBindinggel, SSL-lel, a csatornán Deflate tömörítéssel (.NET 4.5).

HashSet.Add végtelen ciklus oka és megoldása

Sunday, February 10th, 2013

Alapszabály, hogy HashSet, Dictionary vagy bármi más asszociatív kollekciókban kulcsként csak immutable objektumokat lehet használni. Ennek az az oka, hogy amikor berakunk egy elemet, akkor a kollekció meghívja az objektumon a GetHashCode-ot, és az azonos hashkódú objektumokat listába szervezi. Ha két objektum egyenlőnek tekintendő (az Equals alapján), akkor a hascodejuknak azonosnak kell lenni. Ha ez nem teljesül, vagy idővariáns, azaz később már nem igaz, akkor ezek a kollekciók meghülyülnek.
A példám így nézett ki:


public virtual bool Equals(EntityBase other)
		{
			if (other == null)
			{
				return false;
			}
			if (Id == 0)
			{
				//Transient object
				return ReferenceEquals(this, other);
			}
			return other.Id == Id;
		}

		public override bool Equals(object obj)
		{
            if (!(obj is EntityBase))
            {
                return false;
            }
			return Equals((EntityBase)obj);
		}

		public override int GetHashCode()
		{
			return Id;
		}

Eleve látszik, a GetHashCode és az Equals nem azonos logika szerint működik. A GetHashCode akkor ad vissza azonos hascodeot, ha az idk azonosak. Az Equals viszont az idtől függően vagy az idkat hasonlítja össze, vagy referenciális komparálást csinál. Ez már kapásból bug.

A másik bug, hogy ez az objektum nem immutable, az Id változik, amikor adatbázisba beszúrjuk az entitást, és kap egy idt. Emiatt az egész koncepció beteg.

A HashSet.Add meghívja a HashSet.AddIfNotPresetet, amiben van egy ciklus:


for (int i = this.m_buckets[num % this.m_buckets.Length] - 1; i >= 0; i = this.m_slots[i].next)
	{
		if (this.m_slots[i].hashCode == num && this.m_comparer.Equals(this.m_slots[i].value, value))
		{
			return false;
		}
		num3++;
	}

Ez az ciklus akadt be. Az if feltétele sose teljesült, így sose lépett kis a ciklusból, mivel idt kapott az entitás, így megváltozott az Equals logikája.

IOC containerből példányosított WCF szerviz ojjektumok

Saturday, February 2nd, 2013

Ilyet még nem csináltam, percall alapú szervizeknél félnék is a DI framework sebességétől, de ezzel a módszerrel be lehet vezetni a DI-t a szerviz infrastruktúra tövénél. Még kevesebb Service Locator antipattern lesz a kódban.

WCF and SSL Processing Load Balancers

Saturday, February 2nd, 2013

Hogyan bízzuk az sslt a load balancerre, és használjunk sima httpt mögötte, a wcf szervizekig?
A bemutatott trükkös megoldás jól jött volna tavaly, amikor az OEP TAJ ellenőrző szervizét nem tudtuk WCF-fel meghívni, mert nem SSL-en kér auth adatokat. Alapban a WCF ezt nem nyeli le, de a cikk megmutatja, hogy lehet átvágni.

WAS alatt hoszolt WCF szerviz

Saturday, February 2nd, 2013

Pár órám elment az életemből, mivel véletlenül abszolút elérési utat írtam be endpoint addressnek egy WAS (IIS) alatt hoszolt WCF szervizbe. Jó szokásától eltérően nem jött semmi világos hibaüzenet, mi a baja a WCF-nek.

WCF REST támogatás vagy ASP.NET WepAPI?

Friday, February 1st, 2013

Jó kis összefoglaló a témáról, miért kellett ASP.NET-be újra megcsinálni a dolgokat.

Compression a 4.5-ös WCF-ben

Friday, January 25th, 2013

Hoppá, már nem házilag kell megoldani. Jövő héten kipróbáljuk.

WCF Proxies: To Cache or Not to Cache?

Thursday, January 24th, 2013

Tömör konklúzió: ha küldünk át credentialokat, akkor a beépített ChannelFactory cache nem működik, kézzel kell cachelni. Ha nem (ez a ritkább), akkor megy magától.
Részletek itt.

Perf counterek olvasása admin jogok nélkül

Tuesday, January 22nd, 2013

http://blogs.msdn.com/b/bclteam/archive/2006/09/08/746900.aspx

Az IIS appol identityt is bele lehet rakni a hivatkozott csoportba, csak kézzel kell beírni a nevét, nem lehet kiválasztani az explorer listájából.

Utálatos WPF .NET memory leak

Sunday, January 20th, 2013

Vannak aranyosak, de ez ronda, sunyi. De könnyű megoldani a cikk alapján.

HashSet.Add végtelen ciklus?

Sunday, January 13th, 2013

HashSetbe akartam berakni hibernate entitásokat, amelyek Equals-sza és GetHashCode-ja így nézett ki:


public virtual bool Equals(EntityBase other)
		{
			if (other == null)
			{
				return false;
			}
			if (Id == 0)
			{
				//Transient object
				return ReferenceEquals(this, other);
			}
			return other.Id == Id;
		}

		public override bool Equals(object obj)
		{
            if (!(obj is EntityBase))
            {
                return false;
            }
			return Equals((EntityBase)obj);
		}

		public override int GetHashCode()
		{
			return Id;
		}

A HashSet.Add-nak beadva egy ilyen entitás példányt az Add soha nem tért vissza, végtelen ciklusba került. Miért?

Nullable típusok attributumokban

Tuesday, September 11th, 2012

CLR attributumokban sajnos nem lehet használni nullable típusokat, mivel ezeket metaadatként tárolja el a compiler, és az nincs felkészítve struktúrákra. Igazából még a decimalt sem eszi, ami elég bosszantó, ez azért beleférhetett volna.
Most mégis kellett nullable és ráadásul decimal att param, így kerültem ki:


public class SteppingSpecificationAttribute : Attribute
{
    private double increment;
    private double startValue;
    private double endValue;

    public SteppingSpecificationAttribute(double defaultValue)
    {
        DefaultValue = defaultValue;
    }

    public bool HasValueIncrement { get; private set; }
    public bool HasValueStartValue { get; private set; }
    public bool HasValueEndValue { get; private set; }

    public double Increment
    {
        get { return increment; }
        set
        {
            increment = value;
            HasValueIncrement = true;
        }
    }

    public double StartValue
    {
        get { return startValue; }
        set
        {
            startValue = value;
            HasValueStartValue = true;
        }
    }

    public double EndValue
    {
        get { return endValue; }
        set
        {
            endValue = value;
            HasValueEndValue = true;
        }
    }

    public double DefaultValue { get; set; }
}

Nem elegáns, de működik. Valakinek van jobb ötlete?

Később, amikor kiveszem a property-k értékét, akkor nézem meg, mi null és mi nem, valamint akkor konvertálok decimalra. A double-decimal konverzió okozhat adatvesztést, de ismerem milyen számok mennek be, ezekkel nincs ilyen gond.

SMS küldés skype-pal

Thursday, September 6th, 2012

Az egyik programomból SMS-t akartam küldeni, ha gond van vele. Skype apival ez nagyon egyszerű:


public class SmsSender
    {
        public static void Main()
        {
            new SmsSender().SendSms("+36XXXXXXXXX", "Test");
        }

        public void SendSms(string phoneNumber, string smsText)
        {
            ISkype skype = new Skype();
            ISmsMessage ismsmessage = skype.CreateSms(TSmsMessageType.smsMessageTypeOutgoing, phoneNumber);
            ismsmessage.Body = smsText;
            ismsmessage.Send();
        }
    }

Valszeg létrehozok neki egy saját skype accountot, rakok rá némi pénzt, aztán jöhet az sms.
A fentihez kell a Skype Desktop API, amihez kicsit hosszadalmasan regisztrálni kell, és a végén kapunk egy COM DLL-t, azt interoppal triviális hívni, mint fent is látható.

Code Contracts a VS 2012-ben

Wednesday, September 5th, 2012

Szegény Code Contracts még másodrangúbb szereplő lett, úgy érzem. VS 2012-ben fel sem jön a plusz füle, hiába telepítjük a Code Contracts csomagot.
Elevated VS 2012 Command Propmtból devenv.exe /setup -pal a helyére kerül a dolog. (Vigyázni, a 2012-es devenvet futtassuk.)

Moles meghülyül a 4.5-től

Tuesday, September 4th, 2012

Mivel a .NET 4.5 sunyin van verziózva, a Moles generátora megőrül tőle. Megoldás itt.
VS 2012-ben Fakes néven van benne egy újabb verziója, de csak az intergalaktikus verzióban. Ez nem volt jó döntés, tudom, hogy el kell adni az Ultimate-et, de csak a Fakes miatt nem fognak 12k$-t adni érte.

Optional parameter zűrzavar

Monday, August 27th, 2012

Érdekes, hogy egy ártatlannak tűnő kis bővítésnek, mint a C# 4-ben bejött default paramétereknek milyen furcsaságai jönnek elő, ha beleütközik a polimorfizmusba.

Mellesleg, gondolom az mindenkinek tiszta, hogy a default paraméterek hasonló verziózási problémákat okoznak, mint az enumok: a hívó oldalba beég a konkrét érték, így a hívott library újrafordítása NINCS hatással a hívó által átadott értékre. Még nem láttam emiatt szívást, de jó észben tartani.

BCLExtensions

Monday, July 30th, 2012

Pár apró, de hasznos dolog van benne, nekem a rendezett IListben történő BinarySearch kellett belőle.

Eddig ezt csináltam:


int pos = ArrayList.Adapter(this).BinarySearch(dateTime, new Bar2DateTimeComparer());

Most már így, nem kell adaptert ráhúzni, és generikus a comparer hívás, így egy kicsit gyorsabb is:


int pos = this.BinarySearch(dateTime, new Bar2DateTimeComparer());

Test Driven Development - I like it

Wednesday, July 25th, 2012

Egyre több dolgot TDD módon írok meg. Ami szembetűnő a korábbi, először implementálok aztán ha van rá idő írok hozzá tesztet (nem fogok) módszerhez az a következő:
-Sokkal kevesebbet, nagyon sokszor egyáltalán nem debugolok. Ez eszement sok időt spóroló tevékenység, gyűlölöm, amikor 2-3 percet is nyomkodok, mire eljutok a betegnek vélt ponthoz a debuggerben. Ott aztán sokszor kilép a debugger (pont timeoutolt a web worker process), release kód miatt nem mutat változókat, továbbléptetem, aztán kezdhetem elölről (lehet mozgatni a current pointert, de ha már okoztunk mellékhatásokat hiába megyünk vissza), stb. Szeretem az advanced debuggung témát, de utálom az időt vele feleslegesen tölteni.
-A GUI-t alig indítom el, ezáltal megint nagyon sok időt takarítok meg.
-Az implementált dolgaim azonnal mennek, amikor a GUI-t is elindítom, már csak élvezni kell az eredményt.
-Hamarabb készen vagyok a feladattal. Tudom, ezt nem lehet objektíven mérni, mert nincs mihez viszonyítani. De az első két pont miatt érzésre annyi időt megtakarítunk, ami felülmúlja a tesztekre szánt időt.

Igazából a TDD nem is a tesztelésről szól. Ez komplexitás és félelem kezelés. Arról szól, hogyan implementáljuk egy bonyolult feladatot sok kicsit változaton keresztül úgy, hogy ne őrüljünk bele, ne legyen sok kudarcunk. Az már csak egy plusz, hogy sokkal jobb lesz a kódminőség.

SP vs. OR mapper

Monday, July 23rd, 2012

Az előző bejegyzésből a kommentek alapján lehet az jött le, hogy én minden dolgot OR mapperrel valósítanék meg. Csudákat, szó sincs erről.

Nézzünk egy példát. Az előző architektúra tervemben (ami prototípus majd framework is lett, nem csak terv) NHibernate és Oracle alapon készült az app. Sok más mellett tetszőleges országban használható emberneveket kellett tudni tárolni, ezért a db terv elég általános lett. Person-PersonName-PersonNamePart, ez a 3 tábla volt a fő váz. PersonNameType, PersonNamePartType, ezek enum jellegű lookup táblák.
Egy lekérdezés célja visszaadni tetszőleges név komponens alapján a beteg nevét. Azaz a PersonNamePart tábla egy oszlopában kellett keresni, ez jól indexelhető, hatékony lekérdezés. Utána jött a mókásabb rész. A PersonNamePartból vissza kellett navigálni a PersonName-re, onnan a Personre, majd a megjelenítés miatt vissza le az összes PersonName-re és onnan tovább a PersonNamePartra.
Ezt a lekérdezést először megírtam P/L SQL-ben, hogy lássam, kb. mit akarok csinálni, ez mennyire hatékony, kell-e hozzá Cluster (nem clustered, ez oracle, ez teljesen más) index az IO csökkentésére, stb.
Amikor a lekérdezéssel elégedett voltam, elkezdtem átírni az NHibernate LINQ providerére. A célom az volt, hogy a query gondolkodásmódját pontosan visszatükrözze a LINQ query, így remélhetőleg a generált sql is hasonlóan hatékony lesz. Sajnos azonban a kísérlet kudarc volt. Az NHibernate LINQ providere egyszerűen nem implementálta amit én akartam (ha jól emlékszem Take(n) után Select). Megnéztem a forrását, ott volt egy nagy büdös throw new NotImplementedException().
Mi maradt? Sp, aminek a kimeneti joinolt sorait az NHibernate alakította vissza entitásokká (ez viszont nagyon ügyes része az NHibnek.)
Azaz a lekérdezés a lehető leghatékonyabb volt, de a végén mégis a domain modellel dolgoztam, ami végül szimpatikus hibrid lett, bár jobb lett volna tisztán LINQ-ban megírni.
Mi lett volna, hogy rendesen megírták volna a LINQ providert? Megnéztem volna a generált SQL-t, és ha az megfelelően hatékony lett volna, akkor maradt volna az.
Ha nincs rá különösebb okom, én szeretem a logikákat C#-ban írni. Az objektumorientáltság miatt jól lehet szervezni az összetettebb kódokat, sokkal jobban, mint a procedurális TSQL-ben.
Lehet memóriában cache-lni dolgokat, SQL Serveren belül nem nagyon. Az appszervereket sokkal könnyebb horizontálisan skálázni mint a DB-t. Magyarul egymás mellé berakni sok azonos szervert, még ha ez marhára nem is triviális, de legalább lehetséges. Emiatt a DB-ről terhelést levenni nem kell félnetek, mindenképpen üdvös tevékenység.
Viszont, ha az adatokhoz közel kell műveleteket végezni, akkor nem fogok feleslegesen átvinni nagyszámú sort az appszerverre. Ha egy gyakran futó kritikus tranzakció hossza fontos, akkor lehet spbe rakom. Ha sok sort érint a művelet nem hozom ki őket a dbből.

Próbálok pár példát konstruálni, illusztrálandó az előbbieket.

1. Be kell olvasni 100 sort egy táblából, lefuttatni valami összetett műveletet rajta, amely kb. 1000 sornyi programkódnak felel meg, majd az eredményt visszaírni egy másik táblába, amely kb. 20 sornyi insertet jelent. OR mapper. A bonyolult logika valószínűleg erősebb érv lesz, mint az, hogy 100 sort át kell vinni a dróton, illetve, hogy 2 roundtrippel jár a művelet. Mivel a logika C#-ban lesz, könnyű lesz teszteket írni rá (a db könnyen kifake-elhető), könnyebb lesz karbantartani, ha módosítani kell.
Ha esetleg az egésznek egy tranzakcióban kell lenni, és a round tripek ideje jelentősen nyújtja a tranzakciók futási idejét, ami miatt az esetleg szigorúbb izolációs szinten futó tranzakciók elkezdenek torlódni akkor, és csakis akkor kezdenék el filózni az spn. De ezt csak méréssel lehet eldönteni, mert az sp esetén meg valószínűleg a logika lesz lassabb, így lehet többet vesztünk, mint nyerünk.

2. Be kell olvasni 5 táblából 1 sort, ezek alapján dönteni, majd visszaírni 2 táblába 1-1 sort. Az alapeset ugye az, hogy megpróbáljuk OR mapperen keresztül megoldani a dolgot, majd belátni, hogy az elég hatékony-e, ha nem akkor áttérünk sp-re. NHibernate vagy EF Extension Pack Future-ökkel az 5 select valószínűleg kiadható 1 roundtrip alatt, ha nincs közöttük függőség. Ha van, akkor lehet, hogy join-nal vagy navigácós relációkon haladva még mindig megoldható 1 roundtrip alatt. Ha nem, akkor nem. A visszaírások normális OR mapper esetén 1 roundtrip alatt végrehajthatók (batching), így 2 roundtrip alatt megvan a feladat. Valószínűleg bőven belefér, marad az OR mapper.

3. Le kell törölni sok sort, ahol a DueDate nem mai dátum. Egyértelműen sp, semmi értelme OR mapperbe behúzni a törlendő sorokat, könnyedén megfogalmazható a feltétel egy sima SQL where-ben. Hozzáteszem, NHibbel lehet delete és update műveleteket is kiadni entitás fogalmak használatával, így annál ezt se spzném, a generált sql az lenne, ami kézzel beírnék az spbe, csak direkt sqlként. Akkor meg minek sp-zzek?

4. Be kell szúrni 500 sort, nagyon gyorsan, naponta sokszor. pl. laborgépek okádják magukból az eredményeket, ezeket kell eltárolni. Ez már érdekesebb kérdés. Első körben azt mondanánk, OR mapper erre nem való. Az első gondolatom az ilyenre valamilyen bulk copy megoldás lenne, pl. SQL Server esetén SqlBulkCopy osztállyal. De pl. Oracle esetén van un. Array Binding, amivel a bulk műveletekkel közel azonos teljesítményt lehet elérni, a bulk műveletek limitációi (nem lehet trigger a táblán, stb.) nélkül. Ami meglepő, hogy a NHibernate oracle esetén kihasználja az array bindingot, és nem csak egyszerűen batcheli a műveleteket, de array bindinggel küldi be. Emiatt veszett gyorsan tud beszúrni, így NHib esetén simán OR mappert használnék erre is. Egy konkrét mérésemben 10000 sor beszúrása így nézett ki Oracle-be (fejből mondom, de a nagyságrendek jól lesznek):
1. NHibernate identity id generator (othodox SQL Serveres gondolkodásmód): 52 sec
2. ADO.NET soronkénti insert: 12 sec
3. NHibernate hilo id generátorral: 2.7 sec
4. ADO.NET Array Bindinggal: 2.2 sec
5. Direct Path Loading: 2.1 sec

Mik a tanulságok ebből a mérésből?
A. Az identity alapú ID generálás megöli az insertek teljesítményét (1. és 2. példa alapján)
B. A sok roundtrip mindenképpen káros, emiatt lett lassú 2. is, mivel itt nem volt OR mapper overhead.
C. Az NHib elképesztően gyors volt (3. példa), csak 20%-kal lassabb volt, mint az Array Binding nyersen használva (2.2/2.7).
D. Az Array Binding eszement gyors, SQL Serveren a tábla típusú paraméterekkel lehet hasonlót elérni, de ehhez nem tudok OR mapper támogatást (mivel ehhez explicit kell fogadó kódot írni, az orához NEM).
E. A bulk copy mindenhol a leggyorsabb, de az oránál ez nagyon limitált, le is van írva, hogy arra gyúrnak, hogy az array binding legyen ugyanolyan gyors, így kiválthatja azt.

Azaz, ha megfelelően optimalizált az OR mapper adatelérése, akkor még olyan esetekben is használható, ahol első körben nem jutna az eszünkbe.

5. Két tábla között kell átmozgatni sorokat, egyszerű leképezések mentén. Sp, insert-select és delete, tranzakcióban.

6. Pessimistic Offline Lock pattern implementációban volt a következő. Meg kell nézni, létezik-e lock egy erőforrásra, ez egy egy szűrt select. Ha nem, akkor beszúrni egyet, ha igen, beszúrni egy várakozó sort. Mindezt serializable izolációs szinten, mivel meg kell akadályozni fantom sorok beszúrást a select és az insert között (orának is van ilyenje, bár nem lockol mint az Sql server alapban, hanem úgy működik, mint az RCSI az SQL servernél).
Érezhetően itt észnél kell lenni, mivel ha sokszor fut le a folyamat, akkor a serializable miatt lassú lehet a dolog. Design szempontból értelemszerűen az offline lockkal védeni kívánt dolgok lekérdezése NEM a serializable tranzakcióban volt. A védendő lekérdezés előbb lefut, majd utána jön a lock fogása, és ha sikerül, megkapja a kliens az eredményt, ha nem, akkor eldobjuk a lekérdezése eredményét. Ez pazarlás, ha ütközés van, de nem számítunk sok ütközésre. Ha egy tranzakcióban lenne a két feladat torlódás lenne, ha előbb a lock, aztán a művelet, akkor meg sikertelen művelet esetén (pl. secu beint) törölni kellene a lockot, de közben már lehet más vár rá…
Ezt a feladatot OR mapperrel írtam meg. Nem tudok pontos végrehajtási időt mondani, de ha jól emlékszem kb. 20 ms volt a teljes lock vizsgálat ideje. Ha serializable szinten egy erőforráson versengenének a folyamatok, akkor másodpercenként 50-nél többet nem tudna a szerver végrehajtani. Valójában azonban szórnak a lockolandó erőforrások, így a serializable nem fog jelentős torlódást okozni (megfelelő csak indexekkel persze).
Sokan szerintem ezt kapásból SP-ben írnák meg. Mit nyernénk vele? Mit akarunk minimalizálni? Az SQL szintű tranzakció idejét. A tranzakció kb. így nézne ki:
begin tran
select …
if () insert…
else másik insert…
commit

A kérdés, ezek ideje hogy aránylik a roundtripek által megnövelt időhöz? 3 roundtrip van, 1 select, 1 valamilyen insert és egy commit. Ha a roundtrip ideje mondjuk 3ms, akkor 9 ms a roundtrip overheadje. Ha az SQL műveletek kb. 10ms-ig tartottak, akkor kétszer olyan gyors lehet az sp-s megoldás. Azaz itt elgondolkodtató a dolog, de megint, én csak konkrét mérések alapján állnék neki átrefactorolni a megoldást spre, ilyen spekulatív úton nem. Hisz ha a roundtrip valójában csak 500 usec, akkor máris 1 napig felesleges dologgal múlattuk az időt.

7. Sok id alapján kell behozni pár száz sort. Lehetsége megoldás OR-mapper future queryvel, egy batchben, de ennek hátránya, hogy pl. SQL Servernél nem lehet 1-2 ezérnél több paramétert definiálni, így a generált sql nem lesz végrehajtható. Itt spt írtunk, amely oracle collection paraméterként vette át a kulcsok listáját, belül pedig joinoltunk az alaptáblához. SQL Servernél ezt tábla típusú paraméterrel csináltam volna meg. A bemenetet elegánsan át lehetett adni, mivel az NHibben lehet saját típust definiálni, így Guid tömb típus direkben átpasszolható volt az spbe.

Más. A már említett architektúrában volt sok olyan cross-cutting concern, amit db szinten implementálni véres lett volna. Például a WCF-ből kijövő objektum gráfot módosítani kellett, mivel sor (entitás) is mező (property) szintű ACL secu miatt meg kellett metszeni a kimenete fát (gráfot, mikor-mit), property-k értékét kimaszkolni, de közben feljegyezni a maszkoltakat, szöveget nyelvi fordítását beinjektálni a gráf tetszőleges helyére, tetszőleges entitást kiegészítő adatokat belerakni a fa megfelelő részére, be kellett küldeni minden tranzakció nyitása után egy user és tran azonosítót, offline lock információt, stb. Ezek nagyon komplex feladatok voltak, amelyeket ráadásul millisecundum nagyságrendű idő alatt kellett tudni megcsinálni tízezer elem feletti gráfokra is. Ezt én dbben nem tudtam volna megcsinálni, intenzív appszerver szintű cache-eléssel, illetve kihasználva a .NET adatstruktúráit sikerrel és elegánsan (központilag implementálva, mint az AOP-ban) megoldottuk.
Ha viszont ilyen mindent keresztülvágó követelményektől hemzseg a feladat, akkor ez ember kétszer is meggondolja, hogy kilépjen az entitások, a logikai modell világából, és elkezdjen spzni, mivel az spkre ráhúzni ezeket a megoldásokat nehézkes.
Szóval az OR mapper vs. sp kérdés igen komplex, nem lehet 2 perc alatt dönteni róla, és mindkettőnek megvan a helye, feladatonként egyesével kell eldönteni, melyiket használjuk.

RadioButton MVVM alatti használata

Thursday, June 14th, 2012

Jó kis megoldás, elegáns.