\

Školení Návrhové vzory, OOP a UML


 Friday, 24 February 2012
Entity Framework 4.3. Code First - (nechutný) problém s TPC mapováním?

 

Update 25. 2.2011: Tak chyba potvrzena EF týmem. Jedná se skutečně o chybu, která je částečně popsána v known issues.

Diego B Vega : @Rene Stein: Thanks for reporting this and for the repro. What you describe seems to be a bug in TPC mapping that we are already aware of and that we are planning to fix in the upcoming EF 4.3.1. Please take a look at the list of known issues above for more information.

Jedná se tedy o chybu, kterou někdo zmínil i  v komentářích. Zarážející ale je, že k chybě “chybí sloupec v databázi” se dostanete teprve tehdy, kdy vygenerujete databázi s jiným než požadovaným schématem, odchytnete výjimku při dotazování a podíváte se na popisek vnořené výjimky. V “known issues” EF by spíš podle mě mělo být  - ve verzi 4.3 se vám ani nepodaří vygenerovat databázi s TPC mapováním dědičnosti a volání metody MapInheritedProperties  při konfiguraci entit je jen zbytečná dekorace v kódu a cvičení v marnosti.

Mohl by prosím někdo ověřit, že jsem buď udělal nějakou triviální chybu při mapování, anebo potvrdit mé podezření, že je EF Code First v poslední verzi 4.3 natolik prolezlý chybami,  že v něm nefunguje ani tento triviální scénář.

Problém se snažím reprodukovat na tomto kódu.

Mám třídy Base a Derived. Jejich role asi vysvětlovat nikomu nemusím.Smile

Snažím se pro mapování třídy Derived do databáze použít v db kontextu strategii TPC – table per (concrete) class (metoda MapInheritedProperties). 

Po spuštění se aplikace vytvoří databáze se dvěma tabulkami. Struktura databáze ale odpovídá TPT strategii pro mapování dědičnosti:

Tabulka Base má sloupce Id a BaseProperty, tabulka Derived Id a Note. Volání MapInheritedProperties je tedy zcela ignorováno.

 

EFTables

Jak popisuju i v kódu, matoucí je to, že Entity Framework sice mapuje třídy do databáze podle TPT strategie, ale dotazy klade, jako kdyby v databázi byly třída Derived namapována TPC strategií.
Vygenerovaný SQL dotaz do tabulky Derived vypadá takto:

SELECT '0X0X' AS [C1], [Extent1].[id] AS [id], [Extent1].[BaseProperty] AS [BaseProperty], [Extent1].[Note] AS [Note] FROM [dbo].[Derived] AS [Extent1]

Schizofrenní Entity Framework se beze všech skrupulí snaží dohledat v tabulce Derived sloupec BaseProperty (TPC mapování), což pochopitelně skončí výjimkou při vykonávání dotazu, protože se jiná část jeho vícečetné osobnosti složené z nespolupracujících spoluautorů EF rozhodla při generování databáze, že TPT je pro každého aplikačního vývojáře vždycky jasná volba.

A protože perverzních projevů EF se při pátečním večeru nelze nabažit, tak tady je skript pro založení databáze, který jsem vytáhl z podkladového ObjectContextu a který by měl mapovat podle TPC, což se ale nestane, protože je proti databázi spuštěn skript zcela jiný.

Výsledek Trace.WriteLine(((IObjectContextAdapter) context).ObjectContext.CreateDatabaseScript());

Projekt s reprodukcí problému ke stažení.



Friday, 24 February 2012 22:06:21 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | C# | Entity Framework | LINQ


 Monday, 21 March 2011
Prezentace Moderní trendy ve vývoji aplikací

Přibližně před rokem jsem u dvou firem začínal sérii technologických kurzů subjektivním shrnutím změn (nejen) v aplikacích psaných v .Net Frameworku. Nedávno jsme ji s kolegou náhodou otevřeli a pobavili jsme se nad tím, jak je rok v IT stále dlouhá doba a že zde dvojnásobně platí “tempus fugit”. Napadlo mě, že se nad prezentací možná se pobaví i někdo další, hlavně v pasážích, kde jemně naznačuju zálibu Microsoftu v zařezávání technologií.Smile

U prezentace je třeba mít na paměti:

  1. Jedná se jen o osnovu “přehledové“ a cca dvouhodinové přednášky.

  2. Témata, typy projektů a technologie jsou v přednášce voleny podle zájmu zákazníka.

  3. Snažil jsem se nebýt hned  v této úvodní přednášce příliš ostrý a konfliktní.Smile

  4. Zvolená témata se týkala oblastí, které jsme v dalších dnech probíraly detailněji na konkrétních projektech vytvořených na návazných kurzech. Po pár zkušenostech si myslím, že jediný smyslupný kurz zabývající se technologií či programovacím jazykem je ten, na kterém píšete před účastníky kód. Tato přednáška byla koncipována jako motivační úvod k dalším tématům.


Monday, 21 March 2011 13:11:41 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | ASP.NET | C# | Compact .Net Framework | LINQ | RX Extensions | Silverlight | Web Services | Windows Forms | WP7


 Monday, 23 August 2010
C# - kontrola existence vlastnosti u typu dynamic bez vyvolání výjimky RuntimeBinderException.

Dan Steigerwald mě na Facebooku upozornil na článek “Challenge: Dynamically dynamic” na blogu Ayende Rahiena. Jak se můžete sami podívat, celá výzva se týká toho, jak zjistit, jestli u dané instance typu dynamic existuje vlastnost se zadaným jménem, aniž byste museli odchytávat  výjimku RuntimeBinderException, která vás na chybějící vlastnost sice drsně upozorní, ale zároveň vás nutí používat kód řízený výjimkami.

 

Jak vypadá kód detekující existenci vlastnosti s vy/zneužitím RuntimeBinderException?

   private static bool HasPropertyNaive(IDynamicMetaObjectProvider dynamicProvider, string name)
        {
            try
            {
                var callSite =
                                CallSite<Func<CallSite, object, object>>.Create(Binder.GetMember(CSharpBinderFlags.None, name, typeof(Program),
                                                         new[]
                                                                 {
                                                                     CSharpArgumentInfo.Create(
                                                                         CSharpArgumentInfoFlags.None, null)
                                                                 }));
                callSite.Target(callSite, dynamicProvider);
                return true;
            }
            catch (RuntimeBinderException)
            {

                return false;
            }

        }

A použití:

static void Main(string[] args)
        {
            dynamic testDynamicObject = new ExpandoObject();
            testDynamicObject.Name = "Testovaci vlastnost";
            Console.WriteLine(HasPropertyNaive(testDynamicObject, "Name"));
            Console.WriteLine(HasPropertyNaive(testDynamicObject, "Id"));            
            Console.ReadLine();
        }

 

Stejně jako v zadání na blogu Ayende metoda HasPropertyNaive pracuje s každým objektem dynamic skrytým za rozhraním IDynamicMetaObjectProvider. V metodě napodobíme chování kompilátoru C# – vytvoříme “kontext operace”, tzv. CallSite, které předáme hlavně tzv. “Binder” voláním tovární metody metody Binder.GetMember. Binder, v našem případě binder pro get akcesor vlastnosti, jejíž přítomnost testujeme a jejíž název jsme předali metodě HasPropertyNaive v argumentu name,  si lze zjednodušeně představit jako objekt, který je odpovědný za dohledání hodnoty vlastnosti u dynamického objektu za běhu aplikace.

U CallSite použijeme metodu Target, které předáme samotnou instanci callSite a objekt dynamic, u nějž chceme otestovat existenci vlastnosti. Jestliže vlastnost u objektu dynamic neexistuje, metoda Target vyvolá výjimku RuntimeBinderException a my vrátíme false, jinak ignorujeme návratovou hodnotu metody target a vracíme true, což je pro kód volající metodu HasPropertyNaive potvrzení, že vlastnost existuje.

Metoda HasPropertyNaive plní svůj účel, ale za cenu vyvolání výjimky RuntimeBinderException. A toho se týká právě “challenge”. Zkusme se výjimky zbavit.

Kdybychom měli testovat existenci vlastnosti jen u instancí “ExpandoObject”, měli bychom hned hotovo.

private static bool HasPropertyExpandOnly(IDynamicMetaObjectProvider dynamicProvider, string name)
{
return ((IDictionary)dynamicProvider).ContainsKey(name);
}

ExpandoObject totiž podporuje rozhraní IDictionary a klíčem v objektu Dictionary jsou názvy vlastností.

Zadání ale vyžaduje, abychom zkontrolovali přítomnost vlastnosti u ktreréhokoli objektu dynamic typu IDynamicMetaObjectProvider. Když předáte metodě HasPropertyExpandOnly instanci dynamic, která dědí z DynamicObject nebo přímo implementuje rozhraní IDynamicMetaObjectProvider, při pokusu o přetypování instance na rozhraní IDictionary dojde k výjimce.

Problém s detekcí přítomnosti vlastnosti by také zcela zmizel, kdybychom měli zaručeno, že každá instance typu IDynamicMetaObjectProvider a s ní asociovaný “DynamicMetaObject” z metody GetDynamicMemberNames vrátí seznam s názvy všech dynamických členů.

private static bool HasProperty(IDynamicMetaObjectProvider dynamicProvider, string name)
{
return dynamicProvider
                    .GetMetaObject(Expression.Constant(dynamicProvider))
                    .GetDynamicMemberNames()
                    .Contains(name);
} 

Bohužel ani to garantováno nemáme a metoda GetDynamicMemberNames u mnoha instancí dynamic bez skrupulí vrátí prázdné pole, i když vlastnosti existují.

Musíme si tedy poradit jinak.

Následuje kód metody HasProperty včetně podpůrných konstrukcí, která pracuje s libovolnou instanci typu IDynamicMetaObjectProvider a ke zjištění, zda je, či není vlastnost přítomna, nepotřebuje vyvolávat výjimku RuntimeBinderException.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Dynamic;
using Microsoft.CSharp.RuntimeBinder;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;


namespace DynamicCheckPropertyExistence
{
    class Program
    {                
        private static bool HasProperty(IDynamicMetaObjectProvider dynamicProvider, string name)
        {



            var defaultBinder = Binder.GetMember(CSharpBinderFlags.None, name, typeof(Program),
                             new[]
                                     {
                                         CSharpArgumentInfo.Create(
                                         CSharpArgumentInfoFlags.None, null)
                                     }) as GetMemberBinder;


            var callSite = CallSite<Func<CallSite, object, object>>.Create(new NoThrowGetBinderMember(name, false, defaultBinder));


            var result = callSite.Target(callSite, dynamicProvider);

            if (Object.ReferenceEquals(result, NoThrowExpressionVisitor.DUMMY_RESULT))
            {
                return false;
            }

            return true;

        }

      

    }

    class NoThrowGetBinderMember : GetMemberBinder
    {
        private GetMemberBinder m_innerBinder;        
        
        public NoThrowGetBinderMember(string name, bool ignoreCase, GetMemberBinder innerBinder) : base(name, ignoreCase)
        {
            m_innerBinder = innerBinder;            
        }
        
        public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
        {


            var retMetaObject = m_innerBinder.Bind(target, new DynamicMetaObject[] {});            
            
            var noThrowVisitor = new NoThrowExpressionVisitor();
            var resultExpression = noThrowVisitor.Visit(retMetaObject.Expression);

            var finalMetaObject = new DynamicMetaObject(resultExpression, retMetaObject.Restrictions);
            return finalMetaObject;

        }
        
    }

    class NoThrowExpressionVisitor : ExpressionVisitor
    {        
        public static readonly object DUMMY_RESULT = new DummyBindingResult();
        
        public NoThrowExpressionVisitor()
        {
            
        }

        protected override Expression VisitConditional(ConditionalExpression node)
        {
            
            if (node.IfFalse.NodeType != ExpressionType.Throw)
            {
                return base.VisitConditional(node);
            }
            
            Expression<Func<Object>> dummyFalseResult = () => DUMMY_RESULT;
            var invokeDummyFalseResult = Expression.Invoke(dummyFalseResult, null);                                    
            return Expression.Condition(node.Test, node.IfTrue, invokeDummyFalseResult);
        }

        private class DummyBindingResult {}       
    }
}

Proč se metoda HasProperty obejde bez vyvolání výjimky? Použil jsem trik, kdy objektu CallSite nepředávám přímo výchozí GetMemberBinder, ale vlastní NoThrowGetMemberBinder, který je potomkem bázové třídy GetMemberBinder z DLR. Můj NoThrowGetMember v kostruktoru přijímá další objekt GetMemberBinder, který interně použije pro zjištění hodnoty vlastnosti. Metoda HasProperty předává instanci NoThrowGetMember do konstruktoru tovární metodou Binder.CreateBinder vytvořený výchozí C# Binder, takže nemusíme v třídě NoThrowGetMember naštěstí duplikovat veškerou logiku pro přístup k vlastnosti, která je  již součástí výchozího C# Binderu.

NoThrowGetBinderMember se spoléhá na to, že při pokusu o přístup k dynamickým metodám a vlastnostem u objektu IDynamicMetaProvider třída GetMemberBinder dovoluje odvozeným třídám, aby aplikovaly vlastní logiku pro práci s “dynamickými” členy v tzv. “fallback” metodách. NoThrowGetBinderMember tedy dostane šanci dohledat vlastnost v přepsané metodě FallbackGetMember.

Metoda FallbackGetMember pracuje takto:

1. Použije metodu Bind předaného Binderu (m_innerBinder) , které předá jako první argument “DynamicMetaObject” v argumentu target a druhým argumentem je prázdné pole objektů “DynamicMetaObject”. Výchozí Binder udělá svou práci a vrátí nám další DynamicMetaObject, který si uložíme do proměnné retMetaObject.

var retMetaObject = m_innerBinder.Bind(target, new DynamicMetaObject[] {});
 

2. V retMetaObject je vyhodnocovací výraz (Expression tree) pro získání hodnoty vlastnosti, který pro vlastnost Name může vypadat zjednodušeně takto. Tučně je vyznačena část, která je odpovědná za vyvolání výjimky, jestliže vlastnost neexistuje.

IIF(ExpandoTryGetValue(Convert($arg0), value(System.Dynamic.ExpandoClass), 0, "Name", False, value), value, throw(new RuntimeBinderException("'System.Dynamic.ExpandoObject' does not contain a definition for 'Name'")))
IIF(ExpandoCheckVersion(Convert($arg0), value(System.Dynamic.ExpandoClass)), {var value; ... }, gotoCallSiteBinder.UpdateLabel)

My ale výjimku vyvolávat nechceme, a proto vlastním vizitorem NoThrowExpressionVisitor modifikujeme “expression tree” tak, že  místo vyvolání výjimky, když vlastnost neexistuje, vrátíme hodnotu statické proměnné  DUMMY_RESULT. Vlastnost Expression u proměnné retMetaObject je určena pouze pro čtení, proto vytvoříme nový DynamicMetaObject s upravenou “Expression” a původními restrikcemi a uložíme ho do proměnné finalMetaObject, která je také návratovou hodnotou metody FallbackGetMember.

 


            
            var noThrowVisitor = new NoThrowExpressionVisitor();
            var resultExpression = noThrowVisitor.Visit(retMetaObject.Expression);

            var finalMetaObject = new DynamicMetaObject(resultExpression, retMetaObject.Restrictions);
            return finalMetaObject;

Úplný kód třídy NoThrowExpressionVisitor naleznete ve výpisu výše.

Metoda HasProperty  vyvolá metodu Target na objektu CallSite a zkontroluje, zda její návratová hodnota je referenčně shodná s hodnotou v proměnné NoThrowExpressionVisitor.DUMMY_RESULT a pokud tomu tak je, vrátí false, protože nyní místo vyvolání výjimky byla vrácena zástupná hodnota signalizující “vlastnost u objektu dynamic neexistuje”, jinak vrátí true -  “vlastnost existuje”.

 

var result = callSite.Target(callSite, dynamicProvider);
if (Object.ReferenceEquals(result, NoThrowExpressionVisitor.DUMMY_RESULT))
            {
                return false;
            }
            
            

Použití metody HasProperty.

static void Main(string[] args)
        {
            dynamic testDynamicObject = new ExpandoObject();
            testDynamicObject.Name = "Testovaci vlastnost";            
            Console.WriteLine(HasProperty(testDynamicObject, "Name"));
            Console.WriteLine(HasProperty(testDynamicObject, "Id"));
            Console.ReadLine();
        }

/*Výsledek:
True 
False
*/

Zkoušel jsem metodu HasProperty použít i na zjišťování existence vlastnosti u potomků třídy DynamicObject pro zpracování rss a vše funguje dle očekávání.

“Challenge” pokořen. :-) Obvyklá poznámka na závěr – za nic neručím, kód nemusí fungovat v dalších verzích DLR, C# a .Net Frameworku, ale to vy určitě víte.:)



Monday, 23 August 2010 14:33:03 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | C# | LINQ | Programátorské hádanky


 Friday, 12 February 2010
Doplnění metod FillPie a DrawPie do objektu Graphics v Compact .Net Frameworku

Nedávno se na fóru vývojáře objevil dotaz, jak nahradit chybějící metodu FillPie v objektu Graphics na Compact .Net Frameworku, protože prý ani tradiční zuřivé googlování žádné výsledky nepřineslo. Zkusil jsem napsat implementaci metody FillPie, a protože se podobných dotazů na internetu dá najít více, dávám kód obohacený nyní i o metodu DrawPie na blog, aby nezůstal utopen jen v diskuzním fóru.

Compact .Net Framework sice nemá metodu FillPie ani DrawPie, ale má obecné metody DrawPolygon a FillPolygon, se kterými nakreslíte, co se vám zlíbí.  Zhýrale jsem kód opět trochu zlinqovatěl, asi začínám být na LINQu a extenzních metodách závislý. Inu, jak říkáme my C# vývojáři, původně odříkané extenzní metody plný zásobník volání. :-)

 

static class GraphicsExtensions
    {

        public static readonly float ANGLE_MULTIPLY = (float) Math.PI / 180;
        
        public static void FillPie(this Graphics g, SolidBrush brush, int x, int y, int width, int height, float startAngle,  float sweepAngle)
        {
            var tempPoints = GetTempPoints(sweepAngle, startAngle, x, y, width, height);

            g.FillPolygon(brush, tempPoints);
        }

        public static void DrawPie(this Graphics g, Pen pen, int x, int y, int width, int height, float startAngle, float sweepAngle)
        {
            var tempPoints = GetTempPoints(sweepAngle, startAngle, x, y, width, height);

            g.DrawPolygon(pen, tempPoints);
        }

        private static Point[] GetTempPoints(float sweepAngle, float startAngle, int x, int y, int width, int height)
        {
            const float HALF_FACTOR = 2f;
            const int TEMP_POINT_FIRST = 0;
            const int TEMP_POINT_LAST= 100;
            const int TOTAL_POINTS = TEMP_POINT_LAST - TEMP_POINT_FIRST;            
            

            float angleInc = (sweepAngle - startAngle) / TOTAL_POINTS;

            float halfWidth = width / HALF_FACTOR;

            float halfHeight= height / HALF_FACTOR;
            
            return (new[] {new Point
                               {
                                   X = x,
                                   Y = y
                               }
                          })
                                   
                .Union(
                               
                          (from i in Enumerable.Range(TEMP_POINT_FIRST, TOTAL_POINTS)
                           let angle = i == TEMP_POINT_LAST - 1? sweepAngle : startAngle + (i * angleInc)
                           select new Point
                                      {
                                          X = (int) (x + (Math.Cos(angle*(ANGLE_MULTIPLY))*(halfWidth))),
                                          Y = (int) (y + (Math.Sin(angle*(ANGLE_MULTIPLY))*(halfHeight)))
                                      })).ToArray();
        }
    }

 

Použití metod:

public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            using (var redBrush = new SolidBrush(Color.Red))
            using (var blueBrush = new SolidBrush(Color.Blue))
            using (var greenBrush = new SolidBrush(Color.ForestGreen))
            {
                e.Graphics.FillPie(redBrush, Width / 2, Height / 2, Width / 2, Height / 2, 0, 35f);
                e.Graphics.FillPie(blueBrush, Width / 2, Height / 2, Width / 2, Height / 2, 35f, 80f);
                e.Graphics.FillPie(greenBrush, Width / 2, Height / 2, Width / 2, Height / 2, 80f, 360f);                
            }

            using (var redPen = new Pen(Color.IndianRed))
            {
                e.Graphics.DrawPie(redPen, Width / 5, Height / 5, Width / 5, Height / 5, 0, 60f);
            }
        }
    }

 

A zde je náhled na formulář:

FillPieResult



Friday, 12 February 2010 13:17:54 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  Compact .Net Framework | LINQ


 Tuesday, 02 February 2010
Hrátky s Reaktivním frameworkem (RX extenze)

V předchozím článku jsem ukazoval, jak volat asynchronně metody z C# Posterous API v Silverlightu. C# Posterous API nabízí asynchronní zpracování pomocí jednoho z doporučovaného přístupu k asynchronním operacím v .Net Frameworku – metoda s konvenčním sufixem Async (LoadPostsAsync) spustí vykonání operace v jiném vlákně a výsledky operace jsou nabídnuty v argumentech události, která je (opět) jen dle jmenné konvence spojena s asynchronní operací (událost LoadPostsCompleted). C# Posterous API nenabízí ve svém rozhraní  metody pro podporu dalšího a již od verze 1.0 .Net Frameworku přítomného asynchronního vzoru, který je spojen s dvojicí metod začínajících prefixem Begin a End. (BeginGetRequest, EndGetRequest, BeginRead, EndRead apod.)

Dále předpokládám, že oba přístupy k vytváření asynchronních opreací znáte a že jste si vědomi i toho, jak se způsob práce s asynchronními API odlišuje od práce s běžnými synchronnními metodami.

V již odkazovaném článku bylo dobře patrné, jak je řízení toku asynchronních operaci odlišné od sady volání běžných synchronních operací.

Pro připomenutí:

posterousAccount.SitesLoaded += (o, e) =>
                      {
                          throwIfAsyncEx(e.Exception);
                          posterousAccount.PrimarySite.PostsLoaded += (_, e2) =>
                                                                          {
                                                                              throwIfAsyncEx(e2.Exception);
                                                                              Posts = (from p in e2.Value
                                                                                      select new ViewPost
                                                                                                 {
                                                                                                     Title = p.Title,
                                                                                                     Body = p.Body,
                                                                                                     Url = p.Url

                                                                                                 }).ToList();                                                                                                            
                                                                              
                                                                          };
                          posterousAccount.PrimarySite.LoadAllPostsAsync();
                      };



posterousAccount.LoadSitesAsync();

Jediné, co tento kód dělá, je, že nejprve (!) nahraje všechny blogy (příkaz k asynchronnímu nahrání posterousAccount.LoadSitesAsync(); je na posledním (!) řádku. Na prvním (!) řádku máme zpracování výsledku volání metody LoadSitesAsync, ve kterém opět nejdříve (!) lambdou přihlášenou k odběru události  PostsLoaded (posterousAccount.PrimarySite.PostsLoaded += (_, e2)) řekneme, jak zpracujeme výsledek následného (!) volání další asynchronní metody (posterousAccount.LoadSitesAsync());. Tato “inverzní“ práce s asynchronními metodami a zpracováním jejich výsledku je na hony a možná ještě dále vzdálena intuitivní práci se synchronními metodami.:-)

Zkusme se nyní podívat, jak by nám s “převrácením starších asynchronních metod z hlavy zpět na synchronní nohy” mohl pomoci RX Framework. Úplné základy v tomto článku nezazní a začátečníky odkazuji na sérii přednášek na Channel 9, kde dozvíte i zajímavé podrobnosti o genezi celého RX Frameworku  a matematické dualitě rozhraní IEnumerable a IObservable (jinými slovy o společných rysech dobře známých GoF návrhových vzorů Iterátor a Observer).

Současné příklady jsou vytvořeny v aplikaci Windows Forms pro .Net 3.5. Silverlight má své zvláštnosti a a rozchození příkladů v SL si zaslouží další článek, protože teď by řešení problémů specifických pro SL zamlžovalo cíl příkladu. Aplikace je pro .Net 3.5, protože stejná aplikace pro .Net 4.0 hlásí konflikt (ambiguous reference) mezi NF typy a RX typy.

Upozornění: Nic z toho, co napíšu neberte ani jako dogmata ani, nedej bože, jako best practices. RX Framework je v Betě, zdokumentován je mizerně a z jednoho řádku u každé metody se dá jen těžko bez dalších experimetů vytušit, co přesně metoda dělá. Tento článek je výsledkem hraní si pro účely jednoho projektu, kam se RX extenze hodí  a zjednodušují (alespoň to tak prozatím vypadá :-) ) dost rutinních činností.

Zde j výsledek našeho snažení, abychom měli motivaci se RX Frameworkem zabývat.

 var resultPosts = from sites in account.GetSites()
                              from site in sites.ToObservable()
                              from posts in site.GetPosts()
                              from post in posts.ToObservable()
                              where post.Private == false
                              select post;

Získání blogů (Sites) i blogpostů (post) je stále asynchronní, ale výsledný kód vypadá jako běžný LINQ (To Enumerable) dotaz. Žádné inverzní volání a práce s výsledkem, jen prostý dotaz, jehož zvláštností je pouze to, že v některých místech voláme metodu ToObservable.

Jak jsem dosáhl tohoto výsledku?

Podíváme-li se na první řádek, vidíme, že voláme metodu account.GetSites. Metoda GetSites součástí C# Posterous API není a jedná se o extenzní metodu. Tato extenzní metoda je zvláštní tím, že její návratovou hodnotou je je jedno z klíčových rozhraní v RX Frameworku – rozhraní IObservable<T>.

        public static IObservable<IEnumerable<IPosterousSite>> GetSites(this IPosterousAccount account)

Rozhrani IObservable má v RX Frameworku podobný význam jako rozhraní IEnumerable v celém .Net Frameworku.  Zjednodušeně můžeme rozhraní IObservable popsat jako ceduli, kterou třída implementující rozhraní dává celému světu najevo: “Miluju voyery, jestliže chcete sledovat, co se ve mně děje, dejte mi sem pozorovatele a já na sebe všechno podstatné, co se od této chvíle stane, postupně  vyzvoním ”.

Rozhraní IObservable je tedy příslib, že zainteresovaný pozorovatel dostane data, která třída podporující toto rozhraní nabízí. Svůj zájem pozorovatel deklaruje tak, že předá odkaz sám na sebe do metody Subscribe.

public interface IObservable<T> 
{
 IDisposable Subscribe(IObserver<T> observer); 
}

Pozorovatel (IObserver) reaguje (proto reaktivní framework) na informace, které jsou mu poskytnuty objektem podporujícím rozhraní IObservable.

public interface IObserver<T> 
{ 
void OnCompleted(); 
void OnNext(T value); 
void OnError(Exception exn); 
}

Metoda OnNext je na IObserver volána vždy, když Observable objekt má k dispozici další data. Metodou OnError Observable objekt signalizuje chyby a metodou OnCompleted Observeru říká “jsem u konce, nic dalšího už pro tebe nemám”.

Naše metoda GetSites tedy říká – zavolejte mě a já vám nabídnu IObservable objekt, který, až budou data k dispozici, vašemu observeru (IObserver) vydá kolekci (IEnumerable) objektů IPosterousSite.

Extenzní metoda GetSites vypadá takto:

 public static IObservable<IEnumerable<IPosterousSite>> GetSites(this IPosterousAccount account)
        {
             
            checkAccountNotNull(account);            
             var sitesEvents = Observable.FromEvent<EventArgsValue<IEnumerable<IPosterousSite>>>(handler => account.SitesLoaded += handler,
                                                                                                handler => account.SitesLoaded -= handler)
                                        .Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT);



             return sitesEvents.GetFinalObservableEvents(account.LoadSitesAsync);
            
        }

 

Po kontrole, zda předaný IPosterousAccount není null, využijeme pomocnou metodu Observable.FromEvent z RX Frameworku, která nám vrátí IObservable objekt. Tento IObservable objekt notifikuje případného observera o každé nastalé události sites.Loaded. V našem případě Observera notifikuje o právě jedné události, protože jsme použili metodu Take (Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT)) a konstanta DEFAULT_TAKE_EVENTS_COUNT má hodnotu 1. Jak si můžete všimnout, metoda FromEvent nám dovoluje s událostmi, které postupně nastávají, zacházet jako (s potenciálně nekonečnou) kolekcí hodnot. Metodě FromEvent jsme pouze museli říct, jaká třída nese argumenty událost (EventArgsValue<IEnumerable<IPosterousSite>) a poskytli jsme ji dva delegáty pro registraci/deregistraci obslužných handlerů, které nám RX Framework předá  (handler => account.SitesLoaded += handler, handler => account.SitesLoaded –= handler). U našeho volání metody Take bych ještě poznamenal, že po vyvolání první události dojde automaticky RX Frameworkem k deregistraci obslužného handleru.

Proměnná sitesEvents je IObserver tohoto typu.

IObservable<IEvent<EventArgsValue<IEnumerable<IPosterousSite>>>>

Argumenty události jsou vždy zabaleny do instance třidy IEvent, která je vydána zaregistrovanému observeru v jeho metodě OnNext. Všimněte si ale, že návratovou hodnotou metody GetSites je již Observable, který observeru předá hodnoty bez IEvent (IObservable<IEnumerable<IPosterousSite>>).

Vidíme, že na sitesEvents je volána další má extenzní metoda GetFinalObservableEvents, které je předán delegát Action ukazující na asynchronní metodu account.LoadSitesAsync, a výsledek volání GetFinalObservableEvents je vrácen klientovi.

Metoda GetFinalObservableEvents:

  public static IObservable<TEventData> GetFinalObservableEvents<TEventData>(this IObservable<IEvent<EventArgsValue<TEventData>>> sourceEvents, Action runAction)
        {
            if (sourceEvents == null)
            {
                throw new ArgumentNullException("sourceEvents");
            }
            var retObservable = new DelegateObservable<TEventData>(
                observer =>
                    {
                        
                        var eventObserver = new EventObserver<TEventData>(observer);
                        var unsubScribe = sourceEvents.Subscribe(eventObserver);
                        runAction();
                        return unsubScribe;
                    });

            return retObservable;
        }

 

Metoda GetFnalObservableEvents vrací opět Observable, ale tentokrát jde o Observable typu IObservable<TEventData>  - jinými slovy, v našem případě IObservable<IEnumerable<IPosterousSite>>. Jak je toho dosaženo? Zdrojový IObservable objekt nazvaný sourceEvents je předán instanci třídy DelegateObservable, což je v současném scénaři již ten hledaný Observable podporující rozhraní IObservable<IEnumerable<IPosterousSite>>. DelegateObservable je tedy adaptér, který převádí události zabalené do IEvent na “rozbalené” hodnoty očekávané observerem. DelegateObservable je můj pomocný IObservable, který dostává do konstruktoru lambdu představující tělo jeho metody Subscribe, abychom nemuseli reimplementovat rozhraní IObservable v různých třídách stále dokola.

Výpis třídy DelegateObservable

 public class DelegateObservable<T> : IObservable<T>
    {
        private readonly Func<IObserver<T>, IDisposable> m_subscribeDel;

        public DelegateObservable(Func<IObserver<T>, IDisposable> subscribeDelegate)
        {
            m_subscribeDel = subscribeDelegate;
            if (m_subscribeDel == null)
            {
                throw new ArgumentNullException("subscribeDelegate");
            }
        }

        #region Implementation of IObservable<out T>

        public IDisposable Subscribe(IObserver<T> observer)
        {
            if (observer == null)
            {
                throw new ArgumentNullException("observer");
            }
            
           return m_subscribeDel(observer);
        }

        #endregion
    }

Předaná lambda v našem případě vytvoří instanci třídy eventsObserver, což je observer, který bude zpracovávat přicházející události, a do konstruktoru mu podhodí observer předaný klientským kódem – eventsObserver je tedy další adaptér, který je zdopovědný za “rozbalení” dat z instance IEvent a za předání těchto dat klientskému (“konečnému”) observeru.

Třída EventObserver:

 public class EventObserver<T> : IObserver<IEvent<EventArgsValue<T>>>                        
    {
        private readonly IObserver<T> m_innerObserver;
        private bool m_exceptionOccured;

        public EventObserver(IObserver<T> innerObserver)
        {
            if (innerObserver == null)
            {
                throw new ArgumentNullException("innerObserver");
            }
            
            m_innerObserver = innerObserver;
            m_exceptionOccured = false;
        }

        #region Implementation of IObserver<T>
        

        public virtual void OnNext(IEvent<EventArgsValue<T>> eventData)
        {
            if (eventData.EventArgs.Exception != null)
            {
                OnError(eventData.EventArgs.Exception);
                return;
            }
            //Rozbalení a předání dat Observeru
            m_innerObserver.OnNext(eventData.EventArgs.Value);
        }

        public virtual void OnError(Exception exception)
        {
            m_innerObserver.OnError(exception);
            m_exceptionOccured = true;
        }

        public virtual void OnCompleted()
        {
           //Chyba ukončí sekvenci sama o sobě
            if (!m_exceptionOccured)
            {
                m_innerObserver.OnCompleted();                
            }
        }

Třída EventObserver implementuje rozhraní IObservable s těmito generickými argumenty - IObserver<IEvent<EventArgsValue<T>>> . V C# Posterous API všechny události předávají svá data v instanci třídy EventArgsValue<T>, což znamená, že pro naše účely je EventObserver univerzálně použitelný observer pro zpracování výsledků asynchronní operace.

Pro úplnost zde je výpis třídy EventArgsValue

 public class EventArgsValue<T> : EventArgs
    {
        private readonly T m_value;
        private readonly Exception m_exception;

        internal EventArgsValue(T value, Exception exception)
        {
            m_value = value;
            m_exception = exception;
        }
            
        public T Value
        {
            get
            {
                return m_value;
            }
        }

        public Exception Exception
        {
            get
            {
                return m_exception;
            }
        }
    }

V lambdě předané do objektu DelegateObservable také musíme spustit asynchronní operaci – to je volání Action delegáta runAction, který nám předala již metoda GetSites. Každý IObservable také z metody vrací objekt implementující rozhraní IDisposable – volání metody Dispose dovoluje klientovi odpojit se objektu IObservable. Lambda  předaná do instance DelegateObservable vrátí  IDisposable objekt, který je vydán po připojení EventObservera k “streamu události“ (sourceEvents).

Přidání extenzních metod k dalším třídám je triviální – zde je extenzní metoda pro IPosterousSite, která nahraje všechny blogposty.

public static IObservable<IEnumerable<IPosterousPost>> GetPosts(this IPosterousSite site)
        {            

            throwIfSiteNull(site);
            var postsEvents = Observable.FromEvent<EventArgsValue<IEnumerable<IPosterousPost>>>(handler => site.PostsLoaded += handler,
                                                                                               handler => site.PostsLoaded -= handler)
                                      .Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT);
            

            return postsEvents.GetFinalObservableEvents(site.LoadAllPostsAsync);
            
            

        }
 
 

A nyní se můžeme znovu podívat, jak naše API použijeme v klientském kódu – díky alternativní implementaci Query vzoru v RX frameworku  můžeme používat staré dobré známé LINQ dotazy.

Metoda loadPosts:

private void loadPosts()
        {
            toolStripStatusLabel1.Text = TEXT_LOAD_DATA_START;

            IPosterousApplication app = PosterousApplication.Current;
            IPosterousAccount account = app.GetPosterousAccount("<Posterous user name>", "Posterous password");

            var syncContext = SynchronizationContext.Current;

            var resultPosts = from sites in account.GetSites()
                              from site in sites.ToObservable()
                              from posts in site.GetPosts()
                              from post in posts.ToObservable()
                              where post.Private == false
                              select post;

            resultPosts.Subscribe(post =>
                                      {
                                          lock (m_threadsSet)
                                          {
                                              m_threadsSet.Add(Thread.CurrentThread.ManagedThreadId);
                                          }


                                          syncContext.Post(_ =>
                                                               {
                                                                   var UCpost = new UC_Post
                                                                                    {
                                                                                        Title = post.Title,
                                                                                        Body = post.Body
                                                                                    };

                                                                   flowLayoutPanel1.Controls.Add(UCpost);
                                                               },
                                                           null

                                              );
                                      },

                                  ex => syncContext.Post(_ =>
                                                             {

                                                                 throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex);
                                                             },

                                                         null),

                                  () => syncContext.Post(_ =>
                                                             {
                                                                 lock (m_threadsSet)
                                                                 {
                                                                     m_threadsSet.Run(tId => lstThreads.Items.Add(tId));
                                                                 }

                                                                 toolStripStatusLabel1.Text = TEXT_LOAD_DATA_END;
                                                             },
                                                         null

                                            ));



        }

V proměnné resultPosts jsou uloženy všechny blogposty (IPosterousPost) ze všech blogů (IPosterousSite). Blogy i blogposty jsou nahrány asynchronně, ale v klientském kódu nevidíme žádná specialitky kvůli asynchronnímu nahrávání dat. Na proměnných sites i posts v dotazu je volána další extenzní metoda z RX Frameworku ToObservable, protože jak víme, výsledkem volání asynchronních metod byly typy IEnumerable<IPosterousPost> a IEnumerable<IPosterousSite>.

Důležité je, že zpracování dotazu je opět “lazy” – to znamená, že k získání dat dojde až poté, co k resultsPosts zaregistruju svého Observera metodou Subscribe (analogie “Lazy” vyhodnocování v LINQ To IEnumerable a procházení dotazu v cyklu foreach). Metoda Subscribe má několik variant a jedna z nich nám dovoluje pro metody OnNext, OnError a OnCompleted předat delegáty, aniž bychom byli nuceni vytvářet svou třídu implementující rozhraní IObserver.

První delegát (OnNext) vezme předaný post a vytvoří pro něj UserControl, který vloží do FlowPanelu na formuláři. Ještě předtím pro zajímavost do Hashsetu ukládám identifikátory vláken, které se na zpracování dotazu podílí. S prvky na formuláři můžeme pracovat jen z UI threadu, a proto je vložení User controlu provedeno přes SynchronizationContext uložený do proměnné syncContext před spuštěním dotazu.

Druhý delegát (OnError)  pouze přes SynchronizationContext zpropaguje výjimku, která nastala při asynchronním zpracování, do UI threadu. Všimněte si, jak je zpracování výjimek jednoduché – rozdíl vynikne při srovnání s opakovaným voláním metody throwIfAsyncEx v kódu na začátku tohoto článku.

Třetí delegát (OnCompleted) naplní listbox na formuláři ID použitých threadů a změní text ve status baru.

Zde je výsledný formulář. V listboxu nahoře si můžete všimnout, že u mě byly k vykonání dotazu použity celkem 3 thready.

 

AsyncForm

 

Tím bychom mohli skončit, ale RX Framework má pro asynchronní operace ještě další zajímavou podporu. Pomocí metody Observable.FromAsyncPattern můžeme vytvořit IObservable rychle a bezpracně ze standardního a výše již zmíněného asynchronního Begin/End vzoru. V C# Posterous API metody Begin*/End* nejsou, proto je zkusme dodat pomocí extenzních metod.

Rozhraní IPosterousAccount bude obohaceno o extenzní metody BeginLoadSites a EndLoadSite.

Metoda BeginLoadSites

public static IAsyncResult BeginLoadSites(this IPosterousAccount account, AsyncCallback callback, object context)
        {
            checkAccountNotNull(account);
            var loadSiteAction = new Action(account.LoadSites);
            
            return RXEventsHelper.GetAsyncResultEx(loadSiteAction, callback, context);
       
            
        }

Jak vidíte, přesně dle konvencí .Net vzoru metoda vrací odkaz na rozhraní IAsyncResult a přijímá callBack, což je tedy u tohoto vzoru metoda, která má být vyvolána po dokončení asynchronního zpracování, a jak také vzor vyžaduje, posledním argumentem je libovolný objekt reprezentující libovolný “stavový token” operace, který v metodě End* klient používá pro korelací mezi požadavkem a odpovědí.

Veškerá práce je přenesena na metodu GetAsyncResultEx v mém RXEventsHelperu – metoda vyžaduje, abyste ji poslali v delegátu Action metodu, která má být spuštěna asynchronně.

Metoda RXEventsHelper.GetAsyncResultEx.

 public static IAsyncResult GetAsyncResultEx(Action runAction, AsyncCallback callback, object context)
        {
            if (runAction == null)
            {
                throw new ArgumentNullException("runAction");
            }

            Exception ex = null;
            
            var proxyCallback = new AsyncCallback(ar =>
                                                      {
                                                          IAsyncResult proxyResult = new AsyncResultEx(ar, runAction);                                                                                         
                                                           callback(proxyResult);
                                                      });

            return runAction.BeginInvoke(proxyCallback, context);
                      

            
                       
        }

Hlavním trikem je využití možností delegátů – každý delegát v .Net Frameworku vždy obsahuje asynchronní metody BeginInvoke a EndInvoke, které splňují nároky asynchronního vzoru. My tedy na předaném delegátu runAction zavoláme metodu BeginInvoke, ale místo klientské callBack Funkce podhodíme svou proxy funkci (proxyCallback), která po dokončení asynchronního volání připraví pro naši End metodu vlastní IAsyncResult (AsyncResultEx).

Třída AsyncResultEx zapouzdřuje původní IAsyncResult  (argument ar předaný  do konstruktoru v předešlém výpisu) a navíc, když na její instanci zavoláme metodu EndAction, na předaném delegátovi (argument runAction v předešlém výpisu) je zavolána metoda EndInvoke, čehož využije naše metoda EndLoadSites.

Třída AsyncResultEx

 public class AsyncResultEx : IAsyncResult
    {
        
        #region private variables
        private IAsyncResult m_originalAsyncResult;
        private readonly Action m_originaldelegate;
        #endregion private variables

        public AsyncResultEx(IAsyncResult origAsyncResult, Action originaldelegate)
        {
            if (origAsyncResult == null)
            {
                throw new ArgumentNullException("origAsyncResult");
            }


            m_originalAsyncResult = origAsyncResult;
            m_originaldelegate = originaldelegate;
        }

        #region properties
        public virtual IAsyncResult OriginalAsyncResult
        {
            get
            {
                return m_originalAsyncResult;
            }

        }

        public virtual Action OriginalDelegate
        {
            get
            {
                return m_originaldelegate;
            }
        }
        
        #endregion properties

        #region methods

        public virtual void EndAction()
        {
            
            if (OriginalDelegate != null)
            {
                OriginalDelegate.EndInvoke(OriginalAsyncResult);
            }                                                

        }
            
        #endregion methods
        #region Implementation of IAsyncResult

        
        public virtual bool IsCompleted
        {
            get
            {
                return m_originalAsyncResult.IsCompleted;
            }
        }

        public virtual object AsyncState
        {
            get
            {
                return m_originalAsyncResult.AsyncState;
            }
        }

        public virtual WaitHandle AsyncWaitHandle
        {
            get
            {
                return m_originalAsyncResult.AsyncWaitHandle;
            }
        }

        public virtual bool CompletedSynchronously
        {
            get
            {
                return m_originalAsyncResult.CompletedSynchronously;
            }
        }
                
        #endregion
    }

Extenzní metoda EndLoadSites

 public static IEnumerable<IPosterousSite> EndLoadSites(this IPosterousAccount account, IAsyncResult result)
        {
            checkAccountNotNull(account);
            
            var exResult = result as AsyncResultEx;
            
            if (exResult == null)
            {
                throw new ArgumentException("result");
            }
            
            exResult.EndAction();
                                               
            return account.Sites;
        }

Zde vidíme volání metody EndAction na podhozené instanci AsyncResultE. Poté metoda EndLoadSites jen vrátí kolekci Sites objektu account, protože ta  nyní již musí být po asynchronním volání naplněna daty.

Se stávající infrastrukturou si opět si můžeme rychle připravit další Begin a End metody. Zde jsou extenzní metody BeginLoadPosts a EndLoadPosts pro IPosterousSite.

public static IAsyncResult BeginLoadPosts(this IPosterousSite site, AsyncCallback callback, object context)
        {
            throwIfSiteNull(site);
            var loadPostsAction = new Action(site.LoadAllPosts);
            
            return RXEventsHelper.GetAsyncResultEx(loadPostsAction, callback, context);
        }

        public static IEnumerable<IPosterousPost> EndLoadPosts(this IPosterousSite site, IAsyncResult result)
        {
            throwIfSiteNull(site);
            var exResult = result as AsyncResultEx;

            if (exResult == null)
            {
                throw new ArgumentException("result");
            }

            exResult.EndAction();

            return site.Posts;
        }

 

A metoda loadPosts2, která dělá to samé, co předchozí metoda loadPosts, ale používá naše nové extenzní Begin/End metody.

 private void loadPosts2()
        {
            toolStripStatusLabel1.Text = TEXT_LOAD_DATA_START;

            IPosterousApplication app = PosterousApplication.Current;
            IPosterousAccount account = app.GetPosterousAccount("posterousname", "posterouspassword");

            var syncContext = SynchronizationContext.Current;            


            var resultPosts = from sites in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)())
                              from site in sites.ToObservable()
                              from posts in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)())
                              from post in posts.ToObservable()
                              where post.Private == false
                              select post;

            resultPosts.Subscribe(post =>
                                      {
                                          lock (m_threadsSet)
                                          {
                                              m_threadsSet.Add(Thread.CurrentThread.ManagedThreadId);
                                          }


                                          syncContext.Post(_ =>
                                                               {
                                                                   var UCpost = new UC_Post
                                                                                    {
                                                                                        Title = post.Title,
                                                                                        Body = post.Body
                                                                                    };

                                                                   flowLayoutPanel1.Controls.Add(UCpost);
                                                               },
                                                           null

                                              );
                                      },


                                  ex => syncContext.Post(_ =>
                                                             {
                                                                 
                                                                 throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex);
                                                             },

                                                         null),

                                  () => syncContext.Post(_ =>
                                                             {
                                                                 lock (m_threadsSet)
                                                                 {
                                                                     m_threadsSet.Run(tId => lstThreads.Items.Add(tId));
                                                                 }

                                                                 toolStripStatusLabel1.Text = TEXT_LOAD_DATA_END;
                                                             },
                                                         null

                                            )


                );


        }

Upozornil bych jen na dvě specialitky či zrádná místa, která (alespoň v této BETA verzi RX) dělají kód méně intuitvním, než by bylo žádoucí:

Jedná se o tyto dva řádky:

from sites in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)())  
from posts in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)()) 

Metoda FromAsyncPattern přijímá delegáty na naše asynchronní metody, ale místo spolehnutí se na typovou inference jsem musel generický argument předat explicitně(Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>) – pokud argument nezadáte, kompilátor hlásí “ambigous reference”.

Dále je patrné, že výsledek funkce FromAsyncPattern, kterým je další funkce vracející IObservable, je předán jako argument metodě Observable.Defer. Metoda Observable.Defer zajistí, že k vyhodnocení předané funkce dojde až poté, co je k výsledkům dotazu přihlášen observer – jinými slovy, metoda Defer nám pomáhá zachovat “lazy” vyhodnocení dotazu.

Dotaz bude fungovat i v této podobě (bez Defer):

var resultPosts = from sites in  Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)()
                              from site in sites.ToObservable()
                              from posts in Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)()
                              from post in posts.ToObservable()
                              where post.Private == false
                              select post;

Ale jeho vyhodnocení už není “lazy”. Všimněte si závorek na konci výrazu - FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)()  - výsledné IObservable získám okamžitým zavoláním funkce vrácené z metody FromAsyncPattern. Vadit vám to začne v okamžiku, kdy zkonstruujete dotaz, ihned se odpálí asynchronní volání, dojde k chybě a vy budete mít v aplikaci neošetřenou výjimku v threadu na pozadí, protože IObserver ještě není přihlášen (není možné zavolat druhého delegáta - ex => syncContext.Post(_ => { throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex); }, null), ).

 

Snad se vám tato exkurze líbila. Já ještě na RX Framework konečný názor nemám, ale něco neodbytného ve mně říká, že by mohlo jít o další LINQ, který otřese programátorským světem. :-) Některé extenze pravděpodobně zahrnu do samostatného jmenného prostoru v C# Posterous API.



Tuesday, 02 February 2010 07:43:00 (Central Europe Standard Time, UTC+01:00)       
Comments [3]  .NET Framework | C# Posterous API | LINQ | Návrhové vzory | RX Extensions | Windows Forms


 Wednesday, 20 January 2010
Ukázka práce s Posterous API – zálohování blogu

Stáhnout výsledné exe -RSPosterousBackup.

Po jednoduchém přehledu možností mého C# Posterous API wrapperu se nyní podíváme, jak se dá API použít k zálohování vašeho blogu. Pro účely tohoto článku předpokládám, že jste úvodní článek o API wrapperu četli.

Zálohovač blogu (RSPosterousBackup.exe) je jednoduchá konzolová aplikace, které stačí předat uživatelské jméno (parametr –u)  a heslo (parametr –p)  vašeho účtu na Posterous a také adresář vašem počítači (parametr bd), do kterého chcete blog zazálohovat.

Jednoduchá ukázka:

RSPosterousBackup.exe -u:rene@renestein.net -p:mojeheslo -bd:c:\_Archiv\PosterousBackup

Blog uživatelského účtu reneATrenestein.net s heslem mojeheslo bude zazálohován do adresáře c:\_Archiv\PosterousBackup.

Program referencuje samozřejmě knihovnu RStein.Posterous.API a pro (své, uznávám :-)) pobavení jsem také použil RX for .Net Framework 3.5 SP1. Z RX Frameworku jsou referencovány assembly System.CoreEx, System.Interactive a System.Threading. Nejzajímavější na RX Frameworku je, že pro verzi 3.5 .Net Frameworku zpřístupňuje Parallel Linq – PLINQ, který je součástí připravovaného .Net Frameworku 4.0.

Ještě upozornění – v žádném případě nechci tvrdit, že kód, který uvidíte, využívá PLINQ správným způsobem. Program je jen pískovištěm, na kterém jsem si zkoušel a ověřoval, co RX a PLINQ umí a jak vypadá výsledný kód.

Po spuštění RSPosterousBackup.exe funkce Main pouze předá argumenty z příkazové řádky metodě BackupData v třídě BackupEngine, která představuje výkonný mozek celého zálohovače Posterous blogu.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace RSPosterousBackup
{
    class Program
    {
        static void Main(string[] args)
        {
            var engine = new BackupEngine();
            engine.BackupData(args);
            Console.ReadLine();
        }
    }
}

 

Zde je třída BackupEngine

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using RStein.Posterous.API;

namespace RSPosterousBackup
{
    public class BackupEngine
    {
        #region constants
        public static readonly string POSTEROUS_USER = "-u";
        public static readonly string POSTEROUS_PASSWORD = "-p";
        public static readonly string BACKUP_DIRECTORY= "-bd";
        public static readonly string HELP_KEY = "-?";
        public static readonly string HELP_ALT_KEY = "-h";
        public static readonly string POSTEROUS_FILE_EXTENSION = ".posterous";
        public static readonly string POSTEROUS_MEDIA_FORMAT = "Media_{0}";
        public static readonly string POSTEROUS_COMMENT_FORMAT = "Comment_{0}";
        public static readonly string POSTEROUS_SITE_FORMAT = "Site_{0}";
        #endregion constants
        #region private variables        
        #endregion private variables

        #region constructors
        public BackupEngine()
        {
        }
        #endregion constructors
        
        public void BackupData(string[] commandLine)
        {
            if (commandLine == null)
            {
                throw new ArgumentNullException("commandLine");
            }

            var parser = new CommandLineParser();
            Dictionary<string, string> cmSwitches = parser.Parse(commandLine);

            Debug.Assert(cmSwitches != null);

            if (cmSwitches.Keys.Any(key => (key.Equals(HELP_KEY, StringComparison.OrdinalIgnoreCase)) ||
                                       (key.Equals(HELP_ALT_KEY,StringComparison.OrdinalIgnoreCase))))
            {
                logMessage(GlobalConstants.CMDLINE_SHOW_USAGE);
                return;
            }

            if (!checkSwitches(cmSwitches))
            {
                logMessage(GlobalConstants.CMDLINE_SHOW_USAGE);
                return;                
            }

            backupDataInner(cmSwitches);
        }

        private void backupDataInner(Dictionary<string, string> cmSwitches)
        {
            string pUser = cmSwitches[POSTEROUS_USER];
            string pPassword = cmSwitches[POSTEROUS_PASSWORD];
            string backupDir = cmSwitches[BACKUP_DIRECTORY];
            try
            {
                IPosterousAccount account = PosterousApplication.Current.GetPosterousAccount(pUser, pPassword);
                if (!Directory.Exists(backupDir))
                {
                    Directory.CreateDirectory(backupDir);
                }

                var currDirBackupName = new StringBuilder(DateTime.Now.ToLocalTime().ToString());
                currDirBackupName.ToSafeFileName();
                
                
                string currentBackupDir = Path.Combine(backupDir, currDirBackupName.ToString());
                account.Sites.Run(site =>
                                     {
                                         string sitePath = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
                                         Directory.CreateDirectory(sitePath);
                                     });


                var processedPosts = (from site in account.Sites.AsParallel()
                                      from post in site.Posts.AsParallel()
                                         .Do(postPost =>
                                                  Console.WriteLine(String.Format(GlobalConstants.POST_BACKUP_MESSAGE_FORMAT,
                                                                         postPost.Title,
                                                                         Thread.CurrentThread.ManagedThreadId)))
                                        
                                         .Do(postPost =>
                                                 {                                                  
                                                     string siteDir = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
                                                     
                                                     var titlBuilder = new StringBuilder(postPost.Title + postPost.Id.ToString());
                                                     titlBuilder.ToSafeFileName();

                                                     string postDirName = Path.Combine(siteDir,
                                                                                       titlBuilder.ToString()
                                                                                       );
                                                     
                                                     Directory.CreateDirectory(postDirName);
                                                     string postFileName = Path.Combine(postDirName, titlBuilder.ToString() + POSTEROUS_FILE_EXTENSION);
                                                     using (var fileStream = File.Open(postFileName, FileMode.Create))
                                                     using (var stremWriter = new StreamWriter(fileStream, Encoding.UTF8))
                                                     {
                                                         stremWriter.Write(postPost.Body);
                                                     }

                                                     postPost.Media
                                                         .AsParallel()
                                                         .Run(media =>
                                                                 {
                                                                                                             
                                                                     var mediaName = new StringBuilder(String.Format(POSTEROUS_MEDIA_FORMAT, Guid.NewGuid()));
                                                                     mediaName.ToSafeFileName();
                                                                     string mediaFile = Path.Combine(postDirName, mediaName.ToString());


                                                                     using (var fileStream = File.Open(mediaFile, FileMode.Create))
                                                                     {


                                                                         media.Content.CopyToStream(fileStream);

                                                                     }
                                                                 });


                                                     postPost.Comments
                                                         .AsParallel()                                                         
                                                         .Run(comment =>
                                                                 {
                                                                                                             
                                                                     var commentFileName = new StringBuilder(String.Format(POSTEROUS_COMMENT_FORMAT, Guid.NewGuid()));
                                                                     commentFileName.ToSafeFileName();
                                                                     string commentFile = Path.Combine(postDirName, commentFileName.ToString());


                                                                     using (var fileStream = File.Open(commentFile, FileMode.Create))
                                                                     using (var stremWriter = new StreamWriter(fileStream, Encoding.UTF8))
                                                                     {


                                                                         stremWriter.WriteLine(comment.Author.Name);
                                                                         stremWriter.Write(comment.Body);

                                                                     }
                                                                 });
                                                 })

                                     select post).ToList();





                logMessage(String.Format(GlobalConstants.FINAL_BACKUP_MESSAGE_FORMAT, processedPosts.Count));
                
                
            }
            catch (Exception e)
            {
                 Trace.WriteLine(e);
                 Console.WriteLine(e);                 
            }
           
            
        }

        private bool checkSwitches(Dictionary<string, string> cmSwitches)
        {            

            return ckeckIfSwitchNullOrEmpty(POSTEROUS_USER, cmSwitches) &&
                    ckeckIfSwitchNullOrEmpty(POSTEROUS_PASSWORD, cmSwitches) &&
                    ckeckIfSwitchNullOrEmpty(BACKUP_DIRECTORY, cmSwitches);
            
        }

        private bool ckeckIfSwitchNullOrEmpty(string switchKey, Dictionary<string, string> cmSwitches)
        {
            string val;
            cmSwitches.TryGetValue(switchKey, out val);
            if (String.IsNullOrEmpty(val))
            {
                logMessage(String.Format(GlobalConstants.INVALID_CML_SWITCH_FORMAT_STRING_EX, switchKey));
                return false;
            }
            return true;
        }

        private void logMessage(string message)
        {
            
            Console.WriteLine(message);
        }
    }
}

Třída BackupEngine nejdříve v metodě BackupData pomocí instance třídy CommandLineParser rozpársuje příkazový řádek a voláním pomocné metody checkSwitches zkontroluje, zda byly předány všechny vyžadované parametry. Jestliže nějaký parametr chybí nebo byl zadán parametr pro zobrazení nápovědy (-h, –?), program zobrazí nápovědu a k zálohování nedojde.

Ihned po dokončení všech předběžných kontrol je volána privátní metoda backupDataInner, která je odpovědná za zálohování blogu. Metoda backupDataInner získá odkaz na Posterous účet (PosterousApplication.Current.GetPosterousAccount(pUser, pPassword); a poté pro každou Site (samostatný blog) založí nový podadresář v adresáři, jehož názvem je aktuální lokální datum a čas a který je vytvořen v adresáři předaném v parametru –bd uživatelem. 

Adresářová struktura pro každý zálovaný blog:

<adresář určený –bd přepínačem>\<adresář  - názvem je aktuální datum a čas>\Site_<Site Id>

Příklad adresářové struktury:

"c:\_Archiv\PosterousBackup\20.1.2010 15_57_07\Site_851694"

Zálohován tedy není jeden blog, ale všechny blogy, které jsou asociovány s daným posterous účtem.

Můžete si všimnout, že pro založení podaresáře používvám jednu z RX extenzních metod – metodu Run.

                account.Sites.Run(site =>
                                     {
                                         string sitePath = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
                                         Directory.CreateDirectory(sitePath);
                                     });

Metoda Run je náhradou za extenzní metodu ForEach, kterou jste si dříve museli sami dopsat nebo jste byli nuceni použít metodu ForEach ve třídě List takto.

account.Sites.ToList().ForEach(site => {//zbytek kódu lambdy identický s kódem výše…}); 



Zdůrazním, že metoda Run vykoná nějaký vedlejší efekt nad každým elementem v IEnumerable, tedy v našem případě založí adresář, a dále již s elementy nepracuje – vrací void. Za chvíli uvidíme metodu, která nám umožní to samé, co metoda Run, ale elementy pošle po “vykonání vedlejšího efektu nad elementem” (tedy spuštění námi předané funkce jakou je např. lambda  pro založení adresáře) ke zpracování dalším LINQ extenzním metodám.

Signatura metody Run:

public static void Run<TSource>(
    this IEnumerable<TSource> source,
    Action<TSource> action
)

Dále do proměnné processedPosts uložíme pomocí speciálního a pro naši kratochvíli jediného LINQ dotazu všechny zpracované blogposty (instance podporující rozhraní IPosterousPost). Jak vidíte, stačí se přes Posterous API dotázat do kolekce Sites (blogy) a poté z každého blogu zpracovat všechny blogspoty.

from site in account.Sites.AsParallel()
                                      from post in site.Posts.AsParallel()…

Na více místech v dotazu si můžete všimnout volání extenzní metody AsParalllel, kterým dáváte najevo, že zpracování jednotlivých blogpostů a také médií (IPosterousMedium) a komentářů (IPosterousComment) může proběhnout ve více vláknech – o detaily se ale postará PLINQ, vy sami žádná nová vlákna nespouštíte ani nespravujete.

Můžete si také všimnout, že na několika místech volám extenzní metodu Do .Metoda Do pracuje podobně jako před chvílí zmiňovaná metoda Run. Na každý element v zdrojové kolekci aplikuje předanou funkci, ale poté na rozdíl od metody Run element předá k dalšímu zpracování.

Signatura metody Do:

public static IEnumerable<TSource> Do<TSource>(
	this IEnumerable<TSource> source,
	Action<TSource> action
)
 

Zde vypíšeme přes RX extenzní metodu Do titulek právě zpracovávaného blogpostu a Id vlákna, které blogspot zpracovává. Tato metoda je zde jen na ukázku, že každý element ve zdrojové kolekci je metodou Do předán dále ke zpracování

 .Do(postPost =>
                                                  Console.WriteLine(String.Format(GlobalConstants.POST_BACKUP_MESSAGE_FORMAT,
                                                                         postPost.Title,
                                                                         Thread.CurrentThread.ManagedThreadId)))

Na metodu Do navazuje další metoda Do, ve které proběhne zpracování každého blogspotu. V adresáří každé Site (blogu) je vytvořen pro každý blogspot vytvořen nový podadresář, jehož názvem je titulek (vlastnost Title) společně s  Id blogpostu. Do tohoto podadresáře je uložen text blogspotu. Soubor s textem blogspotu má příponu posterous a také jsou do podadresáře uložena média (zvukové, obrazové a video soubory) a všechny komentáře k blogspotu.

Hlavní část programu je za námi. Zde jsou ještě výpisy pomocných tříd.

Třída CommandLineParser pro pársování hodnot předaných uživatelem v příkazovém řádku.

using System;
using System.Collections.Generic;
using System.Linq;

namespace RSPosterousBackup
{
    public class CommandLineParser
    {
        #region constants

        public static readonly char COMMAND_PARTS_SEPARATOR = '-';
        public static readonly char COMMAND_VALUE_SEPARATOR = ':';
        public const int MIN_KEY_PARTS = 1;
        public const int MAX_KEY_PARTS = 2;
        #endregion constants
        
        #region constructors
        public CommandLineParser()
        {
        }
        #endregion constructors
        #region methods
        public virtual Dictionary<string, string> Parse (string[] commandLine)
        {
            if (commandLine == null)
            {
                throw new ArgumentNullException("commandLine");
            }
            
            return parseInner(commandLine);
        }

        private Dictionary<string, string> parseInner(string[] commandLine)
        {            
            var dict = (from part in commandLine
                        let keyValuePair = part.Split(new[] {COMMAND_VALUE_SEPARATOR}, MAX_KEY_PARTS)
                        select new
                                   {
                                       Key = keyValuePair.First().Trim().ToLower(),
                                       Value = keyValuePair.Length > MIN_KEY_PARTS ? keyValuePair.Last().Trim() : String.Empty
                                   }).ToDictionary(kv => kv.Key, kv => kv.Value);

            return dict;
        }

        #endregion methods
    }
}

Konstanty

namespace RSPosterousBackup
{
    public static class GlobalConstants
    {
        public static readonly string INVALID_CML_SWITCH_FORMAT_STRING_EX = "Invalid switch {0}";
        public static readonly char SAFE_FILE_PATH_CHAR = '_';        
        public static readonly string POST_BACKUP_MESSAGE_FORMAT = "Processing post: {0}  - in thread {1}";
        public static readonly string FINAL_BACKUP_MESSAGE_FORMAT = "Total posts: {0}";
        public static readonly string CMDLINE_SHOW_USAGE =
@"Usage: 
RSPosterousBackup.exe -u:<posterous user name> p:<posterous password> -bd:<backup directory>
Example:RSPosterousBackup.exe -u:GIC@Roma.com p:Rubicon -bd:c:\PosterousBackup
RSPosterousBackup.exe -?  - show this help";
                                                                 
    }
}

Třída StringBuilderExtensions s extenzní metodou ToSafeFileName, která v navrhovaném jménu souboru nahradí nepovolené znaky podtržítkem.

using System.IO;
using System.Linq;
using System.Text;

namespace RSPosterousBackup
{
    public static class StringBuilderExtensions
    {
        public static void ToSafeFileName(this StringBuilder builder)
        {
            Path.GetInvalidFileNameChars().Run(ch => builder.Replace(ch, GlobalConstants.SAFE_FILE_PATH_CHAR));
        }
    }
}

Stáhnout výsledné exe -RSPosterousBackup.



Wednesday, 20 January 2010 17:54:21 (Central Europe Standard Time, UTC+01:00)       
Comments [2]  .NET Framework | C# Posterous API | LINQ | RX Extensions


 Tuesday, 03 March 2009
LINQ a logování na příkladu logování kroků Dijsktrova algoritmu

Na LINQu je pěkné, jak jednoduše můžeme LINQ výraz upravit nebo jej bezbolestně rozšířit o další části. Nedávno jsem publikoval článek Dijsktrův alogritmus pomocí LINQu, extenzních metod a lambda výrazů a nyní si ukážeme drobnou úpravu v kódu, která způsobí, že se před každým rekurzivním voláním vždy vypíšou i prozatímní výsledky hledání nejkratší cesty.

 

Abychom mohli zalogovat výsledek, vytvoříme si vlastní extenzní metody pro výpis informací z předaného libovolného generického IEnumerable<T> do konzole.

static class MiscExtensions
    {
        public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, Func<T, String> logDataSelector, string beginString, string endString)
        {
            if (source == null)
            {
                throw  new ArgumentNullException("source");
            }

            if(logDataSelector == null)
            {
                throw new ArgumentNullException("logDataSelector");
            }

            return innerLogToConsole(source, logDataSelector, beginString, endString);
        }

        public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, Func<T, String> logDataSelector)
        {
            return LogToConsole(source, logDataSelector, null, null);
        }

        public static IEnumerable<T>LogToConsole<T>(this IEnumerable<T> source)
        {
            return LogToConsole(source, (obj => obj.ToString()), null, null);
        }

        public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, string beginString, string endString)
        {
            return LogToConsole(source, (obj => obj.ToString()), beginString, endString);
        }

        private static IEnumerable<T> innerLogToConsole<T>(IEnumerable<T> source, Func<T, String> selector, string beginString, string endString)
        {
            if (beginString != null)
            {
                Console.WriteLine(beginString);
            }

            foreach (var obj in source)
            {
                String val = selector(obj);
                Console.WriteLine(val);
                yield return obj;
            }

            if (endString != null)
            {
                Console.WriteLine(endString);
            }
        }

    }

Metod pro logování máme více, abychom nemuseli pokaždé předat všechny argumenty. Prvním argumentem je vždy zdrojová sekvence, o jejíchž prvcích budou logovány informace. Argument logDataSelector nese odkaz na funkci, která umí z objektu ve zdrojové sekvenci získat jeho textovou reprezentaci. Jestliže delegát logDataSelector není předán, je k získání textové reprezentace objektu použita metoda ToString() zdrojového objektu. Další nepovinné argumenty beginString a endString jsou řetězce, které má extenzní funkce zapsat do konzole předtím, než jsou vypsána data o prvním objektu v zdrojové sekvenci (beginString), a po zalogování všech objektů v sekvenci (endString). V našem případě argumenty beginString a endString  použijeme k vypsání řetězců, které ohraničí jednolivé kroky algoritmu. Naše extenzní funkce je “neinvazivní”, což znamená, že nefiltruje ani nekonvertuje objekty ve zdrojové sekvenci, ale po vypsání informace o zdrojovém objektu je nezměněný objekt příkazem yield return předán k dalšímu zpracování. Předchozí věta obsahuje varování, že nechcete-li se dočkat nepříjemných překvapení, delegát předaný v argumentu logDataSelector by neměl žádným způsobem měnit data zdrojového objektu, ale pouze je pasivně číst.

Celý algoritmus i s podrobným popisem už zde nebudu opakovat, vložím sem jen pro nás zajímavou rekurzivní metodu getShortestPathInner. Podpora logování je jednoduchou úpravou, protože pouze na námi vybraném neuralgickém místě v LINQ výrazu, které chceme špehovat, zavoláme naši extenzní funkci LogToConsole. Pro lepší orientaci je přidaný kód v následujícím výpisu zvýrazněn tučným červeným písmem.

private static IEnumerable<GraphPath<A0>> getShortestPathInner<A0, A1>(IEnumerable<GraphPath<A0>> initialGraphPath, IEnumerable<A0> processed, IEnumerable<A1> edges)
                                                        where A1 : IGraphEdge<A0>
        {
            var candidates = (from node in edges
                              where !processed.Contains(node.From)
                              select node.From).Distinct();

            if (candidates.Count() == 0)
            {
                return initialGraphPath;
            }

            var minimum = initialGraphPath.Where(gPath => candidates.Contains(gPath.Current)).Min(gPath => gPath.TotalDistance);

            var minimumGPath = (from gPath in initialGraphPath
                                where candidates.Contains(gPath.Current) &&
                                      gPath.TotalDistance == minimum
                                select gPath).First();



            var newGraphPath = from cNode in edges
                               where cNode.From.Equals(minimumGPath.Current)
                               select new GraphPath<A0>
                                       {

                                           Current = cNode.To,
                                           Previous = minimumGPath.Current,
                                           TotalDistance = cNode.Distance + minimumGPath.TotalDistance

                                       };



            var newGraphResult =
                                   (initialGraphPath.Concat(newGraphPath).Where(obj =>
                                                            !initialGraphPath.Any(
                                                                                   obj2 => obj2.Current.Equals(obj.Current) &&
                                                                                   (obj2.TotalDistance < obj.TotalDistance))))
                                                                                   .LogToConsole(obj => String.Format("{0} - {1} - {2}", 
                                                                                                                    obj.Previous, obj.Current, obj.TotalDistance),"--Další kolo algoritmu--", "--Konec kola--")
                                                                                   .ToArray();
            






            var newProcessed = processed.Union(new[] { minimumGPath.Current });

            return getShortestPathInner(newGraphResult, newProcessed, edges);

        }
    }

A zde je ukázka, jak vypadá výstup.

image

Logovat nemusíte jen do konzole, ale můžete si přidat další extenzní metody, které zohlední vaše speciální nároky, kam a jak se mají informace o objektech v sekvenci logovat. Cílem článku bylo jen ukázat, jak bezbolestné a hlavně elegantní :-) je přidání logování do stávajících LINQ výrazů.



Tuesday, 03 March 2009 16:34:35 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | Compact .Net Framework | LINQ


 Wednesday, 11 February 2009
Dijsktrův alogritmus pomocí LINQu, extenzních metod a lambda výrazů

Pokusil jsem se napsat Dijsktrův algoritmus pomocí LINQ konstrukcí. Pokud někdo z vás tápe, k čemu je Dijsktrův algoritmus dobrý a k čemu slouží, odkážu jej na podrobný článek na Wikipedii. Zde jen připomenu, že Dijsktrův algoritmus slouží k nalezení nejkratších cest v grafu z jednoho zdrojového uzlu ke všem ostatním uzlům. Je tak možné najít například nejkratší cestu z jednoho města do druhého. Na internetu jsem našel jeden graf, který budeme mít stále před očima a na který budeme Dijkstrův algoritmus napsaný v LINQu aplikovat.

 

Dijkstra_01_

 

Uzly A, B atd. reprezentují města, ohodnocení hrany vyjadřuje počet kilometrů (město A je od města B vzdáleno 5 km). My budeme chtít spočítat nejkratší cestu z města H do města A a nebude nás zajímat jen počet ujetých kilometrů, ale také jak vypadá celá trasa - jinými slovy, přes jaká města naše nejkratší cesta vede.

 

Nejprve si vytvoříme potřebné třídy:

public interface IGraphEdge<T>
    {
        T From { get; }
        T To { get; }
        int Distance { get; }
    }

Rozhraní IGraphEdge reprezentuje hranu grafu - vlastnost From nám říká, odkud hrana vede (z města A), vlastnost To sděluje, kam hrana vede (město B). Vlastnost Distance nese vzdálenost mezi uzly (města A a B jsou vzdálena 5 km).

 

My pracujeme s vzdálenostmi mezi městy, a proto si vytvoříme jednoduchou třídu reprezentující město. Z města nás zajímá jen název.

 

class City : IEquatable<City>
    {
        private Guid Id;
        public City(string name)
        {
            Id = Guid.NewGuid();
            Name = name;
        }

        public string Name
        {
            get;
            set;
        }

        public override bool Equals(object obj)
        {
            City secondCity = obj as City;

            if (secondCity == null)
            {
                return false;
            }
            return Equals(secondCity);

        }

        public override int GetHashCode()
        {
            return Id.GetHashCode();
        }


        public bool Equals(City other)
        {
            if (other == null)
            {
                return false;
            }

            if (other.GetType() != this.GetType())
            {
                return false;
            }

            return (other.Id == Id);
        }

        public override string ToString()
        {
            return Name;
        }
    }

 

Hrany grafu představují silnice mezi městy, a proto si vytvoříme třídu CityEdge, která implemetuje rozhraní  IGraphEdge a za generický parametr T dosadí třídu City.

 

 

class CityEdge : IGraphEdge<City>
    {
        #region Implementation of IGraphNode<City>

        private City m_from;
        private City m_to;
        private int m_distance;

        public CityEdge(City from, City to, int distance)
        {
            if (from == null)
            {
                throw new ArgumentNullException("from");
            }

            if (to == null)
            {
                throw new ArgumentNullException("to");
            }

            if (distance <= 0)
            {
                throw new ArgumentException("value must be greater than zero", "distance");
            }

            m_from = from;
            m_distance = distance;
            m_to = to;
        }

        public City From
        {
            get
            {
                return m_from;
            }
        }

        public City To
        {
            get
            {
                return m_to;
            }
        }

        public int Distance
        {
            get
            {
                return m_distance;
            }
        }

        #endregion

        public override string ToString()
        {
            return String.Format("From: {0}, To {1}, Distance{2}", From, To, Distance);
        }


    }

Dále potřebujeme třídu, která ponese informaci o nalezené nejkratší cestě do daného bodu a o předchozím městu, přes které musíme cestovat. Na konkrétním příkladu - z města A (námi zvolený jediné výchozí město, z nějž se počítá nejkratší cesta ke všem ostatním městům) vede nejkratší cesta do města  C (vlastnost Current), která má délku 7 Km (vlastnost TotalDistance) a současně nám vlastnost Previous vrátí přechozí město (vlastnost Previous), přes které musíme jet (město B).

 

public class GraphPath<T> : IEquatable<GraphPath<T>>
    {
        private const int DUMMY_HASH_PLACEHOLDER = 100;
        
        public T Current
        {
            get;
            set;
        }

        public T Previous
        {
            get;
            set;
        }

        public int TotalDistance
        {
            get;
            set;
        }

        public override bool Equals(object obj)
        {
            GraphPath<T> second = obj as GraphPath<T>;

            if (second == null)
            {
                return false;
            }

            return Equals(second);

        }

        public override int GetHashCode()
        {
            var prevHash = (EqualityComparer<T>.Default.Equals(Previous, default(T)) ? DUMMY_HASH_PLACEHOLDER : Previous.GetHashCode());
            
            return (Current.GetHashCode() ^ prevHash ^ TotalDistance.GetHashCode());
        }

        #region Implementation of IEquatable<T>

        public bool Equals(GraphPath<T> other)
        {
            if (other == null)
            {
                return false;
            }

            if (other.GetType() != this.GetType())
            {
                return false;
            }

            return (EqualityComparer<T>.Default.Equals(Current, other.Current) &&
                    EqualityComparer<T>.Default.Equals(Previous, other.Previous) &&
                    TotalDistance.Equals(other.TotalDistance)
                    );
        }

        #endregion
    }

 

Přípravu máme za sebou, nyní se můžeme podívat na kostru celého algoritmu a poté rozpitvat jeho jednotlivé kroky. Pamatujme - důsledně se snažíme používat, kde to jen jde, LINQ konstrukce. ;-)

 

static void Main(string[] args)
        {
            var cities = (from i in Enumerable.Range(0, 8)
                          let key = ((char)(i + 'A')).ToString()
                          select new
                                     {
                                         Key = key,
                                         Value = new City(key)
                                     }).ToDictionary(el => el.Key, el => el.Value);




            var cityNodes = new List<CityEdge>
                                {
                                    new CityEdge(cities["A"], cities["B"], 5),
                                    new CityEdge(cities["A"], cities["F"], 3),
                                    new CityEdge(cities["B"], cities["C"], 2),
                                    new CityEdge(cities["B"], cities["G"], 3),
                                    new CityEdge(cities["C"], cities["H"], 10),
                                    new CityEdge(cities["C"], cities["D"], 6),
                                    new CityEdge(cities["D"], cities["E"], 3),
                                    new CityEdge(cities["E"], cities["H"], 5),
                                    new CityEdge(cities["E"], cities["F"], 8),
                                    new CityEdge(cities["F"], cities["G"], 7),
                                    new CityEdge(cities["G"], cities["H"], 2),
                                };


            var allCityNodes = cityNodes.Concat(from cn in cityNodes
                             select new CityEdge(cn.To, cn.From, cn.Distance)).ToArray();


            var resultPath = getShortestPath(cities["H"], allCityNodes);


            var pathFromTo = resultPath.EnumerateShortestPathTo(cities["H"], cities["A"]).Reverse();




            Array.ForEach(pathFromTo.ToArray(), myP =>
                                                Console.WriteLine("Přes město {0}, Ujetá vzdálenost: {1}", myP.Current, myP.TotalDistance));

            
            Console.ReadLine();
        }

 

Na začátku metody vygenerujeme objekt Dictionary (proměnná cities), kde klíčem je název města a hodnotou objekt City. Do proměnné cityNodes uložíme hrany grafu (existující silnice mezi městy) s jejich ohodnocenim (kilometry). Silnice ale nevede jen z města A do města B, ale také z města B do A, proto do proměnné allCityNodes uložíme i zpáteční cesty mezi městy. Poté voláme metodu getShortestPath, které předáme výchozí město (zde H), pro které chceme spočítat nejkratší cesty ke všem ostatním městům, a veškeré hrany-silnice. Metoda getShortestPat vrátí nejkratší cesty, nově vytvořené extenzní metodě EnumerateShortestPathTo řekneme, že chceme vypsat nejkratší cestu z města H (první argument) do města A (druhý argument) - metoda vrátí pouze města, přes která musíme jet z města H do města A a my metodou Reverse otočíme jejich pořadí, abychom viděli cestu od H do A a nezačínali koncovým městem A. Výsledek vypíšeme s využitím metody Array.ForEach na konzoli.

Skeleton algoritmu je hotov, čas začít nabalovat extenzní maso (doslova... :-)) a LINQ svaly.

 

Zde je metoda getShortestPath.

private static IEnumerable<GraphPath<A0>> getShortestPath<A0, A1>(A0 startPoint, IEnumerable<A1> nodes)
                                            where A1 : IGraphEdge<A0>
        {

            var initialGraphPath = (from cNode in nodes
                                    where !cNode.From.Equals(startPoint)
                                    select new GraphPath<A0>
                                    {

                                        Current = cNode.From,
                                        Previous = default(A0),
                                        TotalDistance = int.MaxValue

                                    }).Distinct();


            initialGraphPath = (initialGraphPath.Concat(from cNode in nodes
                                                       where cNode.From.Equals(startPoint)
                                                       select new GraphPath<A0>
                                                           {

                                                               Current = cNode.To,
                                                               Previous = startPoint,
                                                               TotalDistance = cNode.Distance

                                                           }));



            return getShortestPathInner(initialGraphPath, new[] { startPoint }, nodes);




        }

V metodě getShortestPath vygenerujeme objekty GraphPath, které po všech výpočtech ponesou nejkratší cestu v grafu. První dotaz vybere nejdříve ze seznamu hran pouze ty hrany, v nichž vlastnost From (Odkud) nepředstavuje počáteční město (where !cNode.From.Equals(startPoint) ). Jediné, co víme, je že vlastnost TotalDistance nového objektu GraphPath po ukončení algoritmu ponese informaci o nejkratší cestě z výchozího města do konkrétního města (vlastnost Current). U těchto uzlů tedy nejkratší vzdálenost neznáme, a proto vlastnost TotalDistance inicializujeme konstantou int.MaxValue, která říká "nejkratší cestu do města Current neznám a ještě ke všemu může být hodně dlouhá" :-). Z jednoho města může vést více silnic, což by vedlo k duplicitním objektům GraphPath, a proto pro každé město ponecháme pouze jeden objekt GraphPath voláním metody Distinct. 

K objektům  GraphPath z prvního dotazu připojíme objekty GraphPath  komplementárním dotazem, který vybere pouze ty hrany, v nichž vlastnost From (Odkud) představuje počáteční město (where !cNode.From.Equals(startPoint) ). Každý objekt GraphPath z druhého dotazu má tedy vlastnost TotalDistance rovnu kilometrům z fixně stanoveného počátečního města (vlastnost Previous - v našem příkladu město H) do města (Vlastnost Current), k němuž vede z H silnice přímo. Pro počáteční město H tedy druhý dotaz vrátí dva objekty GraphPath naplněné takto - {1 - Current=C, Previous=H, TotalDistance=10} {2-Current=E, Previous=H, TotalDistance=5}.

Poté zavoláme metodu getShortestPathInner, které předáme vygenerované objekty GraphPath, seznam objektů, pro něž jsme zjišťovali  nejkratší cestu v grafu (zatím jen počáteční město  - město H), a dříve vytvořený seznam hran.

 

private static IEnumerable<GraphPath<A0>> getShortestPathInner<A0, A1>(IEnumerable<GraphPath<A0>> initialGraphPath, IEnumerable<A0> processed, IEnumerable<A1> edges)
                                                        where A1 : IGraphEdge<A0>
        {
            var candidates = (from node in edges
                              where !processed.Contains(node.From)
                              select node.From).Distinct();

            if (candidates.Count() == 0)
            {
                return initialGraphPath;
            }

            var minimum = initialGraphPath.Where(gPath => candidates.Contains(gPath.Current)).Min(gPath => gPath.TotalDistance);

            var minimumGPath = (from gPath in initialGraphPath
                                where candidates.Contains(gPath.Current) &&
                                      gPath.TotalDistance == minimum
                                select gPath).First();



            var newGraphPath = from cNode in edges
                               where cNode.From.Equals(minimumGPath.Current)
                               select new GraphPath<A0>
                                       {

                                           Current = cNode.To,
                                           Previous = minimumGPath.Current,
                                           TotalDistance = cNode.Distance + minimumGPath.TotalDistance

                                       };



            var newGraphResult =
                                   (initialGraphPath.Concat(newGraphPath).Where(obj =>
                                                            !initialGraphPath.Any(
                                                                                   obj2 => obj2.Current.Equals(obj.Current) &&
                                                                                   (obj2.TotalDistance < obj.TotalDistance)))).ToArray();





            var newProcessed = processed.Union(new[] { minimumGPath.Current });

            return getShortestPathInner(newGraphResult, newProcessed, edges);

        }
    }

V rekurzivní metodě getShortestPathInner je soustředěno jádro Dijkstrova algoritmu. Do proměnné candidates uložíme nejprve veškerá města, která jsme ještě nezpracovali - nezjišťovali jsme, jak daleko je to od nich k jejich přímým sousedům. Do proměnné minimumGPath uložíme objekt GraphPath, který reprezentuje prozatímní nejkratší zjištěnou cestu z výchozího města a jehož vlastnost Current nese město, pro něž jsme výpočet nejkratší cesty ještě neprovedli.

Do proměnné newGraphPath uložíme množinu objektů GraphPath, které jsou sestaveny tak, že jejich předchůdcem (vlastnost Previous) je vždy právě zpracovávané město. Vlastnost To nese město, do něhož se můžeme dostat z města Previous. Vlastnost TotalDistance je inicializována součtem  hodnoty TotalDistance z procházeného objektu minimumGPath (s prozatímní nejkratší cestou) s vzdáleností z města v Previous do města ve vlastnosti Current.

Do proměnné newGraphPathResult jsou uloženy jen prozatím nejkratší cesty - jestliže tedy v předchozím kroku vypočítáme, že z města H se do města D můžeme dostat po ujetí 16 km, ale v proměnné initialGraphPath máme spočítáno, že do města H se můžeme dostat i po ujetí 8 km, je ponechána pouze lepší, tedy pro naše účely kratší cesta.

Poté na konci metody do seznamu již zpracovaných měst přidáme právě zpracované město (minimumGPath.Current) a jdeme na další kolo. Rekurzivně zavoláme metodu getShortestPathInner s vypočítanými mezivýsledky. Rekurze skončí po zpracování všech měst - nejsou  nalezeni další kandidáti na zpracování (viz podmínka if (candidates.Count() == 0 )).

 

Ještě nám zbývá extenzní metoda, která enumeruje přes města z výchozího bodu do cílového. První argument jsou vypočítané nejkratší cesty (objekty GraphPath), argument start je zvolené počáteční město a argument end je koncové město, ke kterému chceme vypsat nejkratší cestu.

static class DijkstraExtensions
    {
        public static IEnumerable<A0> EnumerateShortestPathTo<A0, A1>(this IEnumerable<A0> paths, A1 start, A1 end)
                                   where A0 : GraphPath<A1>
        {
            var pathDictionary = paths.ToDictionary(obj => obj.Current);
            var currentNode = pathDictionary[end];


            while (currentNode != null)
            {
                yield return currentNode;
                if (currentNode.Previous.Equals(start))
                {
                    yield break;
                }
                currentNode = pathDictionary[currentNode.Previous];
            }

        }
    }

 

 

Metoda si vytvoří další objekt Dictionary a postupně vrací veškerá objekty GraphPath na ceste z jednoho města (start) do druhého (end) tak, že v cyklu while z objektu Dictionary vyzvedává město-předchůdce (vlastnost Previous), přes které musíme jet, dokud se nedostaneme do výchozího města.

 

A zde je výsledek - nejkratší cesta z H do A.

 

Dijkstra_Result

Použité algoritmy, LINQ konstrukce a  extenzní metody by šlo určitě vylepšit a vytunit. Nemyslím si a nikdy jsem si narozdíl od některých rozjuchaných "VsechnoSLinqemJeTedCoolTyRetarde" MSDN blogů nemyslel, že LINQ a extenzní metody jsou univerzální kladivo na každý problém universa, včetně precizní analýzy finální odpovědi '42' :-), ale jako příklad, co lze s těmito nástroji dělat, mi Dijkstrův algoritmus přišel zajímavý. :-)

Update 12. 2.:

Petr poptával v komentářích zdrojové kódy. Zde jsou.



Wednesday, 11 February 2009 19:13:14 (Central Europe Standard Time, UTC+01:00)       
Comments [2]  .NET Framework | Compact .Net Framework | LINQ


 Sunday, 21 December 2008
Extenzní metoda - binární operace And pro enumerace

V diskuzním fóru se (po dlouhé době :-)) objevil jeden zajímavější dotaz, který se netýká ani toho, jak zobrazit druhý formulář v aplikaci, ba ani autor nebojuje s mizením dat po postbacku v ASP.NET aplikaci.

Ale vážně - autor dotazu by chtěl mít lepší syntaxi pro binární operaci And v enumeracích označených metaatributem Flags. Mně stávající C syntaxe (Rights & Rights.Add == Rights.Add) zcela vyhovuje a žádný další syntaktický cukřík hltat nechci,  ale přesto mě zaujalo, jak by se dal problém, tedy spíš estetická preference náročného tazatele :-),  řešit.

Tazatel přeposlal svůj dotaz i do konference na vývojáři, kde bylo nabídnuto řešení přes dočasné přetypování na typ Object a poté z typu Object na typ int. Jak zaznělo v kritice na vývojáři - "dirty" řešení je funkční, ale opakovaný boxing a unboxing hodnot není zrovna ta pravá vývojářská slast.

Zkusil jsem vymyslet jiné řešení  - přiznám se, že IL jsem nezkoumal a žádné výkonnostní testy nedělal.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace BinaryOpGenericTest
{
    [Flags]
    enum MyFlags
    {
        A = 1,
        B = 2,
        C = 4

    }

    static class EnumExtensions
    {
        private static Dictionary<Type, Delegate> m_operations = new Dictionary<Type, Delegate>();

        public static bool Contains<T>(this T firstOperand, T secondOperand) 
                                                  where T : struct
        {

            Type enumType = typeof(T);
            
            
            if (!enumType.IsEnum)
            {
                throw new InvalidOperationException("Enum type parameter required");
            }


            Delegate funcImplementorBase = null;
            m_operations.TryGetValue(enumType, out funcImplementorBase);

            Func<T, T, bool> funcImplementor = funcImplementorBase as Func<T, T, bool>;
            
            if (funcImplementor == null)
            {
                funcImplementor = buildFuncImplementor(secondOperand);
            }



            return funcImplementor(firstOperand, secondOperand);
        }


        private static Func<T, T, bool> buildFuncImplementor<T>(T val)
                                                            where T : struct
        {
            var first = Expression.Parameter(val.GetType(), "first");
            var second = Expression.Parameter(val.GetType(), "second");
                    
            Expression convertSecondExpresion = Expression.Convert(second, typeof(int));
            var andOperator = Expression.Lambda<Func<T, T, bool>>(Expression.Equal(
                                                                                                       Expression.And(
                                                                                                            Expression.Convert(first, typeof(int)),
                                                                                                             convertSecondExpresion),
                                                                                                       convertSecondExpresion),
                                                                                             new[] { first, second });
            Func<T, T, bool> andOperatorFunc = andOperator.Compile();
            m_operations[typeof(T)] = andOperatorFunc;
            return andOperatorFunc;
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            MyFlags flag = MyFlags.A | MyFlags.B;

            Console.WriteLine(flag.Contains(MyFlags.A));            
            Console.WriteLine(EnumExtensions.Contains(flag, MyFlags.C));
            Console.ReadLine();
        }
    }
}

Pár poznámek ke kódu.

  1. Nejdůležitější v kódu je generická extenzní metoda Contains. Nelze použít negenerickou metodu rozšiřující třídu Enum, protože poté začneme mít další problémy s přetypováváním hodnot z obecné Enum na konkrétní enumeraci. Řešením není ani negenerická metoda, v níž pracujeme pouze s třídou Enum. Enumerace jsou hodnotové typy, a proto je na  generický parametr T aplikováno alespoň omezení struct. Jestliže bude zájem, mohu tato rozhodnutí, zde jen zběžně zmíněná, vysvětlit v nějakém dalším spotu.
  2. Když si nejsme jisti, že nám byla za generický typ předána enumerace, musíme provést v metodě Contains další kontrolu za běhu aplikace.
  3. Binární operace And je reprezentována delegáty. Delegáty dynamicky vytvářím za běhu aplikace pro každou enumeraci v metodě buildFuncImplementor. Delegát je vytvořen pomocí abstraktního syntaktického stromu (Expression tree) a poté je na výsledném výrazu (Expression) volána metoda Compile, která vrátí delegáta typu Func<T, T, bool>.
  4. Delegáty Func<T, T, bool> ukládáme do objektu Dictionary jako obecný typ Delegate - důvodem je to, že třída EnumExtensions není generická. Místo přetypovávání delegáta z předka Delegate na typ Func<T, T, bool> by bylo možné také použít pozdní vazbu voláním metody DynamicInvoke dostupné  přímo ve třídě Delegate.
  5. Jestliže byste chtěli operaci And rozšířit i na další typy, které lze konvertovat na typ Int, mohla by se Vám hodit moje extenzní metoda pro detekci konverze generické proměnné na typ Int za běhu aplikace.


Sunday, 21 December 2008 12:22:20 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | Compact .Net Framework | LINQ


 Sunday, 05 October 2008
Adaptéry pro funktory v C++ => Adaptéry pro funkce v C#

V C++ je snadné napsat takzvané adaptéry pro funkce, respektive pro funktory - objekty, chovající se jako funkce. K čemu jsou adaptéry dobré? Představme si, že máme napsanou funkci equal_to, která přijímá dva argumenty a vrátí true, jestliže jsou oba argumenty shodné, jinak vrátí false. Jedná se tedy o binární funktor, protože přijímá dva argumenty. Nyní potřebujeme pomocí stl metody find_if vyhledat v naší kolekci všechny prvky, jejichž hodnota je rovna 10. Podmínku v metodě find_if musí představovat unární funktor (funktor přijímající jeden argument - prvek v kolekci - a vracející true jen v případě, že prvek v kolekci podmínku splňuje). Je zřejmé, že binární funktor nemůžeme použít na místě, kde je očekáván unární funktor. V C++ můžeme ale v této situaci namísto psaní dalšího unárního jednoúčelového funktoru využít speciálního adaptéru, jehož účelem je konverze binárního funktoru na unární. Adaptér, který přijde vhod pro naše účely, se jmenuje binder1st (zde by bylo možné použít i adaptér binder2nd). Adaptér binder1st očekává, že mu předáte binární funktor, který má být převeden na unární  a hodnotu, která má být vždy použita jako první argument (proto ...1st) při volání binárního funktoru. Adaptér binder2nd se od adaptéru binder1st liší jen tím, že předaná hodnota bude použita vždy jako druhý argument předaného binárního funktoru. Jinými slovy - při volání funktoru binder1st je kolekce spokojena, že dostala unární funktor, ale náš funktor binder1st interně deleguje volání na binární funktor, kterému předá jako první argument hodnotu, kterou jsme zadali při vytvoření adaptéru binder1st, a jako druhý argument objekt z kolekce, na kterém se má otestovat platnost podmínky.

binder1st<equal_to<int> > equalPredicate = bind1st(equal_to<int>(), 10);
iterator it1 = find_if(v1.begin(), v1.end(), equalPredicate);

V předchozím kódu jsme vytvořili adaptér (unární funktor) nazvaný equalPredicate, který zprostředkovává přístup k binárnímu funktoru equal_to. Skutečnost, že je  funktor equal_to binárním funktorem, poznáme z jeho deklarace.

template<class Type>
   struct equal_to : public binary_function<Type, Type, bool> 
   {
      bool operator()(
         const Type& _Left, 
         const Type& _Right
      ) const;
   };

Na druhém řádku  příkladu adaptér equalPredicate předáme funkci find_if, která porovná každý element v kolekci (const Type&  _Right)  s hodnotou 10. Funkce vrátí první prvek, který vyhoví podmínce _Left==Right  (konkrétně v našem případě jde o podmínku  10 == PrvekVKolekci). Konstantní hodnota 10 byla předána funkci bind1st a bude  představovat při každém volání "adaptovaného" funktoru equal_to  adaptérem equalPredicate  hodnotu argumentu _Left operátoru(). Funkce bind1st je "syntaktickým cukrem", který zjednodušuje vytváření adaptéru, protože nemusíme specifikovat všechny typové parametry adaptéru binder1st, ale spolehneme se na typovou inferenci provedenou kompilátorem.

Konec rychlé exkurze do C++. I v C# nám mohou adaptéry pro delegáty přijít vhod. Představme si, že již máme napsanou třídu, která vrací výsledek porovnání dvou hodnot ("je menší než", "je větší než").

    static class ComparerEx
    {
        public static bool GreaterThan<T>(T a, T b)
        {
            return Comparer<T>.Default.Compare(a, b) > 0;
        }

        public static bool LessThan<T>(T a, T b)
        {
            return Comparer<T>.Default.Compare(a, b) < 0;
        }
    }

Funkce chceme použít v LINQ podmínkách (např. můžeme chtít z kolekce celých čísel vrátit jen všechna čísla, jež jsou větší než 10). Ale také můžeme chtít sadu podmínek, které můžeme libovolně kombinovat a skládat tak jednoduše výrazy typu "všechny hodnoty z kolekce, jež jsou větší než 20, ale menší než 90". Stejně tak můžeme chtít za chvíli podmínku znegovat a máme zájem o hodnoty nepatřící do intervalu 20-90. Namísto psaní "jednoúčelových" (i anonymních) metod si můžeme jednotlivé podmínky předpřipravit a pomocí adaptérů pro delegáty je skládat do složitějších podmínek. Také můžeme chtít stejnou podmínku použít při restrikci v LINQu (Where extenze pracující s delegátem typu Func<  >) i při práci se staršími metodami (např. FindAll u List<T>), které očekávají odkaz na delegáta typu Predicate. To vše nám speciální adaptéry pro delegáty v C# umožní.

Nejprve se podívejme na použití adaptérů.

 

class Program
    {
        static void Main(string[] args)
        {

            
            Random rand = new Random();

            //Vygenerování náhodných čísel v rozsahu 1..100
            List<int> myList = new List<int>(Enumerable.Range(1, 100).Select((i) => rand.Next(1, 100)));

            //Vytvoření predikátu pro where část LINQ dotazu (všechna čísla, kromě čísel v rozsahu 10 - 90
            var predicate = FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);
            predicate = FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan));
            predicate = FuncExtension.Not(predicate);
            
            //LINQ dotaz  - v selectu je do anonymního typu vyzvednut i index prvku v kolekci
            var result = myList
                                .Where(predicate)
                                .Select((elem, index) => new {elem, index});
                         

            //Výpis LINQ dotazu
            foreach (var res in result)
            {
                
                Console.WriteLine("{0}:{1}", res.index, res.elem);
                
            }

            //Ukázka konverze podmínky (Func<?, bool> na delegáta typu Predicate očekávaného funkcí FindAll
            var vals = myList.FindAll(FuncExtension.ToPredicate(predicate));
            
            //Musíme dostat stejné výsledky jako v předchozím dotazu s využitím LINQu
            foreach (var val in vals)
            {
                Console.WriteLine(val);
            }

            Console.ReadLine();
        }
    }

V příkladu jsme si naplnili myList náhodnými čísly v intervalu od 1 do 100. Proměnná predicate představuje podmínku.

Použitím adaptéru Bind2nd(FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);) vytvoříme podmínku "všechna čísla větší než  10". Vidíme, že jsme funkci ComparerEx.GreaterThan, která očekává dva argumenty, "adaptovali-převedli" na funkci (přesněji řečeno na delegáta), který očekává jeden argument. Druhým argumentem funkce ComparerEx.GreaterThan je vždy konstantní hodnota 10 předaná  při volání funkce Bind2nd.

V dalším kroku vytvoříme podmínku ("všechna čísla menší než 90" - FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan)); ) a zkombinujeme ji s předchozí podmínkou pomocí speciálního adaptéru, který představuje operátor And (FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan))). Operátor And je pro zbytek aplikace stále jen obyčejným (unární) delegátem na funkci, která přijímá jeden argument a vrací true nebo false. Nyní máme tedy podmínku "všechna čísla větší než 10 a menší než 90".

Naše konečná podmínka ale má mít podobu (""všechna čísla s výjimkou čísel větších než 10 a menších než 90"). Proto použijeme další speciální adaptér Not, který v předchozích krocích sestavenou podmínku zneguje (FuncExtension.Not(predicate);)

Z kolekce myList vybereme přes LINQ všechna čísla splňující podmínku (proměnná predicate s definicí podmínky je argumentem extenzní metody Where) a vypíšeme je do konzole.

Nakonec ještě stejnou podmínku chceme předat metodě FindAll. Metoda FindAll ale očekává delegáta nazvaného Predicate, a proto použijeme další "adaptující" funkci ToPredicate, který stávající definici podmínky konvertuje na Predicate.

Jak adaptéry pracují? Podívejme se na funkci Bind2nd.

 public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(arg, bindValue));
        }

Bind2nd je generická funkce, která jako argument (T1 bindValue) očekává hodnotu, která bude představovat vždy druhý argument adaptovaného delegáta (Func<T0, T1, R> originalFunc - funkce přijímající dva argumenty, první typu T0, druhý typu T1 a vracející R). Funkce vrátí nového delegáta (Func<T0, R>), který ukazuje na funkci očekávající jeden argument  typu  T0 a vracející instanci generického typu R. Delegát při svém spuštění pouze vezme předaný argument (arg) a poskytne jej jako první argument delegátovi originalFunc, kterému současně vždy předá jako druhý argument hodnotu v původním argumentu bindValue.

Podobně fungují i další adaptéry. Pro zajímavost se podívejme na adaptér ToPredicate, který z předaného delegáta vytvoří delegáta typu Predicate, čehož jsme využili v předchozím příkladu.

public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
        {
            return arg => originalFunc(arg);
        }

Funkce očekává ve svém argumentu originalFunc odkaz na delegáta typu Func, který přijímá jeden argument typu T a vrací bool. My vrátíme delegáta typu Predicate<T>, přičemž vrácený lambda výraz deleguje vykonání funkce na původního delegáta originalFunc. Pro zbytek aplikace je delegát Func<T, bool> skryt za rozhraním adaptéru Predicate<T>, který nám pomohl pro funkci FindAll přeložit podmínku "v neznámém jazyce" do srozumitelné řeči.

Následuje kompletní výpis kódu adaptérů. To, co nám prozatím chybí, je ekvivalent funkce bind1st (bind2nd) z C++, který by nám zjednodušil zápis podmínek bez nutnosti zadávat "ručně" generické argumenty. Ale o tom popřemýšlím zase "někdy jindy". :-)

 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FunctionExtensions
{
    static class FuncExtension
    {
        
        public static Func<T1, R> Bind1St<T0, T1, R>(T0 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(bindValue, arg));
        }

        public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(arg, bindValue));
        }

        
        public static Func<T0, bool> Not<T0>(Func<T0, bool> originalFunc)
        {
            return (arg => !originalFunc(arg));
        }

        public static Func<T0, T1, bool> Not<T0, T1>(Func<T0, T1, bool> originalFunc)
        {
            return ((arg1, arg2) => !originalFunc(arg1, arg2));
        }

        public static Func<T0, T1, T2, bool> Not<T0, T1, T2>(Func<T0, T1, T2, bool> originalFunc)
        {
            return ((arg1, arg2, arg3) => !originalFunc(arg1, arg2, arg3));
        }

        public static Func<T0, T1, bool> And<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
        {
            return ((arg1, arg2) => originalFunc(arg1, arg2) && originalFunc(arg1, arg2));
        }

        public static Func<T0, bool> And<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
        {
            return (arg1  => originalFunc(arg1) && originalFunc2(arg1));
        }

        public static Func<T0, T1, bool> Or<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
        {
            return ((arg1, arg2) => originalFunc(arg1, arg2)  || originalFunc(arg1, arg2));
        }
        
        public static Func<T0, bool> Or<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
        {
            return (arg1 => originalFunc(arg1) || originalFunc2(arg1));
        }

        public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
        {
            return arg => originalFunc(arg);
        }
    }
}


Sunday, 05 October 2008 17:34:45 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | Compact .Net Framework | LINQ | Návrhové vzory


 Monday, 15 September 2008
Několik poznámek k přetypovávání generických kolekcí

Při práci s generickými kolekcemi asi každy občas zatouží převést generickou kolekci s objekty typu B na generickou kolekci s objekty typu A, přičemž instinktivně očekává, že když je typ A předkem typu B, žádný problém při konverzi nenastane a navíc půjde o konverzi implicitní - automatickou. Instinkty, intuice a další feminní rysy jsou ale při programování spíš zátěží (že by jeden z hlavních důvodů, proč je stále tak málo programátorek? ) :-) )

Konkrétně - mějme tyto dvě třídy.

 

    class Test
    {

        public override string ToString()
        {
            return "Test";
        }
    }


    class SpecialTest : Test
    {
        public override string ToString()
        {
            return "SpecialTest";
        }
    }
    
Vytvoříme si kolekci (List) odvozených tříd SpecialTest.
List<SpecialTest> srcList = new List<SpecialTest> { new SpecialTest(), new SpecialTest() };

Při pokusu přetypovat List<SpecialTest> na kolekci objektů typu Test (List<Test>)  neuspějeme.

List<Test> invalidAttemptList = srcList;

Cannot implicitly convert type 'System.Collections.Generic.List<CollectionInheritance.SpecialTest>' to 'System.Collections.Generic.List<CollectionInheritance.Test>'). Důvod je zřejmý - dva generické objekty List, jeden s generickým argumentem Test a druhý s generickým argumentem SpecialTest, jsou dvě zcela rozdílné a nezávislé třídy a skutečnost, že třída SpecialTest je potomkem třídy Test, neznamená, že by stejný vztah platil mezi třídami List<SpecialTest> a List<Test>. Autoři jazyka C# (dle svých vyjádření prozatím?) zavedli toto omezení kvůli typové bezpečnosti.

Možností, jak konverzi provést, je mnoho. Vyjmenujme alespoň ty, které nově přinesl LINQ.

LINQ nám nabízí pro daný účel metodu Cast, která zkonvertuje prvky ze zdrojové kolekce (IEnumerable) na (generický) typ předaný metodě.

IEnumerable<Test> ieList = srcList.Cast<Test>();

Jestliže by nebylo možné všechny prvky v kolekci převést na 'Test', bude vyvolána výjimka. Když máme v kolekci směs objektů z různých tříd nebo podporujících různá rozhraní, můžeme přetypovat pomocí dalšího standardního LINQ operátoru OfType, který do výsledné kolekce vloží jen ty objekty, které se pomocí operátoru as podaří přetypovat na cílový typ. Objekty, které přetypovat na cílový typ (v následujícím kódu tedy na  typ Test) nelze, jsou ignorovány.

IEnumerable<Test> ieList2 = srcList.OfType<Test>();

Jestliže nechceme mít jako výslednou kolekci typ IEnumerable, můžeme použít další LINQ operátor ToList() a výsledek přetypování nám bude vrácen v instanci List<T>.

List<Test> testList = srcList.OfType<Test>().ToList();

Alternativou k předchozímu zápisu může být využití konstruktoru třídy List.

List<Test> testList2 = new List<Test>(srcList.OfType<Test>());

 

Jestliže nechcete vždy pracovat jen s kolekcí typu List a chcete přetypovávat například na typové kolekce, využijete mé extenzní metody.

 

Collection<Test> trgList = srcList.WideningConvert<SpecialTest, Test, Collection<Test>>();

Metodě WideningConvert předáte jako typové argumenty aktuální typ v generické kolekci (SpecialTest), cílový-výsledný typ, na který má být zdrojový typ převeden (Test) a typ kolekce, která má být vrácena. Kolekce musí mít bezparametrický konstruktor a také musí podporovat rozhraní ICollection<T>.

Pomocí extenzní metody lze přetypovávat i z kolekce objektů typu "předek" na kolekci "potomků".

  

Collection<SpecialTest> nextList = trgList.NarrowingConvert<Test, SpecialTest, Collection<SpecialTest>>();
 
 

Metoda WideningConvert svým názvem dává najevo, že je určena pro implicitní ("bezpečnou") konverzi, kdy převádíte kolekci potomků na kolekci předků. Obdobně, metoda NarrowingConvert přetypovává kolekci objektů typu "předek" na kolekci objektů typ "potomek (explicitni konverze). Metoda NarrowingConvert se pokusí převést každý objekt ve zdrojové kolekci na cílový typ ("Potomek") pomocí operátoru as. Jestliže přetypování selže, je zdrojový objekt ignorován, a proto může výsledná kolekce vrácená metodou NarrowingConvert obsahovat méně prvků než kolekce zdrojová.

 

Zde je kompletní výpis metod. Bylo by samozřejmě možné začít uvažovat nad zjednodušením kódu pro přetypovávání, ale to si necháme "napříště".

 

    public static class ConvertExtensions
    {
        /// <summary>
        /// Metoda převede kolekci - metoda je určena pro přetypování generické kolekce s "potomky" na generickou kolekci s "předkem"
        /// </summary>
        /// <typeparam name="T0">Typ elementu v zdrojové kolekci</typeparam>
        /// <typeparam name="P">Typ elementu v cílové kolekci</typeparam>
        /// <typeparam name="R">Typ cílové kolekce</typeparam>
        /// <param name="source">Zdrojová kolekce</param>
        /// <returns>Cílovou kolekci s převedenými elementy</returns>
        public static R WideningConvert<T0, P, R>(this IEnumerable<T0> source) 
                                             where R : ICollection<P>, new()
                                             where T0: P            
        {

            if (source == null)
            {
                throw new ArgumentNullException();
            }
            R retCol = new R();

            foreach (T0 srcElem in source)
            {
                retCol.Add(srcElem);
            }
            return retCol;
        }

        /// <summary>
        /// Metoda převede kolekci - metoda je určena pro přetypování generické kolekce s "předkem" na generickou kolekci "potomků"
        /// </summary>
        /// <typeparam name="T0">Typ elementu v zdrojové kolekci</typeparam>
        /// <typeparam name="P">Typ elementu v cílové kolekci</typeparam>
        /// <typeparam name="R">Typ cílové kolekce</typeparam>
        /// <param name="source">Zdrojová kolekce</param>
        /// <returns>Cílovou kolekci s převedenými elementy</returns>
        ///<remarks>Počet prvků v cílové kolekcí může být menší než počet prvků v zdrojové kolekci, protože veškeré objekty, které nelze zkonvertovat na <typeparamref name="R"/>, jsou ignorovány</remarks>
        public static R NarrowingConvert<T0, P, R>(this IEnumerable<T0> source)
                                                  where R : ICollection<P>, new()
                                                  where P : class, T0
        {

            if (source == null)
            {
                throw new ArgumentNullException();
            }
            R retCol = new R();

            foreach (T0 srcElem in source)
            {

                P retValue = srcElem as P;

                if (retValue != null)
                {
                    retCol.Add(retValue);
                }
            }
            return retCol;
        }
      
                                             
    }


Monday, 15 September 2008 13:29:48 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | Compact .Net Framework | LINQ


 Friday, 09 May 2008
LINQ II - přetypovávání i vnořených anonymních datových typů z jiné assembly

V předchozím spotu jsem byl schopen pracovat s anonymními datovými typy, i když byly dotazy a výsledné sady dat vytvořeny v jiné assembly. Odstranění vrozené xenofobie v praxi.:)

Náš kód ale vygeneruje výjimku, jestliže anonymní datový typ z jiné assembly obsahuje další vnořené anonymní datové typy jako v následujícím upraveném příkladu. Vlastnost InnerAT vrací další anonymní datový typ, který  pro zajímavost obsahuje odkaz ještě na další anonymní datový typ.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LINQTEST
{
    public class TestAT
    {
        public static object GetResult()
        {
            string[] rows = { "Toyota", "Lexus", "Audi" };

            var test = from row in rows
                       select new
                       {
                           FirstLetter = row[0],
                           Index = 110,
                           Original = row,
                           InnerAT = new { X = row[1], B = new {A=1}}
                       };
            
            return test;

        }
    }
}

Řešení spočívá v úpravě extenzí a to tak, že přidáme privátní metodu GetTypeInstance a přeneseme do ní většinu kódu z extenze ToAnonymousType. Metoda GetTypeInstance při neshodě datového typu očekávaného parametrem "našeho - v naší assembly dostupného" konstruktoru anonymního datového typu a datového typu vlastnosti anonymního datového typu z "cizí" assembly rekurzivně přenese data z "cizího" anonymního datového typu do "našeho".

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections;
using LINQTEST;

namespace LINQAnonymous
{
    /// <summary>
    /// Rozšíření pro LINQ
    /// </summary>
    static class RSLinqExtensions
    {

        /// <summary>
        /// Metoda přetypuje objekt na anonymní typ, jehož struktura byla předána v parametru <paramref name="prototype"/>
        /// </summary>
        /// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
        /// <param name="prototype">Prototyp se strukturou anonymního typu</param>
        /// <returns>Instanci anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
        /// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
        public static T ToAnonymousType<T>(this object obj, T prototype)
                                        where T: class
        {
            
            
            T atiObj = obj as T;
            
            if (atiObj == null)
            {
                
                atiObj = GetTypeInstance(obj, prototype.GetType()) as T;
               
            }
                                                     
            return (atiObj);
        }
    

        private static object GetTypeInstance(object obj, Type expected)
        {
            object atiObj = null;

            ConstructorInfo constructorInfo = expected.GetConstructors()[0];
                
                if (constructorInfo == null)
                {
                    return null;
                }

                ParameterInfo[] paramInfos = constructorInfo.GetParameters();                
                PropertyInfo[] origProperties = obj.GetType().GetProperties();
                

                if (paramInfos.Count() != origProperties.Count())
                {
                    return null;
                }

                object[] paramArgs = new object[paramInfos.Count()];


                for (int i = 0; i < paramArgs.Length; i++)
                {
                    PropertyInfo origProperty = origProperties.Where(prop => prop.Name == paramInfos[i].Name).FirstOrDefault();

                    if (origProperty == null)
                    {
                        return null;
                    }

                    object val = origProperty.GetValue(obj, null);
                    if (origProperty.PropertyType != paramInfos[i].ParameterType)
                    {
                        val = GetTypeInstance(val, paramInfos[i].ParameterType);
                    }

                    paramArgs[i] = val;
                }

                atiObj = constructorInfo.Invoke(paramArgs);
                return atiObj;
        }
        /// <summary>
        /// Metoda vrátí
        /// </summary>
        /// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
        /// <param name="prototype">Prototyp se strukturou anonymního typu</param>
        /// <returns>List instancí anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
        /// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
        public static List<T> CastToList<T>(this object obj, T prototype)
                                 where T : class
        {
            List<T> list = new List<T>();
            IEnumerable<T> enumerable = obj as IEnumerable<T>;

            if (enumerable != null)
            {
                list.AddRange(enumerable);
            }
            else
            {
                    IEnumerable enumObjects = obj as IEnumerable;
                    if (enumObjects == null)
                    {
                        return null;
                    }
                    
                foreach (object enumObject in enumObjects)
                    {
                        T currObject = ToAnonymousType(enumObject, prototype);
                        if (currObject == null)
                        {
                            //K čistění listu by neměl být důvod, ale garantujeme, že nevrátíme částečně naplněný list
                            list.Clear();
                            return list;
                        }

                        list.Add(currObject);
                    }
                
            }

            return list;
        }
    }
    

Při přetypovávání stačí stále jen zadat prototyp anonymního datové typu.

 

//Anonymní typ z jiné assembly!
            var result2 = TestAT.GetResult().CastToList(new {FirstLetter = default(char), 
                                                        Index =default(int),
                                                        Original = default(string),
                                                        InnerAT = new { X = default(char), B = new { A = default(int) } }
            })
                                                       ;
            foreach (var res in result2)
            {
                Console.WriteLine(res.FirstLetter);
                Console.WriteLine(res.Original);
            }


            Console.WriteLine(TestAT.
                                    GetResult().
                                    CastToList(new
                                    {
                                        FirstLetter = default(char),
                                        Index = default(int),
                                        Original = default(string),
                                        InnerAT = new { X = default(char), B = new { A =default(int)} }
                                    }
                                    ).
                                    Where(car => car.FirstLetter == 'T')
                                     .FirstOrDefault()
                                     .ToString());
            Console.ReadLine();


Friday, 09 May 2008 09:09:26 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms


 Thursday, 08 May 2008
LINQ - anonymní typ deklarovaný v jedné assembly dostupný v metodách další assembly?
.Net Framework

Anonymní datové typy v LINQu nelze použít jako návratový typ z metody a jediný způsob, jak anonymní typ z metody předat, je použít jako návratovou hodnotu typ object, protože v .Net Frameworku - jak je všeobecně známo - všechny třídy přímo či nepřímo dědí z třídy Object. Navíc platí, že anonymní typ je kompilátorem vždy deklarován jako internal a jeho použití je tak striktně omezeno na jednu assembly.

Jde o rozumné omezení a anonymní datové typy bychom neměli zneužívat k nesmyslům typu "hezká syntaxe pro generování objektů Dictionary", které si našly cestu i do připravovaného (a už dnes "přehypovaného") MVC frameworku pro ASP.NET.

V různých diskuzích se ale stále dokola objevuje dotaz, jak anonymní typ z metody vráti. A každé omezení se dá samozřejmě obejít - když nefunguje ani bodový systém na silnicích, proč nenajít hrubý trik ve stylu "osoby blízké" i pro erozi různých omezení u anonymního datového typu. :) Znovu alibisticky varuji všechny před zařazením následujících nehezkých triků do svého arzenálu běžných postupů při vývoji, protože všechny postupy spoléhají na chování kompilátoru C#, které není garantováno a které se může v další verzi nebo i jen při vydání service packu .Net Frameworku bez varování změnit.

Pro vrácení anonymního datového typu z metody použijeme hezký hack od Tomáše, který se ujal pod názvem "Cast By Example". Zjednodušeně řečeno - sice nemůžeme používat při přetypovávání názvy anonymních datových typů (tříd), protože anonymní datové typy jsou generovány až při kompilaci, ale můžeme kompilátoru dát při přetypování "vzor", jaký anonymní datový typ nám bude vyhovovat. Podrobnosti si můžete najít v odkazovaném článku Tomáše Petříčka  = zde jen připomenu, že technika využívá současného chování kompilátoru, který pro různé deklarace anonymních datových typů se stejnými vlastnostmi generuje v jedné assembly vždy právě jednu třídu.

Napsal jsem jednoduše použitelné extenze, které vám dovolí nejen přetypovat jednu instanci "object" na anonymní datový typ, ale můžete přetypovat množiny záznamů na (anonymně ;-)) typovou kolekci List<NějakýAnonymniTyp>, a dokonce je možné jednoduše použít anonymní datové typy z jiné assembly.

 

/// <summary>
    /// Rozšíření pro LINQ
    /// </summary>
    static class RSLinqExtensions
    {

        /// <summary>
        /// Metoda přetypuje objekt na anonymní typ, jehož struktura byla předána v parametru <paramref name="prototype"/>
        /// </summary>
        /// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
        /// <param name="prototype">Prototyp se strukturou anonymního typu</param>
        /// <returns>Instanci anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
        /// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
        public static T ToAnonymousType<T>(this object obj, T prototype)
                                        where T: class
        {
            
            
            T atiObj = obj as T;
            
            if (atiObj == null)
            {

                ConstructorInfo constructorInfo = typeof(T).GetConstructors()[0];
                
                if (constructorInfo == null)
                {
                    return null;
                }

                ParameterInfo[] paramInfos = constructorInfo.GetParameters();                
                PropertyInfo[] origProperties = obj.GetType().GetProperties();
                

                if (paramInfos.Count() != origProperties.Count())
                {
                    return null;
                }

                object[] paramArgs = new object[paramInfos.Count()];


                for (int i = 0; i < paramArgs.Length; i++)
                {
                    PropertyInfo origProperty = origProperties.Where(prop => prop.Name == paramInfos[i].Name).FirstOrDefault();
                    
                    if (origProperty == null)
                    {
                        return null;
                    }
                                        
                    
                    paramArgs[i] = origProperty.GetValue(obj, null);                    
                }
                
                atiObj = constructorInfo.Invoke(paramArgs) as T;
            }
            
            return (atiObj);
        }
    
        /// <summary>
        /// Metoda vrátí
        /// </summary>
        /// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
        /// <param name="prototype">Prototyp se strukturou anonymního typu</param>
        /// <returns>List instancí anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
        /// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
        public static List<T> CastToList<T>(this object obj, T prototype)
                                 where T : class
        {
            List<T> list = new List<T>();
            IEnumerable<T> enumerable = obj as IEnumerable<T>;

            if (enumerable != null)
            {
                list.AddRange(enumerable);
            }
            else
            {
                    IEnumerable enumObjects = obj as IEnumerable;
                    if (enumObjects == null)
                    {
                        return null;
                    }
                    
                foreach (object enumObject in enumObjects)
                    {
                        T currObject = ToAnonymousType(enumObject, prototype);
                        if (currObject == null)
                        {
                            //K čistění listu by neměl být důvod, ale garantujeme, že nevrátíme částečně naplněný list
                            list.Clear();
                            return list;
                        }

                        list.Add(currObject);
                    }
                
            }

            return list;
        }
    }

Komentáře u metod by měly dostatečně popisovat funkci extenzí. Metoda ToAnonymousType předpokládá, že chcete přetypovat na instanci anonymního typu (např. při použití metody Single v LINQu), metoda CastToList pracuje s množinou (IEnumerable<T>) instancí anonymního datového typu. Většina kódu v obou metodách ošetřuje situaci, kdy pracujete s anonymním datovým typem z jiné (referencované) assembly, jehož data je potřeba přenést do instance anonymního datového typu v aktuální assembly.

Použití extenzí - nejprve u anonymního datového typu deklarovaného v assembly, ve které je také náš LINQ dotaz.

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections;
using LINQTEST;

class Program
    {

        //Anonymní typ deklarovaný v této (exe) assembly
        private static object GetLetters()
        {
           string[] names = {"Rene", "Petra", "Kamilka"};

           var test = from name in names
                      select new {FirstLetter = name[0], Index=1};
           return test;
        }


        static void Main(string[] args)
        {
            var result = GetLetters().CastToList(new {FirstLetter = default(char),
                                                      Index =default(int)}
                                                 );
            foreach (var res in result)
            {
                Console.WriteLine(res.FirstLetter);
            }

}

Metodě CastToList jsme predali "vzor" anonymího datového typu (new {FirstLetter = default(char), Index =default(int)}) a hodnoty vlastností jsme u prototypu inicializovali s využitím klíčového slova default. V metodě Main v cyklu foreach je funkční intellisense a můžeme pracovat zcela typově s proměnnou res. Jenom zdůrazním, že nyní žádná reflexe nebyla použita! Metoda CastToList s využitím automatické typové inference kompilátoru pouze zkopírovala prvky v IEnumerable<T> do našeho typového generického Listu.

if (enumerable != null)
            {
                list.AddRange(enumerable);
            }

Reflexe je využita při konverzi anonymního typu deklarovaného v jiné assembly. Předpokládejme, že v jiné assembly nazvané např. LINQTest máme další metodu vracející množinu dat skrytou opět za obecným rozhraním "služebníka zcela neužitečného" neboli třídy object.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LINQTEST
{
    public class TestAT
    {
        public static object GetResult()
        {
            string[] rows = { "Toyota", "Lexus", "Audi" };

            var test = from row in rows
                       select new { FirstLetter = row[0],
                                    Index=110,
                                    Original = row
                                  };
            
            return test;

        }
    }
}

Zkompilovanou assembly LINQTest zareferencujeme v našem projektu. Kód pro práci s anonymní datovým typem v jiné assembly se z pohledu uživatele LINQ extenze nijak nezměnil od předchozího příkladu.

 

class Program
    {


        static void Main(string[] args)
        {
            //Anonymní typ z jiné assembly!
            var result2 = TestAT.GetResult().CastToList(new {FirstLetter =  default(char), 
                                                        Index =default(int),
                                                        Original = default(string)}
                                                       );
            foreach (var res in result2)
            {
                Console.WriteLine(res.FirstLetter);
                Console.WriteLine(res.Original);
            }


            Console.WriteLine(TestAT.
                                    GetResult().
                                    CastToList(new
                                    {
                                        FirstLetter = default(char),
                                        Index = default(int),
                                        Original = default(string)
                                    }).
                                    Where(car => car.FirstLetter == 'T')
                                     .FirstOrDefault()
                                     .ToString());
            Console.ReadLine();
        }
    }

Jak si můžete všimnout, po cyklu foreach si požádám o data z jiné assembly znovu a poté nad vrácenou typovou kolekci vytvořím další projekci. A ani mě nemusí zajímat, že se mi pod rukama zcela změnil typ používaných objektů. :-)

Docela zábavná záležitost ne? ;-)

LINQ II - přetypovávání i vnořených anonymních datových typů z jiné assembly



Thursday, 08 May 2008 15:00:43 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms