XAML Organizer

XAML Organizer is a Visual Studio 2008 add-in for organizing your XAML code.

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.

By Michael Detras   Popularity  (4684 Views)