[PL] Visual Studio: File Header Add-Inn
Ostatnio zainstalowałem sobie narzędzie StyleCop, które służy do analizy kodu C# pod kątem przestrzegania ustalonego stylu kodowania. Wśród olbrzymiej ilości błędów jakie zostały zwrócone przez narzędzie, był błąd SA1633: FileMustHaveHeader. Błąd informuje nas o braku nagłówka w danym pliku. Nagłówek musi znajdować się na początku pliku i mieć następującą postać:
//----------------------------------------------------------------------- // <copyright file="NameOfFile.cs" company="CompanyName"> // Company copyright tag. // </copyright> //-----------------------------------------------------------------------
W celu dodania takiego nagłówka do każdego pliku CS w projekcie, najłatwiej byłoby napisać własne makro w Visual Studio. Niestety makra możemy pisać jedynie w VB, a mnie interesowało użycie C#. Postanowiłem, więc napisać prostego Add-Inn’a, który będzie dodawać nagłówek do pliku.
Podstawy tworzenia Add-Inn’a są opisane w dokumentacji. Po przebrnięciu przez wizzarda dostajemy szkielet naszego Add-Inn’a. Domyślnie została stworzona klasa Connect, która jest sercem całego projektu. Implementuje ona kilka metod, z których interesują nas OnConnection, QueryStatus oraz Exec. Metoda OnConnection wywoływana jest w momencie załadowania Add-Inn’a do Visual Studio. Jej implementacja wygląda następująco:
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom) { this.applicationObject = (DTE2)application; this.addInInstance = (AddIn)addInInst; if (connectMode == ext_ConnectMode.ext_cm_UISetup) { object[] contextGUIDS = new object[] { }; Commands2 commands = (Commands2)this.applicationObject.Commands; //Place the command on the tools menu. //Find the MenuBar command bar, which is the top-level command bar holding all the main menu items: CommandBar menuBarCommandBar = ((CommandBars)this.applicationObject.CommandBars)["MenuBar"]; //Find the Tools command bar on the MenuBar command bar: CommandBarControl toolsControl = menuBarCommandBar.Controls["Tools"]; CommandBarPopup toolsPopup = (CommandBarPopup)toolsControl; //This try/catch block can be duplicated if you wish to add multiple commands to be handled by your Add-in, //just make sure you also update the QueryStatus/Exec method to include the new command names. try { //Add a command to the Commands collection: Command command = commands.AddNamedCommand2(addInInstance, "AddHeader", "Add header to files", "Add header to all CS files in solution", false, 1, ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled, (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton); //Add a control for the command to the tools menu: if ((command != null) && (toolsPopup != null)) command.AddControl(toolsPopup.CommandBar, 1); } catch (System.ArgumentException ex) { //If we are here, then the exception is probably because a command with that name // already exists. If so there is no need to recreate the command and we can // safely ignore the exception. Debug.WriteLine(ex.ToString()); } } }
Na początku pobieramy referencję do interfejsów DTE2 oraz AddInn. Interfejs DTE2 zapewni nam m.in. dostęp do GUI Visual Studio oraz solucji. W dalszej części metody pobieramy referencję do menu “Tools”, a następnie dodajemy tam nową pozycję przy pomocy metody AddNamedCommand2. Nasza nowa pozycja w menu będzie miała nazwę “Add header to files” oraz będzie wyświetlała ikonkę praw autorskich. Sposób na dodanie własnej ikonki został opisany w dokumentacji. Nasza pozycja w menu “Tools” będzie wyglądać następująco:
Następnie musimy zwrócić uwagę na implementację metody QueryStatus:
public void QueryStatus(string commandName, vsCommandStatusTextWanted neededText, ref vsCommandStatus status, ref object commandText) { if (neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone) { if (commandName == "FileHeaderAddin.Connect.AddHeader") { status = vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled; return; } } }
Metoda ta jest wywoływana, gdy użytkownik zacznie używać menu Visual Studio. Należy do zmiennej “status” przypisać odpowiednią wartość w zależności czy w danym momencie wspieramy wywołanie naszego Add-Inn’a. Zmienna “commandName” zawiera nazwę polecenia jakie zostało wybrane z menu. W metodzie OnConnection dodaliśmy tylko jedno polecenie o nazwie “AddHeader”.
Najważniejsza metodą jest metoda Exec:
public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled) { handled = false; if (executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault) { if (commandName == "FileHeaderAddin.Connect.AddHeader") { handled = true; if (this.formMain == null || this.formMain.IsDisposed) { this.formMain = new FormMain(this.applicationObject); } formMain.Show(); return; } } }
Zostaje ona wywołana w momencie kliknięcia na pozycję w menu. Również i w tej metodzie musimy sprawdzić zawartość zmiennej “commandName”. W mojej implementacji metody tworzony jest nowy formularz Windows Forms, do którego przekazywana jest zmienna “applicationObject”. Wygląd formularza jest następujący:
Funkcjonalność formularza jest dość intuicyjna, więc zajmiemy się teraz kodem. W konstruktorze formularza tworzony jest obiekt mojej klasy HeaderManager. Klasa ta zawiera jedną publiczna metodę oraz trzy zdarzenia:
#region private members private DTE2 applicationObject; private int filesProcessed = 0; private int filesCount = 0; #endregion #region properties public event EventHandler<AddHeaderToFilesStartedEventArgs> AddHeaderToFilesStarted; public event EventHandler<OnAddHeaderToFilesProgressChanged> AddHeaderToFilesProgressChanged; public event EventHandler<AsyncCompletedEventArgs> AddHeaderToFilesCompleted; #endregion #region constructors public HeaderManager(DTE2 applicationObject) { this.applicationObject = applicationObject; } #endregion #region public methods public void AddHeaderToFilesAsync() { ThreadPool.QueueUserWorkItem((state) => this.AddHeaders()); } #endregion
Starałem się tutaj trzymać tutaj zasad Event-based Asynchronous Pattern. Właściwe dodawanie nagłówków rozpoczyna się w metodzie AddHeaders:
private void AddHeaders() { this.filesCount = this.GetFilesCount(); this.filesProcessed = 0; this.OnAddHeaderToFilesStarted(new AddHeaderToFilesStartedEventArgs(this.filesCount)); this.AddHeaderToFiles(); this.OnAddHeaderToFilesCompleted(new AsyncCompletedEventArgs(null, false, null)); }
Wywoływana jest tutaj na początku metoda GetFilesCount, która oblicza ilość plików CS jakie znajdują się w bieżącej solucji:
private int GetFilesCount() { int filesCount = 0; Solution solution = this.applicationObject.Solution; foreach (Project project in solution.Projects) filesCount += this.GetFilesCountForProject(project.ProjectItems); return filesCount; } private int GetFilesCountForProject(ProjectItems projectItems) { int filesCount = 0; foreach (ProjectItem projectItem in projectItems) { if (projectItem.SubProject != null && projectItem.SubProject.ProjectItems != null && projectItem.SubProject.ProjectItems.Count > 0) { filesCount += GetFilesCountForProject(projectItem.SubProject.ProjectItems); } else if (projectItem.ProjectItems != null && projectItem.ProjectItems.Count > 0) { filesCount += GetFilesCountForProject(projectItem.ProjectItems); } if (projectItem.Name.EndsWith(".cs")) filesCount++; } return filesCount; }
Metoda GetFilesCount iteruje po wszystkich projektach w danej solucji i wywołuje kolejna metodę GetFilesCountForProject. Ta ostatnia metoda zajmuje się iteracją po wszystkich obiektach danego projektu. Uwzględnione zostało tutaj przechodzenie w głąb drzewa danego projektu. Jeśli nazwa danego obiektu w projekcie kończy się na “.cs” (czyli jest to plik z kodem C#) jest on zliczany.
Po metodzie GetFilesCount wywoływana jest kolejna metoda AddHeaderToFiles:
private void AddHeaderToFiles() { Solution solution = this.applicationObject.Solution; foreach (Project project in solution.Projects) this.AddHeaderToFilesInProject(project.ProjectItems); } private void AddHeaderToFilesInProject(ProjectItems projectItems) { foreach (ProjectItem projectItem in projectItems) { if (projectItem.SubProject != null && projectItem.SubProject.ProjectItems != null && projectItem.SubProject.ProjectItems.Count > 0) { this.AddHeaderToFile(projectItem); this.AddHeaderToFilesInProject(projectItem.SubProject.ProjectItems); } else if (projectItem.ProjectItems != null && projectItem.ProjectItems.Count > 0) { this.AddHeaderToFile(projectItem); this.AddHeaderToFilesInProject(projectItem.ProjectItems); } else { this.AddHeaderToFile(projectItem); } } }
Metoda AddHeaderToFiles również iteruje po wszystkich projektach w danej solucji i wywołuje metodę AddHeaderToFilesInProject. Ta ostatnia metoda przechodzi po drzewku danego projektu i wywołuję metodę AddHeaderToFile, która zajmuje się dodawaniem nagłówka do danego pliku:
private bool IsFileAutoGenerated(string filePath) { if (filePath.Contains(".Designer.cs")) return true; using (StreamReader streamReader = new StreamReader(new FileStream(filePath, FileMode.Open))) { foreach (string line in streamReader.ReadAsLines().Take(10)) if (line.Contains("<auto-generated>")) return true; } return false; } private void AddHeaderToFile(ProjectItem projectItem) { if (projectItem.Name.EndsWith(".cs")) { string filePath = projectItem.Properties.Item("FullPath").Value.ToString(); if (File.Exists(filePath) && !this.IsFileAutoGenerated(filePath)) { string section = "------------------------------------------------------------------------------"; string commentString = @"//"; FileStream fileStream = File.Open(filePath, FileMode.Open); StreamReader streamReader = new StreamReader(fileStream); List<string> lines = new List<string>(streamReader.ReadAsLines()); //check if header already exists if (lines[0] == commentString + section) //header exists, skip it lines = new List<string>(lines.Skip(8)); streamReader.Close(); fileStream = File.Open(filePath, FileMode.Create); StreamWriter streamWriter = new StreamWriter(fileStream); streamWriter.WriteLine(commentString + section); streamWriter.WriteLine(String.Format("{0} <copyright file=\"{1}\" company=\"{2}\">", commentString, projectItem.Name, HeaderSettings.Default.CompanyName)); streamWriter.WriteLine(String.Format("{0} {1}", commentString, HeaderSettings.Default.Copyright)); streamWriter.WriteLine(commentString + " </copyright>"); streamWriter.WriteLine(String.Format("{0} <author>{1}</author>", commentString, HeaderSettings.Default.Author)); streamWriter.WriteLine(String.Format("{0} <email>{1}</email>", commentString, HeaderSettings.Default.Email)); streamWriter.WriteLine(commentString + section); streamWriter.WriteLine(); streamWriter.WriteLines(lines); streamWriter.Flush(); streamWriter.Close(); this.filesProcessed++; this.OnAddHeaderToFilesProgressChanged(new OnAddHeaderToFilesProgressChanged(this.filesProcessed)); } } }
Na początku sprawdzane jest czy nazwa pliku kończy się na “.cs”, a następnie czy ten plik w ogóle istnieje. Właściwość Properties klasy ProjectItem zapewnia dostęp do informacji jakie możemy wyczytać w okienku Properties w Visual Studio. Nas interesuje pozycja “Full Path”, która zawiera ścieżkę do pliku. Dostęp do tej wartości możliwy jest poprzez użycie metody Item. W metodzie IsFileAutoGenerated sprawdzane jest czy dany plik został wygenerowany automatycznie przez jakieś narzędzie. Dodawanie nagłówka w takich plikach nie ma sensu, ponieważ wprowadzone zmiany i tak zostaną nadpisane przy następnym użyciu narzędzia, które ten plik wygenerowało. Metoda IsFileAutoGenerated sprawdza najpierw czy w nazwie pliku znajduję się łańcuch “.Designer.cs”. Jeśli go nie ma pobierane jest pierwsze 10 linii i następuje sprawdzenie pod kątem istnienia łańcucha “<auto-generated>”. Przykładowy nagłówek stworzony przez generator plików “settings” w Visual Studio wygląda następująco:
//------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. // Runtime Version:4.0.30319.1 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------
W dalszej części metody AddHeaderToFile następuje sprawdzenie czy w danym pliku istnieje już nagłówek. Jeśli tak to pomijane jest pierwsze 8 linii. Metoda ta nie jest doskonała ponieważ sprawdzana jest jedynie pierwsza linia danego pliku. W dalszej części metody następuje już dodanie nagłówka do pliku. Wynikiem działania metody AddHeaderToFile będzie dodanie nagłówka o następującej postaci:
//------------------------------------------------------------------------------ // <copyright file="ApplicationPaths.cs" company="Damian Antonowicz"> // copyright © 2010 Damian Antonowicz // </copyright> // <author>Damian Antonowicz</author> // <email>poczta@damianantonowicz.pl</email> //------------------------------------------------------------------------------
Przedstawiony Add-Inn można łatwo rozbudować o dodawanie komentarzy np. do plików HTML lub XAML.
Tutaj znajduje się projekt mojego Add-Inn’a. W celu jego instalacji należy oczywiście najpierw projekt skompilować 😉 Następnie z katalogu projektu kopiujemy plik “FileHeaderAddin.AddIn” do katalogu, w którym Visual Studio przechowuje pliki “AddInn”. W moim przypadku (Windows 7) będzie to katalog “C:\Users\Damian\Documents\Visual Studio 2010\Addins”. Następnie “FileHeaderAddin.AddIn” należy otworzyć w edytorze tekstu i znaleźć węzeł <Assembly></Assembly>. Należy w nim podać pełną ścieżkę do DLL z Add-Inn’em. Na końcu odpalamy Visual Studio i przechodzimy do menu Tools -> Add-In Manager. Zaznaczamy pole przy nazwie Add-Inn’a i klikamy na “OK”. Add-Inn w tym momencie powinien się załadować.
Naprawdę… niezły artykuł !!! 🙂
Można to też zrealizować edytując: link
Maciek co masz na myśli? 😉
[Edit: 20:23]
Dopiero teraz zauważyłem, że ucięło wklejonego przez Ciebie linka. Poprawiłem Twój komentarz. Dzięki za linka 🙂
Wielkie dzięki!
Super artykuł oraz pomocne narzędzie!
Pozdrawiam