Introduction
Most of the time, I would make my XAML code look neater by fixing the indentation
of elements and attributes, and removing unused namespaces. Sometimes, not removing
unused namespaces causes the validation error template not to appear in the UI
so I always remove these namespaces when I notice that they are not in use. The
XAML Organizer add-in tries to address these common problems. Here is a screenshot
of the add-in.

Figure 1. XAML Organizer Screenshot
This add-in can do the following: remove unused namespaces, sort attributes, and
fix format. It is important to note that each command only works when the XAML
is valid because I used LINQ to XML to parse the XAML. To better understand what
these commands do, let’s try to use the add-in on the following XAML code.
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication"
Title="MainWindow" Height="300" Width="300">
<Grid>
<Button Height="23" Margin="130,0,72,8" Name="button1" VerticalAlignment="Bottom">Button</Button>
<Label Height="28" Margin="30,26,128,0" Name="label1" VerticalAlignment="Top">Label</Label>
<ScrollViewer Margin="48,82,36,91" Name="scrollViewer1">
<RadioButton Height="16" Name="radioButton1" Width="120">RadioButton</RadioButton>
</ScrollViewer>
</Grid>
</Window>
Listing 1. XAML Code Example
First, let’s remove unused namespaces. In the preceding listing, the namespace clr-namespace:WpfApplication
named local is not used. Clicking on the Removed Unused Namespaces menu item
yields the following XAML.
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="300">
<Grid>
<Button Height="23" Margin="130,0,72,8" Name="button1" VerticalAlignment="Bottom">Button</Button>
<Label Height="28" Margin="30,26,128,0" Name="label1" VerticalAlignment="Top">Label</Label>
<ScrollViewer Margin="48,82,36,91" Name="scrollViewer1">
<RadioButton Height="16" Name="radioButton1" Width="120">RadioButton</RadioButton>
</ScrollViewer>
</Grid>
</Window>
Listing 2. Unused Namespaces Removed
As you can see, the declaration of the clr-namespace:WpfApplication namespace is
replaced with an empty string. What happened here is that the command searches
for the string “local:” in all nodes that are elements and its attributes. Other
nodes such as comments are excluded from the search. Please note that the command
only checks the namespaces in the root element. I usually add the namespaces
I need to the root element only, so I chose to check only the namespaces in the
root element. Another important thing to note is that this command might remove
a namespace declaration that is in use if there are two or more namespace declarations
pointing to the same namespace. This is as designed, since I used LINQ to XML
to parse the XAML. If there are two namespace declarations pointing to a single
namespace, only the last declaration is actually used. Next, let’s try to sort
the attributes by clicking on the Sort Attributes menu item.
<Window
x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300"
Title="MainWindow"
Width="300">
<Grid>
<Button
Height="23"
Margin="130,0,72,8"
Name="button1"
VerticalAlignment="Bottom">Button</Button>
<Label
Height="28"
Margin="30,26,128,0"
Name="label1"
VerticalAlignment="Top">Label</Label>
<ScrollViewer
Margin="48,82,36,91"
Name="scrollViewer1">
<RadioButton
Height="16"
Name="radioButton1"
Width="120">RadioButton</RadioButton>
</ScrollViewer>
</Grid>
</Window>
Listing 3. Sorted Attributes
You can see in preceding listing that all the attributes got sorted. The command
also adds a new line after each attribute and sets the proper indentation. Note
that the command arranges the attributes according to this order: 1) the attributes
that use the http://schemas.microsoft.com/winfx/2006/xaml namespace, 2) attributes
that are namespace declarations, and 3) other attributes. Within the attributes
that are namespaces, the attribute containing the default namespace comes first.
Notice that some elements are not indented properly. We can fix this by using
the Fix Format menu item.
<Window
x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300"
Title="MainWindow"
Width="300">
<Grid>
<Button
Height="23"
Margin="130,0,72,8"
Name="button1"
VerticalAlignment="Bottom">Button</Button>
<Label
Height="28"
Margin="30,26,128,0"
Name="label1"
VerticalAlignment="Top">Label</Label>
<ScrollViewer
Margin="48,82,36,91"
Name="scrollViewer1">
<RadioButton
Height="16"
Name="radioButton1"
Width="120">RadioButton</RadioButton>
</ScrollViewer>
</Grid>
</Window>
Listing 4. Formatted XAML
Note that this command removes the whitespaces and fix the indentation of the elements.
This also formats the attributes the same way the command for sorting attributes
does, except that the attributes will not get sorted. You can download the installer
and source code on the following links.
XAML Organizer Installer
XAML Organizer Source Code
The following sections of this article details how the add-in is created.
Getting Started
First, create a project of type Visual Studio Add-in. The project template is found
under the Other Project Types > Extensibility section. This will lead you
to the Add-in Wizard, which will ask you some questions: programming language
to use, application host, name and description of the add-in and other options.
In this example, I’ll be using the default values. These values can be updated
later.

Figure 2. XAML Organizer Solution
Currently, we are interested in the Connect class because the first step that we
need to do is to add items to the XAML editor context menu.
Getting the XAML Editor Context Menu
Before we can add commands to the XAML editor context menu, we need to get the CommandBar
object that represents the XAML editor context menu. There are 2 ways wherein
we could get this. The first one is to get the list of all the CommandBar objects
and get the specific CommandBar from the list by specifying the name. The second
one is to get the CommandBar by specifying a GUID:ID pair. Let’s discuss the
first one.
In the Connect class, we have a DTE2 member variable called _applicationObject. Assuming
that the name of the CommandBar we like to get is CommandBarName, we can obtain
the CommandBar like this:
CommandBar myCommandBar = ((CommandBars)_applicationObject.CommandBars)["CommandBarName"];
Our problem now is how to get the name of the CommandBar. I’ve found this blog post by Dr. Ex very helpful. It details how to get a CommandBar. Basically, I’m just
reiterating what’s discussed in that blog post. Let’s create a macro to list
all the CommandBar names. First, open the Macro Explorer under the Tools >
Macros menu. Create a macro that will list the CommandBar names.

Figure 3. Macro Explorer
So edit the PrintCmdBarNames, copy and paste the following code. Note that we can
only use Visual Basic language for creating a macro.
Public Module Module1
Sub PrintCmdBarNames()
Dim ow As OutputWindow = DTE.Windows.Item(Constants.vsWindowKindOutput).Object
Dim pane As OutputWindowPane
Try
pane = ow.OutputWindowPanes.Item("CommandBar Names")
Catch ex As Exception
pane = ow.OutputWindowPanes.Add("CommandBar Names")
End Try
pane.Clear()
Dim nameList As New List(Of String)
For Each cmdBar As CommandBar In DTE.CommandBars
nameList.Add(cmdBar.Name)
Next
nameList.Sort()
For Each name As String In nameList
pane.OutputString(name & vbCr)
Next
End Sub
End Module
Listing 5. Macro for Displaying CommandBar Names
Go back to the Macro Explorer, right-click and run the PrintCmdBarNames macro. This
will show the list of CommandBar names in the Output window, as shown in the
following figure.

Figure 4. List of CommandBar Names in the Output Window
The preceding figure shows a partial list of CommandBar names. We can deduce that
the name of the CommandBar that we want to get is XAML Editor. So using this
method requires a bit of guessing on our part. Another disadvantage of using
this method is that two or more CommandBar objects may have the same name. You
can see in the figure that there are two CommandBar objects having the name Wild
Card Expression Builder. Fortunately, we can get a CommandBar by using a GUID:ID
pair. A CommandBar is uniquely identified by a GUID:ID pair. We can get the CommandBar
using the following code, again by Dr. Ex.
[
ComImport,
Guid("6D5140C1-7436-11CE-8034-00AA006009FA"),
InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)
]
internal interface IOleServiceProvider
{
[PreserveSig]
int QueryService([In]ref Guid guidService, [In]ref Guid riid,
[MarshalAs(UnmanagedType.Interface)] out System.Object obj);
}
private CommandBar FindCommandBar(Guid guidCmdGroup, uint menuID)
{
// Make sure the CommandBars collection is properly initialized, before we attempt
to
// use the IVsProfferCommands service.
CommandBar menuBarCommandBar = ((CommandBars)_applicationObject.CommandBars)["MenuBar"];
// Retrieve IVsProfferComands via DTE's IOleServiceProvider interface
IOleServiceProvider sp = (IOleServiceProvider)_applicationObject;
Guid guidSvc = typeof(IVsProfferCommands).GUID;
Object objService;
sp.QueryService(ref guidSvc, ref guidSvc, out objService);
IVsProfferCommands vsProfferCmds = (IVsProfferCommands)objService;
return vsProfferCmds.FindCommandBar(IntPtr.Zero, ref guidCmdGroup, menuID) as CommandBar;
}
Listing 6. Method for Getting a CommandBar Using GUID:ID Pair
We just need to put the FindCommandBar method in our Connect class since we have
access to the_applicationObject variable there. The IOleServiceProvider interface
is needed by the FindCommandBar method. This interface is actually defined in
the Microsoft.VisualStudio.OLE.Interop.dll in Visual Studio SDK, but we can redefine
this in our code so we don’t need to download the SDK. The FindCommandBar method
accepts two parameters, the GUID and ID pair. To get the GUID:ID pair, we need
to add a DWORD registry value named EnableVSIPLogging under the HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\9.0\General
key for Visual Studio 2008. Set the value to 1. To get the XAML editor CommandBar,
open Visual Studio and create or open a WPF application. Open a control in the
XAML editor, press and hold CTRL + SHIFT, then right-click anywhere in the XAML
editor. This will show a popup that shows the GUID:ID pair of the CommandBar
for the XAML editor context menu.

Figure 5. XAML Editor CommandBar Details
The details that we need here are the Guid and CmdID, which corresponds to the guidCmdGroup
and menuID parameters of the FindCommandBar method, respectively. Since we can
now access the XAML editor context menu, let’s add our menu items.
Adding Context Menu Items
First, we need to define when the add-in commands will be added. The Connect class
implements the IDTExtensibility2 interface. The methods in this interface are
called when certain events are raised such as when the add-in is loaded or the
host application has completed loading.
#region IDTExtensibility2 Members
/// <summary>Implements the OnConnection method of the IDTExtensibility2 interface.
Receives notification that the Add-in is being loaded.</summary>
/// <param term='application'>Root object of the host application.</param>
/// <param term='connectMode'>Describes how the Add-in is being loaded.</param>
/// <param term='addInInst'>Object representing this Add-in.</param>
/// <seealso class='IDTExtensibility2' />
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst,
ref Array custom)
{
_applicationObject = (DTE2)application;
_addInInstance = (AddIn)addInInst;
// Check if the add-in is loaded after Visual Studio has finished startup, then add
the commands.
// If the add-in is not loaded after startup (e.g. when Visual Studio is started),
// wait for the OnStartupComplete event handler to execute before adding the commands.
if (connectMode == ext_ConnectMode.ext_cm_AfterStartup)
{
AddMenu();
}
}
/// <summary>Implements the OnDisconnection method of the IDTExtensibility2
interface. Receives notification that the Add-in is being unloaded.</summary>
/// <param term='disconnectMode'>Describes how the Add-in is being unloaded.</param>
/// <param term='custom'>Array of parameters that are host application specific.</param>
/// <seealso class='IDTExtensibility2' />
public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
{
}
/// <summary>Implements the OnAddInsUpdate method of the IDTExtensibility2
interface. Receives notification when the collection of Add-ins has changed.</summary>
/// <param term='custom'>Array of parameters that are host application specific.</param>
/// <seealso class='IDTExtensibility2' />
public void OnAddInsUpdate(ref Array custom)
{
}
/// <summary>Implements the OnStartupComplete method of the IDTExtensibility2
interface. Receives notification that the host application has completed loading.</summary>
/// <param term='custom'>Array of parameters that are host application specific.</param>
/// <seealso class='IDTExtensibility2' />
public void OnStartupComplete(ref Array custom)
{
AddMenu();
}
/// <summary>Implements the OnBeginShutdown method of the IDTExtensibility2
interface. Receives notification that the host application is being unloaded.</summary>
/// <param term='custom'>Array of parameters that are host application specific.</param>
/// <seealso class='IDTExtensibility2' />
public void OnBeginShutdown(ref Array custom)
{
}
#endregion
Listing 7. Implementation of the IDTExtensibility Interface
We have updated the OnConnection and OnStartupComplete methods. The OnStartupComplete
method gets called when the add-in is loaded, either during Visual Studio startup
or when Visual Studio has finished loading. Then, we can call our AddMenu method
to add our menu to the XAML editor context menu. As implied, the OnStartupComplete
method won’t be called when the add-in is loaded after Visual Studio startup.
To handle this scenario, we added some code in the OnConnection method to check
if the add-in is loaded after Visual Studio startup, using the ext_ConnectMode
enumeration. You can define when your add-in is loaded using the Add-in Manager
under the Tools menu of Visual Studio. For the possible ext_ConnectMode values
and detailed explanations, you can check this blog post by Carlos Quintero. Now, let’s see what’s inside the AddMenu method.
/// <summary>
/// Add the XAML organizer menu.
/// </summary>
private void AddMenu()
{
try
{
CommandBar xamlEditorCommandBar = FindCommandBar(new Guid("4C87B692-1202-46AA-B64C-EF01FAEC53DA"), 259);
if (xamlEditorCommandBar == null)
{
throw new Exception("Cannot get the XAML Editor context menu.");
}
// Organize context menu item
_xamlEditorCommandBarPopup = (CommandBarPopup)xamlEditorCommandBar.Controls.Add(
MsoControlType.msoControlPopup, Type.Missing, Type.Missing, 1, true);
_xamlEditorCommandBarPopup.CommandBar.Name = "XamlOrganizer";
_xamlEditorCommandBarPopup.Caption = "Organize";
// Commands
_commands = new Dictionary<string, CommandBase>();
XamlRemoveUnusedNamespacesCommand xamlRemoveUsingsCommand = new XamlRemoveUnusedNamespacesCommand(_applicationObject, _addInInstance);
xamlRemoveUsingsCommand.RegisterCommandBarControl(_xamlEditorCommandBarPopup, "Remove Unused Namespaces");
_commands.Add(xamlRemoveUsingsCommand.CommandName, xamlRemoveUsingsCommand);
XamlSortAttributesCommand xamlSortAttributesCommand = new XamlSortAttributesCommand(_applicationObject, _addInInstance);
xamlSortAttributesCommand.RegisterCommandBarControl(_xamlEditorCommandBarPopup, "Sort Attributes");
_commands.Add(xamlSortAttributesCommand.CommandName, xamlSortAttributesCommand);
XamlFixFormatCommand xamlFixFormatCommand = new XamlFixFormatCommand(_applicationObject, _addInInstance);
xamlFixFormatCommand.RegisterCommandBarControl(_xamlEditorCommandBarPopup, "Fix Format");
_commands.Add(xamlFixFormatCommand.CommandName, xamlFixFormatCommand);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK);
}
}
Listing 8. Adding a Menu to the XAML Editor Context Menu
On the AddMenu method, the FindCommandBar helper method is called to get the CommandBar
object that represents the XAML editor context menu. To add a new menu, we must
call the CommandBar.Controls.Add method. We have to specify the type of control
we like to add using the MsoControlType enumeration. In this case, we needed
a submenu so the msoControlPopup enumeration member is used. This returns a CommandBarPopup
object where we can add our commands.
Adding Commands
A Command gets executed when you click on a menu item or a toolbar button in Visual
Studio. To add a Command object that we can later associate with a menu item,
we have to use the _applicationObject.Commands.AddNamedCommand method. In our
code, this method is called inside the CommandBase constructor. The CommandBase
class is basically a wrapper class for a Command object. This class is also abstract
and we have a specific CommandBase-derived class for each action we want to execute.
Some of the codes here are based on Karl Shifflett’s XamlPowerToys code. The
following image shows the class diagram for the CommandBase classes.

Figure 6. The CommandBase Class
Here we have our CommandBase-derived classes. These classes will implement each of
their own Execute and CanExecute methods. To instantiate a CommandBase-derived
class, we need to supply the _applicationObject and _addInInstance objects to
the constructor. Afterwards, we can associate a menu item to our Command by using
the RegisterCommandBarControl. We just need to specify the CommandBarPopup where
the menu item will be added and the caption. Here is the CommandBase code.
/// <summary>
/// Base class of objects that wraps a Command.
/// </summary>
public abstract class CommandBase : IDisposable
{
#region Fields
protected Command _command;
private CommandBarControl _commandBarControl;
#endregion
#region Constructor
/// <summary>
/// Constructor.
/// </summary>
/// <param name="applicationObject">Visual Studio application object
where this
/// command will be added.</param>
/// <param name="addInInstance">The add-in instance that will add
the command.</param>
/// <param name="commandName">The command name that will identify
the command.</param>
public CommandBase(DTE2 applicationObject, AddIn addInInstance, string commandName)
{
try
{
// Check if the command already exists. If so use this command.
_command = applicationObject.Commands.Item(addInInstance.ProgID + "." + commandName, -1);
}
catch { }
if (_command == null)
{
// If the command does not exist yet, create a new command.
object[] contextGuids = null;
// Last parameter is equivalent to vsCommandStatusSupported | vsCommandStatusEnabled
_command = applicationObject.Commands.AddNamedCommand(addInInstance,
commandName, string.Empty, string.Empty, false, 0, ref contextGuids, 3);
}
}
#endregion
#region Properties
/// <summary>
/// Gets the command name.
/// </summary>
public string CommandName
{
get
{
return _command.Name;
}
}
/// <summary>
/// Gets the command's status. If the command is not always enabled or supported,
/// then the derived class should override this implementation.
/// </summary>
public virtual vsCommandStatus Status
{
get
{
return vsCommandStatus.vsCommandStatusSupported |
vsCommandStatus.vsCommandStatusEnabled;
}
}
#endregion
#region Methods
/// <summary>
/// Execute the command logic.
/// </summary>
public abstract void Execute();
/// <summary>
/// Checks if the command can be executed depending on the executeOption value.
/// Default implementation is that the command can execute when the executeOption
/// is equal to vsCommandExecOptionDoDefault, meaning the default behavior is
/// performed. This could be overridden by a derived class to perform a different
/// checking.
/// </summary>
/// <param name="executeOption">The execution option.</param>
/// <returns>True if the command can execute. False otherwise.</returns>
public virtual bool CanExecute(vsCommandExecOption executeOption)
{
return executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault;
}
/// <summary>
/// Adds a CommandBarControl that is associated with this command to the specified
/// CommandBarPopup object.
/// </summary>
/// <param name="commandBarPopup">The CommandBarPopup where the CommandBarControl
will
/// be added.</param>
/// <param name="caption">The command caption.</param>
public void RegisterCommandBarControl(CommandBarPopup commandBarPopup, string caption)
{
_commandBarControl = (CommandBarControl)_command.AddControl(commandBarPopup.CommandBar,
commandBarPopup.CommandBar.Controls.Count + 1);
_commandBarControl.Caption = caption;
}
#endregion
#region IDisposable Members
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
// Check to see if Dispose has already been called.
if(!this.disposed)
{
if(disposing)
{
if (_commandBarControl != null)
{
_commandBarControl.Delete(null);
_commandBarControl = null;
}
if (_command != null)
{
_command.Delete();
_command = null;
}
}
disposed = true;
}
}
~CommandBase()
{
Dispose(false);
}
#endregion
}
Listing 9. Command Wrapper
In the CommandBase constructor, we automatically add the Command to the environment.
We have to check first if the Command already exists because a Command may be
saved by the environment and loaded the next time Visual Studio starts, even
when the add-in is not yet loaded. The CommandBase class also has a Status property
that returns the Command’s status. By default, a Command is enabled and supported.
This can be overridden by a derived class, so that it can enable or disable the
menu item. Derived classes must also implement the Execute method to provide
the Command’s execution logic, and override the CanExecute method to provide
custom checking. Currently, even if our CommandBase objects have implemented
their own Execute methods, they won’t get executed yet because the Connect class
still needs to implement the IDTCommandTarget interface.
Command Execution Logic
For our Command’s execution logic to be called, our Connect class must implement
the IDTCommandTarget interface. This interface contains two methods: Exec and
QueryStatus. The Exec method is called to execute the command. The QueryStatus
method is called to get the current status of the command, so that the control
associated with the command can be enabled or disabled. In our code, these methods
relay the execution logic to the specific CommandBase object whose name matches
the CmdName parameter value.
#region IDTCommandTarget Members
/// <summary>
/// Executes the specified named command.
/// </summary>
/// <param name="CmdName">The name of the command to execute.</param>
/// <param name="ExecuteOption">A vsCommandExecOption constant specifying
the execution options.</param>
/// <param name="VariantIn">A value passed to the command.</param>
/// <param name="VariantOut">A value passed back to the invoker Exec
method after the command executes.</param>
/// <param name="Handled">Boolean value that will indicate whether the command is executed.</param>
public void Exec(string CmdName, vsCommandExecOption ExecuteOption, ref object VariantIn, ref
object VariantOut, ref bool Handled)
{
Handled = false;
CommandBase command = null;
if (_commands.TryGetValue(CmdName, out command) && command.CanExecute(ExecuteOption))
{
try
{
command.Execute();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK);
}
Handled = true;
}
}
/// <summary>
/// Returns the current status (enabled, disabled, hidden, and so forth) of the specified
named command.
/// </summary>
/// <param name="CmdName">The name of the command to check.</param>
/// <param name="NeededText">A vsCommandStatusTextWanted constant
specifying if information is
/// returned from the check, and if so, what type of information is returned.</param>
/// <param name="StatusOption">A vsCommandStatus specifying the current
status of the command.</param>
/// <param name="CommandText">The text to return if vsCommandStatusTextWantedStatus
is specified.</param>
public void QueryStatus(string CmdName, vsCommandStatusTextWanted NeededText, ref vsCommandStatus
StatusOption, ref object CommandText)
{
CommandBase command = null;
if (_commands.TryGetValue(CmdName, out command))
{
StatusOption = command.Status;
}
else
{
StatusOption = vsCommandStatus.vsCommandStatusUnsupported;
}
}
#endregion
Listing 10. IDTCommandTarget Implementation
Next, we have to provide each CommandBase-derived class their Execute implementation. Let’s discuss the command for removing unused namespaces.
public class XamlRemoveUnusedNamespacesCommand : CommandBase
{
public XamlRemoveUnusedNamespacesCommand(DTE2 applicationObject, AddIn addInInstance)
: base(applicationObject, addInInstance, "XamlRemoveUnusedNamespacesCommand")
{
}
public override void Execute()
{
// Get the text from the XAML editor and load it as XML
Document document = _command.DTE.ActiveDocument;
TextDocument textDocument = (TextDocument)document.Object("TextDocument");
EditPoint startPoint = textDocument.StartPoint.CreateEditPoint();
EditPoint endPoint = textDocument.EndPoint.CreateEditPoint();
XElement rootElement = XElement.Parse(startPoint.GetText(endPoint));
// Remove the nodes that are not elements so that these nodes won't
// be included in the search (e.g. nodes of type Comment)
List<XNode> nonElementNodes = new List<XNode>();
foreach (XNode node in rootElement.DescendantNodes())
{
if (node.NodeType != XmlNodeType.Element)
{
nonElementNodes.Add(node);
}
}
foreach (XNode node in nonElementNodes)
{
node.Remove();
}
nonElementNodes.Clear();
// Cast the root element to string and match the attribute name.
// If it does not match any, replace the namespace declaration with an
// empty string.
string xmlString = rootElement.ToString();
foreach (XAttribute attribute in rootElement.Attributes().Where(
a => a.IsNamespaceDeclaration && a.Name.LocalName != "xmlns"))
{
Regex regEx = new Regex("[^A-Za-z0-9_]" + attribute.Name.LocalName + ":");
if (!regEx.Match(xmlString).Success)
{
document.ReplaceText("xmlns:" + attribute.Name.LocalName + "=\"" +
attribute.Value + "\"", string.Empty, 0);
}
}
}
}
Listing 11. Command Implementation for Removing Unused XML Namespaces
Basically, the algorithm here is to find a string that matches the XML namespace
name, together with the colon. If there is none found, then the namespace is
not in use so the namespace declaration is replaced with an empty string. I opted
to use LINQ to XML to traverse the elements and check their attributes instead
of searching the document directly so that comments won’t be included in the
search. I used a regular expression to match the namespace name although I’m
not sure if the expression will match all valid cases. I did not override the
CanExecute method since the command will always be enabled. To start debugging,
just hit the F5 key (Start Debugging Button). This will open up another instance
of Visual Studio. Load the add-in using the Add-in Manager in the Tools menu.
In our example, we need to open a WPF application, show the context menu on the
XAML editor, and click on the menu item associated with the command.
Unloading the Add-in
To unload the add-in, just go to the Add-in Manager and uncheck the add-in. In our
example, we haven’t yet implemented the OnDisconnection method in the Connect
class so even if we unload the add-in, our menu is still there. Here is the updated
code.
public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
{
foreach (KeyValuePair<string, CommandBase> command in _commands.Reverse())
{
command.Value.Dispose();
}
_commands.Clear();
_xamlEditorCommandBarPopup.Delete(null);
_xamlEditorCommandBarPopup = null;
}
Listing 12. Unloading the Add-in
If you remember, the CommandBase class implements the IDisposable interface and deletes
the associated Command and CommandBarControl objects. Since we only added only
one CommandBarPopup, I decided not to use a collection that will store CommandBarPopup
objects. I made the _xamlEditorCommandBarPopup a member variable instead.