BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Rozszerzamy funkcjonalność Visual Studio

17-12-2004 20:55 | xqsnake
Czytając ten artykuł dowiesz się między innymi jak utworzyć Add-in (wtyczkę) do Visual Studio .NET 2003 z dodatkowymi przyciskami, z zakładką konfiguracyjną, jak zmodyfikować programowo kod źródłowy w edytorze oraz zebrać ciekawe informacje o kodzie i procesie tworzenia aplikacji. Zapraszam do lektury.

 

UWAGA: Ten artykuł jest także dostępny w wersji HTML (milszej dla oka) do pobrania z sekcji załączników (na wypadek gdyby się źle sformatował).

Zachęcam również do przeczytanie artykułu Radosława Zawartko – „Visual Studio .NET Add-In”

 

 

  

Wstęp:

Programowanie wtyczek do Visual Studio (szczególnie dla początkującego użytkownika) nie jest wcale takie łatwe jak by się mogło wydawać, mimo że dysponujemy ogromną bazą wiedzy i dokumentacji, jaką jest MSDN. Poznanie mechanizmów i zasad działania VS jest rzeczą czasochłonną – szczególnie, że dokumentacja do samego VS w niektórych miejscach powiedzmy sobie szczerze - po prostu kuleje. Celem tego artykułu jest przybliżenie niektórych mechanizmów i tajników działania VS oraz zaoszczędzenie czasu innym - mającym ochotę napisać własną wtyczkę. Możliwości oferowane przez mechanizm wtyczek są naprawdę ogromne, toteż pomysłów nikomu nie powinno zabraknąć. Zaczynajmy więc!

 

Lista poruszonych tematów:

Wszystko w tym artykule opiera się o konkretne przykłady, toteż na początek krótka lista tematów, jakie zostaną w nim poruszone, po to, by można było szybko odnaleźć interesującą nas rzecz.

 

1. Obsługa kreatora tworzenia wtyczek

2. Dodanie przycisków poleceń do menu głównego („Tools”)

3. Dodanie przycisków poleceń do menu kontekstowego edytora kodu

4. Tworzenie i pisanie po panelu VS

5. Podpinanie się pod zdarzenia w VS

6. Przechwytywanie i reagowanie na polecenia

7. Tworzenie prostego „undo”

8. Utworzenie panelu konfiguracyjnego wtyczki

9. Odczyt i zapis do rejestru

10. Modyfikacja zawartości okna edycji kodu

- tworzenie nagłówka pliku

- tworzenie regionu

11. Kilka interesujących funkcji dla ciekawskich

12. Szybka konfiguracja instalatora

 

 

1. Obsługa kreatora tworzenia wtyczek

 

 

Aby utworzyć nową wtyczkę do VS skorzystamy z kreatora.

File->New Project a następnie wybieramy

Other projects->Extensibility Projects->Visual Studio .NET Add-in.

Na kolejnych ekranach wybieramy:

język programowania  = C#

aplikację hostującą  = Visual Studio (nie piszemy dodatku dla środowiska makr VBasic)

wpisujemy nazwę wtyczki (nasz przykład nazywa się „Develogger”) oraz krótki opis

zaznaczamy tylko, że chcemy aby nasza wtyczka ładowała się automatycznie po starcie VS

określamy (dowolnie) jej dostępność dla innych użytkowników

nie tworzymy about box’a

Rys. 1. Opcje kreatora (krok 4/6)

 

klikamy Next i Finish.

 

Jako wynik działania kreatora otrzymujemy dwa projekty: pierwszy to projekt wtyczki, a drugi jej instalatora (instalatorem zajmiemy się później).

W projekcie wtyczki mamy wygenerowany kod z wstępnym szkicem klasy Connect, która implementuje metody interfejsu IDTExtensibility2 i od razu uzupełniamy ją o klasę bazową (interfejs) IDTCommandTarget. Dwie metody tego interfejsu będą przez nas implementowane w celu obsłużenia naciskanych przycisków. Dodajemy także od razu potrzebne nam zmienne klasy Connect.

 

//zdarzenia

private EnvDTE.SolutionEvents solutionEvents;

private EnvDTE.DebuggerEvents debuggerEvents;

private EnvDTE.BuildEvents buildEvents;

private EnvDTE.ProjectItemsEvents CSharpProjectItemsEvents;

 

//zmienne pomocnicze

private _DTE applicationObject;

private AddIn addInInstance;

...

//stałe nagłówka

...

 

Ważne jest by zdarzenia były zadeklarowane wewnątrz klasy Connect, a nie w ciele metody OnConnection ponieważ w niedługim czasie zaopiekowałby się nimi nasz znajomy śmieciarz, a my stracilibyśmy funkcjonalność wtyczki.

Zaraz po załadowaniu naszej wtyczki wykonywana jest metoda  OnConnection klasy Connect, w której po prostu inicjalizujemy wszelkie struktury danych i przygotowujemy naszą wtyczkę do działania. Tworzymy w niej także potrzebne przyciski, wczytujemy jej konfigurację oraz podpinamy się pod interesujące nas zdarzenia zachodzące w VS. Wszystko to odbywa się przy użyciu dwóch obiektów (udostępnianych przez metodę OnConnection), do których pobieramy referencje:

applicationObject = (_DTE)application;

     addInInstance = (AddIn)addInInst;

Pierwszy z nich reprezentuje całe środowisko programowanie VS IDE i to poprzez operowanie na jego metodach i obiektach, które udostępnia, możliwe jest działanie naszej wtyczki (DTE = Development Tools Extensibility). Drugi to po prostu instancja naszej wtyczki do VS.

 

 

2. Dodanie przycisków poleceń do menu głównego („Tools”)

 

 

Aby dodać przyciski do menu głównego „Tools” umieszczamy w metodzie OnConnection następujący kod:

 

object [] contextGUIDS = new object[] { };//konteksty środowiska - nie wykorzystywane przez VS

Commands commands = applicationObject.Commands;

_CommandBars commandBars = applicationObject.CommandBars;

 

//próba utworzenia nowych poleceń - mogą już takie istnieć!

Command command;

CommandBar commandBar;

CommandBarControl commandBarControl;

//bierzemy menu (w postaci paska przycików)

commandBar = (CommandBar)commandBars["Tools"];

try {

      //tworzymy polecenie

      command = commands.AddNamedCommand(addInInstance, "AddHeader",

            "Add Header", "Adds header to current document", true, 6346, ref contextGUIDS,

            (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled);

      //dodajemy polecenie (przycisk)

      commandBarControl = command.AddControl(commandBar, 1);

}     catch {

}

try {

      //tworzymy polecenie

      command = commands.AddNamedCommand(addInInstance, "ShowInfo",

            "Solution Info", "Shows info for the current opened solution", true, 487, ref contextGUIDS,

            (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled);

      //dodajemy polecenie (przycisk)

      commandBarControl = command.AddControl(commandBar, 2);

}     catch {

}

Rys. 2. Przyciski w menu głównym „Tools”

 

Zauważmy, że tworzone przez nas (za każdym załadowaniem wtyczki) komendy środowiska (polecenia) mogą już istnieć, więc ignorujemy wszelkie wyjątki. Może jest to mało elegancki sposób, ale bezpieczny, ponieważ często zdarza się, że przyciski po prostu znikają z pasków i menu z powodu sposobu, w jaki VS nimi zarządza (szukanie i sprawdzanie czy nasze przyciski istnieją powoduje duże opóźnienia w ładowaniu VS). Powyższy kod tworzy dwa polecenia menu głównego „Tools” o nazwach: „AddHeader” i „ShowInfo”. Nazwy ich są później kluczowe przy reagowaniu na wydane przez użytkownika komendy. Z ciekawszych parametrów mamy: wyświetlaną nazwę, krótki opis, numer ikonki w VS, która ma symbolizować daną komendę oraz status sygnalizowany dwiema flagami, które trzeba określać, aby nasza komenda była widoczna (Supported) oraz aktywna (Enabled). Tym jednak zajmiemy się w dalszej części naszej przygody z wtyczkami.

 

 

 3. Dodanie przycisków poleceń do menu kontekstowego edytora kodu

 

 

Dodanie przycisków do menu kontekstowego jest niemal identyczne, z tą jednak różnicą, że tym razem naszym commandBar’em jest "Code Window", czyli okno edytora kodu.

 

//bierzemy menu kontekstowe edytora kodu (w postaci paska przycików)

commandBar = (CommandBar)commandBars["Code Window"];

//dodajemy 2 pozycje do menu kontekstowego edytora kodu

try {

      //tworzymy polecenie

      command = commands.AddNamedCommand(addInInstance, "AddHeader2",

            "Add Header", "Adds header to current document", true, 6346, ref contextGUIDS,

            (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled);

      //dodajemy polecenie (przycisk)

      commandBarControl = command.AddControl(commandBar, 1);

}     catch {

}

try {

      //tworzymy polecenie

      command = commands.AddNamedCommand(addInInstance, "MakeRegion",

            "Make Region", "Makes region from selected text", true, 1554, ref contextGUIDS,

            (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled);

      //dodajemy polecenie (przycisk)

      commandBarControl = command.AddControl(commandBar, 2);

}     catch {

}

Rys. 3. Menu kontekstowe edytora kodu (z zaznaczonym tekstem)

 

W dalszej części metody OnConnection wczytujemy z rejestru konfigurację wtyczki i tworzymy panel, na którym będziemy mogli wyświetlać różne użyteczne informacje.

 

 

4. Tworzenie i pisanie po panelu VS

 

 

Utworzenie panelu jest jeszcze prostsze niż tworzenie przycisków. Posługujemy się oczywiście nadrzędnym obiektem DTE, którym jest applicationObject.

 

//tworzymy output wtyczki

OutputWindow outputWindow = (OutputWindow)applicationObject.Windows.Item(Constants.vsWindowKindOutput).Object;

outputWindowPane = outputWindow.OutputWindowPanes.Add("Develogger output");

 

Wybieramy interesujący nas rodzaj okna i dodajemy panel. Jeszcze prostsze jest pisanie po nim. Wykorzystujemy do tego celu funkcje Message/MessageLn(string str), które wykonują następujący kod, w zależności od konfiguracji wtyczki (conf.ShowOutput)

 

if (conf.ShowOutput) outputWindowPane.OutputString(str);

 

Rys. 4. Panel wyjściowy wtyczki

 

Ostatnią rzeczą, jaką robimy w ciele metody OnConnection jest wybranie interesujących nas zdarzeń i podpięcie się pod nie.

 

 

5. Podpinanie się pod zdarzenia w VS

 

 

W tym celu posłużymy się głównym obiektem DTE i pobierzemy referencję do obiektu Events, który reprezentuje wszystkie zdarzenia zachodzące w VS IDE.

 

EnvDTE.Events events = applicationObject.Events;

 

Mając już wszystkie zdarzenia na talerzu wybieramy sobie najbardziej nas interesujące kategorie zdarzeń (oczywiście do zdeklarowanych wcześniej zmiennych odpowiednich typów).

Resztę kategorii zdarzeń można uzyskać szukając w MSDN’ie opisu do EnvDTE.Events.

 

solutionEvents = (EnvDTE.SolutionEvents)events.SolutionEvents;

 

CSharpProjectItemsEvents = (EnvDTE.ProjectItemsEvents)applicationObject.Events.GetObject("CSharpProjectItemsEvents");//wyjątkowo przez GetObject - inaczej nie zadziała

 

debuggerEvents = (EnvDTE.DebuggerEvents)events.DebuggerEvents;

                 

buildEvents = (EnvDTE.BuildEvents)events.BuildEvents;

 

Następnie podpinamy się pod właściwe zdarzenia.

 

solutionEvents.Opened += new _dispSolutionEvents_OpenedEventHandler(this.Opened);

solutionEvents.BeforeClosing += new _dispSolutionEvents_BeforeClosingEventHandler(this.BeforeClosing);

...

(sposób podpięcia się pod resztę zdarzeń jest analogiczny)

Wybrałem takie a nie inne zdarzenia z myślą o funkcjonalności, jaką ma realizować nasza wtyczka – możemy się podpiąć pod wszystkie, jeśli trzeba. I tak:

 

solutionEvents.Opened – zachodzi, kiedy otwieramy rozwiązanie (solution) - inicjujemy wtedy wszelkie struktury związane z samym rozwiązaniem

 

public void Opened() {

      outputWindowPane.Clear();

      MessageLn("SolutionEvents::Opened");

 

      OpenTime = DateTime.Now;//zapamiętujemy czas

 

      XmlRead();//czytamy informacje o solutionie z XMLa

      diff = new TimeSpan(0, 0, 0, info.DevelTime);//pobieramy wczytany czas pracy

     

      AutoSaveTimer.Enabled = false;//i jeśli potrzeba uruchamiamy opcję AutoSave

      if (conf.AutoSaveTime > 0) {

            AutoSaveTimer.Interval = (1000 * 60) * conf.AutoSaveTime;

            AutoSaveTimer.Enabled = true;

      }

}

 

solutionEvents.BeforeClosing – zachodzi, kiedy kończymy pracę z rozwiązaniem, tuż przed zamknięciem – zapisujemy wtedy wszystkie dane do pliku i zerujemy struktury danych w oczekiwaniu na otwarcie nowego rozwiązania

 

public void BeforeClosing() {

      MessageLn("SolutionEvents::BeforeClosing");

     

      //ustawienia odpowiednich wpisów do pliku .info

      diff = DateTime.Now - OpenTime;//czas aktualnej pracy

      info.DevelTime += diff.Seconds;//sumujemy do poprzedniej wartości

      info.ProjectsNum = GetCSharpProjects().Count;

      info.TotalLinesNum = GetLinesNum();//policz linie kodu

      //policznie klas, metod

      EnumCodeElems(false, out info.ClassesNum, out info.MethodsNum);

      //reszta pól jest ustawiana na bieżąco

 

      //dodanie otworzenia projektu do historii

      Opening opening = new Opening(OpenTime, diff.Seconds);

      info.History.Add(opening);

     

      XmlWrite();//zapis

     

      diff = new TimeSpan(-1, -1, -1);//"wyzerowanie" czasu pracy - do otwarcia nowego solutiona

}

 

CSharpProjectItemsEvents.ItemAdded – zachodzi, kiedy jakiś nowy element zostanie dodany do projektu CSharp - jeśli posiada on dokument tekstowy oraz konfiguracja wtyczki na to zezwala (conf.WriteHeader) to dopisujemy na początek pliku utworzony przez użytkownika nagłówek (o tym później).

Tak zmodyfikowany dokument zapisujemy.

 

public void CSSolutionItemsEvents_ItemAdded(EnvDTE.ProjectItem projectItem) {

      MessageLn("CsharpSolutionItemsEvents::ItemAdded\n");

      MessageLn("\tProject Item: " + projectItem.Name + "\n");

      if (!conf.WriteHeader) return;

      //do nowego składnika zapisujemy nagłówek

      TextDocument textdoc = GetTextDocument(projectItem);

      if (textdoc != null) {

            AddHeader(textdoc);

            textdoc.Parent.Save(textdoc.Parent.FullName);//zapisz zmiany

      }

}

 

debuggerEvents.OnExceptionThrown +

debuggerEvents.OnExceptionNotHandled – zachodzą, kiedy uruchamiane przez nas rozwiązanie rzuca wyjątkami – wyświetlamy wtedy informacje na temat wyjątku do panelu oraz inkrementujemy liczbę wyjątków wyprodukowanych przez nasze rozwiązanie

 

info.ExceptionsNum++;//zwiększ liczbę wyjątków

 

buildEvents.OnBuildDone – zachodzi po skończonej kompilacji wywołanej poleceniem z menu „Build” – tu podobnie zwiększamy liczbę build’ów (w celach statystycznych ;))

 

info.BuildsNum++;

 

Na samym końcu podpinamy się pod zdarzenie timer’a (znane zapewne już wszystkim), który jest odpowiedzialny za auto zapis wszystkich edytowanych przez nas dokumentów.

 

AutoSaveTimer.Elapsed += new System.Timers.ElapsedEventHandler(OnTimer);

 

Metodą „symetryczną” do metody OnConnection jest metoda OnDisconnection, która wywoływana jest kiedy IDE wyrzuca wtyczkę z pamięci (nie koniecznie przy zamykaniu). W niej to odpinamy uchwyty od zdarzeń, które nas interesowały, ponieważ tak nakazują zasady dobrego programowania, a poza tym nie odśmiecone przez Garbage Collectora uchwyty mogłyby się jeszcze odpalać. Dla jasności przykład:

 

. . .

if(solutionEvents != null) {

      solutionEvents.BeforeClosing -= new _dispSolutionEvents_BeforeClosingEventHandler(this.BeforeClosing);

      solutionEvents.Opened -= new _dispSolutionEvents_OpenedEventHandler(this.Opened);

}

. . .

 

Uff…No to zdarzenia mamy za sobą.

 

 

6. Przechwytywanie i reagowanie na polecenia

 

 

Przy dodawaniu przycisków do menu głównego i kontekstowego utworzyliśmy sobie polecenia z nimi powiązane. Nadszedł czas, aby zaimplementować akcje z nimi im odpowiadające i poznać bliżej mechanizm ich wychwytywania. Pamiętamy, że nasza klasa dziedziczy z interfejsu IDTCommandTarget, więc musimy zaimplementować jego dwie najważniejsze metody, a są to: QueryStatus i Exec.

Pierwsza z nich (QueryStatus) sprawdza aktualny kontekst środowiska oraz inne przez nas wybrane warunki i decyduje (przez ustawienie odpowiedniego status’u) czy dany przycisk pojawi się na swoim miejscu (vsCommandStatusSupported), czy będzie on aktywny (vsCommandStatusEnabled), czy wręcz przeciwnie – polecenie nie jest w danym kontekście dostępne (vsCommandStatusUnsupported). Wszystkie te flagi pochodzą z enumeracji EnvDTE.vsCommandStatus dostarczanej przez metodę QueryStatus. Ważne jest, aby metoda ta była zaimplementowana jak najlepiej, ponieważ częste jej wywołania (przy zmianie aktywnego okienka lub stanu VS) mogą sprawić, że interfejs graficzny VS będzie reagował z opóźnieniem. Jeśli chodzi o zastosowanie tych flag to przykład jest samowyjaśniający się.

 

public void QueryStatus(string commandName, EnvDTE.vsCommandStatusTextWanted neededText, ref EnvDTE.vsCommandStatus status, ref object commandText) {

      //jednyna słuszna wartość – reszta jest zarezerwowana na przyszłość   

      if (neededText == EnvDTE.vsCommandStatusTextWanted.vsCommandStatusTextWantedNone) {

           

            if (!applicationObject.Solution.IsOpen) {//jesli nie otworzono żadnego rozwiązania

                  status = (vsCommandStatus)vsCommandStatus.vsCommandStatusUnsupported;//brak przycisków

                  return;

            }

            string docLanguage = applicationObject.ActiveDocument.Language;//jezyk programowania

            if (!(docLanguage.Equals("CSharp"))) {//jesli nie edytujemy C#

                  //odmowa wykonania polecenia, ukrycie przycisku

                  status = (vsCommandStatus)vsCommandStatus.vsCommandStatusUnsupported;

                  return;

            }

           

            if (commandName == "Develogger.Connect.AddHeader" || commandName == "Develogger.Connect.AddHeader2") {

                  //możemy pokazać odpowiednie przyciski zezwolić na działanie

                  status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;

           

            } else if (commandName == "Develogger.Connect.MakeRegion") {//polecenie MakeRegion

                  //bierzemy zaznaczony tekst z aktualnego dokumentu tekstowego

                  TextSelection txt = (TextSelection)applicationObject.ActiveDocument.Selection;

                  if (txt.Text.Length > 0)//jesli cos zaznaczyliśmy

                        //mozemy pokazać odpowiednie przyciski zezwolić na działanie

                        status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;

                  else {

                        //w przeciwnym razie nic nie pokazujemy - odmowa wykonania polecenia

                        status = (vsCommandStatus)vsCommandStatus.vsCommandStatusUnsupported;

                  }    

                  return;                

            }

            status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;//jest OK

      }

}

 

Druga metoda (Exec) odpowiada za ustalenie, jakie polecenie zostało wydane i za podjęcie odpowiedniej akcji związanej z tym poleceniem. Warto od razu wspomnieć, że przekazywane do tej metody nazwy poleceń są postaci {ProgId}.{NazwaStworzonegoPolecenia}, gdzie {ProgId} jest programistycznym identyfikatorem podzespołu (assembly) wtyczki, który w naszym przypadku przyjmuje wartość "Develogger.Connect" . Więc nazwy naszych poleceń w metodzie Exec to:

- "Develogger.Connect.AddHeader"

- "Develogger.Connect.AddHeader2"

- "Develogger.Connect.ShowInfo"

- "Develogger.Connect.MakeRegion"

 

Ciekawostką jest, że utworzone przez nas na początku polecenia pojawiają się na liście poleceń VS:

Tools->Options zakładka Environment->Keyboard, gdzie możemy je łatwo wyszukać wpisując podane wyżej nazwy i utworzyć do nich skróty klawiszowe (programowo też można - Command.Bindings(…)). Wszystkie polecenia, które „widzi” VS możemy też wywołać programowo (a nie tylko przez kliknięcie przycisku) za pomocą odpowiedniej metody obiektu DTE:

applicationObject.ExecuteCommand("nazwa polecenia", "argumenty");

 

Popatrzmy teraz na kod metody Exec.

 

public void Exec(string commandName, EnvDTE.vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled) {

      handled = true;//jeśli obsłużono to już nie trzeba podawać dalej tego zdarzenia

      if(executeOption == EnvDTE.vsCommandExecOption.vsCommandExecOptionDoDefault) {

            //MessageLn("Command: " + commandName);

            if(commandName == "Develogger.Connect.ShowInfo") {//pokaż info

                  using (SolInfo sinfo = new SolInfo(applicationObject.Solution.FileName, ref info)) {

                        sinfo.ShowDialog();

                  }

                  return;

            }

           

            if (applicationObject.ActiveDocument.ReadOnly) {//czy mozemy zmieniać ten dokument?

                  MessageBox.Show("Document is ReadOnly", "Develogger", MessageBoxButtons.OK, MessageBoxIcon.Asterisk);

                  return;

            }

            if (commandName == "Develogger.Connect.AddHeader" || commandName == "Develogger.Connect.AddHeader2") {//dodaj nagłówek

                  TextDocument textdoc = GetTextDocument(applicationObject.ActiveDocument);

                  applicationObject.UndoContext.Open("AddHeader", false);//tworzymy undo w razie gdybysmy zrezygnowali z naglówka

                  AddHeader(textdoc);

                  applicationObject.UndoContext.Close();//zamykamy operację undo - jednorazowo możemy cofnać cały nagłówek a nie cofać po linijce

                  return;

            } else if(commandName == "Develogger.Connect.MakeRegion") {//utwórz region

                  string regionName = InputBox.InputVal("Enter region name", "Region name", "");

                  if (regionName.Equals("")) return;//rezygnacja z tworzenia regionu

                  applicationObject.UndoContext.Open("MakeRegion", false);//tworzymy undo w razie gdybyśmy zrezygnowali z regionu

                  MakeRegion(regionName);

                  applicationObject.UndoContext.Close();//zamykamy operacje undo

                  return;

            }

      }

      handled = false;//nie obsłużono

}          

 

W powyższym kodzie pojawiają się wywołania następujących metod:

1) sinfo.ShowDialog() – wyświetla okno informacji o rozwiązaniu

 

Rys. 5. Okno informacyjne rozwiązania

 

Informacje z okienka pokazanego powyżej przechowuje serializowana do pliku XML klasa Info. Zawiera ona następujące informacje:

            - autor

            - email

- data utworzenia

- czas pracy nad rozwiązaniem

- liczba składowych projektów

- liczba wszystkich klas

- liczba wszystkich metod

- liczba build’ów

- liczba zgłoszonych wyjątków

- liczba linii kodu we wszystkich projektach (CSharp) naszego rozwiązania

- historia otwarć rozwiązania

            - data otwarcia

            - czas otwarcia

- czas pracy po otwarciu

- komentarze

 

2) AddHeader(textdoc) – dodaje do aktualnie edytowanego kodu nagłówek zdefiniowany przez użytkownika w opcjach

3) MakeRegion(regionName) – tworzy z zaznaczonego kodu region o podanej prze użytkownika nazwie

(opis działania tych metod znajduje się w rozdziale 10.)

oraz…

 

 

7. Tworzenie prostego „undo”

 

 

…kod tworzący proste „undo”, za pomocą którego możemy cofnąć całą operację za jednym przyciśnięciem klawiszy Ctrl+Z (w poniższym przypadku zostanie cofnięte działanie metody JakaśMetodaX(), która np. może modyfikować kod programu).

 

applicationObject.UndoContext.Open("dowolnanazwa", false);//tworzymy undo w razie gdybyśmy zrezygnowali z wyniku działania operacji

JakaśMetodaX();

applicationObject.UndoContext.Close();//zamykamy operację undo

 

 

8. Utworzenie panelu konfiguracyjnego wtyczki

 

 

Wspomniałem w rozdziale 3. artykułu, że metoda OnConnection wczytuje konfigurację wtyczki. Konfigurację tą oczywiście ustawia użytkownik, a robi to przy pomocy CustomOptions Page’a czyli panelu konfiguracyjnego z menu głównego VS - Tools->Options->Develogger->Settings.

 

Rys. 6. Panel konfiguracyjny wtyczki

 

Jak utworzyć taką zakładkę/panel? Po prostu – tworzymy nową kontrolkę użytkownika i budujemy ją na podstawie komponentów dostarczanych przez VS tak jakbyśmy pisali aplikację okienkową.

 

W panelu tym możemy zdecydować, czy chcemy:

- aby nasza wtyczka wstawiała automatycznie nagłówek w każdym nowo dodanym do projektu pliku (z kodem źródłowym)

- włączyć AutoZapis edytowanych plików (i zdecydować, co ile minut ma się on odbywać)

- aby pokazywały się w panelu dodatkowe informacje o działaniu wtyczki.

Dodatkowo, jeśli zdecydujemy się na zapis nagłówka możemy zdefiniować jego postać dodając nowe (bądź usuwając istniejące) linie, składające się z pola oraz jego wartości. Nasza wtyczka interpretuje wartości: "DATE", "LONGDATE" oraz "TIME" jako odpowiednio: aktualną datę, aktualną datę w formacie długim oraz aktualny czas.

Przykład: jeśli zdefiniujemy sobie pole „Data utworzenia:” oraz nadamy mu wartość "DATE" to w nagłówku zobaczymy linijkę: „Data utworzenia: 01-12-2004” (jeśli w momencie dodawanie nagłówka był pierwszy grudnia 2004).

 

Zauważony błąd: ustawiony na panelu konfiguracyjnym taborder kontrolek nie działa – kolejność przechodzenia tabem jest identyczna z kolejnością dodawania kontrolek (można zmienić w kodzie).

 

Jedyne różnice, jakie musimy uwzględnić są takie, że nasza kontrolka:

- wszystkie ustawienia zapisuje do rejestru (o tym za chwilę)

- i musi dziedziczyć z interfejsu IDTToolsOptionsPage i oczywiście implementować jego metody:

OnAfterCreated()– inicjalizacja kontrolki po utworzeniu
OnOK()– użytkownik akceptuje zmiany (klika OK)
OnCancel()– użytkownik rezygnuje ze zmian (klika Cancel)
OnHelp()– użytkownik potrzebuje pomocy (klika Help)
GetProperties() – pobiera ustawienia środowiska

(przyciski OK., Cancel i Help są tworzone prze VS IDE)

 

Implementowany przez nas interfejs znajduje się w przestrzeni nazw EnvDTE, więc ją dodajemy.

W naszym przykładzie zaimplementujemy dwie pierwsze metody:

 

public void OnAfterCreated(DTE DTEObject) {

      Config conf = new Config();//tworzymy nową konfigurację

      conf.ReadFromRegistry();//wczytujemy ją z rejestru

      //ustawiamy kontrolki tak by zgadzały się z konfiguracją

      //header

      header.Checked = conf.WriteHeader;

      fields.Items.Clear();//lista nazw pól nagłówka

      values.Items.Clear();//lista wartości pól nagłówka

      foreach (string field in conf.GetFields()) //przepisujemy je na kontrolkę

            fields.Items.Add(field);

      foreach (string val in conf.GetValues()) //tu podobnie

            values.Items.Add(val);

      //VS output

      output.Checked = conf.ShowOutput;

      //autosave

      minutes.Value = conf.AutoSaveTime;

      autosave.Checked = (conf.AutoSaveTime > 0);

      minutes.Enabled = autosave.Checked;

}

 

public void OnOK(){

      Config conf = new Config();//tworzymy nową konfigurację

      //zapisujemy do niej ustawione przez użytkownika wartości

      //header

      conf.WriteHeader = header.Checked;

      //set new vals

      conf.SetFields(fields.Items);

      conf.SetValues(values.Items);

      //VS output

      conf.ShowOutput = output.Checked;

      //autosave

      int mins = 0;

      if (autosave.Checked) mins = (int)minutes.Value;

      conf.AutoSaveTime = mins;

      conf.WriteToRegistry();//zapisujemy konfigurację do rejestru

      Connect.UpdateConfig(conf);//informujemy wtyczkę o zmianie konfiguracji

}    

Aby nasz panel konfiguracyjny pojawił się w opcjach VS musimy dodać jeden wpis do rejestru. O dodanie tego wpisu poprosimy instalator wtyczki w ostatnim rozdziale artykułu.

Nasza wtyczka działa z ustawieniami wczytanymi z rejestru, a zakładka konfiguracyjna czyta i zapisuje konfigurację do rejestru. Czas więc poznać sposób w jaki się to odbywa.

 

 

9. Odczyt i zapis do rejestru

 

 

Wszystkie ustawienia wtyczki znajdują się w rejestrze systemu Windows w kluczu:

"Software\\Microsoft\\VisualStudio\\7.1\\Addins\\Develogger.Connect\\Options".

Znajduje się tam też podklucz "Header", którym są zapisane pola nagłówka pliku i ich wartości. Operacje zapisu i odczytu rejestru są realizowane przez dwie metody klasy Config.

 

public void ReadFromRegistry() {

      RegistryKey key = Registry.LocalMachine.OpenSubKey(RegKey);//otwieramy klucz HKEY_LM

     

      //nagłówek

      WriteHeader = Convert.ToBoolean((int)key.GetValue("WriteHeader", 1));

      RegistryKey headerkey = key.OpenSubKey("Header");//otwieramy podklucz z postacią nagłówka zdefiniowaną przez użytkownika

      fields = headerkey.GetValueNames();//pobieramy nazwy wartości w kluczu

      int v = 0;

      values = new string[fields.Length];//tworzymy tablicę wartości pól

      foreach (string field in fields)//iterujemy po polach (nazwach wartości)

            values[v++] = (string)headerkey.GetValue(field, "");//wpisujemy odpowiednie wartości pól

      //VS output

      ShowOutput = Convert.ToBoolean((int)key.GetValue("ShowOutput", 1));

      //autosave

      AutoSaveTime = (int)key.GetValue("AutoSaveTime", 5);

}

 

public void WriteToRegistry() {

      RegistryKey key;

      key = Registry.LocalMachine.OpenSubKey(RegKey, true);//otwieramy klucz HKEY_LM do zapisu

 

      //nagłówek

      key.SetValue("WriteHeader", Convert.ToInt32(WriteHeader));

      RegistryKey headerkey = key.OpenSubKey("Header", true);//otwieramy podklucz nagłówka również do zapisu

      //kasacja starych wartości

      string [] names = headerkey.GetValueNames();//pobieramy nazwy pól

      foreach (string name in names)//iterujemy po nazwach

            headerkey.DeleteValue(name, false);//i kasujemy każdu z wpisów

      //zapis nowych

      int v = 0;

      foreach(string field in fields) {//iterujemy po nazwach zdefiniowanych pól

            string val = (string)values[v++];

            headerkey.SetValue(field, val);//zapisujemy pole wraz z odpowiednią wartością

      }

      //VS output

      key.SetValue("ShowOutput", Convert.ToInt32(ShowOutput));

      //autosave

      key.SetValue("AutoSaveTime", Convert.ToInt32(AutoSaveTime));

}

 

Zalecane jest tutaj użycie klasy Convert, która pozwala przeprowadzić konwersję każdego typu podstawowego .NET na inny typ podstawowy. Używamy tej klasy, ponieważ rejestr zawiera tylko kilka podstawowych typów danych (jak np. string lub dword) i musimy konwertować je na typy używane w naszej wtyczce.

 

 

10. Modyfikacja zawartości okna edycji kodu

 

 

W rozdziale 6. pojawiły się w kodzie wywołania metod:

AddHeader(textdoc)i MakeRegion(regionName).

Wiemy już, że pierwsza z nich dodaje nagłówek do pliku, a druga tworzy z zaznaczonego tekstu region o podanej nazwie. Modyfikowanie zawartości okna edycji kodu nie należy do najprostszych rzeczy, ale jest kluczową czynnością wykonywaną pracowicie przez VS IDE. Spójrzmy zatem, w jaki sposób możemy mieszać w kodzie.

 

private void AddHeader(TextDocument textdoc) {

      MessageLn("Adding header...\n");

      if (textdoc == null) return;

      EditPoint head = textdoc.StartPoint.CreateEditPoint();//utwórz punkt edycji na pocz dokumentu

      string [] fields = conf.GetFields();//pobierz pola do zapisu...

      string [] values = conf.GetValues();//...wraz z ich wartosciami

      int v = 0;//licznik aktualnej wartosci związanej z polem

      foreach (string field in fields) {//iteracja po polach nagłówka

            string val = values[v++];//odpowiednia wartość pola

            switch (val) {//interpretowalne wyrażenia...można tu dodać wiele rzeczy

                  case "LONGDATE": val = DateTime.Now.ToString("dddd, dd-MMMM-yyyy");

                        break;

                  case "DATE": val = DateTime.Now.ToString("dd-MM-yyyy");

                        break;

                  case "TIME": val = DateTime.Now.ToString("T");

                        break;

            }

            head.Insert("// " + field + " " + val + "\n");//dodaj zakomentowane pole + wartość

      }

      head.Insert("\n");//linia odstępu na końcu nagłówka

}

 

Najważniejszą rzeczą z powyższego kodu jest utworzenie punktu edycji tekstu w dokumencie tekstowym - EditPoint head = textdoc.StartPoint.CreateEditPoint(), a następnie wstawianie kolejnych linii do kodu - head.Insert(string). Warto także zapoznać się z innymi metodami klasy EditPoint, ponieważ oferuje ona naprawdę dużą funkcjonalność. Implementacja drugiej metody wygląda następująco:

 

private void MakeRegion(string name) {

      int atStartFlags = (int)vsInsertFlags.vsInsertFlagsInsertAtStart;//flaga wstawiania na początek linii

      int atEndFlags = (int)vsInsertFlags.vsInsertFlagsInsertAtEnd;//flaga wstawiania na koniec linii

      TextDocument textdoc = GetTextDocument(applicationObject.ActiveDocument);

      //jeśli nie jesteśmy w nowej lini...

      if(!textdoc.Selection.TopPoint.AtStartOfLine) {

            textdoc.Selection.Insert("\n#region " + name + "\n", atStartFlags);//...to musimy przejść od nowej

            int selelectionStartLine = textdoc.Selection.TopLine + 1;//numer początkowej linii

            int selelectionEndLine = textdoc.Selection.BottomPoint.Line;//numer końcowej linii

            int selelectionEndColumn = textdoc.Selection.BottomPoint.LineCharOffset;//numer ostatniej kolumny

            textdoc.Selection.MoveToLineAndOffset(selelectionStartLine, 1, false);//przesunięcie się na początek bez rozszerzania zaznaczonego tekstu

            textdoc.Selection.MoveToLineAndOffset(selelectionEndLine, selelectionEndColumn, true);//przesunięcie się na koniec i rozszerzenie zaznaczonego tekstu

      }     else textdoc.Selection.Insert("#region " + name + "\n", atStartFlags);

      //wstawienie '\n' przed #endregion jesli brakuje

      if(!textdoc.Selection.Text.EndsWith("\n"))

            textdoc.Selection.Insert("\n", atEndFlags);

      textdoc.Selection.Insert("#endregion\n", atEndFlags);

      textdoc.Selection.SmartFormat();//przeformatuj w razie awarii;)

}

 

W metodzie tej śmiało posługujemy się właściwością Selection klasy TextDocument, która zwraca nam aktualnie zaznaczony w kodzie tekst – pamiętamy, że metoda QueryStatus dba o to, by operacja tworzenia regionu była dostępna tylko przy zaznaczonym tekście. Tutaj również posługujemy się metodą Insert, aby dodać nowy tekst do kodu oraz korzystamy z kilku właściwości zaznaczonego tekstu do „orientacji w terenie”. Na koniec wywołujemy metodę SmartFormat() w celu sformatowania zmodyfikowanego kodu – tak na wszelki wypadek.

 

 

11. Kilka interesujących funkcji dla ciekawskich

 

 

Aby zrealizować kilka ciekawych zadań w naszej wtyczce stworzymy sobie kilka jeszcze ciekawszych funkcji pomocniczych. Z analizy ich kodu można dowiedzieć się paru interesujących rzeczy i zobaczyć, w jaki sposób można pewne operacje wykonać.

Pierwsze dwie funkcje jakie chciałbym przedstawić, korzystają z modelu kodu skojarzonego z każdym elementem projektu, liczą one składniki naszego kodu takie jak: klasy i metody (można łatwo rozbudować o inne elementy) oraz wyświetlają je do panelu VS w postaci drzewa. Popatrzmy zatem na kod:

 

private void EnumCodeElems(bool showOutput, out int classesNum, out int methodsNum) {        

      bool show = conf.ShowOutput;//zapamiętanie wartości z konfiguracji wtyczki

      conf.ShowOutput = showOutput;//ustawienie outputu

      MessageLn("\nEnumerating code elements...\n");

      classesNum = methodsNum = 0;

     

      foreach(Project project in GetCSharpProjects()) {//iteracja po projektach

            foreach (ProjectItem pItem in project.ProjectItems) {//iteracja po elementach projektu

                  FileCodeModel fcodemodel = pItem.FileCodeModel;//pobieramy referencję do modelu kodu w składniku projektu

                  if (fcodemodel != null) {//nie każdy składnik projektu zawiera kod!

                        CodeElements celems = fcodemodel.CodeElements;

                        //rekurencyjne wyświetlanie składowych kodu (przestrzeni nazw, klas, metod)

                        ShowCodeElems(celems, 1, ref classesNum, ref methodsNum); 

                  }

            }

      }

      conf.ShowOutput = show;

}

Jedyną linijką wymagającą komentarza jest:

FileCodeModel fcodemodel = pItem.FileCodeModel;

W której to pobieramy referencję do obiektu reprezentującego model kodu związany z danym elementem projektu. Zwróćmy uwagę, że nie każdy element projektu musi mieć model kodu. Druga funkcja wywołuje się rekurencyjnie w celu przejścia po wszystkich elementach kodu i wykonania jakiejś czynności (liczenie klas i metod).

 

//rekurencyjne wyświetlanie elementów skladowych kodu

//lev - poziom rekurencji, używany do ładnego formatowania outputu

private void ShowCodeElems(CodeElements celems, int lev, ref int classesNum, ref int methodsNum) {

      if (celems == null) return;

      string mess = "Code Elems";

      string tabs = "";

      tabs = tabs.PadLeft(lev * 2, '\t');//odstęp tabulatorowy

      MessageLn(tabs + mess);

      foreach (CodeElement elem in celems) {//iteracja po składowych kodu

            CodeElements children;//dzieci danej składowej

            try {

                  switch (elem.Kind) {

                        case vsCMElement.vsCMElementNamespace://namespace

                             ShowCodeElem(tabs, elem);

                             children = ((CodeNamespace)elem).Members;//rzutowanie

                             break;

                        case vsCMElement.vsCMElementClass://klasa

                             ShowCodeElem(tabs, elem);

                             children = ((CodeClass)elem).Members;//rzutowanie

                             classesNum++;

                             break;

                        case vsCMElement.vsCMElementFunction://metoda

                             ShowCodeElem(tabs, elem);

                             children = null;//nie wchodzimy dalej, ale nic nie stoi na przeszkodzie...

                             methodsNum++;

                             break;

                        default: children = null;

                             break;

                  }

                  //rekurencja z następnym poziomem wyświtlania

                  ShowCodeElems(children, ++lev, ref classesNum, ref methodsNum);

            } catch (Exception e) {

                  MessageLn(e.ToString());//co poszło nie tak?

            }

      }

      MessageLn(tabs + mess + " END");

}

 

Rzeczą, na którą należy zwrócić uwagę w powyższym kodzie jest użycie właściwości Members klasy CodeElement po to, by dostać się do dzieci danego elementu. Użycie tej właściwości musi być poprzedzone rzutowaniem na konkretny typ elementu kodu – w przeciwnym przypadku wywołamy wyjątek. Typ (rodzaj) danego elementu kodu możemy sprawdzić używając właściwości Kind klasy CodeElement. Drzewo elementów kodu jest naprawdę rozbudowane i zachęcam do zajrzenia w dokumentację.

 

Ostatnią funkcją, jaką chciałbym przedstawić w cyklu „ciekawe funkcje” jest funkcja licząca liczbę lini kodu w utworzonych przez nas elementach projektów (zawierających kod C#), we wszystkich projektach rozwiązania.

 

private int GetLinesNum() {

      MessageLn("\nCounting lines...\n");

      int lines = 0;

      foreach (Project project in GetCSharpProjects()) {//iteracja po projektach

            MessageLn("Project: " + project.Name);

            foreach (ProjectItem projectitem in project.ProjectItems) {//iteracja po elementach projektu

                  string name = projectitem.Name.ToLower();

                  try {

                        if (name.EndsWith(".cs")) {//otwieramy tylko kody źródlowe

                             Window win = projectitem.Open(Constants.vsViewKindCode);

                             win.Visible = true;

                        } else continue;

                  } catch {

                        continue;//nie wszystko musi się udać otworzyć

                  }

                  MessageLn("ProjectItem: " + projectitem.Name);

                  Document document = projectitem.Document;

                  if (document == null) continue;

                  if (!document.Language.Equals("CSharp")) continue;//jeszcze się możemy upewnić

                  TextDocument textdoc = GetTextDocument(document);

                  if (textdoc == null) continue;

                  MessageLn(document.FullName);

                  TextPoint end = textdoc.EndPoint;//ustawiamy się na końcu tekstu w dokumencie

                  lines += end.Line;//doliczamy liczbę jego lini

                  MessageLn("CSharp file -> lines: " + end.Line);

            }

      }

      return lines;//wynik

}

 

Nie jest to wcale skomplikowana funkcja, ale warto ją krótka skomentować.

Jeśli chcemy policzyć liczbę lini kodu w danym pliku musimy go otworzyć, a następnie pokazać jego okno.

Window win = projectitem.Open(Constants.vsViewKindCode);

win.Visible = true;

Stała Constants.vsViewKindCode określa, że chcemy oglądać otwarty plik w edytorze kodu. Aby dowiedzieć się ile lini ma dany plik ustawiamy się na jego końcu (TextPoint end = textdoc.EndPoint) i pytamy, która to linia (end.Line). Proste!

 

 

12. Szybka konfiguracja instalatora

 

 

Kreator, którego użyliśmy do utworzenia wtyczki wygenerował nam także projekt instalatora. Podglądając rejestr modyfikowany przez instalator (z menu kontekstowego instalatora wybieramy View->Registry) widzimy, że utworzy on klucz

"HKEY_LOCAL_MACHINE\Software\Microsoft\VisualStudio\7.1\Addins\Develogger.Connect"

a w nim różne pola z wartościami. Możemy jeszcze w tym momencie zmienić nazwę naszej wtyczki wyświetlaną w menadżerze wtyczek (Tools->Add-in Manager) i jej opis.

 

Zauważony błąd: instalator nie potrafi utworzyć w opisie wtyczki polskich znaków (można zmienić w rejestrze).

 

Ale to, co nas najbardziej interesuje to utworzenie dodatkowych podkluczy tak, aby panel konfiguracyjny wtyczki pojawił się w opcjach VS i mógł odczytać konfigurację. Naszym kluczem bazowym jest klucz podany wyżej i w nim tworzymy drzewo podkluczy:

- Options

     ->   - Develogger

         ->   - Settings

         - Header

 

Rys. 7. Drzewo kluczy instalatora wtyczki

 

W kluczu Settings tworzymy nową wartość ciągu (New->String Value) Control o wartości "Develogger.OptionsPage" . Informuje ona VS o tym, że chcemy, aby w zakładce opcji Develogger->Settings pojawiła się nasza kontrolka "Develogger.OptionsPage". We właściwościach projektu instalatora należy jeszcze ustawić usunięcie poprzedniej wersji wtyczki przed instalacją nowej RemovePreviousVersions na True. Na koniec ustawmy jeszcze właściwość ProductName  na "Develogger Add-in". Wreszcie jesteśmy gotowi do instalacji, więc budujemy oba projekty, instalujemy wtyczkę (menu kontekstowe instalatora->Install) i uruchamiając drugą instancję VS możemy cieszyć oczy własnoręcznie napisanym rozszerzeniem do VS. Miłego używania.

 

Podsumowanie:

W artykule tym poruszyłem mechanizm rozszerzenia funkcjonalności Visual Studio przez działanie wtyczki. Stworzona wtyczka:

- potrafi automatycznie wpisywać nagłówki do nowo dodanych elementów projektu

- potrafi także robić to na zawołanie

- ułatwia tworzenie regionów z zaznaczonego kodu

- posiada opcję AutoZapisu dokumentów

- gromadzi i zapisuje różne ciekawe informacje o naszym rozwiązaniu

- daje się w łatwy sposób konfigurować

- jest łatwo rozszerzalna i modyfikowalna

Możliwości wtyczek są naprawdę ogromne, a lista zadań, które możemy przez ich mechanizm zrealizować bardzo długa. Sami widzieliśmy jak wiele i jak różne obszary VS są z poziomu wtyczek dostępne. Niektóre zadania realizuje się bardzo łatwo (np. tworzenie przycisków), a inne trochę trudniej (modyfikacja kodu). Funkcjonalność tworzonych wtyczek może być bardzo różna – zależy tylko od naszych wymagań i potrzeb. Wszystkie jednak mają na celu ułatwienie nam życia - nam, czyli programistom.

 

Książki:

Ms Press - Inside Microsoft Visual Studio .NET 2003

 

Sznurki:

Manipulating the Development Environment - MSDN

Visual Studio Extensibility Center

Automation and Extensibility Overview

Discussions in vstudio.extensibility

 

To już koniec naszej dzisiejszej przygody z wtyczkami do VS.

Zachęcam serdecznie do komentowania artykułu i oczywiście do oceniania.

Ponieważ jest to mój debiut na CodeGuru, więc proszę o wyrozumiałość ;).

 

Do następnej wtyczki!

Michał Zawadzki

 

Załączniki:

Podobne artykuły

Komentarze 5

xqsnake
xqsnake
8 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0

dzięki dodatkowym odstępom między linijkami artykuł przybrał na (i tak nie małej już) długości :\

odsyłam do HTMLa

a61stas
a61stas
1 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0

Jak na Twój "pierwszy występ" to bardzo dobrze - bardzo konkretnie i na temat (to rzadkość).

Na przyszłość, zanim przejdziesz do opisu rozwiązania jasno określ, co zamierzasz - równie konkretnie jak rozwiązanie.

 

xqsnake
xqsnake
8 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Dziekuje ze znalazles czas w wigilijny poranek aby skomentowac artykul ;) Ciesze sie ze Ci sie podobal - mam nadzieje ze wiecej niz na trojke. Reszta glosujacych bez potrzeby uzasadnienia oceny?
janono
janono
21-01-2010
oceń pozytywnie 0
Dzień dobry mam pytanie dosyć długo już nad tym siedzę nie mogę dostać modelu kodu dla metody, wygląda na to jak by samemu trzeba było bo to parsować z tekstu.
Czy ktoś mól by pomóc?
User 131096
User 131096
189 pkt.
Junior
21-01-2010
oceń pozytywnie 0
Ale chyba literek od tego nie przybyło i czyta się tak samo szybko, prawda panie Smith?
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.