Component types
As denoted in Intro, This framework supports a lot of component types.
A full list of components available:
Type | Name | Description | Components V2 only (flag) |
---|---|---|---|
1 | Action Row | Container to display a row of interactive components | No |
2 | Button | Button object | No |
3 | String Select | Select menu for picking from defined text options | No |
4 | Text Input | Text input object | No |
5 | User Select | Select menu for users | No |
6 | Role Select | Select menu for roles | No |
7 | Mentionable Select | Select menu for mentionables (users and roles) | No |
8 | Channel Select | Select menu for channels | No |
9 | Section | Container to display text alongside an accessory component | Yes |
10 | Text Display | Markdown text | Yes |
11 | Thumbnail | Small image that can be used as an accessory | Yes |
12 | Media Gallery | Display images and other media | Yes |
13 | File | Displays an attached file | Yes |
14 | Separator | Component to add vertical padding between other components | Yes |
17 | Container | Container that visually groups a set of components | Yes |
All components have an id field (generated by default) and must be unique within the message. Generation of ids won't use another id that exists in the message if you have one defined for another component. Sending components with an id of 0
is allowed but will be treated as empty and replaced by the API. You may want to manually assign one if you want to later identify a component this way. Some (interactive) components can be identified using a customId
instead, which is a unique string (max 100 chars).
The ActionRow
component is a parent that contains either
- 1 to 5
Buttons
- 1
TextInput
- 1 of (
StringSelect
,UserSelect
,RoleSelect
,MentionableSelect
,ChannelSelect
)
- Must be inside an
ActionRow
or in the accessory field of aSection
- Non-link and non-premium buttons must have a custom_id, and cannot have a url or a sku_id.
- Link buttons must have a url, and cannot have a custom_id
- Link buttons do not send an interaction to your app when clicked
- Premium buttons must contain a sku_id, and cannot have a custom_id, label, url, or emoji.
- Premium buttons do not send an interaction to your app when clicked
String Select (also other Select variants)
- Must be inside an
ActionRow
- 1 to 25
options
- Placeholder (if specified, up to 150 chars)
It supports multi-select. If you want to use this functionality, you must set min_values
(0 - 25) and max_values
(1 - 25).
For all select options:
- Label, value and optionally the description may not exceed
100
chars
- Must be inside an
ActionRow
in aModal
- Label has a max length of
45
By default it will accept up to 4000
characters (this is also the maximum). You can change this using the min_values
and max_values
A parent component, this allows you to put TextDisplays
(1 - 3) next to eachother. It includes an accessory field, which can be used to display a Thumbnail
or Button
.
Just text lol. (With markdown support, just like a regular user; up to 4000 characters)
A small image (to use in a Section
).
- Optionally has a description of max
1024
chars
A component used to display up to 10
images.
- Each image can have a description of max
1024
chars
Used to send a single file. Only supports the attachment://
protocol.
Just something to put space between components (Y axis).
- Spacing can be set to either
1
(small space) or2
(large space) - Visibility can be toggled too. (
IsDivider
)
A parent component with a side bar of customisable colour (like embeds).
In Discord.NET, you typically use these components in conjunction with a ComponentBuilderV2
. The V2 specific components can be added to the builder using the WithX
fluent/chain methods whereas the other supported components are mostly children of ActionRows
and can be added as a component array like used below. You need to know what components can be added to which component though to prevent errors (this is why the above sections exist). If your component structure is wrong, Discord.NET will throw an exception.
This example offers some more insight on how to use them. Below is a component with TextDisplay
, MediaGallery
and ActionRow
(with Buttons
or SelectMenu
).
Code
Some code will not be included here as it is not relevant to this framework. If you want to see the full code, it is here.
The main component container generation method:
private static ComponentBuilderV2 BuildComponentUnsafe(Recipe recipe, Units units = Units.Si)
{
StringBuilder ingredients = new($"## Ingredients{Environment.NewLine}");
StringBuilder instructions = new($"## Instructions{Environment.NewLine}");
ButtonBuilder servingsModalButton = new ButtonBuilder()
.WithCustomId(RecipeServingsModal)
.WithLabel("Set servings")
.WithStyle(ButtonStyle.Primary);
foreach (RecipeIngredient recipeIngredient in recipe.RecipeIngredients)
{
ingredients.Append($"`{recipeIngredient.Quantity} {recipeIngredient.Unit.ToSymbol()}` {recipeIngredient.Ingredient.Name} ");
if (recipeIngredient.Optional)
ingredients.AppendLine("[Optional]");
else
ingredients.Append(Environment.NewLine);
}
for (var i = 0; i < recipe.Instruction.Length; i++)
instructions.AppendLine($"`{i + 1}.` {recipe.Instruction[i]}{Environment.NewLine}");
return new ComponentBuilderV2()
.WithTextDisplay($"# {recipe.Name}", RecipeNameDisplay)
.WithTextDisplay($"-# {recipe.Servings} servings", RecipeServingsDisplay)
.WithMediaGallery([
"https://cdn.discordapp.com/attachments/964253122547552349/1336440069892083712/7Q3S.gif?ex=67a3d04e&is=67a27ece&hm=059c9d28466f43a50c4b450ca26fc01298a2080356421d8524384bf67ea8f3ab&"
])
.WithActionRow([servingsModalButton])
.WithTextDisplay(ingredients.ToString())
.WithTextDisplay($"""
## Oven Settings
Mode: `{recipe.OvenMode.ToHumanReadable()}`
Temperature: `{recipe.Temperature.Convert(Unit.Temperature, Units.Si, units)} {units.ToSymbol()}`
""")
.WithActionRow([
new SelectMenuBuilder(
RecipeUnitInput,
options:[
new SelectMenuOptionBuilder(
"Metric",
"1",
isDefault: units == Units.Metric),
new SelectMenuOptionBuilder(
"Imperial",
"2",
isDefault: units == Units.Imperial),
new SelectMenuOptionBuilder(
"Kelvin",
"0",
isDefault: units == Units.Si)
],
id: RecipeUnitSelectMenu
)
])
.WithTextDisplay(instructions.ToString());
}
Interactions
The button triggers the following modal
private static ModalBuilder CreateServingsModal(short servings)
{
TextInputBuilder? textInput = new TextInputBuilder()
.WithCustomId(RecipeServingsInput)
.WithLabel("Servings")
.WithValue(servings.ToString())
.WithMinLength(1)
.WithMaxLength(3)
.WithStyle(TextInputStyle.Short);
return new ModalBuilder()
.WithCustomId(RecipeServingsButton)
.WithTitle("Set Servings")
.AddTextInput(textInput);
}
Interactions used by this message:
private async Task ClientOnInteractionCreatedAsync(SocketInteraction arg)
{
switch (arg)
{
case SocketMessageComponent component:
switch (component.Data.CustomId)
{
// SET SERVINGS BUTTON CLICKED
case RecipeServingsModal:
var servings = short.Parse(component.Message.Components.FindComponentById<TextDisplayComponent>(RecipeServingsDisplay).Content.Split(' ')[1]);
await component.RespondWithModalAsync(CreateServingsModal(servings).Build());
break;
// ITEM IN COMBOXBOX CHANGED
case RecipeUnitInput:
SelectMenuComponent selectedItem = component.Message.Components.FindComponentById<SelectMenuComponent>(RecipeUnitSelectMenu);
var unitValue = short.Parse(component.Data.Values.First());
var recipeName = component.Message.Components.FindComponentById<TextDisplayComponent>(RecipeNameDisplay).Content[2..];
Recipe recipe = _recipes.First(r => r.Name == recipeName);
Recipe recipe0 = recipe.Clone();
var unit = (Units)Enum.ToObject(typeof(Units), unitValue);
ComponentBuilderV2 newComponentContainer = BuildComponentUnsafe(recipe0, unit);
await component.UpdateAsync(m => m.Components = newComponentContainer.Build());
break;
default:
// Ununsed here
}
break;
// MODAL SUBMIT
case SocketModal modal:
if (modal.Data.CustomId == RecipeServingsButton)
{
var success = short.TryParse(modal.Data.Components.First(c => c.CustomId == RecipeServingsInput).Value, out var servings);
if (!success || servings <= 0)
break;
Recipe recipe = _recipes.First(r => r.Name == modal.Message.Components.FindComponentById<TextDisplayComponent>(RecipeNameDisplay).Content[2..]);
Recipe? recipe0 = recipe.Clone();
recipe0.ChangeServings(servings, true);
ComponentBuilderV2 newComponentContainer = BuildComponentUnsafe(recipe0);
await modal.UpdateAsync(m => m.Components = newComponentContainer.Build());
}
break;
default:
return;
}
}
After the SET SERVINGS modal is submitted (or the COMBOXBOX is changed) the UI is updated:
Troubleshooting
Common issues
- Your interaction may not work if you use components v2 using
ModifyAsync
/UpdateAsync
/..., if that is the case you must set the MessageFlags.ComponentsV2 flag on the message as mentioned in CV2_Flag. - Currently there may be some rare instances where the flag may not be set even when it should, as per CV2_Flag. An example is when more than 5 action rows are used. This might get fixed later though. You should set it manually in this case (You can identify this using method described below).
If you run into any trouble (appliction not responding when sending components), a debugger is (like usually) a useful tool to have at your disposal. More specifically within this context: dnet will do some checks before sending the component configuration to discord, so on building the component array, you can check for errors thrown. If this does not help, setting a breakpoint on the line that sends your component to discord (ModifyAsync/RespondAsync/...) and stepping to the next line (within 3 seconds of triggering it) may yield a more specific error returned by Discord itself.