Skip to content
Shiny.Maui.Shell v6 support for AI routing tools Learn More

Feedback Service

Every interactive control in the library fires feedback events through IFeedbackService — a single, injectable service that lets you customize how your app responds to user interactions. By default, it triggers haptic feedback. Replace it with text-to-speech, sound effects, analytics, or any combination.

  • NuGet downloads for Shiny.Maui.Controls
Frameworks
.NET MAUI

All Shiny controls that support feedback have a UseFeedback property (default: true). When an interaction occurs — a button tap, a pin digit entered, a toast shown, a message received — the control calls IFeedbackService.OnRequested() with:

ParameterDescription
controlThe actual control instance (e.g. the ChatView, SecurityPin, Button) — use pattern matching like control is ChatView to identify the source
eventNameThe interaction that occurred (e.g. "MessageSent", "Completed", "Clicked")
argsOptional context — for ChatView, this is the ChatMessage object; for standard MAUI controls, the native EventArgs; "LongPress" string for SecurityPin completion

UseShinyControls() registers HapticFeedbackService as the default implementation. It performs a click haptic for most events and a long press haptic when eventName is "LongPress".

builder.UseShinyControls(); // registers HapticFeedbackService by default

Replace the default by calling SetCustomFeedback<T>() during setup:

builder.UseShinyControls(cfg =>
{
cfg.SetCustomFeedback<MyFeedbackService>();
});
public class ChatTtsFeedbackService(ITextToSpeech tts) : HapticFeedbackService
{
public override async void OnRequested(object control, string eventName, object? args)
{
// Let haptic do its thing first
base.OnRequested(control, eventName, args);
// Speak incoming chat messages aloud
if (control is ChatView && eventName == "MessageReceived" && args is ChatMessage { IsFromMe: false } msg)
{
var say = $"Message from {msg.SenderId}. {msg.Text}";
await tts.SpeakAsync(say);
}
}
}
public class SoundFeedbackService : IFeedbackService
{
public void OnRequested(object control, string eventName, object? args)
{
var sound = (control, eventName) switch
{
(SecurityPin, "Completed") => "success.wav",
(SecurityPin, "DigitEntered") => "click.wav",
(ChatView, "MessageSent") => "swoosh.wav",
(ChatView, "MessageReceived") => "chime.wav",
_ => null
};
if (sound is not null)
PlaySound(sound);
}
}

Beyond Shiny’s own controls, you can opt in to automatic feedback for standard MAUI controls — Button, Entry, Slider, Switch, and more. The hook system is fully pluggable and AOT-compatible. It uses Application.DescendantAdded/DescendantRemoved to hook into the visual tree lifecycle, binding and unbinding events automatically with no modifications to the controls themselves.

builder.UseShinyControls(cfg =>
{
cfg.AddDefaultMauiControlFeedback();
});

Add your own control hooks on top of the built-in defaults:

cfg.AddDefaultMauiControlFeedback(x =>
{
x.Hook<MyCustomControl>(nameof(MyCustomControl.Tapped),
(c, h) => c.Tapped += h,
(c, h) => c.Tapped -= h);
});

Use AddMauiControlFeedback to register only the hooks you want:

cfg.AddMauiControlFeedback(x =>
{
x.Hook<Button>(nameof(Button.Clicked),
(btn, h) => btn.Clicked += h,
(btn, h) => btn.Clicked -= h);
x.Hook<Slider, ValueChangedEventArgs>(nameof(Slider.ValueChanged),
(s, h) => s.ValueChanged += h,
(s, h) => s.ValueChanged -= h);
});

Two Hook overloads are available:

  • Hook<TControl>(eventName, subscribe, unsubscribe) — for EventHandler events (args passed as EventArgs)
  • Hook<TControl, TEventArgs>(eventName, subscribe, unsubscribe) — for EventHandler<TEventArgs> events (typed args passed through)

The control parameter is the actual control instance, and args is the native event args object.

ControlEventArgsDescription
ButtonClickedEventArgsButton tapped
EntryTextChangedTextChangedEventArgsText input changed
SliderValueChangedValueChangedEventArgsSlider moved
SwitchToggledToggledEventArgsSwitch toggled
CheckBoxCheckedChangedCheckedChangedEventArgsCheckbox toggled
DatePickerDateSelectedDateChangedEventArgsDate selected
TimePickerTimeChangedPropertyChangedEventArgsTime changed
PickerSelectedIndexChangedEventArgsSelection changed
SearchBarSearchButtonPressedEventArgsSearch submitted
StepperValueChangedValueChangedEventArgsStepper changed
EditorTextChangedTextChangedEventArgsEditor text changed
RadioButtonCheckedChangedCheckedChangedEventArgsRadio button toggled

Events are properly unhooked when controls are removed from the visual tree, ensuring no memory leaks.

ControlEvent NameArgsDescription
ChatViewMessageSentChatMessageUser sent a message
ChatViewMessageReceivedChatMessageExternal message arrived
ChatViewMessageTappedChatMessageUser tapped a message bubble
ChatViewAttachImageUser tapped the attach button
FabClickedFab button tapped
FabMenuToggledMenu opened or closed
FabMenuItemClickedMenu item tapped
FloatingPanelOpenedPanel finished opening
FloatingPanelClosedPanel finished closing
FloatingPanelDetentChangedPanel snapped to a detent
ImageViewerOpenedViewer overlay opened
ImageViewerClosedViewer overlay closed
ImageViewerDoubleTappedDouble-tap zoom toggle
ImageEditorToolModeChangedmode nameActive tool changed (Move, Crop, Draw, Text, Line, Arrow)
ImageEditorUndoUndo action
ImageEditorRedoRedo action
ImageEditorRotateImage rotated
ImageEditorResetReset to original
ImageEditorCropAppliedCrop applied
ImageEditorSavedImage saved
SecurityPinCompleted"LongPress"All digits entered
SecurityPinDigitEnteredA digit was entered
SchedulerCalendarViewDaySelectedCalendar day tapped
SchedulerCalendarViewEventSelectedCalendar event tapped
SchedulerAgendaViewEventSelectedAgenda event tapped
SchedulerAgendaViewTimeSlotSelectedAgenda time slot tapped
CellBase (all cells)TappedTable cell tapped
ToasterShowtoast textToast shown

Set UseFeedback="False" on any control to suppress feedback for that instance:

<shiny:FloatingPanel UseFeedback="False" ... />
<shiny:SecurityPin UseFeedback="False" ... />

For toast:

await toaster.ShowAsync("Silent toast", cfg => cfg.UseFeedback = false);
claude plugin marketplace add shinyorg/skills
claude plugin install shiny-controls@shiny
copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny-controls@shiny
View shiny-controls Plugin