Skip to content
Shiny.NET
Shiny MAUI Shell v7 - App Links, App Shortcuts, & Navigation Interception!Shortcut me to it

MediaPickerButton

A button that lets the user add photos from the gallery and/or camera, compresses/re-encodes each to PNG or JPEG at a chosen quality (with optional max-dimension downscale), caps the count with MaxPhotos (added one at a time — the OS pickers are single-select), and shows the collected photos inline as a tappable carousel (opening the ImageViewer, with an optional Edit button that reuses the ImageEditor) or a compact pinch/zoom overlay. It ships inside the base packages — no extra dependency.

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

MAUI

MediaPickerButton

Blazor

MediaPickerButton on Blazor
<shiny:MediaPickerButton Photos="{Binding Photos}"
AllowGallery="True"
AllowCamera="True"
AllowPhotoEdit="True"
ShowAsCarouselInView="True"
MaxPhotos="5"
CompressionQuality="85"
OutputFormat="Jpeg"
PermissionDeniedText="Photo access was denied — enable it in Settings." />
[ObservableProperty]
ObservableCollection<MediaPickerItem> photos = new(); // Shiny.Maui.Controls.Media

OutputFormat is Shiny.Maui.Controls.ImageEditor.ImageExportFormat (Png / Jpeg).

<MediaPickerButton @bind-Photos="photos"
AllowGallery="true"
AllowCamera="true"
AllowPhotoEdit="true"
ShowAsCarouselInView="true"
MaxPhotos="5"
CompressionQuality="85"
OutputFormat="jpeg"
PermissionDeniedText="Photo access was denied." />
@code {
IReadOnlyList<Shiny.Blazor.Controls.MediaPickerItem> photos = [];
}

On Blazor OutputFormat is a string ("jpeg" / "png"), and each MediaPickerItem exposes a DataUri you can bind directly to <img src>.

A picked photo stays in the browser. What crosses into .NET is a descriptor — id, an object URL for <img src>, dimensions, content type and size — and the bytes move only when something asks for them, always as binary:

You want What happens
MediaPickerItem.Data Filled from a chunked IJSStreamReference when the photo is picked (the default)
await item.OpenReadStreamAsync() / ReadAllBytesAsync() The same stream, on demand
UploadUrl set The browser posts the photo as multipart form data itself — the bytes never enter .NET

Nothing is base64-encoded on any path. That matters most on Blazor Server: a base64 photo is one SignalR message, and a 4MB photo becomes a 5.4MB string — far past the default 32KB MaximumReceiveMessageSize, so the server closes the circuit and the upload never happens.

<MediaPickerButton @ref="picker"
@bind-Photos="photos"
MaxPhotos="5"
MaxImageDimension="1600"
CompressionQuality="85"
UploadProgress="OnProgress" />
@code {
MediaPickerButton? picker;
IReadOnlyList<MediaPickerItem> photos = [];
int percent;
// Save the record FIRST, so each photo has something that exists to be addressed to.
async Task Save()
{
var id = await SaveTheThing();
var results = await picker!.UploadAllAsync(new MediaPickerUpload($"/photos/{id}")
{
Headers = new Dictionary<string, string> { ["X-Upload-Ticket"] = ticket }
});
foreach (var failed in results.Where(r => !r.Success))
... // still picked, so "try again" costs the user nothing
}
void OnProgress(MediaPickerUploadProgress p)
{
percent = p.Percent ?? 0;
StateHasChanged();
}
}

AutoUpload="true" sends each photo the moment it is picked, for when the address is known up front. Uploaded reports each result, and the server’s answer comes back verbatim in MediaPickerUploadResult.Body. A successful upload takes the photo out of the picker unless you pass keep: true, which is how the same photos go to more than one place.

Property Type Default Description
UploadUrl MediaPickerUpload? null Where the browser posts each photo — Url, FieldName (default file), FileName, Method, Headers, WithCredentials
AutoUpload bool false Upload as soon as a photo is picked, rather than waiting for UploadAllAsync
LoadBytes bool? null Fill MediaPickerItem.Data on pick. Defaults to true, or false when UploadUrl is set
MaxReadSize long 32MB Refuses to read a photo larger than this into .NET

Methods: UploadAllAsync(upload?, keep?, ct), UploadAsync(item, upload?, keep?, ct), ClearAsync().

Property Type Default Description
AllowGallery bool true Offer “choose from gallery”
AllowCamera bool true Offer “take photo”. When both are enabled, tapping shows a gallery/camera chooser
AllowPhotoEdit bool false Show an Edit button in the viewer that opens the ImageEditor; edits are re-saved into the collection
PermissionDeniedText string “Permission denied…” Message shown when camera/gallery access is denied
NoImagesTemplate DataTemplate? / RenderFragment? null Shown when there are no photos yet (default: “No photos yet”)
ShowAsCarouselInView bool true true → inline thumbnail carousel; false → compact preview that opens a paged pinch/zoom overlay
MaxPhotos int 1 Maximum photos; the add trigger hides once reached
CompressionQuality int 92 Encoder quality as a percentage (1–100)
MaxImageDimension int 0 If > 0, the longest edge is downscaled to this many pixels
OutputFormat ImageExportFormat / string Jpeg / "jpeg" Output encoding — PNG or JPEG
Photos IList / IReadOnlyList of MediaPickerItem empty The collected photos (two-way)
AddButtonText string “➕ Add Photo” Text on the add trigger
GalleryActionText / CameraActionText string “Choose from Gallery” / “Take Photo” Chooser labels
UseFeedback bool (MAUI) true Haptic/sound feedback on actions
  • PhotoAdded / PhotoRemoved — a photo was added or removed (MediaPickerItem).
  • PhotosChanged (MAUI event + PhotosChangedCommand; Blazor EventCallback) — the collection changed.
  • PermissionDenied — camera/gallery access was denied (message string).
  • Uploaded (Blazor) — one photo finished uploading: MediaPickerUploadResult with Success, StatusCode and the server’s Body.
  • UploadProgress (Blazor) — how far a photo’s upload has got, from the browser’s own upload events (Percent is null when the size is unknown).
  • Multi-photo is add-one-at-a-time — each tap picks/captures a single photo and appends until MaxPhotos. The OS pickers do not offer native multi-select here.
  • MAUI is a plain ContentView using the built-in Microsoft.Maui.Media.MediaPicker — no handler or builder registration. The MediaPicker drives the OS permission prompts; a denial surfaces PermissionDeniedText.
  • Blazor self-imports its JS module (no DI). Gallery/camera use a hidden <input type="file" accept="image/*"> (with capture="environment" for the camera); compression/resize/format-conversion happen on an offscreen canvas, and the encoded blob is kept there — see Where the bytes live.
  • Viewing reuses the ImageViewer (pinch/zoom); editing reuses the ImageEditor.
claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One plugin installs all 36 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One plugin installs all 36 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

View shiny-controls Skill