Zine.net online

Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc
w Szukaj

arkadiusz.wasniewski

  • TreeNode w wersji koguciej

    Kilkukrotnie już zdarzyło się, iż potrzebowałem klasy, która umożliwiłaby zapamiętanie typowanych (typed, generic) struktur hierarchicznych (hierarchical collection) czyli dowolnego obiektu wraz z jego elementami potomnymi. W ramach platformy .NET istnieją już klasy implementujące podobną funkcjonalność. Mowa tu oczywiście o TreeNode z TreeNodeCollection oraz o, bardziej hermetycznym, MenuItem wraz z wewnętrznym MenuItemCollection. Klasy przeznaczone do obsługi menu trudno byłoby użyć do własnych rozwiązań. TreeNode jest zaś “ciężka” i brak w niej typowania, czyli możliwości określenia typu przechowywanego obiektu (precz z rzutowaniem!).

    Z tego też powodu stworzyłem razu pewnego własną implementację struktury hierarchicznej. Nie jest ona skomplikowana. Punktem wyjścia jest klasa przechowująca dane wraz z kolekcją elementów potomnych:

        internal class LightTreeNode<T>

        {

            private readonly T _item;

            private readonly LightTreeNodeCollection<T> _nodes;

     

            public LightTreeNode(T item)

            {

                _item = item;

                _nodes = new LightTreeNodeCollection<T>(this);

            }

     

            public T Value

            {

                get { return _item; }

            }

     

            public LightTreeNodeCollection<T> Nodes

            {

                get { return _nodes; }

            }

        }

    Kolekcja przechowująca potomków dla ułatwienia operowania implementuje interfejs ICollection<T>:

        internal class LightTreeNodeCollection<T> : ICollection<LightTreeNode<T>>

        {

            private readonly List<LightTreeNode<T>> _list;

            private readonly LightTreeNode<T> _owner;

     

            public LightTreeNodeCollection(LightTreeNode<T> owner)

            {

                _owner = owner;

                _list = new List<LightTreeNode<T>>();

            }

     

            public LightTreeNode<T> Owner

            {

                get { return _owner; }

            }

     

            public LightTreeNode<T> Add(T item)

            {

                var node = new LightTreeNode<T>(item);

                _list.Add(node);

                return node;

            }

     

            #region Implementation of IEnumerable

     

            public IEnumerator<LightTreeNode<T>> GetEnumerator()

            {

                return _list.GetEnumerator();

            }

     

            IEnumerator IEnumerable.GetEnumerator()

            {

                return GetEnumerator();

            }

     

            #endregion

     

            #region Implementation of ICollection<LightTreeNode<T>>

     

            public void Add(LightTreeNode<T> item)

            {

                _list.Add(item);

            }

     

            public void Clear()

            {

                for(int i = 0; i < _list.Count; i++){

                    _list[i].Nodes.Clear();

                }

                _list.Clear();

            }

     

            public bool Contains(LightTreeNode<T> item)

            {

                return _list.Contains(item);

            }

     

            public void CopyTo(LightTreeNode<T>[] array, int arrayIndex)

            {

                _list.CopyTo(array, arrayIndex);

            }

     

            public bool Remove(LightTreeNode<T> item)

            {

                int index = _list.IndexOf(item);

                if(index < 0){

                    return false;

                }

                _list[index].Nodes.Clear();

                _list.Remove(item);

                return true;

            }

     

            public int Count

            {

                get { return _list.Count; }

            }

     

            public bool IsReadOnly

            {

                get { return false; }

            }

     

            #endregion

        }

    Przykład zastosowania to chociażby przygotowanie dla widoku (View) poleceń menu z poziomu prezentera (Presenter, Presentation Model):

                var menu = new LightTreeNode<Command>(new Command("Opcje"));

                LightTreeNode<Command> submenu = menu.Nodes.Add(

                    new Command("Nawigacja"));

                submenu.Nodes.Add(new Command("Pierwszy", MoveFirst));

                submenu.Nodes.Add(new Command("Ostatni", MoveLast));

    PS. Dla uproszczenia kodu usunięto wszystkie wywołania Debug.Assert sprawdzające poprawność wywołań.

  • (K)Cultura w PowerShell

    “Bo kultura tu naprawdę jest, świadczy o tym nasz wspaniały Dom Kultury” śpiewał w 1988 roku w Jarocinie zespół “Zielone Żabki”. Ktoś pamięta? Dziś też będzie o kulturze, ale przez literę c czyli o Culture. Tekst zaś dotyczył będzie tak prozaicznej kwestii jak polskie znaki narodowe.

    Zacznijmy od początku. Utwórzmy, np. w Notatniku, plik w formacie CSV zawierający nazwy ptaków z polskimi znakami narodowymi:

    	id,nazwa
    	1,Gżegżółka
    	2,Żuraw
    	3,Łabędź

    I spróbujmy go wczytać korzystając ze standardowego polecenia PowerShell:

        Import-Csv 'c:\ptaki.csv'

    Jak sie można domyśleć (wstęp to sugerował) to, co ujrzą nasze oczy nie będzie ładne. Zamiast polskich znaków będą mniej lub bardziej nieokreślone śmieci (zależy gdzie uruchomimy nasz kod) zgodne z UTF8. Skąd wiemy, że z UTF8? Empirycznie będzie można to sprawdzić za chwil kilka.

    Wstępnie problem został więc rozwiązany. Jak natomiast zmusić PowerShell do wczytania pliku CSV zgodnie z polską stroną kodową? Okazuje się, iż najłatwiej jest skorzystać z polecenia Get-Content, które domyślnie bierze pod uwagę ustawienia regionalne systemu operacyjnego komputera.

        Get-Content 'c:\ptaki.csv' | ConvertFrom-Csv

    Dzięki zaś poleceniu ConvertFrom-Csv oraz skorzystania z potoku możemy cieszyć się poprawnością wyświetlania:

    	id	nazwa                                         
    	--	-----                                         
    	1	Gżegżółka                                     
    	2	Żuraw                                         
    	3	Łabędź                                        

    Polecenie Get-Content posiada również, co ciekawe, parametr Encoding, który pozwala ustawić kilka możliwych stron kodowych. Nie są to powalające ilości, ale zawsze coś. W przypadku ustawienia kodowania na UTF8 otrzymamy dane w postaci identycznej jak w przypadku polecenia Import-Csv. Aby natomiast uzyskać efekt identyczny jak w przypadku wywołania tego polecenia bez strony kodowej należy parametr Encoding ustawić na wartość String.

        Get-Content 'c:\ptaki.csv' -Encoding String | ConvertFrom-Csv

    Co zaś w przypadku kiedy możliwości lingwistyczne standardowych poleceń PowerShell nas nie usatysfakcjonują? Zawsze możemy skorzystać z metod klas platformy .NET:

        [System.IO.File]::ReadAllText(
            'c:\ptaki.csv', 
            [System.Text.Encoding]::GetEncoding(1250)) | ConvertFrom-Csv

    I na koniec inny, niż skorzystanie z Notatnika, wariant tworzenia plików CSV:

        Set-Content 'c:\ptaki.csv' 'id,nazwa'
        Add-Content 'c:\ptaki.csv' '1,Gżegżółka'
        Add-Content 'c:\ptaki.csv' '2,Żuraw'
        Add-Content 'c:\ptaki.csv' '3,Łabędź'

    Oczywiście w tych poleceniach również możemy skorzystać z parametru Encoding.

  • Pobranie projektów rozwiązania czyli Regex w akcji

    Asumpt do poniższego rozwiązania dostarczył skrypt PowerShell, który kompiluje projekty pewnego mojego rozwiązania, i gdzie zapałałem chęcią automatycznego uaktualnienia numeru wersji we wszystkich plikach AssemblyInfo.cs. Ale gdzież są te wszystkie pliki? No w projektach…

    Najprostszy sposób dotarcia do projektów to odczytanie pliku .sln. Hm… ale to oznacza analizowanie zawartości. Z pomocą przyszły wyrażenia regularne oraz świadomość istnienia jasno określonej struktury pliku .sln.

    Project\("\{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC\}"\) = "(?<projectName>[^"]+)", "(?<projectFolder>[^"]+)", "(?<projectGuid>[^"]+)"\nEndProject

    Oczywiście kilka słów wyjaśnienia. Po pierwsze wszystkie projekty rozwiązania jak i również foldery (Solution Folders) są umieszczane w sekcji zaczynającej się od słów Project a kończącej się na EndProject. Elementy będące projektami są oznaczone odpowiednim identyfikatorem Guid (konkretnie {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}). Następnie w cudzysłowach danej sekcji mamy kolejno nazwę projektu, ścieżkę względną do projektu oraz unikalny identyfikator Guid przypisany do danego projektu. Dla ułatwienia pobierania informacji zastosowałem grupy nazwane. Czas na prosty przykład użycia:

    $pattern =
        "Project\(`"\{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC\}`"\) = " +
        "`"(?<projectName>[^`"]+)`", " +
        "`"(?<projectFolder>[^`"]+)`", " +
        "`"(?<projectGuid>[^`"]+)`"\r\nEndProject"
    $regex = [regex] $pattern
    $solution = [System.IO.File]::ReadAllText('d:\Solution\MySolution.sln')
    $regex.Matches($solution) | ForEach-Object {
        Write-Host $_.Groups['projectName']
        Write-Host $_.Groups['projectFolder']
        Write-Host $_.Groups['projectGuid']
        Write-Host
    }
    

    Ze względu na składnię PowerShell wzorzec wyrażenia regularnego został nieco zmodyfikowany.

    PS. Oczywiście można również zastosować prostsze wyrażenie regularne:

    \{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC\}"\) = "(?<projectName>[^"]+)", "(?<projectFolder>[^"]+)", "(?<projectGuid>[^"]+)

    lub też bardziej odporne na potencjalne błędy (czy aby na pewno takie będą?):

    \{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC\}"\)\s*=\s*"(?<projectName>[^"]+)",\s*"(?<projectFolder>[^"]+)",\s*"(?<projectGuid>[^"]+)

    Ale to już sztuka dla sztuki.

  • Zacznij od nowej strony, ale nie drukuj pustej

    Jakiś czas temu podałem sposób na rozwiązanie problemu drukowania w ramach kontrolki List podraportów zaczynając każdorazowo od nowej strony. Efektem było niestety drukowanie na koniec pustej strony. Wydawałoby się, iż wystarczy jedynie kontrolkę Rectangle na koniec wyłączyć i marnotrawstwo papieru oraz nadszarpywanie naszej reputacji zostanie zlikwidowane. W tym celu właściwości Hidden przypisałem wyrażenie:

    =IIF(RowNumber(“DataSet”) < CountRows(“DataSet”), False, True)

    Analiza jest trywialna: dopóki bieżący numer wiersza jest mniejszy od liczby wszystkich wierszy w danym zbiorze danych kontrolka Rectangle jest wyświetlana.

    Właściwości kontrolki Rectangle

    Właściwości kontrolki Rectangle

    Okazuje się jednak, iż jakiekolwiek wyrażenie powoduje ignorowanie wstawiania znaku końca strony (PageBreakAtEnd = True)! Prosty eksperyment polega na zamianie wartości False na wyrażenie =False. Ot błąd w implementacji (nota bene zdaje się, iż jest on od wersji Microsoft SQL Server 2000).

    Rozwiązanie?!

    Ech… Rozbicie tego na dwie kontrolki Rectangle. Pierwsza sprawdza i ustawia tylko właściwość Hidden oraz zawiera drugą kontrolkę Rectangle. Ta druga, wewnętrzna kontrolka dopiero ustawia PageBreakAtEnd na True.

    PS. Opisywany problem dotyczy na pewno Reporting Services w wersji 2005. Pozostałych wersji nie sprawdzałem.

  • Hook scripts w PowerShell

    Dawno, dawno temu (choć może nie aż tak dawno) popełniłem notkę na temat skryptów przechwytujących (hook scripts) dla repozytoriów systemu kontroli wersji Subversion. Chodziło o uniemożliwienie zapisania w repozytorium zmian, jeśli nie został podany do nich żaden komentarz wyjaśniający. Proponowany kod wyglądał mniej więcej tak:

            private static int Main(string[] args)

            {

                string repositoryPath = args[0];

                string transactionName = args[1];

     

                var process = new Process();

                process.StartInfo.FileName = "svnlook.exe";

                process.StartInfo.Arguments = string.Format("log -t {0} {1}",

                    transactionName, repositoryPath);

                process.StartInfo.UseShellExecute = false;

                process.StartInfo.RedirectStandardOutput = true;

                process.StartInfo.CreateNoWindow = true;

                process.Start();

                string output = process.StandardOutput.ReadToEnd();

                process.WaitForExit();

     

                var regex = new Regex("[a-zA-Z0-9]");

                if (!regex.IsMatch(output)){

                    Console.Error.WriteLine("Brak opisu poczynionych zmian.");

                    return 1;

                }

     

                return 0;

            }

    Ostatnio pomyślałem czemu by nie wykorzystać do wspomożenia Subversion języka PowerShell. Wykoncypowałem, iż w katalogu, w którym są składowane repozytoria założę folder scripts, który będzie zawierał skrypty PowerShell, a do którego będą sięgały pliki wsadowe (batch files) wywoływane przez SVN w katalogu hooks danego repozytorium.

    Pierwsza wersja skryptu sprawdzającego istnienie komentarza do zachowywanych zmian była następująca:

            $repositoryPath = $args[0]
            $transactionName = $args[1]
            
            $message = svnlook.exe log -t $transactionName $repositoryPath
            if($message -notmatch '[a-zA-Z0-9]'){
                Write-Error "Brak opisu poczynionych zmian."
                return 1
            }

    Okazało się jednak, iż nie funkcjonuje on w zadowalający sposób. Po pierwsze niepoprawnie był zwracany do systemu status zakończenia skryptu (dos exit code). Rozwiązań znalezionych po poszukiwaniach w sieci było kilka. Ale najbardziej sensowne, w mojej opinii oczywiście, polegało na skorzystaniu ze specjalnej zmiennej PowerShell $host. Drugi problem wiązał się z komunikatem zwrotnym, który chciałem przekazać użytkownikowi próbującemu zatwierdzić zmiany. Aby wiadomość mogła być wyświetlona musi ona zostać wysłana do standardowego strumienia zawierającego błędy. Niestety okazało się, iż skorzystanie z polecenia Write-Error jest niemal tożsame z wyrzuceniem w tym miejscu wyjątku throw. Użytkownik, poza komunikatem zwrotnym, ze skryptu otrzymywał również dodatkowe informacje na temat kategorii błędów, miejsca wystąpienia etc. Niedobrze. Na szczęście PowerShell będąc opartym o platformę .NET pozwala bez problemów korzystać z jej bibliotek. Wiadomość dla użytkownika wysłałem więc tak jak w kodzie C# do standardowego strumienia błędów System.Console.Error. A oto końcowa wersja skryptu:

            $repositoryPath = $args[0]
            $transactionName = $args[1]
            
            $message = svnlook.exe log -t $transactionName $repositoryPath
            if($message -notmatch '[a-zA-Z0-9]'){   
                [System.Console]::Error.WriteLine("Brak opisu poczynionych zmian.")
                $host.SetShouldExit(1)
            }

    Jak już delikatnie powyżej zasugerowałem, skrypt PowerShell nie może być niestety wywołany bezpośrednio przez Subversion. Do tego konieczny jest pośredni plik wsadowy (batch file). I tutaj również nie obyło się bez niespodzianek. Pierwsza wersja pliku prezentowała się tak:

            powershell -noprofile ..\..\scripts\pre-commit.ps1 %1 %2

            exit errorlevel

    Okazało się jednak, iż ścieżka dostępu do katalogu ze skryptami nie jest tworzona poczynając od miejsca wywołania pliku wsadowego, tylko od ścieżki C:\Windows\system32. W ramach plików wsadowych można skorzystać ze zmiennej %CD%, która zawiera aktualny katalog. W tym wypadku nic to jednak nie dało. Nie pomogło nawet zapisanie wartości %CD% w zmiennej tymczasowej skryptu i późniejsze wykorzystanie jej przy tworzeniu ścieżki. Rozwiązaniem okazało się natomiast skorzystanie z makra %~dp0. Ostatecznie zawartość pliku wsadowego ustabilizowała się na poniższym zapisie:

            powershell -noprofile  %~dp0\..\..\scripts\pre-commit.ps1 %1 %2

            exit errorlevel

    Co ciekawe zupełnie nie sprawdziło się również polecane również sprawdzanie zmiennej $LASTEXITCODE zamiast ERRORLEVEL.

    Wiemy już, iż przedstawiane rozwiązanie posiada jedną niedogodność – konieczne jest posiłkowanie się plikami wsadowymi aby osiągnąć zamierzony efekt. Czy można to pominąć? Ano można. Należy skorzystać z parametru –Command w czasie wywołania powłoki PowerShell w pliku wsadowym i zapisać wewnątrz ciągu “& “ cały kod skryptu pamiętając, aby wszystko znalazło się w jednej linii oraz, by kolejne bloki kodu oddzielone były znakiem średnika.

            powershell.exe -noprofile -command "& {$message = svnlook.exe log -t $args[1] $args[0];if($message -notmatch '[a-zA-Z0-9]'){[System.Console]::Error.WriteLine('Brak opisu poczynionych zmian.');$host.SetShouldExit(1)}}" %1 %2

            exit errorlevel

    Dla skrócenia zapisu parametry skryptu przekazuję bezpośrednio do aplikacji svnlook.exe bez tworzenia zmiennych pomocniczych $repositoryPath i $transactionName.

    Podsumowanie? Cóż. Skrypty przechwytujące (hook scripts) w PowerShell to chyba jednak w tym przypadku sztuka dla sztuki. Szybciej i sprawniej będzie skorzystanie z kodu C#. Choć zawsze można się czegoś pożytecznego przy okazji nauczyć.

  • Kiedy nie działa tryb zgodności w Windows 7

    W razie problemów z działaniem aplikacji w systemie Windows 7 można we właściwościach danego programu (Properties –> Comaptibility) włączyć tryb zgodności (Compatibility mode) poprzez wybranie wcześniejszej wersji systemu operacyjnego. Do dyspozycji mamy:

    • Windows 95;
    • Windows 98 / Windows Me;
    • Windows NT 4.0 z Service Pack 5;
    • Windows 2000;
    • Windows XP z Service Pack w wersji 2 lub 3;
    • Windows Server 2003 z Service Pack 1;
    • Windows Vista;
    • Windows Vista z Service Pack 1 lub 2.

    Rozwiązanie to jednak nie zadziała poprawnie jeśli aplikacja samodzielnie sprawdza wersję systemu i od tego uzależnia swoją dalszą pracę. Weźmy dla przykładu poniższy kod:

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]

        public class OSVERSIONINFO

        {

            public int OSVersionInfoSize;

            public int MajorVersion;

            public int MinorVersion;

            public int BuildNumber;

            public int PlatformId;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]

            public string CSDVersion;

        }

     

        class Program

        {

            [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]

            public static extern bool GetVersionEx([In, Out] OSVERSIONINFO ver);

     

            static void Main()

            {

                var structure = new OSVERSIONINFO();

                structure.OSVersionInfoSize = Marshal.SizeOf(structure);

                GetVersionEx(structure);

                Console.WriteLine(string.Format("Wersja systemu: {0}.{1}",

                    structure.MajorVersion,

                    structure.MinorVersion));

                Console.ReadKey();

            }

        }

    Jaki system nie wybierzemy w trybie zgodności, dla Windows 7 RC wynik zawsze będzie identyczny: Wersja systemu: 6.1 (podobnie zresztą jak dla Windows Server 2008 R2).

    PS. Z powyższych przyczyn w Windows 7 póki co nie działa między innymi WD Anywhere Backup. Jest to więc kwestia aplikacji a nie problemów z samym systemem operacyjnym.

  • Usuwanie informacji o lokalnej kopii roboczej Subversion

    Z przyczyn mniej lub bardziej zrozumiałych koniecznym było, aby usunąć z kilku projektów katalogi zawierające informacje o lokalnej kopii roboczej Subversion _svn. Przy braku połączenia z repozytorium kodu ręczne usuwanie to dłubanina i gwarantowana depresja. Z pomocą przyszedł PowerShell. Jak zwykle. Poniżej skrypt usuwający to co trzeba tam gdzie trzeba:

    Clear-Host
    $path = Read-Host "Folder przeznaczony do wyczyszenia z katalogów _svn: "
    Get-ChildItem -Path $path -Include _svn -Force -Recurse -Filter FullName | 
        Remove-Item -Force -Recurse
    Write-Host "Operacja usuwania katalogów _svn zakończona pomyślnie."

    Krótko, zwięźle i na temat.

  • Pokaż wszystkim swoje hasło

    Aneta Sidorowicz na swoim blogu (a jakże!) zrobiła małe, subiektywne zestawienie osób organizujących (mających wkład w) konferencję C2C 2009.  Zestawienie przeczytałem i ponieważ zabrakło mi w nim jednej osoby, popełniłem komentarz do wpisu. Jakież było moje zdziwienia (przerażenie wręcz) kiedy jako podpis osoby wystawiającej komentarz wyświetliła się moja nazwa użytkownika i hasło (!!!) mojego profilu Windows Live. Zgroza, to po prostu jest zgroza. Dobrze, że użytkownik dodający komentarz może też go usunąć. Okazało się, iż domyślna nazwa wyświetlana profilu zawierała takie dane… Brrrr…

  • Communities to Communities – nowy adres witryny

    Chciałbym poinformować, iż nastąpiła zmiana adresu Communities to Communities. Witryna konferencji dostępna jest obecnie pod adresem http://communities2communities.org.pl. Przepraszamy z zamieszanie i utrudnienia tym spowodowane.

  • TDD versus BDD

    Testy, testy, testy. Najpierw. Przed właściwym kodem. A może po? Co testować? Wszystko? Jeśli tworzymy zbiór publicznych typów (interfejs pakietu) to testowane powinno być jak najbardziej wszystko i to szczegółowo. To jest Test Dirven Driven Development (TDD). Część publiczna jest naszym kontraktem informującym jaki będzie  rezultat operacji przy określonych parametrach. Niestety w chwili obecnej możliwości języków programowania są ograniczone jeśli chodzi o precyzyjne definiowanie dozwolonych wartości przekazywanych do metod i właściwości. Dlatego też musimy się posiłkować wyjątkami oraz dokumentacją do poszczególnych elementów kontraktu.

    Ale jeśli powstające metody i właściwości są w typach wewnętrznych względem przestrzeni nazw, a co za tym idzie pakietu? Czy mam sprowadzać swoje życie do absurdu? Jest oczywiste, że nie będę sprawdzał w każdej metodzie parametrów i wyrzucał w przypadku wadliwych danych wyjątków. Na takim poziomie bardziej interesuje mnie poprawność wykonania ścieżki logicznej, zachowania. Moje intencje oznaczam wówczas w kodzie metodami Debug.Assert. W dalszym ciągu dostarczany przeze mnie kod stanowi kontrakt, ale nie sprawdzam poprawności wszystkich parametrów, nie wyrzucam wyjątków. Taki zaś jest Behavior Driven Development (BDD).

  • Raymond Lewallen w Polsce

    Wraz z konferencją 4Developers swoje tourne po Polsce rozpoczął Raymond Lewallen - znany bloger portalu CodeBetter.com. W czasie wizyty odwiedzi i spotka się z grupami off-line z następujących miast:

    Zaprezentuje on między innymi następujące tematy:

    • Behavior Driven Development;
    • Building DSLs and Fluent Interfaces in C#.

    Więcej informacji można znaleźć na stronach grup w ramach portalu ms-groups.pl. Zapraszamy!

  • Zacznij od nowej strony

    Wyświetlanie w Reporting Services podraportu w ramach raportu nie jest trudne. Schody zaczynają się jednak wtedy, kiedy zapragniemy dany podraport wyświetlić wiele razy zaczynając za każdym razem od nowej strony. To już nie jest takie łatwe. Istnieje wiele zasad co i w jakiej kolejności jest wykonywane w czasie przygotowania strony do  wyświetlenia. Istnieje również wiele pomysłów jak zlikwidować problemy z niemożnością rozpoczynania podraportów na nowej stronie. Najzabawniejsze rozwiązanie jakie widziałem to takie, w którym autor przekonywał, iż podraport należy po prostu przekopiować do raportu…

    Moje rozwiązanie jest proste i skuteczne – zawsze działa. W ramach raportu umieszczam kontrolkę List, do której wstawiam kontrolkę Subreport. I teraz najważniejsze. Poniżej kontrolki Subreport, ale ciągle w ramach List umieszczam kontrolkę Rectangle. Ustalam wysokość bramowania na 0,1 cm aby nie burzyć wyglądu. Na koniec ustawiam właściwość PageBreakAtEnd na True. I to wystarczy. Życie staje się prostsze.

  • Konfiguracja Reporting Services przy pomocy PowerShell

    Od pewnego czasu mam przyjemność budować od podstaw system raportowy w jednej z firm finansowych. Dzięki temu możliwe jest przejście przeze mnie całej ścieżki związanej z instalacją, konfiguracją serwera i uprawnień, tworzeniem raportów oraz ich zarządzaniem.

    Pierwsze czynności są zawsze takie same. Trzeba pogrupować raporty według określonych przez właściciela biznesowego kategorii oraz nadać uprawnienia dostępu do poszczególnych raportów. Najłatwiej powiązać kategorie z działami występującymi w danej firmie oraz nałożyć uprawnienia dostępu na poziomie folderów. Oczywiście z biegiem czasu, kiedy raportów powstaje coraz więcej i rośnie świadomość użytkowników, zaczynają się pojawiać żądania dotyczące modyfikacji uprawnień, dostępu grup do folderów lub poszczególnych raportów. Przy dużej liczbie raportów i dużej liczbie tego typu żądań zarządzanie uprawnieniami zaczyna stawać się zadaniem, któremu może się okazać, iż poświęcamy zbyt wiele czasu.

    Ograniczenia

    Załóżmy, iż na serwerze raportów mamy zdefiniowaną następującą strukturę folderów oraz zawartych w nich raportów:

    Struktura folderów i raportów

    Przykładowa struktura folderów i raportów

    Do folderu Folder_1 dostęp ma grupa użytkowników Grupa_1, która jednocześnie nie ma dostępu do folderu Folder_2. W jaki sposób Grupa_1 ma mieć zrealizowany dostęp do raportu Raport_2_3?

    Pierwszy sposób do zrobienie skrótu (link) w Folder_1 raportu Raport_2_3. Ale w zaprezentowanej strukturze Raport_2_3 korzysta z podraportu Podraport_2_3_1 oraz wywołuje raport Raport_2_2. To oznacza, iż podraport oraz odniesienie do raportu Raport_2_2 w folderze Folder_1 nie będą działać z powodu braku uprawnień. Czy zrobienie w związku z tym skrótu do Podraport_2_3_1 i Raport_2_2 w folderze Folder_1 rozwiąże nam problem? Niestety nie. Raport_2_3 dalej będzie się odwoływać do zawartości folderu Folder_2.

    Jak inaczej można rozwiązać ten problem? Drugi sposób polega na wgraniu interesujących nas raportów całkowicie od nowa do właściwego folderu. Tylko, że takie podejście powoduje, iż przy dużej ilości takich zależności zarządzanie uaktualnianiem definicji raportu staje się pracochłonne i podatne na błędy – trzeba bowiem uaktualnić wszystkie wersje danego raportu w systemie.

    Trzeci sposób wiąże się ze zmianą zarządzania uprawnieniami. Zamiast zarządzać dostępem na poziomie folderów należy przejść na poziom poszczególnych raportów. Problem w tym, iż uprawnienia folderu nadrzędnego to suma uprawnień wszystkich raportów i podfolderów. Jest to konieczne aby użytkownik mógł dostać się do folderu w celu przeglądania dostępnych raportów. Przez to łatwiejszym staje popełnienie błędu i udostępnienie dowolnego raportu wszystkim grupom, które mają dostęp do danego folderu.

    Ze skrótami do raportów wiąże się jeszcze jeden ważny problem. Załóżmy, iż z jakiś względów do raportów w folderze Folder_2 dostęp uzyskuje grupa Grupa_1. Uprawnienia są przyznawane na poziomie folderu. Następnie w folderze Folder_2 tworzony jest skrót do raportu Raport_3_1 z folderu Folder_3. Efektem tych zmian jest możliwość wywoływania raportu Raport_3_1 przez grupę Grupa_1 mimo, iż może wcale nie to było naszym celem.

    Wybór rozwiązania

    Co w takim razie powinniśmy zrobić aby ogarnąć temat konfiguracji uprawnień? To co mnie się od razu nasunęło było skorzystanie ze skryptu. Nad wyborem języka skryptowego niewiele się zastanawiałem. Wybór PowerShell’a był w sumie dość oczywisty. Pozostało jedynie wybrać sposób komunikacji z  usługami raportującymi. Pierwsza możliwość to skorzystanie z programu rs.exe dostępnego po instalacji Reporting Services. Aplikacja ta nie umożliwia jednak wykonywania bardziej zaawansowanych czynności przez co nie będziemy mogli z niej skorzystać. A sposób drugi?

    Usługi raportujące składają się z dwóch aplikacji webowych:

    • ReportManager;
    • ReportServer.

    Użytkownik końcowy najczęściej korzysta z ReportManager udostępnianej pod nazwą Reports. Aplikacja ta umożliwia między innymi przeglądanie i wyświetlanie raportów w ramach przeglądarki internetowej. ReportServer dostarcza natomiast usługi sieciowe wykorzystywane przez ReportManager w celu pobierania, wyświetlania i modyfikowania zawartości bazy danych serwera raportów.

    Usługi sieciowe i PowerShell. Czemu nie…

    PowerShell i Policy

    Kilka słów na temat zabezpieczeń PowerShell. Domyślnie po instalacji można uruchamiać jedynie skrypty podpisane. Poziom uprawnień można sprawdzić wpisując w konsoli PowerShell polecenie Get-ExecutionPolicy. Dozwolone wartości to:

    • Restricted;
    • AllSigned;
    • RemoteSigned;
    • Unrestricted.

    Domyślne ograniczenie może być dla nas zbyt bolesne. Dlatego też jeśli mamy ustawiony poziom zabezpieczeń jako Restricted lub AllSigned możemy go zmienić na mniej restrykcyjny:

    Set-ExecutionPolicy RemoteSigned

    Istnieje również możliwość skorzystania z Group Policy o czym można przeczytać na stronach WindowSecurity.com.

    Parametry startowe

    Rozpoczęcie wykonywania skryptu rozpoczynamy między innymi od zdefiniowania stałych:

        # Adres serwera raportów
        [string] $reportServerAddress = 
            "http://localhost/reportserver/reportservice2005.asmx?WSDL"
        
        # Miejsce składowania definicji raportów
        [string] $reportProjectFolder = "C:\Raporty\src"
        
        # Zmienne zawierające źródła danych wymaganych przez raporty
        [string] $dataSourceReferenceName = "/Data Sources/ReportsDataSource"
        
        # Miejsce nadrzędne dla konfigurowanych raportów. Katalog startowy
        [string] $global:root = "/"

    oraz zmiennych globalnych:

        $global:assembly = $null
        $global:proxy = $null

    Adres serwera raportów zawsze będzie taki sam. Jedynie w przypadku konfiguracji zdalnej serwera raportów nazwę localhost należy zastąpić nazwą lub adresem właściwego komputera. Jeśli chodzi o źródła danych to zakładam, iż są one już utworzone w ramach usług raportujących. Dzięki temu unikam zapisywania w pliku konfigurującym ścieżek dostępu i haseł do serwerów baz danych. W powyższym przykładzie zdefiniowane jest tylko jedno źródło danych, ale w może być ich (tak jak u mnie w systemie produkcyjnym) oczywiście więcej. Zmienne $global:assembly oraz $global:proxy będą zawierać klasy umożliwiające zarządzanie serwerem raportów.

    Przygotowanie połączenia z serwerem raportów

    Część rozwiązań, między innymi funkcja New-WebServiceProxy dostępna od PowerShell V2 (CTP3), zwraca obiekt proxy umożliwiający jedynie wykonywanie operacji udostępnianych przez usługi sieciowe serwera raportów. Jest to za mało, ponieważ potrzebować będziemy również możliwości tworzenia nowych obiektów. Dlatego też jako rozwiązanie właściwe wybrałem propozycję Christiana Glessnera. Szczegóły można znaleźć we wpisie PowerShell, WebServices & SharePoint na jego blogu. Źródła można pobrać z witryny http://www.codeplex.com/iLoveSharePoint/Release/ProjectReleases.aspx.

    W swoim rozwiązaniu wykorzystuję zmodyfikowaną wersję metody Get-WebServiceProxy ze skryptu Christiana Glessnera:

        function Create-WebServiceProxy(
            [string] $url = $(throw 'Brak adresu serwera raportów!'))
        {
            Write-Host "Nawiązywanie połączenia z serwerem $url"
    
            $fileName = [System.IO.Path]::Combine(
                [System.Environment]::CurrentDirectory, 
                "Proxy.ReportingServices2005")    
            if(!(Test-Path "$fileName.dll")){
                $WinSDK = "$env:ProgramFiles\Microsoft SDKs\Windows\v6.0A\Bin"
                $Net35 = "$env:SystemRoot\Microsoft.NET\Framework\v3.5"
            
                $null =& $WinSdk\wsdl.exe $url /n:Proxy /out:"$fileName.cs"
                $null =& $Net35\csc.exe /t:library /out:"$fileName.dll" "$fileName.cs"
            }
            $global:assembly = [System.Reflection.Assembly]::LoadFrom("$fileName.dll")
            $proxyType = $global:assembly.GetTypes() | Where-Object { $_.IsSubclassOf(
                [System.Web.Services.Protocols.SoapHttpClientProtocol]) -eq $true}
            $global:proxy = New-Object -TypeName $proxyType
            
            Write-Host "Połączenie nawiązane"
        }

    Jak widzimy, w powyższym kodzie tworzona, ładowana do pamięci i przechowywana w zmiennej globalnej $global:assembly jest biblioteka Proxy.ReportingServices2005.dll zawierająca wszystkie potrzebne typy. Zmienna globalna $global:proxy zawiera instancję klasy umożliwiającej odpytywanie usług sieciowych serwera raportowego znajdującego się w określonej przez nas lokalizacji.

    Utworzenie biblioteki Proxy.ReportingServices2005.dll wymaga Microsoft SDKs, które wgrywane do systemu jest automatycznie w czasie instalacji Visual Studio. Jeśli z jakiś przyczyn na serwerze, na którym uruchamiamy powyższy skrypt nie ma i nie może być zainstalowane Microsoft SDKs, należy bibliotekę Proxy.ReportingServices2005.dll utworzyć na komputerze programisty, a następnie przekopiować razem ze skryptem w miejsce docelowe. Rozwiązanie zadziała. Dzięki warunkowi if(!(Test-Path "$fileName.dll")) biblioteka nie będzie ponownie tworzona jeśli już istnieje.

    Czas na przygotowanie połączenia z serwerem raportów:

        Create-WebServiceProxy $reportServerAddress
        $global:proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials

    Koniecznie musimy pamiętać o przypisaniu danych uprawnionego użytkownika. W przypadku kiedy konfigurujemy localhost wystarczy przypisać aktualnie zalogowanego do komputera użytkownika – z definicji użytkownicy lokalni mają bowiem uprawnienia administracyjne w ramach usług raportowych.

    Konfiguracja folderu startowego

    Po nawiązaniu połączenia z serwerem raportów czyścimy uprawnienia głównego katalogu, w ramach którego będziemy tworzyć foldery i wgrywać raporty.

        $policies = Create-Policy $adminGroups "WAW-RS\ReportUser"
        $global:proxy.SetPolicies("$root", $policies)

    Funkcja Create-Policy na podstawie przekazanych tablic zawierających łańcuchy z nazwami uprawnień tworzy odpowiednią kolekcję obiektów, którą możemy przekazać do usługi sieciowej SetPolicies. Zmienna $adminGroups zawiera minimalne uprawnienia administratora:

        $adminGroups = "BUILTIN\Administrators"

    jeśli system, na którym uruchamiamy skrypt jest w angielskiej wersji językowej lub:

        $adminGroups = "BUILTIN\Administratorzy"

    jeśli system jest w polskiej wersji językowej. W powyższym przykładzie do metody Create-Policy przekazujemy również uprawnienia do przeglądania raportów dla użytkownika "WAW-RS\ReportUser". Serwer raportów może bowiem zawierać raporty dla innych aplikacji webowych, których nie będziemy konfigurować. Ważne jest, aby uprawnienia przekazywane do funkcji Create-Policy w skrypcie podawać zawsze z nazwą komputera!

    Co natomiast robi funkcja Create-Policy? Oto jej kod:

        function Create-Policy([string[]] $adminGroups, [string[]] $browseGroups)
        {
            [Proxy.Policy[]] $policies = Create-AdminPolicy $adminGroups
            foreach($browseGroup in $browseGroups){
                $role = New-Object "Proxy.Role"
                $role.Name = "Browser"
                $policy = New-Object "Proxy.Policy"
                $policy.GroupUserName = $browseGroup
                $policy.Roles += $role
                $policies += $policy
            }
            return $policies
        }

    Oraz kod metody Create-AdminPolicy:

        function Create-AdminPolicy([string[]] $adminGroups)
        {
            [Proxy.Policy[]] $policies = @()
            foreach($adminGroup in $adminGroups){
                $role = New-Object "Proxy.Role"
                $role.Name = "Content Manager"
                $policy = New-Object "Proxy.Policy"
                $policy.GroupUserName = $adminGroup
                $policy.Roles += $role
                $policies += $policy
            }
            return $policies
        }

    W funkcjach Create-Policy oraz Create-AdminPolicy dokonuję pewnych założeń i uproszczeń. Otóż każdej grupie przekazanej przez zmienną $browseGroup przypisywane jest uprawnienie "Browser". Usługi raportujące mają więcej w tej materii możliwości, ale ponieważ nie korzystam z nich nie są przeze mnie w  skrypcie obsługiwane.

    Konfiguracja folderów i raportów

    Proces konfiguracji będzie przebiegał według następującego schematu:

    • Utworzenie jeśli konieczne folderu dla raportów;
    • Nadanie niezbędnych uprawnień do folderu;
    • Wgranie jeśli konieczne pliku rdl z definicją raportu;
    • Konfiguracja właściwości raportu;
    • Przypisanie źródła danych do raportu;
    • Konfiguracja uprawnień dostępu do raportu;
    • Dodanie do folderów nadrzędnych uprawnień koniecznych do dostępu do raportu.

    Przykład załadowania i konfiguracji kilku kombinacji raportów:

        Write-Host
        Write-Host "Konfigurowanie raportów przykładowych"
        $folder = "Raporty przykładowe"
        $reportPath = "$reportProjectFolder\Raporty przykładowe"
        $browseGroups = "AD\Grupa1", "AD\Grupa2", "AD\Grupa3"
    
        Create-Folder $folder $root $adminGroups
        $path = Combine-Path $root $folder
        
        Create-Report "$path" "$reportPath\Raport 1.rdl" $adminGroups
            $browseGroups $dataSourceReferenceName
        Create-Report "$path" "$reportPath\Raport 1 - podraport.rdl" $adminGroups 
            $browseGroups $dataSourceReferenceName $hide
        Create-Report "$path" "$reportPath\Raport 2.rdl" $adminGroups 
            ($browseGroups + "AD\Grupa4") $dataSourceReferenceName
        Create-Report "$path" "$reportPath\Raport 3.rdl" $adminGroups 
            ($browseGroups + "AD\Grupa4", "AD\Grupa5") $dataSourceReferenceName
        Create-Report "$path" "$reportPath\Raport 4.rdl" $adminGroups 
            $browseGroups $null

    Stała pomocnicza $hide zwraca wartość $true i podawana jako parametr funkcji Create-Report umożliwia ukrycie raportu.

    Po zdefiniowaniu wartości zmiennych zaczynamy od utworzenia i aktualizacji uprawnień folderu. Do tego wykorzystujemy metodę Create-Folder:

        function Create-Folder([string] $folder, [string] $parent = "/", 
            [string[]] $adminGroups)
        {    
            [string] $path = Combine-Path $parent $folder
            if(!(ItemExists $parent $folder)){
                Write-Host "Utworzenie folderu $path"
                $properties = @()
                $global:proxy.CreateFolder($folder, $parent, $properties)
            }
            Write-Host "Usunięcie uprawnień dla folderu $path"
            $policies = Create-AdminPolicy $adminGroups
            $global:proxy.SetPolicies("$path", $policies)
        }

    Dwie metody pomocnicze wywoływane przez funkcję Create-Folder to:

        function Combine-Path([string] $path1, [string] $path2)
        {
            [string] $path = [System.IO.Path]::Combine($path1, $path2)
            return $path.Replace("\", "/")
        }

    umożliwiająca budowanie poprawnych ścieżek dostępu, oraz ItemExists, której zadaniem jest sprawdzenie czy w danej lokalizacji istnieje szukany przez nas element: folder, raport, źródło danych...

        function ItemExists([string] $folder, [string] $name)
        {
            [Proxy.BooleanOperatorEnum] $operator = 0
            [Proxy.ConditionEnum] $condition = 1
            $searchCondition = New-Object "Proxy.SearchCondition"
            $searchCondition.Condition = $condition
            $searchCondition.ConditionSpecified = $true
            $searchCondition.Name = "Name"
            $searchCondition.Value = $name
            [Proxy.CatalogItem[]] $items = 
                $global:proxy.FindItems($folder, $operator, $searchCondition)
            return $items -and ($items.Length -cgt 0)
        }

    Największe jednak czynności są wykonywane w czasie wgrywania i konfigurowania konkretnego raportu:

        function Create-Report([string] $parent, [string] $path, 
            [string[]] $adminGroups, [string[]] $browseGroups,
            [string] $dataSourceReferenceName,
            $hide = $false, $overwrite = $false)
        {
            Write-Host
            $name = [System.IO.Path]::GetFileNameWithoutExtension($path)
            $report = "$parent/$name"
            
            if(!$overwrite){
                if(!(ItemExists $parent $name)){
                    Upload-Report $parent $path $name $overwrite
                }
            }
            
            Set-ReportProperty $report $hide
            Set-ReportReferenceDataSource $report $dataSourceReferenceName
            [Proxy.Policy[]] $policies = Create-Policy $adminGroups $browseGroups
            Set-ReportPolicy $report $name $policies
            Set-FolderPolicy $parent $policies
        }

    Domyślnie jeśli dany raport już istnieje to nie jest aktualizowana jego definicja. Poprzez modyfikację wartości domyślnej dla parametru $overwrite możemy zmienić zachowanie skryptu. Pamiętajmy, iż nie należy usuwać raportów w celu modyfikacji definicji aby nie stracić istniejących historii i subskrypcji. Domyślne zachowanie skryptu jest takie, aby można było go uruchamiać w dowolnym (no prawie) momencie w celu sprawdzenia i korekcji uprawnień.

    Funkcja wgrywająca definicję raportu wygląda zaś następująco:

        function Upload-Report([string] $parent, [string] $path, 
            [string] $name, [bool] $overwrite)
        {
            Write-Host "Utworzenie raportu $report"
            $stream = [System.IO.File]::OpenRead($path)
            $definition = New-Object byte[] $stream.Length
            $stream.Read($definition, 0, $stream.Length)
            $stream.Close()        
            $global:proxy.CreateReport($name, $parent, 
                $overwrite, $definition, $null)
        }

    Jak widzimy ułatwiamy sobie życie korzystając z bibliotek platformy .NET!

    Właściwości, które ustawiamy są skromne.W chwili obecnej jest to jedynie ukrywanie raportu. Ma to sens na przykład w przypadku podraportów, które nie stanowią jednocześnie samodzielnego raportu:

        function Set-ReportProperty([string] $report, [bool] $hidden)
        {
            Write-Host "Aktualizacja właściwości raportu $name"
            $property = New-Object "Proxy.Property"
            $property.Name = "Hidden"
            $property.Value = $hidden
            $properties = @($property)
            $global:proxy.SetProperties($report, $properties)
        }

    Po ustawieniu właściwości następuje przypisanie źródła danych do raportu. Oczywiście nie każdy raport musi posiadać źródło danych. Stąd warunek na początku funkcji:

        function Set-ReportReferenceDataSource([string] $report, 
            [string] $dataSourceReferenceName)
        {
            if($dataSourceReferenceName -eq $null -or 
                $dataSourceReferenceName.Length -lt 1){
                return    
            }
            
            Write-Host "Aktualizacja źródła danych raportu $name"
            $dataSourceReference = New-Object "Proxy.DataSourceReference"
            $dataSourceReference.Reference = $dataSourceReferenceName
            $dataSources = $global:proxy.GetItemDataSources($report)
            # Poniższe można odkomentować jeśli nie zależy nam na poprawności
            # przypisań źródła danych
            #if($dataSources -eq $null){
            #    return
            #}
            foreach($dataSource in $dataSources){
                $dataSource.Item = $dataSourceReference
            }
            $global:proxy.SetItemDataSources($report, $dataSources)
        }

    W powyższym kodzie dokonuję kilku ważnych założeń. Po pierwsze jak już wcześniej mówiłem, zakładam, iż źródła danych w ramach usług raportowych są już utworzone. Dlatego też stosuję typ Proxy.DataSourceReference. Po drugie każdy raport korzysta docelowo tylko z jednego źródła danych. Nawet jeśli w czasie tworzenia raportu i jego testów wykorzystywałem dwa czy trzy źródła danych to na serwerze produkcyjnym mam tylko jedno źródło. Jeśli to założenie w Waszym przypadku nie będzie prawdziwe należy dokonać modyfikacji funkcji Set-ReportReferenceDataSource tak, aby parametr $dataSourceReferenceName był tablicą.

    Przypisanie uprawnień do raportu:

        function Set-ReportPolicy([string] $report, [string] $name, 
            [Proxy.Policy[]] $policies)
        {
            Write-Host "Nadanie uprawnień do raportu $name"        
            $global:proxy.SetPolicies($report, $policies)
        }

    I na koniec rekurencyjne ustawienia właściwości folderu zawierającego konfigurowany raport. Znak % jest skrótowym zapisem polecenia Foreach-Object:

        function Set-FolderPolicy([string] $parent,
            [Proxy.Policy[]] $policies)
        {        
            $folderPolicies = @()
            $folderPoliciesInherited = [ref] $false
            $folderPolicies = $global:proxy.GetPolicies($parent,
                $folderPoliciesInherited)
            foreach($policy in $policies){
                if($($folderPolicies | % {$_.GroupUserName}) -notcontains 
                        $policy.GroupUserName){
                    $folderPolicies += $policy
                }
            }
            Write-Host "Aktualizacja uprawnień folderu $parent"
            $global:proxy.SetPolicies($parent, $folderPolicies)
            
            if($parent -eq $global:root){
                return
            }
            
            for($i = $parent.Length; 0, $i--){
                if($parent[$i] -eq "/"){
                    $newParent = $parent.Substring(0, $i)
                    if($newParent.Length -eq 0){
                        $newParent = "/"
                    }
                    Set-FolderPolicy $newParent $policies
                    break
                }
            }
        }

    Pamiętajmy o bardzo ważnym fakcie – uprawnienia folderu zawierający podfoldery i raporty są sumą uprawnień podfolderów i raportów!

    Zakończenie

    Uzbrojeni w taki skrypt możemy spokojnie wypłynąć na szerokie wody konfiguracyjne usług raportowych. Jedyne problemy jakie mogą nas spotkać dotyczyć będą sytuacji, kiedy będziemy próbowali skorzystać z protokołu https przy braku zaufanego certyfikatu. Nie ma bowiem możliwości potwierdzenia świadomego łączenia się z serwerem.

    Osobom, które chciałyby spróbować swoich sił w wykorzystaniu języka PowerShell polecam pobranie darmowego PowerGUI umożliwiającego pisanie skryptów oraz zarządzanie (!) komputerem i nie tylko przy pomocy hierarchicznie zorganizowanych skryptów PowerShell. Warto również z sekcji PowerPacks pobrać pakiet SQL Server 2005 Reporting Services Power Pack umożliwiający zarządzanie usługami raportującymi.

    Grafika (schemat folderów) została wykonana przy pomocy bubbl.us.

  • Implementacja Inversion of Control - wersja 1.1

    Od ostatniej notki opisującej wykorzystywany przeze mnie własnej produkcji kontener IoC wprowadziłem kilka modyfikacji czyniących rozwiązanie bardziej elastycznym, ale wciąż pozostające wierne podstawowym założeniom:

    • Wydajne i łatwe w użyciu;
    • Zminimalizowane użycie refleksji;
    • Brak plików konfiguracyjnych.

    Czymże jest kontener IoC

    Kontener IoC umożliwia programiście wprowadzenie w aplikacji luźnych powiązań pomiędzy obiektami. Programista rejestruje interfejsy i klasy abstrakcyjne wraz z typami implementującymi, instancjami lub procedurami tworzącymi instancje klas na żądanie:

                ObjectLocator.Register<IContactRepository>().

                    WithType<ContactRepository>();

    Możliwa jest również rejestracja typów, które mogą posiadać instancje:

                ObjectLocator.Register<Database>();

    Zwracany w czasie rejestracji interfejs IObjectProfile korzysta ze wzorca Fluent Interface w celu właściwej konfiguracji:

                ObjectLocator.Register<IContactRepository>().

                    AsSingleton().

                    WithType<ContactRepository>().

                    AndBuildUp(new object[] { "test" });

    Skorzystanie z obiektu zarejestrowanego w kontenerze polega na wywołaniu metody GetInstance z ewentualnymi parametrami, które zostaną przekazane do konstruktora:

                var repository = ObjectLocator.GetInstance<IContactRepository>();

    W przeciwieństwie do rozwiązań typu Dependency Injection w opisywanym kontenerze IoC nie jest możliwe skorzystanie z automatycznego uzupełniania parametrów konstruktorów czy wstrzykiwania instancji do właściwości.

    Zmiany

    W porównaniu do wersji wcześniejszej, fasada rozwiązania, statyczna od teraz klasa ObjectLocator zwraca nam zamiast obiektu ObjectProfile interfejs IObjectProfile. Z punktu widzenia programisty korzystającego z IoC jest to najważniejsza zmiana. Może to bowiem oznaczać konieczność zmiany między innymi definicji metod zwrotnych wywoływanych w czasie tworzenia instancji, jeśli oczywiście z takowych korzystamy, na:

            Action<IObjectProfile<TRegisteredAs>, TRegisteredAs>

    oraz

            Func<IObjectProfile<TRegisteredAs>, object[], TRegisteredAs>

    Są to jednak zmiany na poziomie wręcz kosmetycznym. Jednym słowem nie sprawią problemów.

    Wewnętrznie ObjectLocator deleguje teraz wszystkie zadania do implementacji interfejsu IObjectContainer. Dzięki temu korzystanie z ObjectLocator nie determinuje nam konkretnej implementacji kontenera. Innymi słowy wykorzystanie tego rozwiązania nie przekreśla możliwości skorzystania z innego rozwiązania Inversion of Control lub nawet Dependency Injection. Wystarczy bowiem wówczas zaimplementować interfejs IObjectProfile w celu konfiguracji rejestrowanego typu oraz interfejs IObjectContainer do zarządzania rejestrowanymi typami. Podmiana kontenera oznacza przypisanie klasie ObjectLocator poprzez właściwość Container interesującej nas instancji implementującej IObjectContainer.

    Implementacja Inversion of Control

    Implementacja Inversion of Control

    Dzięki wprowadzeniu interfejsu IObjectContainer prostsze stało się korzystanie z kontenera w pakietach (assemblies) zewnętrznych. Do tej pory pobieranie instancji oznaczało konieczność odwoływania się do statycznej klasy ObjectLocator. Teraz wystarczy przekazać do pakietu zewnętrznego zmienną typu IObjectContainer.

    Łatwiejsze stało się również testowanie aplikacji korzystających z rozwiązania IoC. Nie trzeba teraz przed każdym testem konfigurować klasy ObjectLocator poprzez rejestrowanie typów etc. Zamiast tego wystarczy obecnie przypisać do właściwości ObjectLocator.Container testową implementację kontenera. Poniżej przykład takiego kodu wykorzystującego bibliotekę Rhino.Mocks:

                var container = MockRepository.GenerateStub<IObjectContainer>();

                var repository = new CustomerRepository();

                container.Stub(

                    x => x.GetInstance<IRepository<Customer>>()).

                    Return(repository);

                ObjectLocator.Container = container;

                Assert.AreEqual(

                    repository,

                    ObjectLocator.GetInstance<IRepository<Customer>>());

    Nowości

    Poza opisaną już właściwością Container, w klasie ObjectLocator i interfejsie IObjectContainer pojawiła się metoda Contains umożliwiająca sprawdzenie czy dany typ został już zarejestrowany. Jest to cenna możliwość zwłaszcza jeśli potrzebujemy dla danego odbiorcy (użytkownika) oprogramowania przerejestrować konkretny typ. W wersji wcześniejszej konieczne było sprawdzenie czy istnieje profil dla danego typu wykorzystując metodę GetProfile.

    Na poziomie konfigurowania typu (interfejs IObjectProfile) dodano możliwość zarejestrowania parametrów domyślnych przekazywanych do konstruktora nowego obiektu. W poniższym przykładzie parametr ten jest pobierany z kontenera IoC:

                ObjectLocator.Register<IContactRepository>().

                    WithType<ContactRepository>().

                    WithParameters(new object[]

                                       {

                                           ObjectLocator.GetInstance<Database>()

                                       }).

                    AsSingleCall();

    Parametry domyślne mogą być zastępowane nowymi w czasie wywołań metody GetInstance.

    Podsumowanie

    Opisywane rozwiązania używam z powodzeniem od ponad kilkunastu miesięcy w projekcie, którego części składowe działają na urządzeniach mobilnych. Udostępnione kody są oparte na nowej licencji BSD co umożliwia używanie opisywanego kontenera IoC w projektach komercyjnych. Kody źródłowe zawierają testy jednostkowe, które umożliwiają dokładniejsze zapoznanie się z możliwościami rozwiązania.

    Pliki do artykułu:

    • IoC - kody źródłowe.
  • Reporting Services - projekt(y) po polsku?

    Kiedy ponad rok temu zaczynałem budowanie raportów w MS SQL Server 2005 Reporting Services podszedłem do tematu jak rasowy programista. Całe nazewnictwo miało być po angielsku.

    Etap pierwszy

    Założyłem nowy projekt, dodałem do repozytorium (usługi raportowe były nowe, nowiuśkie, bez jakiegokolwiek raportu) i... hm.... Pierwsza niespodzianka to siermiężne warunki przygotowywania zapytań w Visual Studio. Nie będę się rozpisywał. Budowanie zapytań SQL w tym środowisku ma swoje ograniczenia (ma się ochotę przejść na ciemną stronę mocy).

    Etap drugi

    Chciał nie chciał, ale powstał skorelowany z raportami drugi projekt przeznaczony dla Microsoft SQL Server Management Studio. Teraz życie uległo poprawie. W Management Studio powstawało zapytanie, lub zapytania jeśli raport miał zawierać parametry w postaci rozwijalnych list, i testy poprawności zwracanych danych. W Visual Studio zaś wygląd raportu. Zapytanie było przekopiowywane (Ctrl+C, Ctrl+V).

    Etap trzeci

    Raportów przybywa. Nazewnictwo specjalistyczne doprowadza przy tłumaczeniach do szału. W ramach samej usługi dostępnej poprzez stronę WWW raporty te występują pod polskimi nazwami, co jest słuszne - w końcu pracuję w polskiej firmie. Problemy zaczynają sprawiać łączenia raportów (budowanie odnośników w jednych raportach do drugich). Zaczynam się irytować.

    Etap czwarty

    Nazwy wszystkich plików zawierających zapytania SQL oraz definicje raportów zostały przetłumaczone na polski. Powrót do macierzy. Trochę to jeszcze drażni ale tak jest zdecydowanie lepiej. Nie ma oznak paniki. Łatwo znaleźć raport, o który właściciel biznesowy się dopytuje.

    W miarę powstawania kolejnych raportów coraz więcej osób próbuje wykorzystać platformę raportową w celu ułatwienia sobie codziennej pracy. Ponieważ zapytania są budowane na relacyjnej bazie danych ich rozmiar staje się coraz większy i większy.... aż pewnego dnia Visual Studio mówi stop! Przekroczyłem limit 32 KB znaków w zapytaniu. Coś podobnego. Usunięcie komentarzy pomogło.

    Zdarza się również, iż zmieni się logika biznesowa (czytaj znaczenie pól w tabelach) albo zleceniodawca raportu ma fanaberie i trzeba któreś z pól trochę inaczej przeliczyć. Zmiana zapytania, przekopiowanie zapytania do raportu, połączenie się poprzez RDP z serwerem, wgranie raportu.... Ta ścieżka zaczyna być zbyt nużąca.

    Etap piąty

    Kocham procedury składowane. Wszystko składuję teraz w nich (no bez przesady. Własne funkcje i procedury do pomocniczych obliczeń używałem od samego początku). Całe zapytania. Raport wywołuje procedurę i tyle. Żadnego kopiowania. Jeśli pojawia się zmiana obliczania pól, które nie skutkuje modyfikacją raportu to wystarczy zmienić tylko procedurę. Bez konieczności modyfikowania zapytania w definicji raportu.

    Oczywiście procedury mają polskie nazwy. I tak zostanie.

Więcej wypowiedzi Następna strona »
W oparciu o Community Server (Personal Edition), Telligent Systems