Your Revit plugin menu (UI): every Button Type (PushButton, PulldownButton, SplitButton, RadioButtonGroup)

Revit API offers several types of buttons, each serving a purpose in enhancing your plugin’s UI. While the PushButton is the most common, PulldownButtons, SplitButtons, and RadioButtonGroups also have specific use cases that can improve user experience.

PushButton

The PushButton is the most basic and widely used button type. It executes a specific action when clicked. We’ve covered PushButtons in detail in a previous post, including how to configure your project to attach images to buttons. You can revisit that post for a deeper dive.
To recap, start by creating a PushButtonData to define the button’s name (ID, not visible in Revit), text, and the command to execute when the button is clicked. Then, add the PushButtonData to a panel to create a PushButton. You can further customize it with tooltips, contextual help, or images.
C#
var ribbonTabName = "Revit Mastery";
application.CreateRibbonTab(ribbonTabName);
var panel = application.CreateRibbonPanel(ribbonTabName, "Revit Mastery Panel");

var assemblyPath = Assembly.GetExecutingAssembly().Location; //localization of the currently executing assembly. Don't worry, copy ;)
var assemblyDirectory = Path.GetDirectoryName(assemblyPath);


// PushButton
var pushButtonData = new PushButtonData(
	"pushButton",
	"Click me",
	assemblyPath,
	typeof(SimpleCommand).FullName);		

var pushButton = panel.AddItem(pushButtonData) as PushButton;
pushButton.ToolTip = "A simple test button.";

var pushButtonContextualHelp = new ContextualHelp(ContextualHelpType.Url, "https://revitmastery.com/revit-plugin-types/");
pushButton.SetContextualHelp(pushButtonContextualHelp);
	
pushButton.LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "ads_click_32.png")));
The command class associated with the button must implement the IExternalCommand interface. When the button is clicked, the Execute method is executed.
Here’s a simple command that notifies the user when the button is clicked.
C#
[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class SimpleCommand : IExternalCommand
{
	public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
	{
		TaskDialog.Show("Simple command", "The button has been clicked.");
		return Result.Succeeded;
	}
}

PulldownButton

A PulldownButton allows users to select and click one of the options from a drop-down list.
To create a PulldownButton:
Define options in the drop-down list:
For each option, create a PushButtonData specifying the button’s name (ID, not visible in Revit), text, and the command to execute when the button is clicked. Each option can be customized with a tooltip, contextual help, or an image.
Create the PulldownButton:
Start with PulldownButtonData to define the button’s name (ID, not visible in Revit) and text. Add the PulldownButtonData to a panel to create the PulldownButton, which can be customized with a tooltip, contextual help, or image.
Add options to the PulldownButton:
Optionally, add separators to divide options in the drop-down list.
C#
// [...] code before

// PulldownButton

//1. Defining options (in our case, the same for PulldownButton and SplitButton)
var option1Data = new PushButtonData("option1", "Walls", assemblyPath, typeof(SelectWallsCommand).FullName);
option1Data.ToolTip = "Select walls.";
option1Data.LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "counter_1_16.png")));

var option2Data = new PushButtonData("option2", "Floors", assemblyPath, typeof(SelectFloorsCommand).FullName)
{
	//a different code approach to customize the button
	ToolTip = "Select floors.",
	LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "counter_2_16.png"))),
};

var option3Data = new PushButtonData("option3", "Stairs", assemblyPath, typeof(SelectStairsCommand).FullName)
{
	ToolTip = "Select stairs.",
	LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "counter_3_16.png")))
};


// 2. Defining the PulldownButton
var pulldownButtonData = new PulldownButtonData("pulldownButton", "Select elements");
var pulldownButton = panel.AddItem(pulldownButtonData) as PulldownButton;
pulldownButton.LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "widgets_32.png")));


// 3. Adding options to the PulldownButton (with optional separator)
pulldownButton.AddPushButton(option1Data);
pulldownButton.AddPushButton(option2Data);
pulldownButton.AddSeparator();
pulldownButton.AddPushButton(option3Data);
C#
[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class SelectWallsCommand : IExternalCommand
{
	public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
	{
		var uiApp = commandData.Application;
		var doc = uiApp.ActiveUIDocument.Document;
		var selectionManager = uiApp.ActiveUIDocument.Selection;

		var elementsType = typeof(Wall);
		var collector = new FilteredElementCollector(doc).WhereElementIsNotElementType().OfClass(elementsType);
		var collectedElementsIds = collector.ToElementIds();
		
		selectionManager.SetElementIds(collectedElementsIds);
            
		return Result.Succeeded;
	}
}

[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class SelectFloorsCommand : IExternalCommand
{
	public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
	{
		var uiApp = commandData.Application;
		var doc = uiApp.ActiveUIDocument.Document;
		var selectionManager = uiApp.ActiveUIDocument.Selection;

		var elementsType = typeof(Floor);
		var collector = new FilteredElementCollector(doc).WhereElementIsNotElementType().OfClass(elementsType);
		var collectedElementsIds = collector.ToElementIds();

		selectionManager.SetElementIds(collectedElementsIds);

		return Result.Succeeded;
	}
}

[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class SelectStairsCommand : IExternalCommand
{
	public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
	{
		var uiApp = commandData.Application;
		var doc = uiApp.ActiveUIDocument.Document;
		var selectionManager = uiApp.ActiveUIDocument.Selection;

		var elementsType = typeof(Stairs);
		var collector = new FilteredElementCollector(doc).WhereElementIsNotElementType().OfClass(elementsType);
		var collectedElementsIds = collector.ToElementIds();

		selectionManager.SetElementIds(collectedElementsIds);

		return Result.Succeeded;
	}
}
🛈
Avoiding Repetitions in Command Classes
As you can see in the code above, each option requires a separate command class. However, the classes are almost identical; each performs the same action, differing only in the Revit element type (Walls/Floors/Stairs). It leads to repeating code, which is a bad practice.
But curating the code style is important for good programmers – like ourselves. Therefore, let’s implement the DRY (Don’t Repeat Yourself) coding principle. Using C# inheritance and generics, you can create a parent abstract class and subclasses for specific Revit element types.
C#
[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class SelectWallsCommand : SelectElementsOfTypeCommand<Wall>;

[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class SelectFloorsCommand : SelectElementsOfTypeCommand<Floor>;

[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class SelectStairsCommand : SelectElementsOfTypeCommand<Stairs>;

public abstract class SelectElementsOfTypeCommand<TRevitElementType> : IExternalCommand where TRevitElementType : Element
{
	public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
	{
		var uiApp = commandData.Application;
		var doc = uiApp.ActiveUIDocument.Document;
		var selectionManager = uiApp.ActiveUIDocument.Selection;

		var elementsType = typeof(TRevitElementType);
		var collector = new FilteredElementCollector(doc).WhereElementIsNotElementType().OfClass(elementsType);
		var collectedElementsIds = collector.ToElementIds();

		selectionManager.SetElementIds(collectedElementsIds);

		return Result.Succeeded;
	}
}
This approach simplifies code maintenance. Now, when you need to modify the command logic you can change the code in a single place, not in each class. Effective and error-prone.

SplitButton

The SplitButton is an enhanced version of the PulldownButton. In addition to the drop-down list of options, it also displays the last clicked (or default) option as a separate button. This allows users to quickly execute the last used command without opening the drop-down list.
Creating a SplitButton is similar to PulldownButton. The first option will appear as the default button until another option is clicked.
To create a SplitButton :
Define options in the drop-down list:
For each option, create a PushButtonData specifying the button’s name (ID, not visible in Revit), text, and the command to execute when the button is clicked. Each option can be customized with a tooltip, contextual help, or an image.
Create the SplitButton :
Start with SplitButtonData to define the button’s name (ID, not visible in Revit) and text. Add the SplitButtonData to a panel to create the SplitButton, which can be customized with a tooltip, contextual help, or image.
Add options to the SplitButton :
Optionally, add separators to divide options in the drop-down list.
C#
// SplitButton

// 1. Defining options (in our case, the same for PulldownButton and SplitButton)
// [... preevious code]

// 2. Defining the SplitButton
var splitButtonData = new SplitButtonData("splitButton", "Split button");
var splitButton = panel.AddItem(splitButtonData) as SplitButton;

// 3. Adding options to the SplitButton (with optional separator)
splitButton.AddPushButton(option1Data);
splitButton.AddPushButton(option2Data);
splitButton.AddSeparator();
splitButton.AddPushButton(option3Data);

RadioButtonGroup

The RadioButtonGroup is a group of toggle buttons where only one button can be selected at a time.  Therefore, the RadioButtonGroup performs a different role than the previous buttons. It can be used to set user preferences, which can be persisted for further use.
In our example, the user will select whether a single or all Revit elements (Walls/Floors/Stairs) should be selected when the user clicks the previous buttons.
To create a RadioButtonGroup :
Define ToggleButtons as options in the group:
For each option, create a ToggleButtonData specifying the toggle button’s name (ID, not visible in Revit), and text. Each option can be customized with a tooltip, contextual help, or an image.
Create the RadioButtonGroup:
Start with RadioButtonGroupData to define the radio button’s name (ID, not visible in Revit). Add the RadioButtonGroupData to a panel to create the RadioButtonGroup.
Add options to the RadioButtonGroup:
Populate the group with the previously defined toggle buttons.
Assign the RadioButtonGroup to a global variable:
Store the RadioButtonGroup in a private static variable, and create a public static property to retrieve the selected option when needed.
C#
[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class RevitMasteryApplication : IExternalApplication
{
	// 4.1 Needed to access the RadioButton value elsewhere
	private static RadioButtonGroup radioButton;
	public static bool ShouldSingleElementBeSelected => radioButton.Current.ItemText == "Single";			

	public Result OnStartup(UIControlledApplication application)
	{
		// [... previous code]

		// RadioButton

		//1. Defining options
		var toggleButtonData1 = new ToggleButtonData("toggleButton1", "Single")
		{
			ToolTip = "Set to select a single element of type.",
			LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "single_16.png"))),
		};
		var toggleButtonData2 = new ToggleButtonData("toggleButton2", "All")
		{
			ToolTip = "Set to select all elements of type.",
			LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "multiple_16.png"))),
		};

		//2. Defining the RadioButton
		var radioButtonData = new RadioButtonGroupData("radioButtonGroup");
		var radioButton = panel.AddItem(radioButtonData) as RadioButtonGroup;

		//3. Adding options to the RadioButton
		radioButton.AddItem(toggleButtonData1);
		radioButton.AddItem(toggleButtonData2);

		//4. Assigning the RadioButton to a global variable (to use its value elsewhere)
		RevitMasteryApplication.radioButton = radioButton;

		return Result.Succeeded;
	}
}
Now, we can modify commands to select single/all elements, depending on the option from the RadioButtonGroup. You need to modify SelectWallsCommand, SelectFloorsCommand, and SelectStairCommand or just SelectElementsOfTypeCommand if you implemented the Don’t Repeat Yourself approach. The complete code is in the Final Code section.

The Final Code

C#
using System.IO;
using System.Reflection;
using System.Windows.Media.Imaging;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using RevitMastery.Revit.Commands;

namespace RevitMastery.Revit;

[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class RevitMasteryApplication : IExternalApplication
{
	// 4.1 Needed to access the RadioButton value elsewhere
	private static RadioButtonGroup radioButton;
	public static bool ShouldSingleElementBeSelected => radioButton.Current.ItemText == "Single";

	public Result OnStartup(UIControlledApplication application)
	{
		var ribbonTabName = "Revit Mastery";
		application.CreateRibbonTab(ribbonTabName);
		var panel = application.CreateRibbonPanel(ribbonTabName, "Revit Mastery Panel");

		var assemblyPath = Assembly.GetExecutingAssembly().Location; //localization of the currently executing assembly. Don't worry, copy ;)
		var assemblyDirectory = Path.GetDirectoryName(assemblyPath);


		// PushButton
		var pushButtonData = new PushButtonData(
			"pushButton",
			"Click me",
			assemblyPath,
			typeof(SimpleCommand).FullName);

		var pushButton = panel.AddItem(pushButtonData) as PushButton;
		pushButton.ToolTip = "A simple test button.";

		var pushButtonContextualHelp = new ContextualHelp(ContextualHelpType.Url, "https://revitmastery.com/revit-plugin-types/");
		pushButton.SetContextualHelp(pushButtonContextualHelp);
			
		pushButton.LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "ads_click_32.png")));


		// PulldownButton

		// 1. Defining options (in our case, the same for PulldownButton and SplitButton)
		var option1Data = new PushButtonData("option1", "Walls", assemblyPath, typeof(SelectWallsCommand).FullName);
		option1Data.ToolTip = "Select walls.";
		option1Data.LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "counter_1_16.png")));

		var option2Data = new PushButtonData("option2", "Floors", assemblyPath, typeof(SelectFloorsCommand).FullName)
		{
			// a different code approach to customize the button
			ToolTip = "Select floors.",
			LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "counter_2_16.png"))),
		};

		var option3Data = new PushButtonData("option3", "Stairs", assemblyPath, typeof(SelectStairsCommand).FullName)
		{
			ToolTip = "Select stairs.",
			LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "counter_3_16.png")))
		};

		// 2. Defining the PulldownButton
		var pulldownButtonData = new PulldownButtonData("pulldownButton", "Select elements");
		var pulldownButton = panel.AddItem(pulldownButtonData) as PulldownButton;
		pulldownButton.LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "widgets_32.png")));

		// 3. Adding options to the PulldownButton (with optional separator)
		pulldownButton.AddPushButton(option1Data);
		pulldownButton.AddPushButton(option2Data);
		pulldownButton.AddSeparator();
		pulldownButton.AddPushButton(option3Data);


		// SplitButton

		//2. Defining the SplitButton
		var splitButtonData = new SplitButtonData("splitButton", "Split button");
		var splitButton = panel.AddItem(splitButtonData) as SplitButton;

		// 3. Adding options to the SplitButton (with optional separator)
		splitButton.AddPushButton(option1Data);
		splitButton.AddPushButton(option2Data);
		splitButton.AddSeparator();
		splitButton.AddPushButton(option3Data);


		// RadioButton

		//1. Defining options
		var toggleButtonData1 = new ToggleButtonData("toggleButton1", "Single")
		{
			ToolTip = "Set to select a single element of type.",
			LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "single_16.png"))),
		};
		var toggleButtonData2 = new ToggleButtonData("toggleButton2", "All")
		{
			ToolTip = "Set to select all elements of type.",
			LargeImage = new BitmapImage(new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "multiple_16.png"))),
		};

		//2. Defining the RadioButton
		var radioButtonData = new RadioButtonGroupData("radioButtonGroup");
		var radioButton = panel.AddItem(radioButtonData) as RadioButtonGroup;


		//3. Adding options to the RadioButton
		radioButton.AddItem(toggleButtonData1);
		radioButton.AddItem(toggleButtonData2);

		//4. Assigning the RadioButton to a global variable (to use its value elsewhere)
		RevitMasteryApplication.radioButton = radioButton;

		return Result.Succeeded;
	}

	public Result OnShutdown(UIControlledApplication application)
	{
		return Result.Succeeded;
	}
}
C#
using Autodesk.Revit.Attributes;
using Autodesk.Revit.UI;
using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Architecture;

namespace RevitMastery.Revit.Commands;

[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class SelectWallsCommand : SelectElementsOfTypeCommand<Wall>;

[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class SelectFloorsCommand : SelectElementsOfTypeCommand<Floor>;

[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class SelectStairsCommand : SelectElementsOfTypeCommand<Stairs>;

public abstract class SelectElementsOfTypeCommand<TRevitElementType> : IExternalCommand where TRevitElementType : Element
{
	public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
	{
		var uiApp = commandData.Application;
		var doc = uiApp.ActiveUIDocument.Document;
		var selectionManager = uiApp.ActiveUIDocument.Selection;

		var elementsType = typeof(TRevitElementType);
		var collector = new FilteredElementCollector(doc).WhereElementIsNotElementType().OfClass(elementsType);
		var collectedElementsIds = collector.ToElementIds();

		if (RevitMasteryApplication.ShouldSingleElementBeSelected)
		{
			var firstElementId = collectedElementsIds.First();
			selectionManager.SetElementIds(new List<ElementId> { firstElementId });
		}
		else
		{
			selectionManager.SetElementIds(collectedElementsIds);
		}

		return Result.Succeeded;
	}
}

Summary

Use PushButtons for single commands.
Use PulldownButtons and SplitButtons to group similar commands.
Use RadioButtonGroups to allow users to configure a preference.
By understanding and utilizing the variety of button types available in the Revit API, you can significantly enhance the user interface of your Revit plugins.
Do you want to become the Revit Programmer of the Future?