Tag Archives: workflow

Workflow Monitor layouttal

Aki használta már a Windows Workflow Foundationben a tracking service-t, biztosan találkozott már az SDK-ban lévő Workflow Monitor alkalmazással, ami lehetővé teszi, hogy a Visual Studiohoz hasonlóan, grafikusan jelenítsük meg a folyamataink állapotát. Mivel ez az alkalmazás forráskóddal együtt elérhető, remek példa arra, hogy megtanuljuk, hogyan hosztolhatjuk a WorkflowView kontrollt saját alkalmazásainkban. A feladat egyébként nem egyszerű, jobban járunk, ha a kész példából vesszük az osztályokat, többre is szükségünk lesz 😦

Van egy fontos dolog, amire ebben a kódban nem találunk példát, ez pedig a folyamatábrák layoutjának kezelése. Ez szekvenciális workflow esetén nem probléma, hiszen ott úgysem lehet mozgatni az activity-ket, állapotgépek esetén viszont jó lenne, ha a monitorban is úgy jelennének meg a folyamataink, ahogy azokat a Studioban megrajzoltuk. Ha erről nem gondoskodunk, akkor az összes állapot-dobozunk egymás alatt fog elhelyezkedni, keresztül-kasul behálózva az összekötő vonalakkal.

A tetszetős című Everything about re-hosting the Workflow Designer című MSDN cikkben találunk utalást arra, hogyan menthetjük el a layoutot fájlba és tölthetjük vissza a layoutot fájlból. Ezzel csak egy baj van, kell hozzá layout fájl! Nem is probléma, hiszen a VS készít nekünk olyat, ott van .layout kiterjesztéssel, pont arra van szükség. Csakhogy ha ezt a megközelítést választjuk, a layout fájlnak ott kell lennie a futtatandó alkalmazásunk mellett, azaz ezt is telepíteni kell. Amikor pedig a telepítőt készítjük, beleütközünk abba a problémába, hogy a forrásfában kénytelenek leszünk vagy saját build eventeket írni, vagy pedig duplikálni a layout fájlt. Egyik sem túl fincsi.

Az igazi megoldás az lenne, ha fel tudnánk használni ugyanazt a fájlt futási időben, amit a Studio is használ fejlesztési időben. Ha a Properties ablakban megnézzük a .layout fájl Build Actionjét, láthatjuk, hogy Embedded Resource lesz belőle, nincs más dolgunk tehát, mint onnan kiolvasni.

Először megpróbáltam közvetlenül kiolvasni a fájl tartalmát ugyanúgy, mint ahogy más erőforrást kezelni szoktunk. Mivel ez nem vezetett eredményre, természetesen Reflectorhoz nyúltam és az alábbi eredményre jutottam: a Loader.cs fájlban található a WorkflowDesignerLoaderből származó Loader osztály, ami a folyamatok betöltéséért felelős. Ebben írjuk felül az OnEndLoad eseménykezelőt:

  protected override void OnEndLoad( bool successful, ICollection errors )
  {
    base.OnEndLoad( successful, errors );

    IList layoutErrors = null;
    IWorkflowRootDesigner rootDesigner = 
(IWorkflowRootDesigner) ActivityDesigner.GetRootDesigner( base.LoaderHost ); Type type = rootDesigner.Component.GetType(); string manifestResourceName = type.Name + ".layout"; this.LoadDesignerLayoutFromResource( type, manifestResourceName, out layoutErrors ); }

Így már csak arra az egy szerelvényre van szükségünk, amiben a workflow definíció is található és ugyanúgy fog megjelenni a Monitorban, mint a fejlesztőkörnyezetben.

 

Advertisements

Tracking profile importálása szkriptből

Nincs jobb, mint amikor az embernek van egy olyan szkriptje kéznél, ami egyszerűen és átláthatóan teszi a dolgát és igazán megkönnyíti a fejlesztő munkáját. Az egyik gyakori feladat az adatbázis létrehozása, amit két módon lehet megtenni:

  • Adatbázis attachelésével. Ez egyszerű, csak éppen a változtatásokat nehéz követni benne fejlesztés közben. Tipikusan akkor választja az ember ezt a megoldást, amikor azt hiszi, hogy ezzel kevesebb gondja lesz, de később kiderül, hogy mégse.
  • SQL szkriptekkel. Ez kényelmesen szerkeszthető és verzionálható, csak éppen az a kérdés, hogyan fognak egyszerűen lefutni az SQL szkriptek.

Az utóbbi az érdekesebb, hogyan futtatunk SQL szkripteket parancssorból? Korábban az osql.exe vagy az isql.exe volt a jópajtás, az SQL Server 2005-től kezdve ezek helyét átvette az SQLCMD.

Például ha Windows Workflow Foundationt használunk, valószínűleg szükségünk lesz tracking és persistence adatbázisokra, amikhez csak az SQL szkripteket kapjuk a Microsofttól. Nosza gyűrjük be az alábbi néhány sort egy cmd fájlba és máris lehet duplán kattintva adatbázist gyártani (eltördeltem a hosszú sorokat):

    SQLCMD -S .SqlExpress -Q "CREATE DATABASE MyDB"

    SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENSqlPersistenceService_Schema.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENSqlPersistenceService_Logic.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENTracking_Schema.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENTracking_Logic.sql"

Eddig egyszerű volt, de mi van akkor, ha saját tracking profile-t is akarunk használni? Erről annyit érdemes tudni, hogy a profil egy XML fájl, aminek a tartalmát az UpdateTrackingProfile tárolt eljárás segítségével kell egy táblába betöltenünk.

Általánosabban megfogalmazva a feladatot: hogyan tudunk egy olyan tárolt eljárást futtatni, aminek egyik bemeneti paramétere egy fájl teljes tartalma?

A megoldás első lépését a BULK INSERT T-SQL utasítás jelenti. Ennek megadhatunk egy fájl útvonalat, aminek a tartalmát be tudja tölteni egy táblába. Sajnos van néhány nyűgje:

  • Csak tábla típusú változót szeret, tehát nem elég egy DECLARE, csak CREATE TABLE jó neki.
  • A betöltendő fájl útvonalát nem lehet változóban megadni, oda kell írni aposztrófok közé.
  • Ha egy kulturáltan megformázott XML-t akarunk betölteni, akkor minden egyes sor külön rekordba kerül, ami persze nem jó, ha végül egyetlen cellában szeretnénk látni az eredményt.

Íme a megoldás:

    CREATE TABLE #temp( profileXml nvarchar(max) )


BULK INSERT #temp FROM '$(ProfilePath)' WITH ( ROWTERMINATOR = '<<<' -- dummy terminator, hogy az egész fájlt felolvassa )

Csinálunk tehát egy temp táblát, mert mindenképp tábla kell. Az egész fájl beolvasását úgy oldjuk meg, hogy olyan sor elválasztó karaktert adunk meg, ami biztosan nincs a fájlban. A kérdés már csak az, hogy mi a $(ProfilePath)?

Az SQLCMD egyik remek szolgáltatása, hogy lehet változókat megadni, amiket ő behelyettesít. A fenti aposztrófok közé például így varázsolhatunk értéket (a példa kedvéért elhagytam a többi paramétert):

    SQLCMD -v ProfilePath="profile.xml"

Ezzel megint csak gondunk lesz: a BULK INSERT panaszkodik, hogy nem teljes elérési utat adtunk meg. Kérdés: hogyan lehet egy cmd fájlban megtudni az aktuális mappa elérési útvonalát? Meglepő módon a cd parancs és a cd környezeti változó épp ezt adja vissza. Szerencsére az SQLCMD is tud környezeti változókat kezelni, így csak ennyit kell módosítanunk:

    SQLCMD -v ProfilePath="%cd%profile.xml"

Nem maradt más hátra, mint az UpdateTrackingProfile tárolt eljárás meghívása, amihez a már jól ismert EXEC utasítást használjuk. A profil XML-en kívül át kell neki adnunk egy TypeFullName, egy AssemblyName, és egy Version paramétert, melyeknél ugyanúgy járhatunk el, mint a fenti ProfilePath esetén. Ezek típusát és hosszát a tárolt eljárásból lehet kilesni.

Végül nekem a teljes SQL szkript így ez lett:

    -- Bemeneti paraméterek
    DECLARE @TypeFullName nvarchar(128)
    DECLARE @AssemblyName nvarchar(256)
    DECLARE @Version varchar(32)

    SET @TypeFullName = '$(TypeFullName)'
    SET @AssemblyName = '$(AssemblyName)'
    SET @Version = '$(Version)'

    -- Belső változók
    DECLARE @profileXml nvarchar(max)
    CREATE TABLE #temp( profileXml nvarchar(max) )

    -- Tracking profile betöltése fájlból temp táblába
    BULK INSERT #temp 
      FROM '$(ProfilePath)'
      WITH
      (
        ROWTERMINATOR = '<<<'  -- dummy terminator, hogy az egész fájlt felolvassa
      )

    -- Tracking profile XML kiolvasása temp táblából
    SELECT @profileXml = profileXml FROM #temp

    -- Tracking profile adatbázisba mentése
    EXEC dbo.UpdateTrackingProfile @TypeFullName, @AssemblyName, @Version, @profileXml

    -- Temp tábla törlése
    DROP TABLE #temp

És a cmd fájl, ami felépíti a teljes adatbázist és beimportálja a profilt (eltördeltem a hosszú sorokat):

    SQLCMD -S .SqlExpress -Q "CREATE DATABASE SignowDB"

    SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENSqlPersistenceService_Schema.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENSqlPersistenceService_Logic.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENTracking_Schema.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENTracking_Logic.sql" SQLCMD -S .SqlExpress -d MyDB -i "InsertTrackingProfile.sql" -v Version=1.0.0
-v TypeFullName="MyNamespace.MyWorkflow1"
-v AssemblyName="MyLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=443f6c5b661c8da2"
-v ProfilePath="%cd%profile1.xml" SQLCMD -S .SqlExpress -d MyDB -i "InsertTrackingProfile.sql" -v Version=1.0.0
-v TypeFullName="MyNamespace.MyWorkflow2"
-v AssemblyName="MyLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=443f6c5b661c8da2"
-v ProfilePath="%cd%profile2.xml"

A legjobb az egészben, hogy mindezt nem csak fejlesztés közben lehet használni, hanem ezt a szkriptet be lehet építeni a telepítő alkalmazásba is.

 

Technorati tags: , ,

Workflow tulajdonságainak kiolvasása

Gyakori kérdés, hogy egy adott workflow példány belső állapotát, mezőit, tulajdonságait hogyan lehet kiolvasni? A rövid válasz, hogy egyszerűen sehogy:

  • Egy futó workflow esetén az ember a WorkflowInstance osztály környékén próbálkozik, amin keresztül legfeljebb az InstanceId lehet megszerezni. Futó workflow esetén az igazi megoldás az lehet, ha kommunikálunk a folyamattal, azaz a Queuing- vagy az ExternalDataExchangeServicen-en keresztül üzenetet küldünk neki.
  • Egy már lefutott workflow példány esetén már nincs más lehetőségünk, mint a tracking service használata.

Az utóbbi az érdekesebb és kevésbé dokumentált eset. A cél tehát egy SimpleWorkflow típusú folyamat Result tulajdonságának utólagos kiolvasása.

Ehhez szükségünk lesz a tracking szolgáltatásra, használjuk a beépített SqlTrackingService-t, hozzuk létre az adatbázisát például WorkflowTrackingStore néven és adjuk hozzá a runtime-hoz:

    const string connectionString = 
@"Initial Catalog=WorkflowTrackingStore;Data Source=.SqlExpress;Integrated Security=SSPI;"; SqlTrackingService trackingService = new SqlTrackingService( connectionString ); runtime.AddService( trackingService );

A workflow futtatásakor nem kell semmi extrát tennünk, a tracking naplóz. Csakhogy nem azt naplózza, ami nekünk kell, hanem amiről azt hiszi, hogy nekünk kell! A tracking profile szolgál arra, hogy meghatározzuk, mire van szükségünk. Ezt megtehetjük objektum modell segítségével (használva a System.Workflow.Runtime.Tracking.TrackingProfile) osztályt, vagy deklaratívan egy XML segítségével. XML-t senki sem szeret kézzel írni (minden ellenkező híreszteléssel szemben az XML nem arra való, hogy homo sapiensek olvassák), a profil összeállításához használjuk a WF SDK példák között elérhető Tracking Profile Designert (közvetlenül letölthető a WF közösségi oldalról is).

Néhány tipp a Tracking Profile Designer használatához:

  • Ne felejtsük el módosítani a tracking adatbázisunkra mutató connection stringet a .config állományban.
  • Másoljuk az exe mellé a workflow-nkat tartalmazó lefordított szerelvényt.
  • A profil erősen típusosan hivatkozik a naplózandó workflow típusára, ami egyben a workflow-t tartalmazó szerelvényre is erősen típusos hivatkozást jelent. Ha nem akarjuk minden fordítás után módosítani a profilt, akkor a workflow AssemblyInfo.cs fájljában adjunk fix és ne csillagos verziószámot az AssemblyVersion attribútumban.
  • Ne csak adatbázisba mentsük el a létrehozott profilt, hanem archiváljuk magunknak XML-be is.

A profilban workflow, activity és felhasználói eseményeket tudunk meghatározni. Ahhoz, hogy egy adott tulajdonság értékét naplózza a rendszer, ún. data tracking extractot kell definiálnunk. Erre szolgál a TrackingExtract ősosztály, amiből a WorkflowDataTrackingExtract és az ActivityDataTrackingExtract származik. Biztos én bénáztam, de akárhogy kattintgattam a profile designerben, nekem nem sikerült workflow extractot létrehozni, csak activity extractot. Annyi baj legyen, a szekvenciális workflow végülis egy szép nagy sequence activity.

Nekem ez lett a profil:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<TrackingProfile xmlns="http://schemas.microsoft.com/winfx/2006/workflow/trackingprofile" version="1.0.0">
    <TrackPoints>
        <ActivityTrackPoint>
            <MatchingLocations>
                <ActivityTrackingLocation>
                    <Activity>
                        <Type>MyApp.SimpleWorkflow, MyApp, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null</Type>
                        <MatchDerivedTypes>false</MatchDerivedTypes>
                    </Activity>
                    <ExecutionStatusEvents>
                        <ExecutionStatus>Closed</ExecutionStatus>
                    </ExecutionStatusEvents>
                </ActivityTrackingLocation>
            </MatchingLocations>
            <Extracts>
                <ActivityDataTrackingExtract>
                    <Member>Result</Member>
                </ActivityDataTrackingExtract>
            </Extracts>
        </ActivityTrackPoint>
    </TrackPoints>
</TrackingProfile>

Emberi nyelvre fordítva: a SimpleWorkflow típusú activity (!) Closed eseményekor kérjük a Result tulajdonság naplózását. És semmi több! Ha tehát valaki később csak a lefutott workflow példányokat szeretné felolvasni vagy további tracking információkra van szüksége, az kénytelen lesz kiegészíteni a profilt még azokkal is.

Az összekattintgatott profilt mentsük el az adatbázisba. Akit érdekel, hogy mi történik a háttérben, az nézze meg a TrackingProfile táblát vagy az UpdateProfile tárolt eljárást.

Megvan tehát a workflow és a profil, ha most futtatjuk a folyamatot, akkor az adatbázisba be kell kerülnie a naplózandó adatoknak. Érdemes bekukkantani a TrackingDataItem táblába, ha mindent jól csináltunk, ott lesz.

Nem maradt más hátra, le kell kérdeznünk a tracking adatbázist, mégpedig az objektum modell SqlTrackingQuery osztályának segítségével:

    const string connectionString = 
@"Initial Catalog=WorkflowTrackingStore;Data Source=.SqlExpress;Integrated Security=SSPI;"; SqlTrackingQuery query = new SqlTrackingQuery( connectionString );

Csak azokat a workflow példányokat kérjük, amik SimpleWorkflow típusúak:

    SqlTrackingQueryOptions options = new SqlTrackingQueryOptions();
    options.WorkflowType = typeof( MyApp.SimpleWorkflow ); // Egyben szerelvény és verzió hivatkozás is!

Na itt kell észnél lenni, mert ha itt megszokásból megadjuk, hogy csak a Completed állapotú példányokra vagyunk kiváncsiak, de ennek a ténynek a naplózását nem kértük a profilban, akkor nem lesz eredményünk. Kérjük vissza a feltételnek megfelelő példányokat és ha nem akarunk sokat debuggolni, írassuk ki, hogy lett-e eredmény:

    IList<SqlTrackingWorkflowInstance> instances = query.GetWorkflows( options );
    Console.WriteLine( "Talált workflow: {0} db", instances.Count );

Innentől kezdve nincs más hátra, végig kell menni a példányokon. Minden SqlTrackingWorkflowInstance tartalmaz egy ActivityEvents gyűjteményt az activity szintű eseményeknek. Ha a profilban így kértük, most is ezt kell használni. Az ActivityTrackingRecordok Body tulajdonságában találhatjuk meg a kért napló sorokat:

    foreach( SqlTrackingWorkflowInstance instance in instances )
    {
        Console.WriteLine( "nnID: {0}", instance.WorkflowInstanceId );

        foreach( ActivityTrackingRecord activityRecord in instance.ActivityEvents )
        {
            Console.WriteLine( "t{0,-10:T}{1,-20}{2}", activityRecord.EventDateTime, 
activityRecord.QualifiedName, activityRecord.ExecutionStatus ); foreach( TrackingDataItem dataItem in activityRecord.Body ) { Console.WriteLine( "ttData: {0}t{1}", dataItem.FieldName, dataItem.Data ); } } }

Természetesen még véletlenül sincs benne egyik változó nevében sem, hogy extract. Ami a profilban extract, azt itt TrackingDataItemnek hívják. Ennyi, tessék kísérletezni!

A fenti példát tartalmazó teljes forráskód letölthető a devPORTALról.

 

Technorati tags: , ,

Page Flow

A novemberi Foundationökről szóló MSDN konferencián említettem, hogy a Windows Workflow Foundation elméletben használható nem csak üzleti folyamatok, hanem felhasználói interakció vezérlésére is. Másként fogalmazva – és nagyban leegyszerűsítve – a UI kezelésére definiálunk folyamatokat, ez a page flow.

Akkor azt mondtam, hogy ennek implementálásán dolgoznak Redmondban és várhatóan lesz egy ilyen kiegészítés a WF-hez. Nos nem lesz, a szép kód, amit a TechEden mutattak tavaly, mindössze egy letölthető WF példakódban landolt. A kb. 4 MB-os telepítő a C:Program FilesWindows Workflow Foundation SamplesPageFlowSample mappába pakolja be magát és beregisztrál egy új projekt típust a VS-ba, amit a New Project -> Visual C# -> Workflow típusok között a (vigyázat!) My Templates csoportban találunk PageFlow néven.

Erre kattintva legenerálhatunk egy új projekt típust, ami nem csinál semmit, de legalább megnézhetjük, hogy mit kellett legyártaniuk Redmondban ahhoz, hogy ez a funkció megvalósuljon:

  • Új projekt típus
  • Új workflow típus (nem state machine és nem sequential, hanem NavigatorWorkflow) és ahhoz persze designer a Studioba
  • Új scheduler service (SynchronousSchedulerService)
  • Új activity (InteractionActivity), valamint ahhoz designer és validator
  • És sok más körítés (editor, type converter, toolbox item, exception, config section és egy csomó segédosztály)

A felsorolásból talán már lehet érezni, hogy a feladat lényegesen nagyobb, mint azt az ember elsőre gondolná. Példaként egy elég lightos ASP.NET alkalmazást kapunk, de a doksi szerint tud működni WPF-fel is. Apropó doksi, a telepítő mappában találjuk pageflowwhitepaper.doc néven, de érdemes átfutni a DOCSWF_UI_Workflow_Design_and_Implementation.doc fájlt is (egyikre sem készül link a Start menüben). Ugyanott találunk még példakódokat ÉÉÉÉÉS az egész implementációnak a forráskódját, amiben ugyan komment nem sok van, de mazsolázni lehet belőle!

Nem kis feladat megemészteni, de akit ennek ellenére mégis érdekel a téma, annak Matt Winkler blogját tudom ajánlani, ő ennek  a gazdája.

Technorati tags: , ,

Folyamatok dokumentálása saját activity designerrel

Az MSDN Kompetencia Központon belül a BME Információtechnológiai Innovációs és Tudásközpontjával együttműködve épp egy olyan e-dokumentumkezelési projekten dolgozunk, ahol a dokumentumok életciklusát Windows Workflow Foundation alapokon definiált folyamatok vezérlik. sok más mellett a Microsoft szerint a WF előnye, hogy a folyamatok a Visual Studio dizájnere segítségével grafikusan megjeleníthetőek, így a kód szinte önmagát dokumentálja.

Na ez pontosan addig igaz, amíg valaki egy kontroll ID-ja alapján tudja, hogy az mit csinál. A VS ugyanis így jeleníti meg a folyamatok minden egyes elemét, az ún. activity-ket:

Egy hosszú nevű activity

Az esetek többségében tehát az sem derül ki, hogy pontosan hogyan is hívják az adott activity-t, az meg aztán pláne nem, hogy mit csinál. Persze fölévihetem az egeret és akkor megjelenik a leírás tooltipben, de azt többek szerint aránylag nehézkes kinyomtatni.

A feladat tehát az, hogy a VS által rajzolt ábrát használható formára kell hozni. Erre a legkézenfekvőbb lehetőség, ha készítünk egy saját activity designer osztályt, ami meghatározza, hogyan jelenjen meg az adott activity a VS tervezőfelületén. A designer osztályhoz egy saját theme osztályt fogunk rendelni, ami pedig azt határozza meg, hogy az adott komponens milyen színekkel, betűtípusokkal, betűméretekkel ábrázolódjon.

Ez idáig aránylag egyszerű, mindössze a designer OnPaint metódusát kell felüldefiniálni úgy, hogy az összes stílus paramétert a theme osztályból vesszük. Ezt meg is lehet csinálni és hamar rá fogunk jönni, hogy a WF tervezői valószínűleg azért rajzoltak minimális activity-ket, hogy egyszerre sok elférjen a VS ablakában. Ha túl kicsi, akkor is használhatatlan, ha túl nagy, akkor is.

Olyan megoldás kellene, hogy tudjuk változtatni, mekkora az activity felülete fejlesztés közben. Ezt meg lehet oldani úgy, hogy kezeljük az egér eseményeket a designerben, de meg lehet oldani úgy is, ahogy ún. composite activity designert készítünk. A composite activity olyan komponens, aminek gyerekei lehetnek, tehát például az IfElse activitynek a lehetséges kimenetei (branchek) a gyermekei. Az ilyen activity-k elég összetettek lehetnek ahhoz, hogy be lehessen csukni és ki lehessen nyitni őket a dizájnerben, aminek a támogatására létezik egy CompositeActivityDesigner ősosztály, amiből mi származtathatunk.

Készítsünk tehát olyan composite activity designert, amit ki lehet nyitni és be lehet csukni fejlesztés közben!

Saját composite activity designer készítése

Kezdjük a témával, abból is összetett kell, származtassunk egyet a CompositeDesignerTheme osztályból és színezzük ki kedvünk szerint:

    public class DemoDesignerTheme : CompositeDesignerTheme
    {
        public DemoDesignerTheme( WorkflowTheme theme )
            : base( theme )
        {
            this.BorderColor = Color.Brown;
            this.BackColorStart = Color.LightYellow;
            this.BackColorEnd = Color.Orange;
            this.BackgroundStyle = LinearGradientMode.Vertical;
            this.ForeColor = Color.Brown;
        }
    }

Ezek után már létrehozhatjuk a designer osztályunkat és hozzácsaphatjuk a témát is. Nagy buzgalommal definiáljuk felül az Initialize metódust, hogy el tudjuk menteni a megrajzolandó activity-re mutató referenciát (erre majd később szükség lesz):

    [ActivityDesignerTheme( typeof( DemoDesignerTheme ) )]
    public class DemoDesigner : CompositeActivityDesigner
    {
        private Activity _activity;

        protected override void Initialize( Activity activity )
        {
            this._activity = activity;
            base.Initialize( activity );
        }
    }

Következő lépésben definiáljuk felül az OnPaint metódust és bányásszuk ki az értelmes dolgokat az esemény paraméterből:

    protected override void OnPaint( ActivityDesignerPaintEventArgs e )
    {
        Graphics g = e.Graphics;
        CompositeDesignerTheme theme = e.DesignerTheme as CompositeDesignerTheme;

        // Tovább is lesz még...
    }

Ott az a csodás Graphics objektum, azon kell kiélnünk rajz tehetségünket. Mindenek előtt talán rajzoljuk meg az activity keretét, még pedig olyan pompás lekerekített formában, ahogy a standard beépített dizájnerek teszik. A bátrabbak megrajzolják a keretet ívekből, a naívabbak még hisznek az MSDN-ben, a gyakorlottabbak pedig Reflectoroznak, és így rátalálnak az ActivityDesignerPaint osztályra. Ennek az osztálynak van néhány publikus metódusa, amelyek közvetlenül használhatóak, de az internalokból is lehet ötleteket meríteni:

    // Keret megrajzolása
    int radius = 4;
    ActivityDesignerPaint.DrawRoundedRectangle( g, theme.BorderPen, this.Bounds, radius - 2 );

Tartsuk fejben, mert a Google-ben mindössze 5 találat van rá! (Talán ezért írta meg kézzel az, aki a Microsoft oldalán lévő WF hands-on labot kitalálta, bár ez sajnos még a kisebb hiba abban a példakódban… smile_confused)

Fessük ki a keretet, de ne azzal a színnel, ami eszünkbe jut, hanem használjuk a téma beállításait:

    // Háttér kifestése
    GraphicsPath path = ActivityDesignerPaint.GetRoundedRectanglePath( this.Bounds, radius );
    g.FillPath( theme.GetBackgroundBrush( this.Bounds ), path );

Mivel az egész OnPaintet felülírjuk, mindenről nekünk kell gondoskodni, így az ikon megjelenítéséről is:

    // Ikon megjelenítése
    ActivityDesignerPaint.DrawImage( g, this.Image, this.ImageRectangle, DesignerContentAlignment.Fill );

És a szöveg kiírásáról is:

    // Szöveg megjelenítése
    ActivityDesignerPaint.DrawText( g, theme.Font, this.Text, this.TextRectangle, 
StringAlignment.Near, e.AmbientTheme.TextQuality, theme.ForegroundBrush );

Itt jön a trükk: rajzoljuk meg a nyitogató gombot, de ezt is bízzuk a profira:

    // Kinyitó ikon megjelenítése
    ActivityDesignerPaint.DrawExpandButton( g, this.ExpandButtonRectangle, !this.Expanded, theme );

Ha idáig eljutottunk, akkor már van egy szép színes, nyomkodható activity-nk, sőt még a jobb egérkettyintős menüben is megjelent az Expand és Collapse parancs:

Saját composite activity

Az activity méretét nyilvánvalóan meg kell változtatnunk, amikor a felhasználó ki szeretné nyitni. Ehhez nem kell egérkattintás eseményt kezelnünk, a VS gondoskodni fog arról, hogy a kattintás után újrarajzolja a teljes activity-t, csak éppen az Expanded tulajdonság értéke fog mást mutatni.

Tűzzük ki célul azt, hogy kinyitott állapotban megjelenítsük az activity Description mezőjének az értékét is, az activity pedig legyen olyan széles, hogy a teljes szöveg tördelés nélkül beleférjen. A méretet az OnLayoutSize metódus felüldefiniálásával tudjuk megadni:

    protected override Size OnLayoutSize( ActivityDesignerLayoutEventArgs e )
    {
        // Meg kell hívni, mert ez inicialize1lja a TextRectangle-t, 
        // amitől az ImageRectangle és az ExpandButtonRectangle is függ
        Size originalSize = base.OnLayoutSize( e );

        string text;
        int height;

        if( this.Expanded )
        {
            // Kinyitott állapotban a Description mérete a mérvadó
            text = this._activity.Description;
            height = originalSize.Height + 30;
        }
        else
        {
            // Becsukott állapotban az activity azonosítójának mérete a mérvadó
            text = this.Text;
            height = originalSize.Height;
        }

        // Az activity mérete a szöveg szélesslgéhez igazodik.
        SizeF s = e.Graphics.MeasureString( text, e.DesignerTheme.Font );
        return new Size( (int) s.Width + 30, height );
    }

A fenti metódusnak van két érdekessége (a MeasureString metóduson kívül persze):

  • Az ikon és a szöveg pozícionálásánál hivatkoztunk az ImageRectangle és a TextRectangle tulajdonságokra, sőt később az ExpandButtonRectangle-re is. Ezeket is felül lehet definiálni, de nem szükséges, mert az activity méretében származtatják magukat, de csak akkor tudják, ha meghívjuk a szülő osztály OnLayoutSize metódusát.
  • Ki lehet próbálni, hogy bármilyen rövid szövegeket adunk meg, az activity nem csökken akármilyen kicsire. Ezt a MinimumSize tulajdonság határozza meg, amit szintén felül lehet definiálni.

Nem maradt más hátra, mint hogy az OnPaintben kinyitott állapotban az ikon alá kiírjuk az activity-hez tartozó Description tulajdonság értékét:

    // Kinyitott állapotban a Description megjelenítése
    if( this.Expanded )
    {
        Rectangle rect = new Rectangle();
        rect.X = this.Bounds.X;
        rect.Y = this.ImageRectangle.Y + this.ImageRectangle.Height + 10;
        rect.Width = this.Bounds.Width;
        rect.Height = this.TextRectangle.Height;

        ActivityDesignerPaint.DrawText( g, theme.Font, this._activity.Description, rect, 
StringAlignment.Center, e.AmbientTheme.TextQuality, theme.ForegroundBrush ); }

Ennél természetesen tovább is lehet menni, például reflection segítségével fel lehet deríteni az activity tulajdonságait és a hozzájuk tartozó attribútumokat és meg lehet jeleníteni azokat is a tervezőben, a nyitott nézetben. Ha kell fejlesztés közben, akkor egy kattintással kinyithatjuk, sőt ki is nyomtathatjuk. Íme tehát a végeredmény összecsukva és kinyitva:

Saját activity összecsukva Saját activity kinyitva

Többekkel beszélgettem már arról, hogy a Windows Workflow Foundation mennyire számít rapid alkalmazásfejlesztési technológiának, azt hiszem ez egy újabb példa arra, hogy nem az.

Az activity teljes forráskódja a devPORTALról tölthető le.

 

Technorati tags: , ,

Workflow Foundation alapozás

Véget ért a két napos Workflow Foundation Laborgyakorlat, melyet a novemberi MSDN konferenciához kapcsolódóan szerveztünk az MSDN Kompetencia Központban. Minden tanfolyam elején meg szoktam kérdezni a résztvevőket, hogy milyen a fejlesztői előéletük és hogy mit várnak ettől a tanfolyamtól. Jó volt hallani, hogy szinte mindenki ott volt a konferencián és hogy azért jöttek erre a képzésre is, mert felismerték, hogy rengeteg időt lehet azzal spórolni, hogy "több ezer oldalnyi könyv elolvasása helyett más kaparja ki a gesztenyét". Ez a hetedik év, hogy én személy szerint ebben a szellemben dolgozom, jó tudni, hogy van eredménye 🙂

Érdekes volt hallgatni mindkét nap végén, hogy ki hogyan fogalmazza meg magának a Workflow Foundation lényegét: ez nem egy rapid alkalmazásfejlesztési technológia! Ez persze elsőre csalódottságot ébreszt, de megismerve a platform alapszolgáltatásait, mindenki be tudja lőni magának, hogy mit várhat ettől a rendszertől és mit nem. A nap végén mindenki tisztába került azzal, hogy mivel jár a Workflow Foundation használata, mik az előnyei és mik lehetnek a fájdalmas pontjai. (Érdemes elolvasni Novák István első benyomásait.)

Persze sokan (én is) hajlamosak vagyunk a dolgok negatív oldalát látni, hív a sötét oldal: nehézkes a kommunikáció, kevés activity van a Base Activity Library-ben, a Visual Studioba integrálódó designer néha elhal, ráadásul rengeteget kell gépelni. Ezek tények, nem érdemes szépíteni, ez így van. Minél többet foglalkozik vele az ember, annál jobban így érzi.

De ezek mind csak elvárásaink miatt érződnek így – úgy látjuk, hogy a pohár félig üres. Ha sikerül onnan megközelítenünk a kérdést, hogy ez egy platform technológia, akkor már teljesen más lehet a kép – nézzük úgy, hogy a pohár félig tele van! Úgy vettem észre, sokan találkoztak már más workflow technológiákkal. Ha össze akarjuk hasonlítani ezeket a Workflow Foundationnel, akkor általában arra juthatunk, hogy a WF célközönsége az alkalmazásfejlesztő, a programozó, míg más technológiák gyakran másoknak készültek. Másként fogalmazva ez egy "megveszem vagy megcsinálom magamnak" kérdés. A WF nem ad adminisztrációs eszközöket, csili-vili jelentéseket, de még monitorozó alkalmazásokat sem. Ezeket mind nekünk kell megépítenünk, a platform legfeljebb API támogatást tud adni mindehhez. Megold alap problémákat és ad alap szolgáltatásokat, de semmi extra.

Újra és újra felmerül a kérdés, hogy ha ez ennyire "fapados", merjünk-e ráépíteni? A Microsoft egyértelműen ezt az irányvonalat akarja a követni, a SharePoint, a BizTalk, Speech Server, az MIIS és a Microsoft Dynamics következő verziói már biztosan ezt fogják használni. Sőt, az egyik legnépszerűbb workflow termék, a K2 is áttér Workflow Foundationre. Miért? Mert kihasználva egy stabil alaptechnológiát, képes lehet további szolgáltatásokat biztosítani a termékeiben, ami miatt érdemes lehet megvenni a K2-t.

Természetesen minél több felhasználója van egy frameworknek, annál érettebb technológiává növi ki magát. Még csak most jelent meg a .NET 3.0 és vele együtt a WF első kiadása, mégis kiváncsian várhatjuk, mi lesz a következő verzióban…

 

Technorati tags: , ,

Saját események WF activity-ben (kiegészítés)

Az előző hasonló című szösszenetből kimaradt egy dolog, így most ezzel a bejegyzéssel igyekszem leróni adósságomat: hogyan lehet elsütni egy olyan eseményt, ami saját EventArgs osztályt használ?

Az Activity osztálynak van egy RaiseEvent() metódusa, a legtöbb példában ezt találjuk. Ennél lényegesen szebb megoldás, ha a RaiseGenericEvent<T>() metódust használjuk:

protected internal void RaiseGenericEvent<T> (
  DependencyProperty dependencyEvent,
  Object sender,
  T e
) where T : EventArgs

Persze az MSDN (pardon, a Windows SDK, mert a .NET 3.0 dokumentációja persze külön van) nem sokat segít:

Type Parameters
T
The specified type.

Értsd: a T paraméterben kell megadni a saját EventArgs osztályból származtatott típus nevét, valahogy így:

protected override ActivityExecutionStatus Execute( ActivityExecutionContext aec )
{
  // …
  base.RaiseGenericEvent<WorkEventArgs>( MyActivity.OnWorkingEvent, this, new WorkArgs( … ) );
  // …
}

Na így már talán teljes a kép 🙂

Technorati tags: , ,