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í.
U prezentace je třeba mít na paměti:
- Jedná se jen o osnovu “přehledové“ a cca dvouhodinové přednášky.
- Témata, typy projektů a technologie jsou v přednášce voleny podle zájmu zákazníka.
- Snažil jsem se nebýt hned v této úvodní přednášce příliš ostrý a konfliktní.
- 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)
.NET Framework | ASP.NET | C# | Compact .Net Framework | LINQ | RX Extensions | Silverlight | Web Services | Windows Forms | WP7
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.
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)
.NET Framework | C# Posterous API | LINQ | Návrhové vzory | RX Extensions | Windows Forms
Wednesday, 04 February 2009
Textbox nepodporující výběr textu a další specialitky
Někdy se hodí mít textbox, u kterého je skrytý "caret" (netuší někdo, jak se termín caret překládá - pouze kurzor?) a současně nepodporuje označování textu. Také můžete chtít, aby se textbox choval podobně jako při nastavení vlastnosti ReadOnly na true, ale bez "zašedlého" zobrazení textboxu, což je většinou nechtěný průvodní jev textových polí označených pouze pro čtení.
Kód je pro Compact .Net Framework, nic vám ale nebrání přenést jej na "velký" NF. Ke zpracování Windows zpráv je použita třída NativeWindow, která je součástí OpenNetCF frameworku. Opravdu jen tato podivná sekvence zpracování Windows zpráv byla v CNF ta pravá.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using OpenNETCF.Windows.Forms;
namespace HideCaret
{
public class NativeTxtWrapper : NativeWindow
{
[DllImport("CoreDll.dll")]
private static extern bool ShowCaret(IntPtr hWnd);
[DllImport("CoreDll.dll")]
private static extern bool HideCaret(IntPtr hWnd);
private TextBox m_txtBox;
private bool m_hasFullFocus;
private const int WM_LBUTTONDOWN = 0x201;
private const int WM_KEYDOWN = 0x0100;
private const int WM_MOUSEMOVE = 0x0200;
private const int WM_CHAR = 0x0102;
private const int WM_COMMAND = 0x0111;
private const int WM_LBUTTONUP = 0x0202;
private const int WM_LBUTTONDBLCLK = 0x0203;
private const int WM_PAINT = 0x000F;
private const int WM_KILLFOCUS = 0x0008;
private const int WM_SETFOCUS = 0x0007;
public NativeTxtWrapper(TextBox txtBox)
{
init(txtBox);
m_hasFullFocus = false;
}
private void init(TextBox txtBox)
{
if (txtBox == null)
{
throw new ArgumentNullException("txtBox");
}
if (txtBox.Handle != IntPtr.Zero)
{
AssignHandle(txtBox.Handle);
}
txtBox.HandleCreated += ((sender, e) => AssignHandle(((Form)sender).Handle));
txtBox.HandleDestroyed += ((sender, e) => ReleaseHandle());
m_txtBox = txtBox;
}
protected override void WndProc(ref Microsoft.WindowsCE.Forms.Message m)
{
if (m.Msg == WM_CHAR)
{
m_hasFullFocus = true;
}
if (m.Msg == WM_MOUSEMOVE || m.Msg == WM_LBUTTONDBLCLK)
{
return;
}
if (((m.Msg == WM_LBUTTONUP) ||
(m.Msg == WM_LBUTTONDOWN)) && m_hasFullFocus)
{
return;
}
if (m.Msg == WM_SETFOCUS)
{
base.WndProc(ref m);
HideCaret(m_txtBox.Handle);
return;
//m_hasFocus = false;
}
if (m.Msg == WM_KILLFOCUS)
{
m_hasFullFocus = false;
}
base.WndProc(ref m);
}
}
}
Jestliže chcete textbox pouze pro čtení, stačí nepředat ke zpracování bázové třídě (base.WndProc) zprávu WM_CHAR.
Objektem NativeTxtWrapper lze oddekorovat jakýkoli textbox, nebo můžete vytvořit potomka třídy Textbox, který před klienty třídy skryje použití objektu NativeTextWrapper
private void Form1_Load(object sender, EventArgs e)
{
m_wrapper = new NativeTxtWrapper(textBox1);
textBox1.Focus();
}
Podobnou třídu mám i pro nativní projekty psané v MFC, možná se někomu z vás bude hodit. Přepsat kód do Windows API z MFC je také trivální.
#pragma once
#include "afxwin.h"
class CTextBoxEx :
public CEdit
{
private:
bool m_hasFullFocus;
public:
CTextBoxEx(void);
~CTextBoxEx(void);
virtual LRESULT WindowProc(UINT message, WPARAM wparam, LPARAM lparam);
};
#include "StdAfx.h"
#include "TextBoxEx.h"
CTextBoxEx::CTextBoxEx(void) : m_hasFullFocus(false)
{
}
CTextBoxEx::~CTextBoxEx(void)
{
}
LRESULT CTextBoxEx::WindowProc(UINT message, WPARAM wparam, LPARAM lparam)
{
//Readonly textbox
if (message == WM_CHAR)
{
return 1;
}
//end Readonly textbox
POINT mousePoint;
GetCursorPos(&mousePoint);
ScreenToClient(&mousePoint);
RECT clientRect;
GetClientRect(&clientRect);
BOOL isMouseInRect = PtInRect(&clientRect, mousePoint);
if (((message == WM_MOUSEMOVE) || (message == WM_LBUTTONDBLCLK)) && isMouseInRect)
{
return 1;
}
if (((message == WM_LBUTTONUP) ||
(message== WM_LBUTTONDOWN)) && m_hasFullFocus && isMouseInRect)
{
return 1;
}
if (message == WM_SETFOCUS)
{
m_hasFullFocus = true;
HideCaret();
return 1;
}
if (message == WM_KILLFOCUS)
{
m_hasFullFocus = false;
}
return CEdit::WindowProc(message, wparam, lparam);
}
Wednesday, 04 February 2009 16:52:26 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Nativní kód | Windows Forms
Monday, 02 February 2009
Lehká imitace některých rysů windows forms aplikací v non-windows forms aplikacích
Omluvte prosím trochu kryptický název, ale lepší a hlavně výstižnější pojmenování článku mě nenapadlo. Název je stejně jen vábnička na čtenáře, proto se podívejme, co je jím míněno.
Již několikrát mně různí vývojáři tvrdili, jak nepříjemná je pro ně práce s konzolí (windows službou, dosaďte další typy aplikací dle libosti...), protože musí řešit, aby aplikace po svém spuštění ihned neskončila, a také je pro ně problematické zajistit, aby byly některé události zpracovány vždy ve stejném threadu.
Převedeme-li emocionální stížnost do věcného jazyka, zjistíme, že to, co v těchto typech aplikací chybí, jsou následující rysy běžné windows forms aplikace:
- Windows aplikace spustí smyčku Windows zpráv (message loop) a vývojář pouze obsluhuje události formuláře. V (Compact) .Net Frameworku nám stačí zavolat Application.Run(new Form1()) a aplikace neukončí svůj běh, dokud není uzavřen poslední formulář nebo dokud ti drsnější z nás nezavolají Application.Exit. O životní cyklus aplikace, její spuštění a ukončení, se většinou nemusíme nijak starat.
- Při obsluze formuláře máme po volání metody Invoke garantováno, že předaný delegát bude vykonán v takzvaném UI threadu. Hlavním účelem metody Invoke (a sesterských metod BeginInvoke a EndInvoke) je threadově bezpečná komunikace s ovládacími prvky. Ovládací prvky ve stylu windows prvků v konzolových aplikacích (windows službách) nenajdeme, ale přesto bychom i v těchto typech aplikací občas chtěli mít nástroj, který garantuje, že všechny nebo vybrané události budou zpracovány v jednom výkonném threadu.
V tomto článku se objeví návrh, který pro non-windows forms aplikace přinese výše zmíněné rysy a přidá pár věcí navíc.
Pár vysvětlujících poznámek na úvod . Kód (přesněji řečeno draft k dalšímu rozpracování), který za chvíli uvidíte, má běžet na .Net frameworku a na Compact .Net Frameworku. Vím, že existují synchronizační kontexty pro thready, ale metodu Invoke jsem zmiňoval proto, že představuje společný jmenovatel pro obě prostředí, protože Compact .Net Framework je stále tím strýčkem - beznadějným sociálním případem, co nám nikdy nepřiveze žádné úhledně zabalené dárky, v nichž se skrývá třeba nádherná vlastnost SynchronizationContext.Current. Se znalostí tohoto omezení je také jasné, proč jsem nepoužil i další metody/vlastnosti dostupné jen ve "velkém" .Net Frameworku.
Dále v kódu jsou třídy obsahující ve svém názvu slovo *Console*. Nenechte se zmást, že mluvím dále jen o konzolových aplikacích, stejné třídy lze použít ve windows službě a dalších typech aplikací.
Zaveďme si nejdříve abstraktní třídu ConsoleTask, která je předkem všech zpracovávaných úloh v aplikaci. Zjednodušeně si můžeme třídu ConsoleTask a její potomky představit jako výchozí stavební prvky zapouzdřující chování analogické k vybraným a pro nás zajímavým rysům windows formulářů.
/// <summary>
/// Základní rozhraní pro položky zpracovávané v jedné frontě
/// </summary>
internal interface IExecuteWorkItem
{
/// <summary>
/// Implementace metody spustí úlohu
/// </summary>
void Execute();
}
/// <summary>
/// Bázová třída pro všechny úlohy
/// </summary>
abstract class ConsoleTask : IDisposable
{
#region Inner classes
/// <summary>
/// Výchozí implementace rozhraní <see cref="IExecuteWorkItem"/>
/// </summary>
private class WorkThreadItem : IExecuteWorkItem
{
#region Private variables
private Delegate m_del;
private object[] m_vals;
#endregion Private variables
/// <summary>
/// Konstruktor
/// </summary>
/// <param name="del">Delegát, který má být spuštěn ve frontě nadřazeného objektu <see cref="ConsoleTask"/></param>
/// <param name="vals">Argumenty delegáta</param>
public WorkThreadItem(Delegate del, params object[] vals)
{
if (del == null)
{
throw new ArgumentNullException("del");
}
m_del = del;
m_vals = vals;
}
/// <summary>
/// Metoda iniciuje vykonání předaného delegáta
/// </summary>
public virtual void Execute()
{
m_del.Method.Invoke(m_del.Target, m_vals);
}
}
#endregion Inner classes
#region private variables
private ManualResetEvent m_event;
private Thread m_innerWorkingThread;
private Queue<IExecuteWorkItem> m_workQueue;
private AutoResetEvent m_workingThreadEvent;
private object m_lockQueueRoot;
private bool m_continue;
private bool m_disposed;
#endregion private variables
#region constructors
/// <summary>
/// Konstruktor
/// </summary>
protected ConsoleTask()
{
m_lockQueueRoot = new object();
m_workQueue = new Queue<IExecuteWorkItem>();
m_event = new ManualResetEvent(false);
m_workingThreadEvent = new AutoResetEvent(false);
m_innerWorkingThread = new Thread(processWorkerThread);
m_continue = true;
m_disposed = false;
}
#endregion constructors
#region Properties
/// <summary>
///<see cref="WaitHandle"/> běžící úlohy
/// </summary>
public WaitHandle TaskWaitHandle
{
get
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
return m_event;
}
}
/// <summary>
/// Metoda vrátí true, jestliže volající thread je odlišný od threadu, který vyřizuje položky zpracovávané v jedné frontě
/// </summary>
public virtual bool InvokeRequired
{
get
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
if (SlaveWorkingTask != null)
{
return SlaveWorkingTask.InvokeRequired;
}
return (Thread.CurrentThread.ManagedThreadId != m_innerWorkingThread.ManagedThreadId);
}
}
/// <summary>
/// Volitelná instance <see cref="ConsoleTask"/>, která převezme odpovědnost za vyřizování položek zpracovávaných v jedné frontě
/// </summary>
public ConsoleTask SlaveWorkingTask
{
get;
set;
}
#endregion Properties
#region Methods
/// <summary>
/// Metoda garantuje, že dojde k vykonání předaného delegáta v threadu, ktrerý vyřizuje položky zpracovávané v jedné frontě
/// </summary>
/// <remarks>Metoda pouze zařadí položky ke zpracování a nečeká na výsledek volání delegáta. </remarks>
public virtual void Invoke(Delegate del, params object[] vals)
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
if (SlaveWorkingTask != null)
{
SlaveWorkingTask.Invoke(del, vals);
return;
}
lock (m_lockQueueRoot)
{
m_workQueue.Enqueue(new WorkThreadItem(del, vals));
m_workingThreadEvent.Set();
}
}
/// <summary>
/// Metoda spustí úlohu
/// </summary>
/// <remarks>Spuštěním úlohy se rozumí spuštění kódu v přepsané metodě <see cref="DoInternalRun"/> v samostatném threadu. Metoda Run nevrátí řízení, dokud není úloha dokončena.</remarks>
public void Run()
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
m_innerWorkingThread.Start();
ThreadPool.QueueUserWorkItem(obj => DoInternalRun());
m_event.WaitOne();
}
/// <summary>
/// Metoda ukončí úlohu
/// </summary>
public virtual void CloseTask()
{
m_continue = false;
m_workingThreadEvent.Set();
m_event.Set();
}
/// <summary>
/// Metoda pro explicitní uvolnění veškerých nepoužívaných zdrojů - součást implementace "Disposable" vzoru
/// </summary>
public void Dispose()
{
if (m_disposed)
{
return;
}
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// "Destruktor" - součást implementace "Disposable" vzoru
/// </summary>
~ConsoleTask()
{
Dispose(false);
}
/// <summary>
/// Interní implementace "Disposable" vzoru
/// </summary>
/// <param name="disposing">true - jestliže je metoda volána z metody Dispose, false, pokud je volána z destruktoru - metody Finalize</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
try
{
((IDisposable)m_workingThreadEvent).Dispose();
((IDisposable)m_event).Dispose();
m_disposed = true;
}
catch (Exception e)
{
Trace.WriteLine(e);
}
}
}
/// <summary>
/// Metoda, která musí být přepsána v odvozených třídách a která obsahuje logiku specifickou pro každou úlohu
/// </summary>
protected abstract void DoInternalRun();
/// <summary>
/// Obsluha fronty položek, které mají být zpracovány ve stejném threadu
/// </summary>
private void processWorkerThread()
{
const int EXPECTED_MINIMUM_ITEMS = 1;
while (m_continue)
{
m_workingThreadEvent.WaitOne();
if (!m_continue)
{
continue;
}
int m_count = EXPECTED_MINIMUM_ITEMS;
IExecuteWorkItem nextItem = null;
while (m_count > 0)
{
lock (m_lockQueueRoot)
{
m_count = m_workQueue.Count();
if (m_count != 0)
{
nextItem = m_workQueue.Dequeue();
}
}
try
{
if (nextItem != null)
{
nextItem.Execute();
}
}
catch (Exception ex)
{
Trace.WriteLine(ex);
}
m_count--;
}
}
}
#endregion Methods
}
Abstraktní třída ConsoleTask obsahuje ve svém veřejném rozhraní metodu Run, kterou spustíme úlohu. Metoda Run je šablonovou metodou (Template method), protože obsahuje závazný scénář pro veškeré odvozené úlohy. Potomci třídy ConsoleTask do scénáře vstupují na přesně vymezeném místě - v metodě DoInternalRun, která je deklarována jako abstraktní a všechny konkrétní odvozené třídy ji musí přepsat a doplnit vlastní logiku. Třída ConsoleTask tedy garantuje, že je vždy nejprve spuštěn thread vyřizující požadavky, které mají být vykonány ve stejném threadu (podrobný popis viz níže), poté je třída ThreadPool použita ke spuštění kódu v metodě DoInternalRun v jiném threadu a nakonec aktuální thread pozastavíme čekáním na signalizaci instance synchronizačního objektu ManualResetEvent (proměnná m_event). Ve vlastnosti TaskWaitHandle vydáváme stejný objekt ManualResetEvent, který může jiný thread využít k synchronizaci svého běhu s instancí třídy odvozené od třídy ConsoleTask. Tím simulujeme pro uživatele objektů odvozených z třídy ConsoleTask spuštění smyčky zpráv, protože aplikace není ukončena po zavolání metody Run. Za ukončení běhu úlohy zodpovídá metoda CloseTask - metoda uvolní pracovní thread vyřizující frontu požadavků nastavením proměnné m_continue na false a signalizací synchronizační primitivy workingThreadEvent. Dále metoda CloseTask přes signalizaci synchronizačního objektu v proměnné m_event informuje o dokončení celé úlohy - thread pozastavený v metodě Run bude uvolněn.
Třída ConsoleTask dále obsahuje definici privátní třídy WorkThreadItem, která implementuje rozhraní IExecuteWorkItem a má roli adaptéru. Instance třídy WorkThreadItem jsou jednotlivé položky, které mají být vykonány v jednom pracovním threadu. Adaptérem je třída WorkThreadItem proto, že převádí rozhraní jakéhokoli předaného delegáta na rozhraní IExecuteWorkItem. Po volání metody Execute objektu WorkThreadItem je vykonána metoda, na kterou ukazuje delegát.
Jméno vlastnosti InvokeRequired by mělo znít povědomě - metoda vrátí true, jestliže thread, který zjišťuje hodnotu vlastnosti , je odlišný od threadu, který zpracovává položky typu IExecuteWorkItem. Thread poté může použít metodu Invoke, která zajistí, že předaný delegát bude vykonán v pracovním threadu. Je to zmíněno i v dokumentaci metody Invoke, ale zde zdůrazním, že metoda Invoke zařadí pouze novou položku do fronty ke zpracování a dá signál pracovnímu threadu, že je dostupná další položka voláním metody Set na proměnné m_workingThreadEvent, což je instance synchronizační primitivy AutoResetEvent. Metoda Invoke nečeká na výsledek volání delegáta a ani není zaručeno, že po návratu z metody Invoke byl již předaný delegát vykonán. Samotná obsluha fronty položek, které mají být vykonány v jednom threadu, je soustředěna do metody processWorkerThread.
U metody Invoke a vlastnosti InvokeRequired si můžete všimnout podmíněné delegace volání na instanci ve vlastnosti SlaveWorkingTask. Jestliže vlastnost SlaveWorkingTask není null, je odpovědnost za zpracování položek přenesena na jinou instanci třídy ConsoleTask. Jednotlivé tasky mohou tvořit zárodečný řetězec odpovědnosti (Chain of responsibility) a za chvíli uvidíme, k čemu můžeme toto předávání odpovědnosti na jiné instance ConsoleTask využít.
Třída ConsoleTask také implementuje běžný .Net "Disposable" vzor pro uvolňování prostředků (rozhraní IDisposable, chráněná metoda Dispose a destruktor - metoda Finalize).
Mimikry konzolové aplikace, která se v rámci námi vykolíkovaného seznamu požadavků snaží vydávat za windows forms aplikaci, vylepšíme zavedením jednoduché fasády (vzor facade), která bude simulovat metodu Application.Run.
/// <summary>
/// Facade s rozhraním pro spuštění úkolu
/// </summary>
class ConsoleApplication
{
/// <summary>
/// Metoda spustí předaný úkol (Fasáda ke spuštění úloh napodobující známou metodu Application.Run z Windows Forms aplikací)
/// </summary>
/// <param name="task">Úkol, který má být spuštěn</param>
public static void Run(ConsoleTask task)
{
if (task == null)
{
throw new ArgumentNullException("task");
}
task.Run();
}
}
Jak vidno, metoda Run zcela deleguje vykonání na předaný ConsoleTask.
Jak se prozatím s naším modelem pracuje? Nejlepší bude napsat si potomka třídy ConsoleTask a zjistit to. Zkusme vytvořit úlohu, která na Compact .Net Frameworku zpracuje příchozí SMS.
/// <summary>
/// Třída pro zpracování přijatých SMS
/// </summary>
class SMSTask : ConsoleTask
{
#region private variables
private MessageInterceptor m_interceptor;
#endregion private variables
#region Methods
/// <summary>
/// Metoda začne sledovat SMS
/// </summary>
protected override void DoInternalRun()
{
m_interceptor = new MessageInterceptor(InterceptionAction.Notify, false);
ThreadPool.QueueUserWorkItem(
(obj) =>
m_interceptor.MessageReceived += m_interceptor_MessageReceived);
}
/// <summary>
/// Obslužná metoda uálosti <see cref="MessageInterceptor.MessageReceived"/>
/// </summary>
/// <param name="sender">Odesílatel události</param>
/// <param name="e">Argument události</param>
private void m_interceptor_MessageReceived(object sender, MessageInterceptorEventArgs e)
{
if (InvokeRequired)
{
Invoke((Action<SmsMessage>)(handleMessage), e.Message as SmsMessage);
}
else
{
handleMessage(e.Message
as SmsMessage); }
}
public override void CloseTask()
{
m_interceptor.Dispose();
base.CloseTask();
}
/// <summary>
/// Zpracování SMS
/// </summary>
/// <param name="message">SMS zpráva ke zpracování</param>
private void handleMessage(SmsMessage message)
{
Console.WriteLine(message.Body);
CloseTask();
}
#endregion methods
}
Autor tříd odvozených z bázové třídy ConsoleTask má lehkou práci, protože se soustředí jen na úkol (příjem SMS) a ne na to, že jeho kód bude vykonán v konzolové aplikaci. V přepsané metodě si přihlásíme odběr události MessageReceived - zde je událost přihlášena přes ThreadPool, ale není to nutné. Obslužná metoda události MessageReceived (m_interceptor_MessageReceived) po příjmu SMS zaručí, že SMS budou vždy zpracovány ve stejném pracovním vlákně použitím vlastnosti Invoke Required a Invoke. Jestliže je událost vyvolána v jiném než pracovním threadu obsluhujícím frontu položek ke zpracování, zavoláme metodu Invoke, které předáme delegáta ukazujícího na metodu handleMessage. K vytvoření delegáta jsme použili standardního generického delegáta Action<T>, kde jsme za generický parametr T dosadili třídu SmsMessage, jejíž instanci přijímá jako argument metoda handleMessage. Přepsali jsme i metodu CloseTask, která uvolní interceptora pro příjem zpráv a poté vyvolá implementaci metody CloseTask z bázové třídy. Zde je úloha ukončena po příjmu první zprávy voláním CloseTask z metody handleMessage, ale způsob ukončení úlohy je zcela v rukou vývojáře konkrétní úlohy.
Poznámka na okraj: U naší třídy SMSTask by bylo vhodné, když chceme přijmout jen jednu SMS, ihned si odhlásit odběr dalších zpráv, nebo si v interní proměnné nastavit, že již zpráva byla přijata a další zprávy nepředávat ke zpracování.
Novou úlohu spustíme tímto nezáludným a pro vývojáře windows forms aplikací povědomým kódem:
class Program
{
static void Main(string[] args)
{
SMSTask smsTask = new SMSTask();
ConsoleApplication.Run(smsTask);
}
}
Na vývojářské práci je nejlepší, že poté, co máte nějaký nosný nápad, můžete jej rozvíjet ad libitum. Co když chceme ve stejné aplikaci nejen přijímat SMS, ale také reagovat na události v objektu, který nás informuje o spuštěných aplikacích uživatele. Nebo chceme sledovat přes třídu SystemState informace o příchozích hovorech? Napráskat vše do jedné instance potomka třídy ConsoleTask "ResimVzdyckyVsechnoNaJednomMisteAJsemTotalneVPohodeVoe" je sice řešením, ale i jen laxním zastáncům vágně formulovaného principu jedné odpovědnosti třídy (zdravím Aleši :) ) se právě teď nasucho aktivoval podmíněný reflex, protože vědí, že při správě takové aplikace po kolegovi-pohodářovi je vztekem podmíněné zoufalecké uslintávání a hlasité nadávání to nejmenší.
Chceme určitě zachovat stávající strukturu aplikace, chceme spouštět libovolné množství různorodých úloh a navíc chceme mít možnost zpracovávat položky napříč jednotlivými úlohami ve stejném threadu - pracovní frontě. Úkol jako stvořený pro jednu z možných nenásilných inkarnací návrhového vzoru Composite v aplikaci.
/// <summary>
/// Třída reprezentující kompozitní úlohu - viz návrhový vzor Composite
/// </summary>
class CompositeTask : ConsoleTask
{
#region private variables
private ICollection<ConsoleTask> m_tasks;
#endregion private variables
#region Constructors
public CompositeTask(ICollection<ConsoleTask> tasks)
{
if(tasks == null)
{
throw new ArgumentNullException("tasks");
}
if (tasks.Count == 0)
{
throw new ArgumentException("One or more tasks are required");
}
m_tasks = tasks;
}
#endregion Constructors
#region Methods
/// <summary>
/// Spuštění všech úkolů
/// </summary>
protected override void DoInternalRun()
{
foreach (var task in m_tasks)
{
ConsoleTask task1 = task;
task1.SlaveWorkingTask = this;
ThreadPool.QueueUserWorkItem((obj) => task1.Run());
}
}
/// <summary>
/// Metoda ukončí všechny úkoly
/// </summary>
/// <remarks>Metoda pouze zavolá metodu CloseTask na všech předaných objektech <see cref="ConsoleTask"/>, ale nestará se o výsledek volání</remarks>
public override void CloseTask()
{
foreach (var task in m_tasks)
{
task.CloseTask();
}
base.CloseTask();
}
#endregion Methods
}
Metoda CompositeTask je také potomkem třídy ConsoleTask, a proto můžeme ve zbytku aplikace pracovat se stejným rozhraním, na které jsme zvyklí. Jednoduchá i složená úloha mají stejné rozhraní, takže si klient tříd nemusí být skládání úloh vědom, což je mimochodem jedna z hlavních motivací pro zavedení návrhového vzoru Composite. V konstruktoru očekáváme odkaz na kolekci dceřiných úkolů. V metodě DoInternalRun zavoláme v cyklu metodu Run všech předaných úkolů. Ještě před voláním metody Run ale nastavíme u každé úlohy vlastnost SlaveWorkingTask na aktuální objekt CompositeTask, což nám zaručí, že veškeré položky ze všech jednotlivých úloh vložené do pracovní fronty voláním metody Invoke budou zpracovány v jediném pracovním vlákně CompositeTask. Zde vidíme jeden z důvodů, proč máme vlastnost SlaveWorkingTask a proč třída ConsoleTask ve členech Invoke a InvokeRequired nejprve kontroluje, jestli má zpracovat požadavek ve své pracovní frontě, anebo existuje jiný vhodný objekt - "otrok" (SlaveWorkingTask), který se o položky postará sám. Metoda CloseTask opět nejprve zavolá metodu CloseTask na všech objektech ConsoleTask, ze kterých je aktuální instance třídy CompositeTask složena.
Opět poznámka: Snad si rozumíme v tom, že navržená třída CompositeTask není jediná možná. Jiná třída CompositeTask2 nemusí přesměrovávat pracovní frontu na sebe, další po uzavření úloh nejprve vyčká na ukončení všech dceřiných úloh. Další scénáře jistě nalezne laskavý čtenář sám.
Než třídu CompositeTask vyzkoušíme, vytvoříme si dalšího potomka Consoletask, který bude zpracovávat pravidelně vyvolávanou událost z našeho objektu.
Zde je jednoduchá "demo" třída, jejíž srdce tiká v rytmu události AliveEvent.
class MyEventClass
{
public event EventHandler<EventArgs> AliveEvent;
private bool m_continue;
private const int INTERVAL = 1000;
private object m_lockObj;
public MyEventClass()
{
m_continue = true;
m_lockObj = new object();
}
public void Start()
{
ThreadPool.QueueUserWorkItem((state) =>
{
while (m_continue)
{
Thread.Sleep(INTERVAL);
AliveEvent(this, new EventArgs());
}
});
}
public void Stop()
{
lock (m_lockObj)
{
m_continue = false;
}
}
protected void OnAliveEevent(EventArgs e)
{
if (AliveEvent != null)
{
AliveEvent(this, e);
}
}
}
Naše nová konkrétní úloha zpracovává události instance MyEventClass
class ConcreteTask : ConsoleTask
{
private const int HEART_BEAT_LIMIT = 10;
private MyEventClass m_evClass;
private int m_heartBeatcount;
private bool m_processEvent;
protected override void DoInternalRun()
{
m_evClass = new MyEventClass();
m_heartBeatcount = 0;
m_processEvent = true;
m_evClass.Start();
m_evClass.AliveEvent += evClass_AliveEvent;
}
private void evClass_AliveEvent(object sender, EventArgs e)
{
Action myAction = (Action) (
() =>
{
if (!m_processEvent)
{
return;
}
Console.WriteLine("Event fired");
m_heartBeatcount++;
if (m_heartBeatcount >= HEART_BEAT_LIMIT)
{
m_evClass.Stop();
m_processEvent = false;
CloseTask();
}
}
);
if (InvokeRequired)
{
Invoke(myAction);
}
else
{
myAction();
}
}
}
Jenom pro zajímavost je ukázáno, že metodě Invoke můžeme předat složenou (statement) lambdu (nebo anonymní metodu).
Spuštění více úloh pomocí třídy CompositeTask není odlišné od spuštění jedné úlohy.
class Program
{
static void Main(string[] args)
{
var compTask = new CompositeTask(new List<ConsoleTask>
{
new SMSTask(),
new ConcreteTask()
});
ConsoleApplication.Run(compTask);
}
}
Znovu opakuji, že článek měl za cíl ukázat, jak transponovat do jiného aplikačního rámce postupy, které Windows Forms vývojáři dobře ovládají a o kterých mi tvrdili, že jsou "přirozené". Další rozpracování těchto draftů napsaných v půlnoční chvilce nespavosti je už jen variací předvedených postupů.
Monday, 02 February 2009 14:51:15 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Návrhové vzory | Windows Forms
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)
.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?
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)
.NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms
Wednesday, 13 September 2006
Windows Forms DataGridView - nevykreslování záhlaví sloupce
V .Net konferenci na serveru builder se objevil dotaz, jak skrýt záhlaví (header) sloupce v DataGridView. Kolegové, zvyklí pravděpodobně stále na poněkud nehrabaný DataGrid z verze 1.x .Net Frameworku, radili vytvořit nový sloupec a nové záhlaví.
Nejjednodušší řešení ale představuje odchycení události CellPainting DatagridView a a v ní zamezíte vykreslení sloupce nastavením vlastnosti Handled na true. DataGridView má ale mnohem lepší objektový model než DataGrid a jednoduchých změn ve vykreslování DataGridView dosáhneme rychle v obsluze událostí CellPainting, RowPrePaint, RowPostPaint a dalších.
const int hiddenRowIndex = -1; //záhlaví má index -1
const int hiddenColumnIndex = 0; //skryjeme první sloupec
private void dataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (e.RowIndex == hiddenRowIndex && e.ColumnIndex == hiddenColumnIndex)
{
e.Handled = true;
}
}
Update : Samozřejmě nemusíte zcela rezignovat na vykreslení buňky a chcete vybarvit alespoň její pozadí.
Opět je to jednoduché - vyplníme pozadí a nastavíme e.Handled na true
e.Graphics.FillRectangle(myColor, e.CellBounds);
e.Handled = true;
Wednesday, 13 September 2006 17:17:52 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Windows Forms
Thursday, 06 May 2004
Windows forms combobox jen pro čtení
Nedávno jsem potřeboval zakázat v detailu uživateli výběr z comboboxu, ale zjistil jsem, že první nabízející se cesta přes vlastnost Enabled není vhodná. Když nastavíte vlastnost Enabled na false, tak sice uživatel žádnou položku nevybere, ale combobox působí na formuláři jako grafický "umrlec", protože dříve vybraná položka je "zašeděná". Potřeboval jsem vlastnost, která by ponechala vizuální vzhled comboboxu beze změny, ale nedovolila provést nový výběr.
Žádnou takovou vlastnost jsem nenašel, a proto jsem vytvořil potomka comboboxu, kterému jsem přidal vlastnost DisableDropDown. Když je vlastnost DisableDropDown true, tak combobox v přepsané metodě OnWndProc ignoruje zprávy o stisku tlačítka myši (WM_LBUTTONDOWN a WM_LBUTTONDBLCLK) a zprávy z klávesnice (WM_KEYDOWN, WM_KEYUP) . V další přepsané metodě OnKeyPress combobox zamezí zadání nové položky do textového pole nastavením vlastnosti Handled argumentu e na true. To je celý trik, snad se bude odvozený combobox hodit i Vám.
using
System;
using
System.Windows.Forms;
namespace
RStein.UI
{
///
<summary>
///
Combobox, u nějž je možné zakázat rozbalení seznamu položek a editaci textového pole
///
</summary>
public
class
ComboBoxEx : ComboBox
{
private
const
int
WM_KEYDOWN = 0x0100;
private
const
int
WM_KEYUP = 0x0101;
private
const
int
WM_LBUTTONDOWN = 0x0201;
private
const
int
WM_LBUTTONDBLCLK = 0x0203;
#region
private fields
private
bool
m_disableDropDown =
false
;
#endregion
private fields
///
<summary>
///
Konstruktor
///
</summary>
public
ComboBoxEx()
{
m_disableDropDown =
false
;
}
#region
public properties
///
<summary>
///
Zakázat zobrazení položek?
///
</summary>
public
bool
DisableDropDown
{
get
{
return
m_disableDropDown;
}
set
{
m_disableDropDown =
value
;
}
}
#endregion
public properties
#region
protected methods
protected
override
void
OnKeyPress(KeyPressEventArgs e)
{
if
(!DisableDropDown)
base
.OnKeyPress(e);
else
e.Handled = true;
}
protected
override
void WndProc(ref Message m)
{
if (!DisableDropDown)
base.WndProc (ref m);
else
if ((m.Msg != WM_KEYDOWN) &&
(m.Msg != WM_LBUTTONDOWN ) &&
(m.Msg != WM_LBUTTONDBLCLK) &&
(m.Msg != WM_KEYUP))
base.WndProc (ref m);
}
#endregion
protected methods
}
}
Thursday, 06 May 2004 21:15:00 (Central Europe Standard Time, UTC+01:00)
Windows Forms