Every tap, swipe, and keystroke in your app is an opportunity. An opportunity to confirm the user’s action, guide their attention, or add a layer of polish that separates “functional” from “delightful.” Most apps handle this with scattered HapticFeedback.Default.Perform() calls sprinkled across code-behind files. It works — until you want text-to-speech for accessibility, sound effects for a kiosk app, analytics for product telemetry, or different feedback for different controls. Then you’re threading conditional logic through every view in your app.
Shiny Controls v1.0 ships with IFeedbackService — a single injectable service that every interactive control in the library already calls. You implement it once. Every control uses it automatically.
Every Shiny control that supports feedback has a UseFeedback property (default: true). When a user interaction occurs — a message sent, a pin digit entered, a panel opened — the control calls IFeedbackService.OnRequested() with three things:
control — the actual control instance, not a Type. Pattern match directly: control is ChatView, control is SecurityPin.
eventName — what happened: "MessageReceived", "DigitEntered", "Opened".
args — contextual data. For ChatView, this is the full ChatMessage object. For standard MAUI controls, it’s the native EventArgs. For SecurityPin completion, it’s "LongPress".
The default HapticFeedbackService does what you’d expect — click haptic for most events, long press haptic for completion events. But the real power is in replacing it.
Here’s a real example from our sample app. One service, three behaviors — haptic, text-to-speech for incoming chat messages, and audio cues for PIN entry:
Because control is the live instance and args carries typed data, you can make nuanced decisions without parsing strings. The ChatMessage gives you sender, timestamp, text, and image URL. The SecurityPin instance gives you its current value and length. Cast, match, and go.
Shiny’s own controls call IFeedbackService internally. But what about standard MAUI controls — Button, Slider, Entry? The MauiControlFeedbackBuilder hooks them in automatically, with an AOT-compatible, fully pluggable design:
This registers hooks for 12 standard MAUI controls — Button.Clicked, Entry.TextChanged, Slider.ValueChanged, Switch.Toggled, and more. Each hook passes the control instance as control and the native event args as args.
Hook<TControl>(eventName, subscribe, unsubscribe) for plain EventHandler events
Hook<TControl, TEventArgs>(eventName, subscribe, unsubscribe) for typed EventHandler<TEventArgs> events
Under the hood, each hook uses a ConditionalWeakTable to track handlers per control instance — no leaks, no dictionaries to manage, proper unsubscription when controls leave the visual tree. Zero reflection, fully AOT-safe.
Most feedback systems are either too simple (a global haptic toggle) or too complex (per-control event subscriptions scattered across your app). IFeedbackService sits in the sweet spot:
One service, all controls. Implement once, every control calls it.
Instance, not type. You get the actual control, not typeof(Button). Inspect properties, check state, make decisions.
Typed args, not strings.ChatMessage, ValueChangedEventArgs, ToggledEventArgs — not "the message text".
Pluggable hooks, not hardcoded events. Add your own controls to the system with three lambdas.
AOT-safe. No reflection, no expressions, no Delegate.CreateDelegate. Just generics and delegates.
Whether you’re building an accessible app that speaks every incoming message, a kiosk that plays sound effects, or just want consistent haptic feedback across your entire UI — IFeedbackService is one implementation away.
Check out the full documentation and the sample app for a working demo with TTS and audio integration.
Shiny.DocumentDb.Extensions.AI turns your document store into a set of LLM-callable tools. Register types, set capabilities, and let the agent query, insert, update, and delete documents through natural language.
What if every service interface you already have could become an AI tool with a single attribute? Shiny Extensions DI 3.0 makes that happen — no adapter classes, no hand-rolled schemas, no registration boilerplate. Mark your interface with [Tool], add [Description] to the methods that matter, and the source generator handles the rest.
You’ve built your services. Clean interfaces, proper DI registration, everything wired up. Now someone asks you to expose a few of those operations as AI tools for an LLM agent. Suddenly you’re writing AIFunction subclasses by hand — one per operation — each with a constructor that takes the service, a metadata property with hand-written parameter schemas, and an InvokeCoreAsync override that extracts arguments from a dictionary and forwards them to your service method.
For one or two tools, it’s fine. For ten or twenty, it’s tedious. And every time you change a method signature, you have to remember to update the corresponding tool class. The schema drifts, the argument parsing breaks, and the bugs only show up when the LLM calls the tool at runtime.
[Description("Number of units to order")] intquantity
);
[Description("Cancels an existing order")]
TaskCancelOrderAsync(
[Description("The order to cancel")] GuidorderId,
[Description("Reason for cancellation")] stringreason
);
// No [Description] — not exposed as a tool
Task<List<Order>> GetInternalAuditLogAsync();
}
That’s it. The source generator produces a fully typed AIFunction subclass for each described method, wires up the parameter metadata, and generates a registration extension — all at compile time.
The AI tool code is only generated when Microsoft.Extensions.AI is referenced in your project. If you don’t reference it, the [Tool] attribute still compiles (it’s just an attribute), but no AIFunction classes or registration code are emitted. This means existing projects that add the DI package won’t get unexpected dependencies.
The generated InvokeCoreAsync handles the JsonElement-vs-already-deserialized argument problem that trips up most hand-written AI tools. For every standard type, the generator emits a direct JsonElement accessor:
Type
Extraction
Reflection-free
string
GetString()
Yes
int, long, short, byte
GetInt32(), GetInt64(), etc.
Yes
bool
GetBoolean()
Yes
double, float, decimal
GetDouble(), GetSingle(), GetDecimal()
Yes
Guid
GetGuid()
Yes
DateTime
GetDateTime()
Yes
DateTimeOffset
GetDateTimeOffset()
Yes
DateOnly, TimeOnly, TimeSpan
Parse(GetString())
Yes
Enums
Enum.Parse<T>(GetString())
Yes
Complex types
JsonSerializer.Deserialize<T>()
Needs JsonSerializerContext
If the argument arrives as a JsonElement (common when the framework hasn’t pre-deserialized), the correct accessor is used. If it arrives already typed (some frameworks do this), a direct cast is used. Both paths are handled with a single is JsonElement check — no try/catch, no Convert.ChangeType.
If your service method accepts a CancellationToken, the generator does the right thing automatically:
[Description("Searches products")]
Task<List<Product>> SearchAsync(
[Description("Search query")] stringquery,
CancellationTokencancellationToken// not exposed as a tool parameter
);
The CancellationToken is excluded from the tool’s parameter metadata and properties. In InvokeCoreAsync, it’s passed through from the framework’s cancellation token — not extracted from the argument dictionary.
Only methods with [Description] become tools. This gives you fine-grained control over what’s exposed to the LLM. Internal methods, admin operations, or anything you don’t want an AI agent calling — just don’t add the attribute.
The [Tool] attribute goes on interfaces, while [Singleton] / [Scoped] / [Transient] go on implementation classes — same as before. You keep using AddGeneratedServices() for your service registrations and add AddGeneratedAITools() alongside it:
services.AddGeneratedServices();
services.AddGeneratedAITools(); // only if M.E.AI is referenced
The two generators are independent. AI tool generation doesn’t affect or depend on your service registrations.
What if you could write a single C# record and have it automatically become a fully typed AI tool — with zero adapter code? That’s what Shiny Mediator 6.3 delivers.
Building AI tool calling today means writing repetitive adapter code. You define a JSON schema by hand, parse arguments from the LLM response, validate them, call your business logic, and serialize the result back. If you already have a mediator contract for the same operation, you’re duplicating intent across two representations. Multiply that by every tool your agent needs — ten, twenty, fifty tools — and it becomes a real maintenance problem.
Worse, the schema and the code drift apart. You rename a property in your contract but forget to update the JSON schema. You add a new required parameter but the tool adapter still treats it as optional. The LLM hallucinates a parameter name that used to exist, and your hand-written parser silently swallows the error. These bugs are subtle, hard to test, and only surface at runtime.
Because the generated AI tools dispatch through the mediator pipeline, every middleware you’ve already configured applies to AI tool calls automatically. Logging, validation, authorization, exception handling, caching — all of it fires without any extra wiring.
This is a significant advantage over hand-rolled AIFunction implementations. When you write a tool adapter manually, it typically calls your service layer directly, bypassing cross-cutting concerns. With the mediator approach, an AI tool call follows the same pipeline as a UI-triggered action or an API call. Your audit log captures it. Your validation middleware rejects bad input before the handler runs. Your error handling middleware catches exceptions and returns structured errors the LLM can interpret.
You can even write middleware that targets AI calls specifically — for example, injecting a MediatorContext value that tells the handler the call originated from an LLM, so you can apply tighter authorization or rate limiting for AI-initiated operations.
The real power shows when your agent needs many tools. Instead of maintaining dozens of AIFunction subclasses with hand-written schemas, you just add [Description] to your existing contracts. Every contract with a description attribute becomes a tool at the next build.
Adding a new tool to your agent is the same workflow as adding any new mediator operation:
Define the contract record with [Description]
Implement the handler
Done — the tool is registered automatically
No schema files to maintain. No adapter classes to write. No registration code to update. The source generator handles the JSON schema, argument parsing, DI wiring, and AIFunction implementation.
This also means removing a tool is just deleting the [Description] attribute (or the contract itself). There are no orphaned adapters or stale schema definitions to clean up.
The same contract-first approach extends beyond AI tooling. Shiny Mediator also generates HTTP clients and ASP.NET endpoints from your contracts — meaning a single record and handler can serve as an AI tool, a typed HTTP client, and a REST endpoint simultaneously. The transports are generated; you write the logic once.
Traditional tool-calling setups require you to maintain parallel definitions:
Layer
Without Mediator
With Mediator
Business logic
Handler class
Handler class
AI tool schema
Manual JSON schema
Generated from contract
AI tool adapter
Manual AIFunction subclass
Generated
Argument parsing
Manual deserialization
Generated
DI registration
Manual for each tool
Generated
Middleware/validation
Manual per tool
Automatic via pipeline
With the contract-first approach, adding a new capability to your application — whether it’s exposed as an AI tool, an HTTP endpoint, or both — is one record and one handler.
The generated AIFunction classes are fully Native AOT compatible. Here’s what makes that possible:
No reflection. The generator reads [Description] attributes, property types, nullability, and default values at compile time. It emits direct property access code — json.GetProperty("city").GetString()! — instead of relying on JsonSerializer.Deserialize<T>() or reflection-based binding.
Static JSON schema. The schema is a compile-time constant string parsed once into a JsonElement on first use. There’s no runtime schema construction, no JsonSerializerOptions configuration, and no dynamic type inspection.
Constructor-based hydration. The generated code constructs the contract using its primary constructor with named arguments. No Activator.CreateInstance, no FormatterServices, no property setters via reflection.
Concrete types throughout. Each generated class is a sealed, non-generic concrete type. The DI registrations are explicit AddSingleton<AITool>(sp => new GetWeatherAIFunction(...)) calls — no open generics or service descriptor scanning at runtime.
This means your AI tools work in trimmed, ahead-of-time compiled applications — including .NET MAUI apps targeting iOS and Android — without linker warnings or runtime failures. The same tools that power your cloud API also run on-device in a fully native binary.
Add the [Description] attribute to your contracts and their properties
Set <ShinyMediatorGenerateAITools>true</ShinyMediatorGenerateAITools> in your project file
Reference Microsoft.Extensions.AI
Call .AddGeneratedAITools() during mediator setup
Resolve IEnumerable<AITool> from DI and pass to your chat client
Every contract with a [Description] attribute automatically becomes a tool. Add a new contract, and the next build picks it up — no registration changes, no schema files, no adapter classes.
What if your app could understand “My furnace is broken — it’s urgent!” and automatically open the right form with the description filled in and the priority set to Urgent? That’s exactly what the new AI integration in Shiny MAUI Shell does.
Mobile apps have dozens of pages. Users have to know where things are, tap through menus, and manually fill in fields. But with AI chat becoming the norm, we asked: what if the AI could navigate your app for you?
Shiny Shell’s source generator already knows every route in your app and every parameter each page accepts. We just needed to make that metadata available to an AI model — and give it a way to act on what it discovers.
Instead of registering a separate AI tool for every page (which doesn’t scale), we generate just two:
GetAiToolApplicableGeneratedRoutes() — returns all routes that have intent descriptions and parameters. The AI calls this to discover what pages exist and what they do.
NavigateToRoute() — accepts a route name and a Dictionary<string, string> of parameters. The AI calls this to navigate and pre-fill the form.
That’s it. Add a new page with [ShellMap] descriptions and [ShellProperty] inference hints, and the AI automatically discovers it. No tool registration changes needed.
The key insight is that descriptions should express user intent, not page names:
// Good — the AI matches "my pipe burst" to this route
[ShellMap<WorkOrderPage>(description:"Use when the user reports something broken,
malfunctioning, needing repair, maintenance, or service")]
// Bad — the AI has to guess what "Work order page" means
[ShellMap<WorkOrderPage>(description: "Work order page")]
Similarly, property descriptions tell the AI how to infer values from natural language. Properties can use real types — enums, ints, bools — and the generator handles conversion automatically:
[ShellProperty("Infer urgency from the user's tone. Must be: Low, Medium, High, or Urgent", required:true)]
public WorkOrderPriority Priority { get; set; } = WorkOrderPriority.Medium;
The AI sends "Urgent" as a string, and the generated NavigateToRoute converts it to WorkOrderPriority.Urgent via case-insensitive Enum.Parse. The same works for int, bool, double, DateTime, Guid, and other common types.
Note that AI-compatible ViewModels do not need to implement IQueryAttributable. The generated NavigateToRoute sets [ShellProperty] properties directly on the ViewModel instance — no query attribute plumbing required.
The source generator produces GeneratedRouteInfo metadata with full parameter schemas:
publicrecordGeneratedRouteInfo(
stringRoute,
stringDescription,
GeneratedRouteParameter[] Parameters
);
publicrecordGeneratedRouteParameter(
stringParameterName,
stringDescription,
stringTypeName,
boolIsRequired
);
The AI model sees the route descriptions, parameter names, types, requirements, and inference hints — everything it needs to match intent and extract values.
AI extensions are now enabled by default — just install Microsoft.Extensions.AI:
Terminal window
dotnetaddpackageMicrosoft.Extensions.AI
Register the generated AiMauiShellTools class via the AddAiTools() extension:
builder.UseShinyShell(x=> x
.AddGeneratedMaps()
.AddAiTools() // registers AiMauiShellTools as singleton
);
Then inject AiMauiShellTools wherever you need AI-powered navigation. It provides a Prompt property (pre-formatted route descriptions for seeding system messages) and a Tools property (ready-to-use AITool[]):
The sample app includes a full working demo with GitHub Copilot authentication. Users authenticate with their own GitHub account through the OAuth device flow, and the app uses the Copilot API as the chat backend. Try saying things like:
“My furnace is not working! URGENT” — opens the work order form with description and priority filled in
“I’d like to discuss a partnership. My name is Allan, email allan@test.com” — opens the contact form with fields populated
It’s been a long road, but Shiny Client v4 is here. This is a major release that brings Windows support, moves to .NET 10, and packs in a significant number of fixes and enhancements
across almost every module. Let’s dig in.
The headline feature of v4 — BluetoothLE, BLE Hosting, HTTP Transfers, and Locations all now work on Windows. Background support isn’t available yet on the Windows platform, but
foreground scenarios are fully supported. This opens up a whole new set of use cases for desktop and kiosk applications built with .NET MAUI.
HTTP Remote Configuration has been moved directly into Shiny.Extensions.Configuration. No more separate package — remote config is now a first-class citizen in the configuration stack.
Locations got a lot of love in this release, particularly on iOS where we’ve adopted the newer Apple APIs:
iOS 18+ now uses CLMonitor for GPS — the modern replacement for the legacy location APIs
New geofence registration mechanics for iOS 17+ using the new CLMonitor API
Geofence Manager RequestState now works correctly on the new CLMonitor API
GPS background permission on iOS is now requested immediately instead of waiting
GpsDelegate now has a boolean to detect if the device is stationary — useful for battery optimization and movement detection
GpsDelegate batch fix — the base calculations could receive a batch and trigger multiple calculations. This is now a synchronized operation
Android background location permission — no longer requests ACCESS_BACKGROUND_LOCATION unless realtime GPS with less than API 31 or standard background tracking is being used
HTTP Transfers received some of the most impactful enhancements in v4:
New transfer types — Transfers can now be UploadMultipart, UploadRaw (body is raw bytes), or Download. The UploadRaw type is critical for sending directly to services like Azure Blob Storage
Azure Blob Storage helper — AzureBlobStorageRequest.CreateForAzureBlobStorage static helper method makes it dead simple to queue uploads to Azure Blob Storage
New HttpTransferDelegate — allows you to set retries and detect denied authorization, enabling you to refresh your token and issue a new request
Thread-safe HttpTransferMonitor — now uses a thread-safe BindingList
Android file validation — uploads now verify the file exists before queuing, and download directories are checked before queuing
iOS special character fix — filenames with special characters are now sent properly, along with improved form data upload
v2.0.0 is now available. This release focuses on flexibility — you can now map document types to dedicated SQLite tables, use custom Id properties, diff objects against stored documents, batch insert collections efficiently, customize the default table name, and use the core library without any dependency injection framework.
The core Shiny.SqliteDocumentDb package no longer depends on Microsoft.Extensions.DependencyInjection.Abstractions. If you use AddSqliteDocumentStore(), add the new package. If you instantiate SqliteDocumentStore directly, no changes are needed.
}.MapTypeToTable<User>() // auto-derives table name → "User"
.MapTypeToTable<Order>("orders") // explicit table name
);
MapTypeToTable<T>() — auto-derives the table name from the type using the configured TypeNameResolution
MapTypeToTable<T>(string tableName) — maps to an explicit table name
Fluent API — calls chain for concise configuration
Duplicate protection — mapping two types to the same table throws ArgumentException
AOT-safe — type names are resolved at registration time, not at runtime
Tables are lazily created on first use with the same schema (Id, TypeName, Data, CreatedAt, UpdatedAt) and composite primary key. This works seamlessly with all store operations including transactions, the fluent query builder, projections, indexes, and streaming.
Types mapped to a dedicated table can use an alternate property as the document Id instead of the default Id. The property must be Guid, int, long, or string — the same types supported for the standard Id property.
Auto-generation rules still apply — Guid and numeric Ids are auto-generated when default, and the value is written back to the mapped property after insert. Custom Id remapping is only available through MapTypeToTable, keeping the shared table convention simple.
Compare a modified object against the stored document and get an RFC 6902 JsonPatchDocument<T> describing the differences. Returns null if the document doesn’t exist. Powered by SystemTextJsonPatch.
varproposed=newOrder
{
Id ="ord-1", CustomerName ="Alice", Status ="Delivered",
ShippingAddress =new() { City ="Seattle", State ="WA" },
// Apply the patch to any instance of the same type
varcurrent=await store.Get<Order>("ord-1");
patch!.ApplyTo(current!);
The diff is deep — nested objects produce individual property-level operations (e.g. /shippingAddress/city), while arrays and collections are replaced as a whole. Works with table-per-type, custom Id, and inside transactions.
BatchInsert inserts multiple documents in a single transaction with prepared command reuse for optimal performance. Returns the count inserted. If any document fails (e.g. duplicate Id), the entire batch is rolled back. Auto-generates IDs for Guid, int, and long Id types.
The AddSqliteDocumentStore() API is unchanged — just a different package.
If you instantiate directly, no changes required. The default behavior is identical to v1 — all documents go to a table called "documents".
Table-per-type and custom Id are opt-in. Existing databases continue to work without any changes. You can incrementally adopt table mapping for specific types while keeping everything else in the shared table.
Only a little over 3 months, I released v1.0 of Mediator. Since then, I’ve been adding features as fast as they come to mind. The focus on making the Mediator
pattern upfront for apps (in my opinion) has been a huge success. I’ve been using it in my MAUI apps for offline, caching, & resiliency middleware. The amount of time
it saves me is pretty crazy.
Since version the v1 release, I’ve been adding a ton of features including an ASP.NET extension for HTTP endpoints straight to Mediator request handlers. Quite recently - I found a lot of mediated calls would end up just wrapping HTTP calls done with Refit or Kiota. I decided to add my
own source generator to deal with this. Check out the HTTP Extension for more on this.
So what does 2.0 bring that requires a major version bump? The first major feature is that I’ve brought the offline, stream replay, and user error notification middleware to Blazor webassembly. This middleware was already
present in MAUI, but Blazor needed a “connectivity” & “storage” service to match MAUI.
The second major feature is that I’ve been moving a lot of the middleware to be configured via Microsoft.Extensions.Configuration. This allows you to configure
middleware and handlers in a global way and without polluting your code with attributes everywhere. Below is an example of all configuration we offer now:
So let’s unpack this. The first thing to notice is that we have ”*” to “glob” a namespace or ALL calls. If you want to attack a specific contract, just fully label it.
We will find the nearest namespace to your contract before giving up as “not enabled” for a feature to be disabled on a contract.
I’m pretty happy with this release, but I still have a ton of ideas. All the stuff you can do around the handlers with middleware is really exciting. Have an idea for the mediator,
head on over to GitHub and add a feature request. I’m always looking for new ideas.
Shiny Mediator is something new I’ve been working on. I love Jimmy Bogard’s MediatR library on the server, but I just couldn’t
get it to fit the way I wanted for Apps… especially Blazor & .NET MAUI apps.
What is a mediator? It’s a small in-process version of a message bus. MAUI has MessagingCenter, the Community toolkit has the weak message center, and Prism
offers an event aggregator. They’re all great, but they lack in areas that I want “more”.
If an event errors, the whole chain dies in the publish
Events are fired in a foreach (mostly)
You have to tie into them and unsubscribe from them or you can leak memory
They don’t provide the concept of command or request/response models (a command can only be responded to by a single handler)
They don’t provide any sort of middleware (pre & post handling)
Sure - they aren’t geared for these things, but the question is “what is”?
Some might say “this is overengineering” or “too complex”. I would counter that comment by saying that this can actually simplify your architecture
overall by removing a lot of complex plumbing around services, references, and navigation.
Let’s go over some of the problems that we use mediator to solve within our apps
Do you use the MessagingCenter in Xamarin.Forms? It’s a great tool, but it can lead to some memory leaks if you’re not careful. It also doesn’t have
a pipeline, so any errors in any of the responders will crash the entire chain. It doesn’t have a request/response style setup (not that it was meant for it), but
this means you still require other services.
Our amazing friends over in Prism offer the “best in class” MVVM framework. We’ll them upsell you beyond that, but one
of their amazing features is ‘Modules’. Modules help break up your navigation registration, services, etc.
What they don’t solve is providing a strongly typed nature for this stuff (not their job though). We think we can help
addon to their beautiful solution.
A normal call to a navigation service might look like this:
This is great. It works, but I don’t know the type OR argument requirements of “MyPage” without going to look it up. In a small project
with a small dev team, this is fine. In a large project with a large dev team, this can be difficult.
Through our Shiny.Framework library we offer a GlobalNavigationService that can be used to navigate to any page in your app from anywhere, however,
for the nature of this example, we’ll pass our navigation service FROM our viewmodel through the mediator request to ensure proper scope.
Lastly, we offer some absolutely epic middleware that you can use to do things like logging, caching, error handling, etc.
Check out our middleware documentation for more information.
Check out our overall documentation for more information on how to use the mediator in your app. We think you’ll love it!
This release was mainly a large bugfixing release with some cool additions around Push Notifications
However, we did decide to remove two modules from the Shiny offering
Shiny.Logging.AppCenter - this was an easy one. Microsoft is shutting down AppCenter in 2025.
Shiny.SpeechRecognition - this was a tough one. The plugin was never really that great and it was a pain to maintain. The MAUI community toolkit recently released a plugin here that you can use.
We’ve improved the push delegate to handle MORE native stuff, more events like UnRegister to centralize your registration & now the unregistration process.
Apple has a new privacy requirement that you must disclose what you are doing with the user’s data. This is a requirement for all apps in the App Store. The Shiny templates have been updated to include
a new file under the iOS platform that you can fill out to help you comply with this requirement.
We’ve introduced two “guestimate” type helpers to get users rolling. Our Shiny Templates and our App Builder tool will help you generate these files. We generate what most user app requirements
will be in terms of user identity info, as well as all of the necessary Shiny & .NET BCL requirements. We also insert any necessary location permissions if you select one of our location based components.
The templates have been great for Shiny discovery. Wiring up all of the permissions with Android & iOS is difficult. I find that I’m constantly using
App Builder here on shinylib.net to generate them all for me because I constantly forget them all. Cutting a new proof-of-concept or test with
Shiny or any of the whack of 3rd party libraries that are included with the library is a breeze.
This release got the following love:
Removal of AppCenter - maybe this is the opposite of love - but don’t use it since it is officially done in 2025
Updated to the latest versions of many nuget packages
Updated some of the Shiny stuff to 3.3
NEW base template for the upcoming Apple Privacy Manifests under Platforms/iOS
The MAUI Project template is so large that it takes 3 screenshots to get it all.
Setting up push notifications with Apple & Google can be a real pain. This tool will help you test your setup end-to-end. It’s a
simple mobile app that will send a push notification to your app using Azure Notification Hubs or Firebase Cloud Messaging. It’s a great way to test your setup without having to write a bunch of code.
It isn’t pretty, but it does a good job of helping you test your setup without having to wire up a backend or write a bunch of code.
Slim bindings are revolutionary by any means, but they allow you to control the native API surface that you have to bind to. With Swift becoming increasingly popular and not having a true native binding solution with
.NET at this time, Slim bindings allow us to control this narrative by building swift code, but still marking the code properly with objective-C headers to be able to bind within .NET
If you’re a user of Shiny.Push.FirebaseMessaging, the good news is that there will be a direct nuget package update in v3 that will allow you to continue using Firebase messaging on iOS without any other code updates. This update
for Shiny will be available long before the Firebase June 2024 deadline
There are still people using my old dinosaur of a package, so I updated it to the latest AndroidX stuff and .NET 8.
Unfortunately, classic Xamarin and netstandard targets are now gone, but at least it pushes the needle forward if you’re still using
this package.
We may introduce Windows support back into the mix in the future, but it’s not a priority right now.
Today, we released version 3.2 of Shiny that targets .NET 8 along with Classic Xamarin targets. We decided to move with the supported version of MAUI. This also makes things easier
for specific support targets with AndroidX which can be very difficult to multitarget with.
This release also includes the ability to check for the current setup permission without requesting from the permission from the user. This is surprisingly difficult to implement for all modules, so for
now, the following libraries have this:
We took our time, but we feel version 3 touched everything and we did it in our free time, so its a pretty huge release. Everything from new
platforms, new features, new architecture, smaller footprints and new documentation. We hope you like it.
There’s a ton to talk about and we’ll only scratch the surface here. Be sure to check out our Shiny NEW Shiny documentation for more information.
One of our biggest features was our own hosting framework to allow Shiny to plugin into any of the .NET ecosystem including classic Xamarin targets because even some of my
applications aren’t moved yet. We’ve also added Mac Catalyst support to almost all of the Shiny modules.
Obviously, our prime target with v3 though… was .NET MAUI. .NET MAUI makes it easier than ever to get going with Shiny or any other ecosystem plugin.
To get Shiny working with MAUI, install Shiny.Hosting.Maui from nuget and add the following to your MauiProgram.cs
usingShiny;
namespaceMyApp;
publicstaticclassMauiProgram
{
publicstaticMauiAppCreateMauiApp()
{
varbuilder= MauiApp
.CreateBuilder()
.UseMauiApp<App>()
// this is the important line - this wires in all of the lifecycle and base services
HTTP Transfers is honestly, one of the most key components to most apps these days. Any apps that are sending photos, videos, or other large files need to be able to do this in the background as users tend to
“fire and forget” (add the photo, return to home screen). I’m also surprised how many apps do downloads these days, but show indeterministic spinners…!? Let me know how long you think things are going to take and how much
progress has been made. This is another mechanism that is available as part of v3.
As much as you would like to just open up an HTTP client, download a file or upload a stream to a directory… it doesn’t quite work like that on native platforms. iOS & Android
will both kill your transfer within seconds of the app going to the background… unless you play by the rules. On iOS, you hand over to a native process to let it do the hard work.
On Android, well… you get the HTTP client, but you need to do something to keep it alive. There is a built in download manager in Android, but it’s old and doesn’t offer a ton of features, so
we ditched it.
Let’s take a look at how to setup just basic background transfer for now. We’ll cover other features in the docs and future blog posts:
MAUI/Xamarin Essentials are great. They hit all of the platforms. Once you get past the essentials though and get into big enterprise applications,
you need some stronger issues. The only provider currently offering full blown background GPS tracking for Android & iOS on .NET.
Geofencing hasn’t changed much from v3 API wise and still continues to be the only known geofence provider for .NET mobile.
With BLE, we reevaluated the entire API surface for simplicity while shedding some of the fat.
First off, When working with a peripheral, all methods are fired from the peripheral instead of digging down into the service and characterisitic objects. Undernearth the hood, we can
keep track of all the characteristics properly across connections which is notoriously a difficult problem with BLE.
IPeripheralperipheral; // scan it, select it, etc
peripheral.GetCharacteristic("Known Service UUID", "Known Characteristic UUID").Subscribe(args=> {});
You’ll notice all of the methods above use strings for UUID arguments. In version 2, we used GUIDs which made it harder for users to work with 16bit UUIDs.
And lastly, in version 2, we have the Managed Peripheral that took care of things like reconnecting, rehooking characteristics, and funky stuff that generally makes BLE hard. This is now
all built into the regular peripheral. As long as you hook a characteristic, we’ll restore it across connection blips and disconnections.
IPeripheralperipheral; // scan it, select it, etc
varsub= peripheral
.NotifyCharacteristic("Known Service UUID", "Known Characteristic UUID")
.Subscribe(args=>
{
// do something with args
args.Characteristic // characteristic info
args.Data // byte array containing the data
});
// make sure to keep a reference to the subscription and dispose when you're done
sub.Dispose();
BEFORE - you would have had to monitor the IPeripheral.WhenConnected(), then get the characteristic, and lastly hook it. This sucked and it wasn’t without its pain.
We’ve also taken the time to review and remove a lot of junk code that just wasn’t needed in our efforts to reduce our footprint.
We’ve made all of our events async which is a big deal in the fact that everything is async these days. We also added a new “managed model” that makes setting up a hosted characteristic dead simple. Check it out:
[BleGattCharacteristic("My Service UUID", "My Characteristic UUID")]
Periodic Jobs as a whole looks the same, but under the hood - we’ve made a bunch of improvements to cleaning out old jobs (type was deleted or moved will now remove the job) and ensuring system
jobs are registered “fresh” every time to ensure consistency. We’ve also made jobs work much like our stateful services where you can make your job look like a viewmodel
and have its state persisted across app restarts and runs with nothing more than a simple get/set mvvm style.
We’ve also added a base job that you can have to record things
This is one of our “smaller” modules, but we still love it. Push providers come and go, but the underlying push mechanisms remain the same. As such,
we’ve split the provider from the native mechansim and thus allowing you to work with the native mechanism the same way all the time, but plugging in a provider “on top” that Shiny
will call into. Take a look at our Push Provider Documentation for more info.
Out of the box, we obviously support native, but we also support basic features for Azure Notification Hubs and Firebase (Android Native, but also iOS)
Push is small in terms of feature set.
Ability to swap providers in one line of code
We handle all of the underneath complexity to receive notifications & manage the incoming entry (user tapped on the notification)
We deal with all of the registration/deregistration of the native tokens (as well as when they update). All of those weird corner cases are covered
We believe we offer the best of breed in local notifications for Xamarin/MAUI ecosystem. Local notifications are more of a “bonus feature” with Shiny, but you end needing them in so many
background scenarios that we continue to build on our module. This is actually one of the more feature rich modules due to the vast amount of features (and complexities) in the native notification
API surfaces.
In v3, we added a bunch of new features:
Geofencing Notifications for Android & iOS
Repeat Interval Notifications - Great for reminders
Android 13 Permissions
On Android, we moved off our job engine and on to the Android alarm manager to allow for precision times on notifications
Native Arguments on iOS & Android (great in multitarget projects) - Honestly… there is so many argument differences between iOS & Android, we had to do this.
Nothing special here - just the entire way you log your backend. Background services, unlike foreground debugging, are hard to hit. Thus, you need to write to an online service like
AppCenter or perhaps a local SQLite database so you can read it back later. We built our loggers against the excellent Microsoft.Extensions.Logging.
Our AppCenter logger hasn’t changed at all for v3, but we added Shiny.Logging.SQLite for local test logging. Our templates include a page for reading the SQLite logs when you install the SQLite logger.
Shiny.Extensions.Configuration adds platform embedded JSON configuration to the Microsoft.Extensions.Configuration library. What’s really cool for this library is that you can configure for all or on a per platform basis.
AppSettings.json - used by ALL platforms
AppSettings.apple.json - used by iOS & Mac Catalyst
We still have our speech-to-text module (Shiny.SpeechRecognition), but now that this exists in the Maui.CommunityToolkit, we will be deprecating this module in the future.
We also have our iBeacon library. We’ve updated it to use Android foreground services when monitoring. For the most part, Beacons seem to be dying off. We aren’t deprecating
beacons at this time since it isn’t a ton of work to keep around.
A nasty comment we hear from the “not so nice” consumer base is how “Shiny tries to do too much”. This actually isn’t true. Our Core in v2 did have a lot of support functionality for our modules, but it was trimmed when
it wasn’t needed. We’ve taken this a step further by separating support modules out to secondary libraries and linking them into modules where they are needed.
Our Core module now only contains our truly core functionality including hosting. Some will ask, well why hosting if MAUI already has it? Well - the .NET application ecosystem is fragmented right now. Some users are stuck on Xamarin Forms,
Xamarin Full Native, some are moving to MAUI, and some are even looking at newer pastures like Uno Platform. Our hosting model allows us to plugin into this different ecosystems and gives us 1 plugin base to rule them all.
We believe strongly in dependency injection for clean architecture. Many users have certain logging and configuration needs, along with their own sets of services
that they make use of in Shiny delegates. This would be very hard to enable without dependency injection. It allows for a very pluggable & testable model. We realize some don’t like this, but this is
unfortunately a hill we will die on. For those places where DI can’t reach, Shiny has Shiny.Hosting.Host.GetService for you.
Core parts of Shiny are built with Reactive programming especially our BluetoothLE module where things like configurable timeout, method chaining, and event structure matter.
We understand this can sometimes present new paradigms to users, so we add async equivalent methods where applicable. Please review our BluetoothLE feature set above.
Some won’t like Reactive programming and that’s fine - as with any free open source, you can use other libraries. We don’t like events, they leak memory and lack functionality that is easily
available in RX. Even the C# inventors have said they regret events.
We have new documentation - you’re in it already :) We think its starting to look pretty awesome. We’ve built it using the amazing Astro Build for those who are curious.
There will be some gaps at the time of our release, but we continue to improve things. Feel free to contribute as we can use all the help we can get!
Have an existing app and just need to get a list of what configuration you need to plug Shiny into your app, take a look at our new
App Builder that will give a list of every piece of boilerplate you will need to add. Simply select the Shiny libraries you want and let it show you the rest.
Our templates make starting net new apps a breeze with support for many popular Xamarin/MAUI 3rd party libraries including Shiny. Our template
takes care of setting up ALL of the boilerplate plists, manifests, projects, etc and works for Visual Studio for Windows & Mac (2022)
To Install - simply run the following from command line
Our samples do fall under the “kitchen sink” category, but we cover almost everything you can do with the library. We use Prism & ReactiveUI within our samples, but you don’t need them. It just makes life
easier for us to build the samples, but you don’t need them to use Shiny. Our samples also include some basic APIs to show you how to use ASP.NET Core to receive and send HTTP transfers.
We have a ton of modules that we haven’t released to the wild and are considering bringing out piece by piece. Shiny was a platform to build on top as our its modules.
We have already started work on WebAssembly along with some preliminary work on Windows. Windows is not a platform that I currently work on, so any contributors are certainly
welcome!
Remember, this is all given away for free. Don’t like something, offer constructive and POLITE feedback. Please also understand that feedback doesn’t mean we’ll change something, but
we want to make something MOST people will like. I also make mistakes, so bugs, oversights, missed features will happen!
Have some of that constructive & polite feedback - go here and let us know GitHub Issues