* introduce overload for responding to an interaction with an instatiated IModal obj * add inline docs to ModalInfo.PropertyInfo * Apply suggestions from code review Co-authored-by: Casmir <68127614+csmir@users.noreply.github.com> --------- Co-authored-by: Casmir <68127614+csmir@users.noreply.github.com>pull/2585/head
@@ -1,5 +1,6 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Reflection; | |||||
namespace Discord.Interactions.Builders | namespace Discord.Interactions.Builders | ||||
{ | { | ||||
@@ -39,6 +40,11 @@ namespace Discord.Interactions.Builders | |||||
Type Type { get; } | Type Type { get; } | ||||
/// <summary> | /// <summary> | ||||
/// Get the <see cref="PropertyInfo"/> of this component's property. | |||||
/// </summary> | |||||
PropertyInfo PropertyInfo { get; } | |||||
/// <summary> | |||||
/// Get the <see cref="ComponentTypeConverter"/> assigned to this input. | /// Get the <see cref="ComponentTypeConverter"/> assigned to this input. | ||||
/// </summary> | /// </summary> | ||||
ComponentTypeConverter TypeConverter { get; } | ComponentTypeConverter TypeConverter { get; } | ||||
@@ -1,5 +1,6 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Reflection; | |||||
namespace Discord.Interactions.Builders | namespace Discord.Interactions.Builders | ||||
{ | { | ||||
@@ -34,6 +35,9 @@ namespace Discord.Interactions.Builders | |||||
public Type Type { get; private set; } | public Type Type { get; private set; } | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public PropertyInfo PropertyInfo { get; internal set; } | |||||
/// <inheritdoc/> | |||||
public ComponentTypeConverter TypeConverter { get; private set; } | public ComponentTypeConverter TypeConverter { get; private set; } | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
@@ -37,6 +37,8 @@ namespace Discord.Interactions.Builders | |||||
if (!typeof(IModal).IsAssignableFrom(type)) | if (!typeof(IModal).IsAssignableFrom(type)) | ||||
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); | throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); | ||||
Type = type; | |||||
_interactionService = interactionService; | _interactionService = interactionService; | ||||
_components = new(); | _components = new(); | ||||
} | } | ||||
@@ -596,6 +596,7 @@ namespace Discord.Interactions.Builders | |||||
builder.Label = propertyInfo.Name; | builder.Label = propertyInfo.Name; | ||||
builder.DefaultValue = defaultValue; | builder.DefaultValue = defaultValue; | ||||
builder.WithType(propertyInfo.PropertyType); | builder.WithType(propertyInfo.PropertyType); | ||||
builder.PropertyInfo = propertyInfo; | |||||
foreach(var attribute in attributes) | foreach(var attribute in attributes) | ||||
{ | { | ||||
@@ -44,6 +44,44 @@ namespace Discord.Interactions | |||||
await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); | await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); | ||||
} | } | ||||
/// <summary> | |||||
/// Respond to an interaction with an <see cref="IModal"/> and fills the value fields of the modal using the property values of the provided | |||||
/// instance. | |||||
/// </summary> | |||||
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam> | |||||
/// <param name="interaction">The interaction to respond to.</param> | |||||
/// <param name="modal">The <see cref="IModal"/> instance to get field values from.</param> | |||||
/// <param name="options">The request options for this <see langword="async"/> request.</param> | |||||
/// <param name="modifyModal">Delegate that can be used to modify the modal.</param> | |||||
/// <returns></returns> | |||||
public static async Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null, | |||||
Action<ModalBuilder> modifyModal = null) | |||||
where T : class, IModal | |||||
{ | |||||
if (!ModalUtils.TryGet<T>(out var modalInfo)) | |||||
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); | |||||
var builder = new ModalBuilder(modal.Title, customId); | |||||
foreach (var input in modalInfo.Components) | |||||
switch (input) | |||||
{ | |||||
case TextInputComponentInfo textComponent: | |||||
{ | |||||
builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, | |||||
textComponent.MaxLength, textComponent.IsRequired, textComponent.Getter(modal) as string); | |||||
} | |||||
break; | |||||
default: | |||||
throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); | |||||
} | |||||
if (modifyModal is not null) | |||||
modifyModal(builder); | |||||
await interaction.RespondWithModalAsync(builder.Build(), options).ConfigureAwait(false); | |||||
} | |||||
private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action<ModalBuilder> modifyModal = null) | private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action<ModalBuilder> modifyModal = null) | ||||
{ | { | ||||
var builder = new ModalBuilder(modalInfo.Title, customId); | var builder = new ModalBuilder(modalInfo.Title, customId); | ||||
@@ -1,6 +1,7 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.Reflection; | |||||
namespace Discord.Interactions | namespace Discord.Interactions | ||||
{ | { | ||||
@@ -9,6 +10,10 @@ namespace Discord.Interactions | |||||
/// </summary> | /// </summary> | ||||
public abstract class InputComponentInfo | public abstract class InputComponentInfo | ||||
{ | { | ||||
private Lazy<Func<object, object>> _getter; | |||||
internal Func<object, object> Getter => _getter.Value; | |||||
/// <summary> | /// <summary> | ||||
/// Gets the parent modal of this component. | /// Gets the parent modal of this component. | ||||
/// </summary> | /// </summary> | ||||
@@ -40,6 +45,11 @@ namespace Discord.Interactions | |||||
public Type Type { get; } | public Type Type { get; } | ||||
/// <summary> | /// <summary> | ||||
/// Gets the property linked to this component. | |||||
/// </summary> | |||||
public PropertyInfo PropertyInfo { get; } | |||||
/// <summary> | |||||
/// Gets the <see cref="ComponentTypeConverter"/> assigned to this component. | /// Gets the <see cref="ComponentTypeConverter"/> assigned to this component. | ||||
/// </summary> | /// </summary> | ||||
public ComponentTypeConverter TypeConverter { get; } | public ComponentTypeConverter TypeConverter { get; } | ||||
@@ -62,9 +72,12 @@ namespace Discord.Interactions | |||||
IsRequired = builder.IsRequired; | IsRequired = builder.IsRequired; | ||||
ComponentType = builder.ComponentType; | ComponentType = builder.ComponentType; | ||||
Type = builder.Type; | Type = builder.Type; | ||||
PropertyInfo = builder.PropertyInfo; | |||||
TypeConverter = builder.TypeConverter; | TypeConverter = builder.TypeConverter; | ||||
DefaultValue = builder.DefaultValue; | DefaultValue = builder.DefaultValue; | ||||
Attributes = builder.Attributes.ToImmutableArray(); | Attributes = builder.Attributes.ToImmutableArray(); | ||||
_getter = new(() => ReflectionUtils<object>.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo)); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -10,7 +10,6 @@ namespace Discord.Interactions | |||||
internal static class ReflectionUtils<T> | internal static class ReflectionUtils<T> | ||||
{ | { | ||||
private static readonly TypeInfo ObjectTypeInfo = typeof(object).GetTypeInfo(); | private static readonly TypeInfo ObjectTypeInfo = typeof(object).GetTypeInfo(); | ||||
internal static T CreateObject (TypeInfo typeInfo, InteractionService commandService, IServiceProvider services = null) => | internal static T CreateObject (TypeInfo typeInfo, InteractionService commandService, IServiceProvider services = null) => | ||||
CreateBuilder(typeInfo, commandService)(services); | CreateBuilder(typeInfo, commandService)(services); | ||||
@@ -166,6 +165,21 @@ namespace Discord.Interactions | |||||
return Expression.Lambda<Action<T, object>>(assign, instanceParam, valueParam).Compile(); | return Expression.Lambda<Action<T, object>>(assign, instanceParam, valueParam).Compile(); | ||||
} | } | ||||
internal static Func<T, object> CreateLambdaPropertyGetter(PropertyInfo propertyInfo) | |||||
{ | |||||
var instanceParam = Expression.Parameter(typeof(T), "instance"); | |||||
var prop = Expression.Property(instanceParam, propertyInfo); | |||||
return Expression.Lambda<Func<T, object>>(prop, instanceParam).Compile(); | |||||
} | |||||
internal static Func<T, object> CreateLambdaPropertyGetter(Type type, PropertyInfo propertyInfo) | |||||
{ | |||||
var instanceParam = Expression.Parameter(typeof(T), "instance"); | |||||
var instanceAccess = Expression.Convert(instanceParam, type); | |||||
var prop = Expression.Property(instanceAccess, propertyInfo); | |||||
return Expression.Lambda<Func<T, object>>(prop, instanceParam).Compile(); | |||||
} | |||||
internal static Func<object[], object[], T> CreateLambdaMemberInit(TypeInfo typeInfo, ConstructorInfo constructor, Predicate<PropertyInfo> propertySelect = null) | internal static Func<object[], object[], T> CreateLambdaMemberInit(TypeInfo typeInfo, ConstructorInfo constructor, Predicate<PropertyInfo> propertySelect = null) | ||||
{ | { | ||||
propertySelect ??= x => true; | propertySelect ??= x => true; | ||||