Saturday, September 18, 2010
Pozvánka na mé kurzy OOP, UML a návrhových vzorů a odpovědi na některé dotazy zaslané emailem - podzim 2010
Opět bych Vás všechny rád pozval na pravidelný podzimní běh kurzů Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 a Pokročilé návrhové vzory a objektové principy 2. Níže také naleznete odpovědi na pravidelně se opakující dotazy v emailech.
Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1
Datum konání kurzu: 22. 11. – 24. 11. 2010
Místo konání:
Školící středisko Tutor
U Půjčovny 2
110 00 Praha 1
Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurz
Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 2
Datum konání kurzu: 29. 11. – 1. 12. 2010
Místo konání:
Školící středisko Tutor
U Půjčovny 2
110 00 Praha 1
Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurzy
Zde jsou některé nové ohlasy na kurzy. Raději podotýkám, že tato hodnocení jsou mi zaslána účastníky z vlastní vůle a já určitě účastníky nenutím, aby mi psali nějaká hodnocení, natož lichotivá, hned na kurzu ani po něm, jak se prý děje u konkurence.
pan Martin Pospíšil, společnost Team Trackers s. r. o.
Děkuji za materiály a za výborné školení. Líbilo se mi ještě víc než jednička, díky těm praktickým příkladům, které jsme řešili společně.
Ing. Rudolf Plíva, společnost NOWIRE
Děkuji za velmi zajímavé a přínosné školení!
pan Jiří Filous, společnost Česká pojišťovna a. s.
Ano, mám zájem o zasílání informací o nových kurzech a děkuji za skvělé (i když náročné) školení.
Odpovědi na některé dotazy, které jsme obdrželi od dříve přihlášených zákazníků emailem.
Otázka: Na kurz půjdu, ale raději počkám s přihláškou na týden před školením, protože stejně jako konkurence mi dáte “last minute” slevu ne?
Odpověď: Hry s nějakou uměle nadsazenou cenou, která je potom těsně před kurzem snižována a nabízena v rámci “Last minute” nabídky, nehrajeme. Přijde mi nespravedlivé a nesmyslné, aby každý účastník platil jinou částku v závislosti na dni přihlášení. Všichni dosavadní účastníci mohou potvrdit, že zaplatili cenu, která je uvedena u školení a je jedno, zda se přihlásili 3 měsíce nebo 3 dny před kurzem.
Otázka: Na kurzu bychom rádi diskutovali záležitosti, které se týkají našeho projektu, je to možné? Je možné s vámi probrat podrobně i vývojářské specialitky, které se týkají C#, C++,Windows Forms, Silverlightu, Windows Mobile….
Odpověď: Na veřejném kurzu můžeme diskutovat o věcech, které zajímají všechny účastníky. Specifické dotazy, které se týkají vašich projektů, je nejlepší probrat se mnou po 16. hodině, kdy skončí oficiální část kurzu, ale já jsem vám samozřejmě k dispozici od 16:00 do umdlení mého či vašeho. Jestliže chcete 3 dny věnovat jen svému projektu, zvažte inhouse variantu kurzu, kde si můžete témata definovat sami. Pro podrobné informace o podmínkách inhouse kurzů napište prosím Petře Steinové (petra@renestein.net), která Vám velmi ráda obratem pošle nabídku.
Těším se s Vámi na setkání na kurzu!
René Stein
Saturday, September 18, 2010 10:17:31 AM (Central Europe Standard Time, UTC+01:00)
Analytické drobky | Kurzy UML a OOP | Návrhové vzory | UML
Monday, August 23, 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, August 23, 2010 2:33:03 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | LINQ | Programátorské hádanky
Monday, May 24, 2010
Omezení pro argumenty šablony (template) v C++ napodobující “where“ omezení pro generické argumenty v C#
Tento článek je hlavně reakce na stížnost, kterou měl kolega-vývojář z firmy, kde vývojáři použivají C++ i C#. Stížnost byla zaměřena na to, že na rozdíl od C# není možné v C++ zkontrolovat v době kompilace, zda argument předaný do šablony implementuje vyžadované rozhraní, nebo je potomkem námi vyžadované třídy.
Zdůrazním hned v úvodu, že v článku mluvím o “klasickém” C++ a ne o jeho .Net dialektu C++/CLI, který podporuje šablony i generiku.
Příkladem v C# může být následující třída Collection, která vyžaduje, aby za generický parametr T byla dosazena třída podporující rozhraní IBusinessObject. Podrobněji jsem podobnou kolekci už v éře př. lq. (čteme před Linqem) popisoval třeba zde.
public class Collection<T> : where T : IBusinessObject
{
}
Šablony v C++ jsou i přes povrchní syntaktickou podobnost zcela odlišnou jazykovou konstrukcí při srovnání s generikou v C#, a protože jsou “uzavřené šablonové typy” generovány již v době kompilace, kompilátor sám zajistí, že typ předaný do šablony podporuje všechny námi vyžadované metody, vlastnosti a operátory. Přesto existují situace, kdy chceme garantovat, že do naší “šablonové” kolekce jsou vkládány jen objekty podporující stejné rozhraní, nebo vyžadujeme, aby vkládané objekty byly povinně potomky nějaké třídy. Nestačí nám tedy, když argument šablony podporuje metodu Commit, která má stejnou signaturu jako metoda Commit z třídy Transaction, ale chceme mít již v době kompilace ověřeno, že předaná instance je potomkem zřídy Transaction. Důvod? Třeba to, že metoda Commit z třídy Transaction má v sobě vysokoúrovňový scénář, na který spoléháte (vzor Template method, jehož název nemá nic společného s šablonami ani generikou ).
Pro novou verzi jazyka C++0x bylo plánováno, že různá omezení parametru šablony bude možné vyjádřit pomocí tzv. “Concepts”. Bohužel Concepts ani v C++0x nakonec nebudou a my si musíme vystačit s výrazovými prostředky současné verze jazyka C++. S jednou výjimkou - v článku použiju i jednu elegantní konstrukci z C++0x (implementována ve Visual Studio 2010), statickou verifikaci pomocí klíčového slova static_assert, ale ukážu, jak snadno můžeme static_assert nahradit.
Mějme běžnou šablonu třídy Collection. V našem případě jde jen o dostatečný draft třídy Collection s konstruktorem, destruktorem a prázdnou implementací metod Add a Remove.
template
<typename T>
class Collection
{
public:
~Collection(void)
{
}
Collection(void)
{
}
void Add(T& t)
{
}
void Remove(T& t)
{
}
};
Za generický parametr T lze nyní substituovat “cokoli”, ale my chceme omezit typ T pouze na instance podporující rozhraní IBusinessObject.
class IBusinessObject
{
public:
IBusinessObject(void);
virtual void CommitChanges() = 0;
virtual void RollbackChanges() = 0;
virtual ~IBusinessObject(void);
};
V C++ samozřejmě za rozhraní považujeme třídu, jejíž všechny metody jsou abstraktní, a proto převod dvou omezení ze C# ( potomek třídy, implementor rozhraní (realizace) ) se nám v C++ redukuje na nalezení ekvivalentu omezení “parametr šablony musí být potomkem námi vyžadované třídy”.
K vyjádření takového omezení si zavedeme pomocnou šablonovou třídu IsDerivedTest:
#pragma once
template
<typename Base, typename Derived>
class TestIsDerived
{
private:
struct Is_Derived_Helper
{
int dummy;
};
struct Not_Derived_Helper
{
int dummy;
int dummy2;
};
private:
TestIsDerived(void);
~TestIsDerived(void);
static Is_Derived_Helper Test(Base* base);
static Not_Derived_Helper Test(...);
public:
enum TestResult
{
IsDerivedResult = ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper))))
};
};
Co třída TestIsDerived umí?
Dva parametry šablony TestIsDerived mají výmluvné názvy – Base (bázová třída) a Derived (třída, u níž chceme zkontrolovat, zda je z Base odvozena)
Tuto třídu nechceme nikdy instanciovat, proto jsou její konstruktor a destruktor privátní. U privátních struktur Is_Derived_Helper a Not_Derived_Helper je důležitá jen jedna věc – musí mít odlišnou velikost (sizeof(Is_Derived_Helper) != sizeof(Not_Derived_Helper ), a proto první obsahuje jednu "dummy" proměnnou typu int a druhá struktura dvě “dummy” proměnné typu int.
Při kontrole zda parametr šablony Derived je odvozen z parametru šablony Base postupujeme takto:
- Máme dvě privátní statické metody nazvané Test. Tyto funkce jsou jen deklarovány, jejich definici nepotřebujeme, protože nebudou nikdy zavolány. Důležité je jen to, že jedna metoda Test vrací Is_Derived_Helper a druhá metoda Test vrací Not_Derived_Helper - připomňme si, že jde o struktury mající různou velikost.
- Metoda Test, která vrací strukturu Is_Derived_Helper, přijímá jako argument pointer na parametr Base, a může tedy přijmout i pointer na Derived, pokud Derived je potomkem Base. Druhá metoda Test (Test(…)) bude vyvolána vždy, když se nepodaří Derived* převést na Base*.
- Do hodnoty IsDerivedResult v enumeraci TestResult uložíme výsledek výrazu ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper)))) - “voláme” metodu Test tak, že přes operátor static_cast přetypujeme konstantu 0 na pointer na Derived . Ač může vypadat přetypování 0 na Derived* jako “zvěrstvo” za účelem rychlého poslání programu do
řiti říše binárního nebytí, slovo voláme je v předchozí větě záměrně v uvozovkách, protože jak jsem již psal, k žádnému volání metody Test nikdy nedojde. V době kompilace ale kompilátor zjistí, jakou variantu metody Test by zavolal pro Derived* a my z velikosti “potenciální a nikdy skutečně nevrácené” návratové hodnoty zvolené metody Test jsme schopni poznat, jestli je Derived potomkem Base. Pokud je Derived potomkem Base, bude zvolena varianta Test(Base* base), která vrací Is_Derived_Helper, a podmínka ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper) bude pravdivá - uložíme do IsDerivedResult hodnotu 1. Pokud Derived není potomkem Base, bude zvolena varianta metody Test(…), která vrací Not_Derived_Helper, a podmínka ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper) bude nepravdivá (uložíme do IsDerivedResult hodnotu 0). Znovu zdůrazňuji, že k vyhodnocení podmínky dojde již v době kompilace.
Scéna je připravena, pozveme hlavní aktéry:
Mějme dvě třídy. Třída Invoice je potomkem třídy IBusinessObject a třída NotBusinessObject (nomen omen :) ) překvapivě není.:)
class NotBusinessObject
{
public:
NotBusinessObject(void);
virtual ~NotBusinessObject(void);
};
#pragma once
#include "IBusinessObject.h"
class Invoice : public IBusinessObject
{
public:
virtual void CommitChanges();
virtual void RollbackChanges();
Invoice(void);
~Invoice(void);
};
#include "StdAfx.h"
#include <iostream>
#include "Invoice.h"
using namespace std;
Invoice::Invoice(void)
{
}
Invoice::~Invoice(void)
{
}
void Invoice::CommitChanges()
{
cout << "Commit";
}
void Invoice::RollbackChanges()
{
cout << "Rollback";
}
Naše třída Collection má přijímat jen instance potomků třídy IBusinessObject.
#pragma once
#include "TestIsDerived.h"
template
<typename T>
class Collection
{
public:
~Collection(void)
{
static_assert(TestIsDerived<IBusinessObject, T>::IsDerivedResult, "Invalid base class, IBusinessObject required");
}
Collection(void)
{
}
void Add(T& t)
{
}
void Remove(T& t)
{
}
};
Do destruktoru jsme vložili statickou kontrolu, kdy námi vytvořené pomocné třídě TestIsDerived substituujeme za parametr šablony Base třídu IBusinessObject a za parametr Derived přímo parametr šablony třídy Collection, který musí být vždy potomkem IBusinessObject. Do destruktoru jsme kód vložili proto, aby bylo garantováno, že při instanciaci šablony Collection ke statické kontrole vždy v době kompilace dojde – kód bychom mohli vložit i do konstruktoru, ale budeme-li mít konstruktorů více, museli bychom kontrolu duplikovat v každém konstruktoru. Statická kontrola v destruktoru spoléhá na to, že vytvořený konkrétní objekt Collection<T> je v aplikaci zlikvidován - buď jde o likvidaci automatické (lokální) proměnné, nebo vy sami použijete operátor delete atd. Jestliže by destruktor objektu v aplikaci nikdy nebyl volán, kompilátor nebude destruktor v šablonové třídě při rozvíjení šablony instanciovat, stejně jako nikdy neinstanciuje další nepoužité metody v šabloně.
A použití:
Tento kód bez problémů projde. Invoice je potomkem IBusinessObject.
// TemplateConstraint_Console.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "Collection.h"
#include "Invoice.h"
#include "NotBusinessObject.h"
int _tmain(int argc, _TCHAR* argv[])
{
Collection<Invoice> col1;
}
Tento kód nahlásí chybu. NotBusinessObject není potomkem třídy IBusinessObject.
// TemplateConstraint_Console.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "Collection.h"
#include "Invoice.h"
#include "NotBusinessObject.h"
int _tmain(int argc, _TCHAR* argv[])
{
Collection<NotBusinessObject> col1;
}
Ve Visual Studiu 2010 dostanu v době kompilace tuto chybu:
Error 1 error C2338: Invalid base class, IBusinessObject required c:\users\stein\documents\visual studio 2010\projects\templateconstraint_console\templateconstraint_console\collection.h 16 1 TemplateConstraint_Console
Tím je náš úkol splněn, ale jak jsem sliboval, ukážu nyní, i jak se můžeme obejít bez klíčového slova static_assert z C++0x.
Třída TestIsDerived zůstane beze změny, ale napíšeme další dvě šablony, ve kterých budeme reagovat na hodnotu IsDerivedResult z třídy TestIsDerived, což původně dělal přímo static_assert.
#pragma once
template
<int T>
struct Invalid_Base_Class
{
public:
Invalid_Base_Class(void)
{
}
~Invalid_Base_Class(void)
{
}
};
template<>
struct Invalid_Base_Class<0>
{
private:
Invalid_Base_Class(void);
~Invalid_Base_Class(void);
};
Šablona Invalid_Base_Class má jeden šablonový parametr typu int. Struktura není svým rozhaním zajímavá, podstatný je pro nás jen název struktury, abychom v době kompilace viděli chybovou hlášku “Špatná bázová třída”. První šablona Invalid_Base_Class je běžnou strukturou, která bude použita pro instanciaci všech hodnot typu int krome hodnoty 0. Pro hodnotu 0, o níž víme, že je uložena v hodnotě enumerace TestIsDerived::IsDerivedResult, jestliže DERIVED NENÍ potomkem BASE, je určena explicitní specializace Invalid_Base_Class<0>, která má ale privátní konstruktor, takže pokus o její vytvoření vždy již při kompilaci selže.
V hrubé podobě, tedy bez vlastního makra assert_is_derived apod., které skryje práci s našimi strukturami Invalid_base_Class a které by zde jen zamlžovalo průzračnost řešení, můžeme kontrolu na nutnost šablonového parametru T třídy Collection<T> dědit z třídy IBusinessObject přepsat takto:
#pragma once
#include "Invalid_Base_Class.h"
template
<typename T>
class Collection
{
public:
~Collection(void)
{
Invalid_Base_Class<TestIsDerived<IBusinessObject, T>::IsDerivedResult> invalid_base;
invalid_base;
}
Collection(void)
{
}
void Add(T& t)
{
}
void Remove(T& t)
{
}
};
V destruktoru se pokusíme instanciovat strukturu Invalid_Base_Class, které jako šablonový argument předáme hodnotu v IsDerivedResult.
Collection<Invoice> opět bez problémů projde, ale při pokusu vytvořit Collection<NotBusinessObject> dostanu při kompilaci tuto chybovou zprávu:
Error 1 error C2248: 'Invalid_Base_Class<0>::Invalid_Base_Class' : cannot access private member declared in class 'Invalid_Base_Class<0>' c:\users\stein\documents\visual studio 2010\projects\templateconstraint_console\templateconstraint_console\collection.h 14 1 TemplateConstraint_Console
Řešení by se dalo přisladit dalším syntaktickým cukrem, ale princip by měl být z článku zřejmý.
Triky s šablonami jsou úžasné a převod některých rysů generiky ze C# není zase tak problematický. Jen ta nonšalantní elegance, která je C# vlastní, v přihroublém, ale o to výkonnějším, C++ trochu chybí. :) Kdyby někoho triky se šablonami zaujaly, doporučuji ke studiu Alexendrescovu knihu Modern C++ Design: Generic Programming and Design Patterns Applied.
Monday, May 24, 2010 3:50:36 PM (Central Europe Standard Time, UTC+01:00)
C# | Nativní kód
Wednesday, April 7, 2010
Pozvánka na kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 – jaro 2010
Update: Kurz je zcela obsazen včetně náhradníků.
Rád bych Vás pozval na další běh kurzu Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1. Pokud se někdo z Vás (oprávněně) diví, proč tak pozdě a proč Vás nezvu i na kurz OOP 2, níže v tomto spotu nalezne odpovědi.
Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1
Datum konání kurzu: 14. 6. – 16. 6. 2010
Místo konání:
Školící středisko Tutor
U Půjčovny 2
110 00 Praha 1
Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurz
Odpovědi na některé dotazy, které jsme obdrželi od dříve přihlášených zákazníků emailem.
Otázka: Proč se kurz nekoná na předchozím místě (v hotelu Villa), kde byli naši kolegové?
Odpověď: Po vyhodnocení ohlasů na kurz jsme zjistili, že účastníci nebyli (stejně jako já ) příliš spokojeni s jídlem. Také jsme při domlouvání hotelu byli ujišťováni, kolik účastníků se může na kurz přihlásit a jak lze pro ně uspořádat pohodlně prostory, a tyto informace se v průběhu kurzu jevily jako marketingové fráze až blafy velmi optimistickými nebo nepočítaly s průměrným Evropanem, ale standardním liliputem. Stručně řečeno – poměr cena/výkon nebyl u čtyřhvězdičkového hotelu příliš dobrý. Jedinou výhodou hotelu Villa bylo parkování, nyní bude lepší na ”poslední míli” použít MHD.
Otázka: Proč se koná kurz tak pozdě (oproti přechozím rokům) a proč se nekoná kurz OOP2.
Odpověď: Byl a jsem vytížen tento rok dalšími projekty a nalézt vhodné termíny bylo problematické. Také jsme sháněli nové prostory, kde se bude kurz konat (viz předchozí odpověď). Termín kurzu OOP2 vycházel již na červenec a v době dovolených nemá smysl kurz pořádat. Při větším zájmu vyhlásím (co nejdříve) více termínů kurzu OOP2 na podzim.
Těším se na setkání na kurzu!
Wednesday, April 7, 2010 12:48:36 PM (Central Europe Standard Time, UTC+01:00)
Analytické drobky | Kurzy UML a OOP | Návrhové vzory | UML
Sunday, March 21, 2010
Výhody a nevýhody softwarových továren
Emailem jsem dostal zajímavou otázku, jaký je můj názor na softwarové továrny a kde vidím výhody a nevýhody softwarových továren. Odpověď nakonec publikuji i zde – už jen proto, že jsem si při jejím psaní uvědomil, že na továrnu kladu stejné nároky jako na kteroukoli další knihovnu v systému a že výběr softwarové továrny se u mě moc neliší od výběru třeba ORM Frameworku. Nejde o taxativní výčet výhod a nevýhod, ale spíš o volně nahozená témata, která mě za 20 minut psaní příspěvku napadla.
SW továrny jsou jen pokračováním trendu, že nemá smysl vynalézat znovu kolo ani v případě, že by bylo o dvě setiny procenta krásnější než to staré, které je již navržené a hotové. SW továrna říká – namísto opakování chyb svých předchůdců použijte ověřené zkušenosti, osvědčené praktiky, doporučené postupy a již otestovaný kód. Namísto obecného návrhového vzoru nebo vágně popsaného doporučovaného postupu máte k dispozici osvědčenou knihovnu, základní průvodce a odborný polštář, který by měl zabránit, že napácháte v dané oblasti velké množství hrubých bezpečnostních nebo jen s duchem technologie neslučitelných chyb.
Takže výhody:
1) Osvědčená řešení ihned po ruce. Tím se cíl SW faktory moc neliší od obecných frameworků a návrhových vzorů.
2) Není nutné psát infrastrukturní kód přímo v SW firmě (ISV). Je možné se soustředit na problémovou doménu zákazníka a ne na to, jak centrálně publikovat události nebo jak propojit různé pohledy v aplikaci tak, aby spolu dokázaly komunikovat .
3) Pokud stejnou továrnu používáte napříč všemi projekty, je zaučení nového vývojáře na dalším projektu jednodušší a rychlejší.
4) SW továrna používá praktiky osvědčené v dané technologii. Takže odlišně jsou zakódovány původně platformně nezávislá doporučení a všeobecně zaměřené vzory v SW továrně pro ASP.Net a jinak pro Silverlight. Místo popisu obecných návrhových vzorů je již vzor adaptován na konkrétní cílové prostředí.
Nevýhody:
1) Kód nemáte pod kontrolou a musíte se spolehnout, že továrna sama neobsahuje příliš kritických chyb ani otravných chyb s nižší prioritou. To platí o jakékoli abstrakci (frameworku, knihovně), kterou na projektu používáte, ale myslím, že ani v Microsoftu stále nemají SW továrny stejnou podporu jako samotný .Net Framework. A také by měla být známa alespoň orientační roadmapa SW továrny, abyste věděli, kam autoři směřují a jestli jsou si vědomi nevýhod a omezení stávající verze SW továrny. Od továrny, která se ihned po svém uvedení honosí přídomky „experimentální, testovací, zkušební“, bych dal ruce pryč. Jen ti nejodvážnější z nás si dovolí svým zákazníkům za půl roku po zatracení SW továrny jako slepé uličky tvrdit, že musí trochu poštelovat kardiostimulátor výkonného srdce aplikace a že místo plánovaných dvou hodin za přidání dvou vlastností do objednávky zákazník zaplatí dva měsíce migrace na jinou SW továrnu. Nenechal bych se zmýlit tím, že některé firmy pro odvážlivce používající jejich experimentální SW továrny a frameworky razí lichotivý titul „early adopter“ – méně korektní a pravdě bližší překlad z marketingového slovníku totiž zní „natěšený všehoschopný blbec“.
2) Univerzální řešení jako je SW továrna může být pro jednoduché aplikace zabijákem. Více času strávíte integrací aplikace do rámce vynucovaného SW továrnou než psaním obchodní logiky specifické pro aplikaci a důležité pro zákazníka.
3) U některých složitějších aplikací zjistíte, že rámec SW továrny je příliš těsný a vy potřebujete SW továrnu pro svůj projekt upravit nebo rozšířit. Náklady na rozšíření služeb , „hackování“ a ohýbaní SW továrny pro danou problematiku mohou převážit nad výhodami SW továrny. Z velké radosti nad úsporou času v raných fázích projektu, kde byla továrna použita, můžete ještě před dodáním první verze projektu a po pořádném časovém skluzu, vynuceném třeba přepisem částí továrny, které pro výkonnostních testech nestačí stávajícím požadavkům na projekt, přejít k jadrným kletbám nad šílenou SW továrnou, v níž si úprava jednoho modulu kaskádově vynutí úpravy všech dalších modulů. Místo používání původně slibované elegantní černé skříňky s jednoduchými službami se prohrabujete nevábnými vyhřezlými vnitřnostmi mizerně navržené SW továrny.
Z výhod a nevýhod snad vyplývá můj postoj k SW továrnám. SW továrny jsou dalším evolučním stádiem na cestě, jejímž cílem je využít pro danou problematiku osvědčená řešení a postupy, nepsat stále se opakující kód nebo v každém řešení už jen změnou typu klienta (Web, Windows Forms, Silverlight, mobilní aplikace) neprocházet znovu a znovu všechny slepé vývojářské uličky specifické pro použitou technologii. Objevování objeveného ponechme těm, kterým stačí zařvat vítězoslavně heuréka, i když jsou v pořadí stí nebo tisící, kteří stejnou infrastrukturní nebo funkční trivialitu konečně zakódovali rozumným a v životním cyklu projektu dále udržovatelným způsobem.
A samozřejmě si nemyslím, že tohle evoluční stádium je konečné. :)
Sunday, March 21, 2010 11:57:10 AM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Analytické drobky | Návrhové vzory | UML
Wednesday, February 24, 2010
Podivné? chování při explicitním přetypování typu dynamic ve Visual Studiu 2010 RC
Na twitteru jsem psal, že si pohraju s implementací rozhraní ve třídě přes automatickou delegaci na privátní proměnnou s využitím nového typu dynamic v C# 4.0. Jestliže se dobře pamatuji, většinou se po nějakém takovém řešení pídí Delphisté. Z příkladu níže bude asi jasné i pro ostatni, co mám předchozími hutnými větami na mysli .
Při hraní si s typem dynamic jsem ale narazil na zvláštní chování při explicitním přetypování a chtěl bych poprosit někoho dalšího z mých čtenářů o vyzkoušení stejného chování ve Visual Studiu 2010 (nejlépe nejen na RC, ale i na starší Betě 2, kterou jsem už smazal). Příklad níže je jen jednoduchý “jednosměrný” prototyp, na kterém vynikne problém s explicitním přetypováním.
Zde je mnou zmiňovaná podivnost (problém):
Mějme rozhraní IWorker:
public interface IWorker
{
void DoWork();
}
A třídu Worker, která toto rozhraní implementuje.
class Worker : IWorker
{
#region Implementation of IWorker
public void DoWork()
{
Console.WriteLine(GetType().ToString());
}
#endregion
}
Dále máme třídu Order, která rozhraní IWorker neimplementuje, ale má privátní proměnnou m_worker implementující toto rozhraní, kterou předá své bázové třídě DirtyCastBase. DirtyCastBase je třída, která zajistí, že bude-li klient přetypovávat instanci Order na rozhraní IWorker, tak toto přetypování projde a klient dostane jako implementora instanci m_worker.
public class Order : DirtyCastBase
{
private IWorker m_worker;
public Order() : base()
{
m_worker = new Worker();
SetImplementors(m_worker);
}
}
Třída DirtyCastBase je potomkem třídy DynamicObject, která nám v .Net 4.0 dovoluje reagovat na “dynamická volání” a přidat jednoduše “dynamické chování” přepsáním metod začínajících písmeny Try (TryGetMember, TrySetMember ) apod. Já jsem přepsal metodu TryConvert, která se s využitím chráněné virtuální metody TryFindImplementor pokusí nalézt objekt, který podporuje rozhraní vyžadované uživatelem. Deskriptor rozhraní je předán ve vlastnosti Type argumentu binder.
public class DirtyCastBase : DynamicObject
{
private IEnumerable<Object> m_implementors;
private Dictionary<Type, object> m_castContext;
public DirtyCastBase()
{
m_castContext = new Dictionary<Type, object>();
}
public override bool TryConvert(ConvertBinder binder, out object result)
{
Type requestedType = binder.Type;
Tuple<bool, Object> FindResult = TryFindImplementor(m_implementors, m_castContext, requestedType);
if (FindResult.Item1)
{
result = FindResult.Item2;
return true;
}
return base.TryConvert(binder, out result);
}
protected virtual Tuple<bool, Object> TryFindImplementor(IEnumerable<Object> implementors, Dictionary<Type, object> currentCastContext, Type requestedType)
{
if (implementors == null)
{
throw new ArgumentNullException("implementors");
}
if (currentCastContext == null)
{
throw new ArgumentNullException("currentCastContext");
}
if (requestedType == null)
{
throw new ArgumentNullException("requestedType");
}
object result = null;
bool found = m_castContext.TryGetValue(requestedType, out result);
if (!found)
{
result = (from implementor in implementors
where implementor != null
let type = implementor.GetType()
where requestedType.IsAssignableFrom(type)
select implementor).FirstOrDefault();
found = result != null;
}
if (found)
{
m_castContext.Add(requestedType, result);
}
return new Tuple<bool, object>(found, result);
}
protected void SetImplementors(params object[] implementors)
{
SetImplementors(implementors.AsEnumerable());
}
protected void SetImplementors(IEnumerable<object> implementors)
{
if (m_implementors != null)
{
throw new InvalidOperationException();
}
m_implementors = implementors ?? Enumerable.Empty<Object>();
}
}
V našem případě by tedy třída Order by měla dovolit přetypování na rozhraní IWorker, i když sama toto rozhraní neimplementuje. Zanedbejme nyní, že není zachována referenční identita při konverzi i že vydáváme jako implementora privátní objekt, protože pro demonstrovanou techniku to není příliš podstatné.
Tento kód ověří, že přetypování projde. Využíváme implicitní (“bez závorek”) konverzi. Samozřejmě že je nutné instanci Order přiřadit do proměnné typu dynamic.
class Program
{
static void Main(string[] args)
{
dynamic order = new Order();
IWorker worker = order;
worker.DoWork();
Console.ReadLine();
}
}
Implicitní konverze projde, a jak jsem očekával, je vyvolána naše metoda TryConvert.
Když ale projde implicitní konverze, proč explicitní konverze selže a metoda TryConvert vyvolá výjimku?
static void Main(string[] args)
{
dynamic order = new Order();
IWorker worker = (IWorker) order;
worker.DoWork();
Console.ReadLine();
}
Ihned je vyvolána výjimka InvalidcastException {"Unable to cast object of type 'DynamicCastTest.Order' to type 'DynamicCastTest.IWorker'."}
Z call stacku výjimky (at CallSite.Target(Closure , CallSite , Object ) se dá odvodit, že výjimku vyhodil C# DLR binder, který se ale ani nepokusil zavolat metodu TryConvert. Metoda TryConvert by ve vlastnosti Explicit argumentu binder měla dostat příznak, že šlo o explicitní konverzi, ale tato metoda evidentně není volána.
Přitom z dokumentace metody TryConvert se dá usoudit, že by metoda TryConvert měla být při explicitní konverzi volána.
Ještě jsem zcela do detailu nestudoval všechna pravidla pro typ dynamic ve specifikaci C# 4.0, ale tohle na mě působí jako bug. Pokud projde implicitní konverze, proč by byla zcela zakázána explicitní? To vůbec není v souladu s pravidly kompilátoru pro konverze v C#:
“The set of explicit conversions includes all implicit conversions. This means that redundant cast expressions are allowed.” (C# specification sekce 6.2 Explicit conversions)
A C# DLR binder se i za běhu snaží podle mých dosavadních zkušeností vždy co co nejvěrněji napodobit chování C# kompilátoru.
Anebo už jsem dnes utahaný, nejde o žádnou anomálii a něco triviálního ve svém kódu přehlížím?
Tedy znovu. Můžete někdo spustit projekt ve svém Visual Studiu (nejlépe i v Betě 2) a podělit se o výsledek?
Zde je projekt ke stažení.
Díky!
Wednesday, February 24, 2010 7:19:03 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | C#
Friday, February 12, 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ář:
Friday, February 12, 2010 1:17:54 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | LINQ
Tuesday, February 2, 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.
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, February 2, 2010 7:43:00 AM (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | LINQ | Návrhové vzory | RX Extensions | Windows Forms
Thursday, January 28, 2010
C# Posterous API pro Silverlight 4 (a .Net Framework 3.5) – verze 0.0.0.2
Stáhnout C# Posterous API pro Silverlight 4.0
Stáhnout C# Posterous API pro .Net Framework 3.5
Vše podstatné k C# Posterous API naleznete v úvodním článku.
Poznámky ke změnám v této verzi:
- Kvůli verzi pro Silverlight přidány další asynchronní metody tak, aby bylo možné získat data z webu asynchronně, jak to Silverlight vyžaduje a jak je toto chování v aplikacích vynuceno asynchronními metodami v rozhraní platformních tříd - příkladem budiž rozhraní tříd WebRequest a WebResponse. Pokud se pokusíte zavolat synchronní verzi metody v SL z UI vlákna, měli byste z Posterous API dostat výjimku – to je lepší varianta, než skončit v paralyzovaném stavu, kdy celá aplikace na nic nereaguje.
Ukázka uložení změn v blogpostu – přidání komentáře. :
var comment = post.CreateNewComment(updateText);
post.SaveChangesCompleted += (_0, _1) =>
{
Assert.IsTrue(comment.Id > 0);
TestComplete();
};
post.SaveChangesAsync();
- V Silverlight verzi aplikace nedostanete v událostech rozhraní IRawRequestResponsePublisher k modifikaci objekty HttpWebRequest a HttpWebResponse, které jsou použity pro stažení obrázku autora (IAuthor.AuthorPicture) a stažení média (IPosterousMedium.Medium). Důvodem je, že pro stažení používám svého potomka třídy WebClient. Tento potomek přepisuje metody GetWebRequest a GetWebResponse, které jsou poté nabídnuty v událostech dostupných přes rozhraní IRawRequestResponsePublisher. V Silverlightu je ale třída WebClient označena jako kritický kód (Critical) a takzvaný transparentní (Transparent) kód, pod který patří i námi psaný kód, nemá právo z takto označené infrastrukturní třídy podědit. Kromě dvou výše zmíněných omezení ale IRawRequestResponsePublisher pracuje stejně jako v desktopové verzi a v další verzi API uvažuji, že místo WebClienta použiju na stažení obrázku autora i všech dalších médií přímo třídy WebRequest a WebResponse, které budou opět nabídnuty i v rozhraní IRawRequestResponsePublisher.
- Silverlight má omezený přístup k souborovému systému, a proto naleznete v blogpostu (IPosterousPost) a twitter postu (ITwitterPost) další dvě metody pro přidání médií. Ve verzi 0.0.0.1 bylo možné předat pouze cestu k souboru, nyní můžete předat jakýkoli stream – např. Stream načtený z IsolatedFileStorage.
Nové metody:
void AppendMedium(Stream stream, string mediaName, Action<Stream> releaseStreamFunction);
void AppendMedium(Stream stream, string mediaName);
Kromě streamu a názvu média můžete do jedné z variant funkce AppendMedium předat delegáta typu Action<Stream> , který bude vyvolán v okamžiku, kdy Posterous API již se streamem nepotřebuje pracovat. Nejasná zodpovědnost, kdo vlastní a hlavně uvolňuje zdroje alokované objektem stream, mě právě vedla k tomu, že v první verzi API pro .Net Framework 3.5 jste mohli pouze předat název souboru a životní cyklus FileStreamu byl zcela pod mou kontrolu. Jestliže delegáta releaseStreamFunction nepředáte, máte životní cyklus streamu zcela ve svých všemocných rukou. Posterous API ani jeho autor si nepřejí být vyzváni na souboj žádným šíleným omnipotentním vývojářem , a proto na předaném streamu Posterous API nikdy nevolá metodu Close ani Dispose.
Ukázka práce s metodou AppendMedium:
IPosterousPost newPost = site.CreatePost(DateTime.Now + " Media Silverlight",
"Príliš žlutoucký kun úpel dábelské ódy", true);
IsolatedStorageFileStream stream = new IsolatedStorageFileStream("AlbumArtSmall.jpg ", FileMode.Open, IsolatedStorageFile.GetUserStoreForSite());
newPost.AppendMedium(stream, "mojeSL.jpg", isoStream => isoStream.Close());
newPost.AddTag("Všechno a nic");
newPost.AddTag("C# omnia vincit");
newPost.SaveChangesCompleted += (_, __) =>
{
Assert.IsTrue(newPost.Id > 0);
TestComplete();
};
newPost.SaveChangesAsync();
Jednoduchá ukázka použití API v Silverlightu. V jednoduchém boxu na stránce zobrazíme titulek spotu a url spotu. Výsledek vypadá takto:
Ve Visual Studiu jsem založil projekt typu běžná navigační aplikace v Silverlightu. Zde je jednoduchý “ViewModel” pro stránku:
public class VM_Posts : INotifyPropertyChanged
{
public static readonly string ASYNC_EXCEPTION_TEXT = "Error while retrieving data. See inner exceptions for details";
private bool m_isBusy;
private IEnumerable<ViewPost> m_posts;
public VM_Posts()
{
m_posts = null;
IsBusy = false;
LinkClickCommand = new DelegateCommand<String>(link => Debug.WriteLine(link) /*m_navigationServices.HandleExternalLink(link)*/,
_ => true);
init();
}
private void init()
{
IsBusy = true;
IPosterousApplication posterousApp = PosterousApplication.Current;
IPosterousAccount posterousAccount = posterousApp.GetPosterousAccount("<PosterousUserName>", "PosterousPassword>");
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();
}
private void throwIfAsyncEx(Exception exception)
{
if (exception != null)
{
IsBusy = false;
throw new Exception(ASYNC_EXCEPTION_TEXT,exception);
}
}
public IEnumerable<ViewPost> Posts
{
get
{
return m_posts;
}
private set
{
m_posts = value;
if (m_posts != null)
{
IsBusy = false;
}
OnPropertyChanged(new PropertyChangedEventArgs("Posts"));
}
}
public bool IsBusy
{
get
{
return m_isBusy;
}
private set
{
m_isBusy = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsBusy"));
}
}
public ICommand LinkClickCommand
{
get;
private set;
}
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
#endregion
}
public class ViewPost
{
public string Title
{
get;
set;
}
public string Body
{
get;
set;
}
public string Url
{
get;
set;
}
}
}
V privátní metodě init, která je volána ihned po vytvoření instance třídy WM_Posts, nahrajeme blogy (Sites –> metoda LoadSitesAsync) a poté opět asynchronně z primárního blogu (PrimarySite) nahraju všechny blogposty (metoda LoadAllPostsAsync). Jestliže při některém asynchronním volání dojde k výjimce, metoda throwIfAsyncEx výjimku zpropaguje do UI vlákna.
Z nahraných blogpostů jsou vlastností Title, Url a Body přeneseny do instancí pomocné třídy ViewPost a kolekce objektů ViewPost je uložena do vlastnosti Posts. Třídu ViewPost vytvořit musíme, protože “Binding” dat, který budeme používat v XAMLu, nedokáže v Silverlightu z bezpečnostních důvodů pře reflection přistupovat ke člelnům “internal” třídy deklarované v jiné assembly. Třída PosterousPost, která v Posterous API implementuje rozhraní IPosterousPost, je označena jako internal. Ze stejného důvodu v Silverlightu nemohu použít místo explicitně definované třídy ViewPost anonymní datový typ – i anonymní datové typy jsou reprezentovány ve výsledném aplikačním dll třídami s modifikátorem viditelnosti “internal” a kód pro “binding” dat je ve zcela jiné platformní assembly.
Kromě vlastnosti Posts nabízí třída WM_Posts vlastnost IsBusy, kterou v XAMLu využijeme k zobrazení indikátoru informujícího uživatele, že právě získáváme data. Vystaven je také LinkClickCommand, který nůže být zavolán například v okamžiku, kdy je stisknuto nějaké tlačítko reprezentující hyperlink s URL blogpostu.
A zde je pro úplnost ještě XAML. Památeční XAML, protože mi při jeho psaní nejméně a bez jakékoli nadsázky 50x spadlo Visual Studio 2010 Beta 2 (s Resharperem).
<navigation:Page x:Class="SL_PosterousAPITest.Home"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
xmlns:ct="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
xmlns:local="clr-namespace:SL_PosterousAPITest;assembly=SL_PosterousAPITest"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"
Title="Home"
Style="{StaticResource PageStyle}">
<Grid x:Name="LayoutRoot">
<Grid.Resources>
<local:VM_Posts x:Key="PostsView"></local:VM_Posts>
<Style x:Name="PostTitleStyle" TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="IndianRed" />
<Setter Property="FontSize" Value="14" />
</Style>
<Style x:Key="PostsRectangle" TargetType="Rectangle">
<Setter Property="RadiusX" Value="30" />
<Setter Property="RadiusY" Value="30" />
<Setter Property="Fill">
<Setter.Value>
<LinearGradientBrush>
<GradientStop Offset="0" Color="LightYellow" />
<GradientStop Offset="1" Color="LightBlue" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ItemsControl">
<Setter Property="Width" Value="500" />
<Setter Property="Height" Value="360" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ScrollViewer BorderThickness="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid>
<Rectangle Style="{StaticResource PostsRectangle}">
</Rectangle>
<ItemsPresenter Margin="15,15,0,0"/>
<ct:BusyIndicator IsBusy="{Binding Path=IsBusy}"
DisplayAfter="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
</Grid>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}">
<StackPanel x:Name="ContentStackPanel" DataContext="{StaticResource PostsView}">
<TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"
Text="Home"/>
<ItemsControl Name="itemsPosts" ItemsSource="{Binding Mode=OneWay, Path=Posts}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource PostTitleStyle}" Text="{Binding Path=Title}"></TextBlock>
</StackPanel>
<HyperlinkButton Content="{Binding Path=Url}"
Command="{Binding Source={StaticResource PostsView}, Path=LinkClickCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Content}"
FontSize="12"
></HyperlinkButton>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Grid>
</navigation:Page>
Thursday, January 28, 2010 4:33:08 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | Silverlight
Wednesday, January 20, 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
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, January 20, 2010 5:54:21 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | LINQ | RX Extensions