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ć.



Visual Studio: File Header Add-Inn « Damian Antonowicz…
Dziękujemy za publikację – Trackback z dotnetomaniak.pl…
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