VSIX Tutorial-Visual Studio events and Commands.
Visual Studio events
Let's start adding a function SetupEvents that will setup event handlers inside the MyExtensionPackage class.
Note that we are saving the _dte instance and all the event-related variables.
This because these will be collected by the GC if we don't save an instance of them
private DTE _dte;
private Events _dteEvents;
private DocumentEvents _documentEvents;
private void SetupEvents()
{
_dte = (DTE)GetService(typeof(SDTE));
_dteEvents = _dte.Events;
_documentEvents = _dteEvents.DocumentEvents;
_documentEvents.DocumentSaved += OnDocumentSaved;
}
Note that several other events are available. BUT ALL MUST BE SAVED INTO VARIABLES!!!
Then we add the OnDocumentSaved implementation.
The parameter passed is a "Document", that means an editable file with content.
We will handle only the events connected to the saving of ".tini" files thus in case other things are saved we will return without errors or messages.
Then we iterate on the project item that are child of our .tini file, searching for the ".cs" file in which we are interested.
Then we must build a document for it.
We open the project item and with this action the source.Document take a value.
Finally we do all the necessary cleanup.
private void OnDocumentSaved(Document document)
{
var path = document.FullName;
var ext = Path.GetExtension(path);
if (ext == null || ext.ToLowerInvariant() != ".tini") return;
ProjectItem source = null;
foreach (ProjectItem csFile in document.ProjectItem.ProjectItems)
{
source = csFile;
break;
}
if (source == null) return;
try
{
source.Open();
}
finally
{
if (source.Document != null) source.Document.Close();
}
}
Getting the text of a document
We add then the functions to save and load document texts.
As usual the Document is an opaque object, and to get the "real" document we use the "Object" variable.
The TextDocument contains the edit points.
You should find the starting and ending point to edit (or read text). In our situation we will read completely or replace totally the content.
And in case of the set we will save the Document.
private static string GetDocumentText(Document document)
{
var textDocument = (TextDocument)document.Object("TextDocument");
EditPoint editPoint = textDocument.StartPoint.CreateEditPoint();
var content = editPoint.GetText(textDocument.EndPoint);
return content;
}
private static void SetDocumentText(Document document, string content)
{
var textDocument = (TextDocument)document.Object("TextDocument");
EditPoint editPoint = textDocument.StartPoint.CreateEditPoint();
EditPoint endPoint = textDocument.EndPoint.CreateEditPoint();
editPoint.ReplaceText(endPoint, content, 0);
document.Save();
}
The parsers
IniParser
Read the content of the ".tini" files and loads the data in a Dictionary<string,Dictionary<string,string>>.
Each section of the ini file will be contained inside the first dictionary.
For each section a dictionary exists with the keys and default values of objects.
IniClassParser
Read the content of the ".cs" file associated with the ".tini" file and does all the replacements on it to obtain the new class.
Several "insertion points" are present:
- INICLASSES: The declaration of the classes corresponding with the sections
- INIVARIABLES: The declaration of variables containing the data for each section
- INIINITIALIZE: The first initialization of section variables and default initialization of their values
- INILOAD: To load the data from the Dictionary create by the ini file (in runtime) into the section classes
- INISAVE: To move the data from the section classes to the dictionary before really saving the ini file
The new OnDocumentSaved
Simply we read the ".tini" and we create a new IniParser.
Then the ".cs" is read and a new IniClassParser is created that will take the IniParser and apply its definitions.
Finally the new ".cs" file is written.
void OnDocumentSaved(Document document)
{
var path = document.FullName;
var ext = Path.GetExtension(path);
if (ext == null || ext.ToLowerInvariant() != ".tini") return;
var iniContent = GetDocumentText(document);
var iniConverter = new IniParser(iniContent);
iniConverter.Load();
ProjectItem source = null;
foreach (ProjectItem csFile in document.ProjectItem.ProjectItems)
{
source = csFile;
break;
}
if (source == null) return;
try
{
source.Open();
var csContent = GetDocumentText(source.Document);
var iniClassConverter = new IniClassParser(csContent, iniConverter);
var newCsContent = iniClassConverter.Parse();
SetDocumentText(source.Document, newCsContent);
}
finally
{
if (source.Document != null) source.Document.Close();
}
}
The command
Connecting the command
We want now connect the new command and the new Menu item callback will regenerate the ".cs" file
private void MenuItemCallback(object sender, EventArgs e)
{
if (_dte.SelectedItems.Count <= 0) return;
foreach (SelectedItem selectedItem in _dte.SelectedItems)
{
if (selectedItem.ProjectItem == null) return;
var projectItem = selectedItem.ProjectItem;
var fullPathProperty = projectItem.Properties.Item("FullPath");
if (fullPathProperty == null) return;
var fullPath = fullPathProperty.Value.ToString();
var ext = Path.GetExtension(fullPath);
if (string.IsNullOrEmpty(ext) || ext.ToLowerInvariant() != ".tini") return;
projectItem.Open();
OnDocumentSaved(projectItem.Document);
}
}
Visibility
Of course we want that the "Regenerate ini" command appears only when we are right clicking on a ".tini" item.
- Change the new MenuCommand in new OleMenuCommand. This to show all the events connected to the menu item.
- Add an handler for the event Before QueryStatus
menuItem.BeforeQueryStatus += OnBeforeQueryStatus;
...
}
void OnBeforeQueryStatus(object sender, EventArgs e)
{
throw new NotImplementedException();
}
- We check that a selected item exist, with a path.
- If the item is a tini then the menu will be visible
void OnBeforeQueryStatus(object sender, EventArgs e)
{
var item = (OleMenuCommand)sender;
item.Visible = false;
foreach (SelectedItem selectedItem in _dte.SelectedItems)
{
if (selectedItem.ProjectItem == null) return;
var projectItem = selectedItem.ProjectItem;
var fullPathProperty = projectItem.Properties.Item("FullPath");
if (fullPathProperty == null) return;
var fullPath = fullPathProperty.Value.ToString();
if (Path.GetExtension(fullPath).ToLowerInvariant() == ".tini")
{
item.Visible = true;
return;
}
}
}
- The vsct must be modified too we will be adding to the button the dynamic visibility
<CommandFlag>DynamicVisibility</CommandFlag>
Conclusion
Now,
- Saving the ".tini" file will regenrate the associated class as like the resources.
- It would be possible through the menu to regenerate the ini class without changing it.
- The menu will show only on ".tini" file
Up to you to handle the rename on the ".tini" file :)

You can download the code