Skip to content

Blog

Mediator v2.0

The Road Here

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.

What’s New in 2.0

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:

{
"Mediator": {
"Http": {
"My.Namespace.Contract": "https://shinylib.net/newbase",
"My.Namespace.*" : "https://shinylib.net/therestofhtenamespace",
"*": "https://shinylib.net/everythingelse"
},
"Performance": {
"*": {
"ErrorThresholdMilliseconds": 5000
}
},
"Offline": {
"Sample.Handlers.OfflineRequestHandler": {
"AvailableAcrossSessions": true
}
},
"ReplayStream": {
"Sample.Handlers.MyRequest": {
"AvailableAcrossSessions": true
}
},
"TimerRefresh": {
"My.Contacts.HttpData": {
"IntervalSeconds": 10
}
},
"Resilience": {
"My.Namespace.ResilientContract": {
"RetryCount": 3,
"RetryDelay": 500
}
},
"Cache": {
"My.Contacts.*": {
"Priority": "High",
"AbsoluteExpirationSeconds": 300,
"SlidingExpirationSeconds": 60
}
},
"UserErrorNotifications": {
"*": {
"*": {
"Title": "ERROR",
"Message" : "Failed to do something"
},
"fr-CA": {
"Title": "ERREUR",
"Message" : "Échec de faire quelque chose"
}
}
}
}
}

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.

What’s Next

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.

Mediator v1.0

What is it?

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

What Does It Solve

We believe that Shiny Mediator is the answer to these problems. It’s a simple, yet powerful library that allows you to create a mediator in your app

Problem #1 - Service & Reference Hell

Does this look familiar to you?

public class MyViewModel(
IConnectivity conn,
IDataService data,
IAuthService auth,
IDialogsService dialogs,
ILogger<MyViewModel> logger
) {
// ...
try {
if (conn.IsConnected)
{
var myData = await data.GetDataRequest();
}
else {
dialogs.Show("No Connection");
// cache?
}
}
catch (Exception ex) {
dialogs.Show(ex.Message);
logger.LogError(ex);
}
}

With a bit of our middleware and some events, you can get here:

public class MyViewModel(IMediator mediator) : IEventHandler<ConnectivityChangedEvent>, IEventHandler<AuthChangedEvent> {
// ...
var myData = await mediator.Request(new GetDataRequest());
// logging, exception handling, offline caching can all be bundle into one nice clean call without the need for coupling
}

Problem #2 - Messages EVERYWHERE (+ Leaks)

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.

public class MyViewModel
{
public MyViewModel()
{
MessagingCenter.Subscribe<SomeEvent1>(this, @event => {
// do something
});
MessagingCenter.Subscribe<SomeEvent2>(this, @event => {
// do something
});
MessagingCenter.Send(new SomeEvent1());
MessagingCenter.Send(new SomeEvent2());
// and don't forget to unsubscribe
MessagingCenter.Unsubscribe<SomeEvent1>(this);
MessagingCenter.Unsubscribe<SomeEvent2>(this);
}
}

Let’s take a look at our mediator in action for this scenarios

public class MyViewModel : IEventHandler<SomeEvent1>, IEventHandler<SomeEvent2>
{
public MyViewModel(IMediator mediator)
{
// no need to unsubscribe
mediator.Publish(new SomeEvent1());
mediator.Publish(new SomeEvent2());
}
}

Problem #3 - Strongly Typed Navigation with Strongly Typed Arguments

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:

_navigationService.NavigateAsync("MyPage", new NavigationParameters { { "MyArg", "MyValue" } });

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.

public record MyPageNavigatonRequest(INavigationService navigator, string MyArg) : IRequest;
public class MyPageNavigationHandler : IRequestHandler<MyPageNavigatonRequest>
{
public async Task Handle(MyPageNavigatonRequest request, CancellationToken cancellationToken)
{
await request.navigator.NavigateAsync("MyPage", new NavigationParameters { { "MyArg", request.MyArg } });
}
}

Now, in your viewmodel, you can do this:

public class MyViewModel
{
public MyViewModel(IMediator mediator)
{
mediator.Request(new MyPageNavigatonRequest(_navigationService, "MyValue"));
}
}

Strongly typed. No page required page knowledge from the module upfront. The other dev team of the module can define HOW things work.

Closing

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!

April Mad Releases

Shiny v3.3

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.

Release Notes

Apple Privacy Info - May 1, 2024 Deadline is Approaching

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.

Shiny Templates v2.5

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.

1 2 3

Check It Out

Push Notification End-to-End Tester for Azure Notification Hubs & Firebase

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.

GitHub

Firebase - Specifically iOS

There is work being done to create fresh new slim bindings for Firebase on iOS. I’m working with Jon Dick on some SLIM Bindings over at https://github.com/Redth/DotNet.Platform.SlimBindings

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

Shiny 4.0 Roadmap

We are starting to plan out Shiny 4.0. We are looking for feedback on what you would like to see in the next major version of Shiny.

Currently on the roadmap are:

  • Simplifying the Job framework
  • Allow for more native configuration of Push & Local Notifications
  • More BLE methods to help with some of the fun edge cases that have been rolling in
  • Removing the old Xamarin/Netstandard targets. This will help with new targets moving forward
  • Continue to improve the hosting model to work with other platform vendors like Uno
  • Trying to get RX out of the core for those that don’t want the fat (MAYBE)

So it’s a “smaller” major release.

Bonus - ACR User Dialogs

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.

Version 3.2 Release

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:

  • Shiny.Locations / IGeofenceManager.CurrentAccess
  • Shiny.Locations / IGeofenceManager.GetCurrentAccess(GpsRequest)
  • Shiny.BluetoothLE / IPeripheralManager.CurrentAccess
  • Shiny.BluetoothLE.Hosting / IBleHostingManager.CurrentAdvertisingAccess & IBleHostingManager.CurrentGattAccess

There are other minor fixes in this release. Be sure to checkout our full release notes

Version 3 Release

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.

Features

MAUI Support + .NET 7 + Mac Catalyst

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

using Shiny;
namespace MyApp;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp
.CreateBuilder()
.UseMauiApp<App>()
// this is the important line - this wires in all of the lifecycle and base services
.UseShiny()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// now add your Shiny services by installing the module nugets and running their setup
// ie. builder.Services.AddBleClient();
return builder.Build();
}
}

HTTP Transfers

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:

First create your delegate

namespace YourNamespace;
public partial class MyHttpTransferDelegate : Shiny.Net.Http.IHttpTransferDelegate
{
public async Task OnCompleted(HttpTransferRequest request)
{
// do something when the transfer completes - send a notification, call the server, etc
}
public async Task OnError(HttpTransferRequest request, Exception ex)
{
// check the error, maybe retry?
}
}
#if ANDROID
public partial class MyHttpTransferDelegate : Shiny.IAndroidForegroundServiceDelegate
{
public void ConfigureNotification(AndroidX.Core.App.NotificationCompat.Builder builder)
{
builder
.SetContentTitle("MyApp")
.SetContentText("My App is sending images")
.SetSmallIcon(Resource.Mipmap.myNotificationIconWhateverThatIs);
})
}
#endif

Next, add it to your MAUI app builder or host builder setup

builder.Services.AddHttpTransfers<MyHttpTransferDelegate>();

Finally, queue up your transfer(s) for upload or download - let them carry on in the background without a worry

using Shiny.Net.Transfers;
IHttpManager manager; // resolve it, inject it, etc
await manager.Queue(new (
"your identifer - must be unique",
"https://myserver.com/api/transfer",
false, // true for upload
path // local file path of WHAT to upload OR WHERE to download to
));

GPS & Geofencing

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.

BluetoothLE

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.

IPeripheral peripheral; // scan it, select it, etc
peripheral.GetCharacteristic("Known Service UUID", "Known Characteristic UUID").Subscribe(args => {});
peripheral.WriteCharacteristic("Known Service UUID", "Known Characteristic UUID", new byte[] { 1, 2, 3 }).Subscribe(args => {});
peripheral.ReadCharacteristic("Known Service UUID", "Known Characteristic UUID").Subscribe(args => {});

As part of our simplified API, we’ve also introduced many Task/Async based methods where you don’t need the full power of reactive extensions.

Task<AccessState> RequestAccessAsync(this IBleManager manager);
Task ConnectAsync(this IPeripheral peripheral, ConnectionConfig? config = null, CancellationToken cancelToken = default, TimeSpan? timeout = null);
Task WaitForCharacteristicSubscriptionAsync(this IPeripheral peripheral, string serviceUuid, string characteristicUuid, CancellationToken cancellationToken = default);
Task<IReadOnlyList<BleServiceInfo>> GetServicesAsync(this IPeripheral peripheral, CancellationToken cancelToken = default);
Task<BleServiceInfo> GetServiceAsync(this IPeripheral peripheral, string serviceUuid, CancellationToken cancelToken = default);
Task<IReadOnlyList<BleCharacteristicInfo>> GetCharacteristicsAsync(this IPeripheral peripheral, string serviceUuid, CancellationToken cancelToken = default, TimeSpan? timeout = null);
Task<BleCharacteristicInfo> GetCharacteristicAsync(this IPeripheral peripheral, string serviceUuid, string characteristicUuid, CancellationToken cancelToken = default);
Task<IReadOnlyList<BleCharacteristicInfo>> GetAllCharacteristicsAsync(this IPeripheral peripheral, CancellationToken cancelToken = default, TimeSpan? timeout = null);
Task<IReadOnlyList<BleDescriptorInfo>> GetDescriptorsAsync(this IPeripheral peripheral, string serviceUuid, string characteristicUuid, CancellationToken cancelToken = default);
Task<BleCharacteristicResult> WriteCharacteristicAsync(this IPeripheral peripheral, string serviceUuid, string characteristicUuid, byte[] data, bool withResponse = true, CancellationToken cancelToken = default, int timeoutMs = 3000);
Task<BleCharacteristicResult> ReadCharacteristicAsync(this IPeripheral peripheral, string serviceUuid, string characteristicUuid, CancellationToken cancelToken = default, int timeoutMs = 3000);
Task<BleDescriptorResult> ReadDescriptorAsync(this IPeripheral peripheral, string serviceUuid, string characteristicUuid, string descriptorUuid, CancellationToken cancelToken = default, int timeoutMs = 3000);
Task<BleDescriptorResult> WriteDescriptorAsync(this IPeripheral peripheral, string serviceUuid, string characteristicUuid, string descriptorUuid, byte[] data, CancellationToken cancelToken = default, int timeoutMs = 3000);
Task<DeviceInfo> ReadDeviceInformationAsync(this IPeripheral peripheral, CancellationToken cancelToken = default, int timeoutMs = 3000);
Task<int> ReadRssiAsync(this IPeripheral peripheral, CancellationToken cancelToken = default, int timeoutMs = 3000);
Task<BleCharacteristicResult> ReadCharacteristicAsync(this IPeripheral peripheral, BleCharacteristicInfo info, CancellationToken cancelToken = default, int timeoutMs = 3000);
Task<BleCharacteristicResult> WriteCharacteristicAsync(this IPeripheral peripheral, BleCharacteristicInfo info, byte[] data, bool withoutResponse = false, CancellationToken cancelToken = default, int timeoutMs = 3000);
Task<IReadOnlyList<BleDescriptorInfo>> GetDescriptorsAsync(this IPeripheral peripheral, BleCharacteristicInfo info, CancellationToken cancelToken = default);
Task<BleDescriptorResult> WriteDescriptorAsync(this IPeripheral peripheral, BleDescriptorInfo info, byte[] data, CancellationToken cancelToken = default, int timeoutMs = 3000);
Task<BleDescriptorResult> ReadDescriptorAsync(this IPeripheral peripheral, BleDescriptorInfo info, CancellationToken cancelToken = default, int timeoutMs = 3000);

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.

IPeripheral peripheral; // scan it, select it, etc
var sub = 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.

BluetoothLE Hosting

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")]
public class MyManagedCharacteristics : BleGattCharacteristic
{
public MyManagedCharacteristics()
{
// DI all the things
}
public override Task OnStart()
{
// need to init before the characterisitic starts?
// start a timer that writes, you have access to the Characteristic notify here
}
public override void OnStop()
{
// do some cleanup?
}
// override to add and advertise Read capability
public override Task<GattResult> OnRead(ReadRequest request)
{
}
// override to add and advertise Write capability
public override Task OnWrite(WriteRequest request)
{
}
// override to add and advertise Notify capability
public override Task OnSubscriptionChanged(IPeripheral peripheral, bool subscribed)
{
}
// this is a special method that puts a write request and emits a notify response - do not override write if you use this
public override Task<GattResult> Request(WriteRequest request)
{
}
}
// in your MauiProgram.cs or hot build registration
builder.Services.AddBleHostedCharacteristic<MyManagedCharacteristics>();
// now inject/resolve the Shiny.BluetoothLE.Hosting.IBleHostingManager and now you can toggle on/off
Shiny.BluetoothLE.Hosting.IBleHostingManager hostingManager;
if (hostingManager.IsRegisteredServicesAttached)
{
hostingManager.DetachRegisteredServices();
}
else
{
await hostingManager.AttachRegisteredServices();
}

Periodic Job

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

Example of stateful job

public class MyJob : Shiny.Jobs.Job
{
public MyJob(ILogger<MyJob> logger) : base(logger) {}
string? myString;
public string? MyString
{
get => this.myString;
set => this.Set(ref this.myString, value);
}
protected override async Task Run(CancellationToken cancelToken)
{
if (this.MyString == null)
{
// this will now be set for every subsequent run
this.MyString = "Hello World";
}
}
}

Push Notifications

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

Local Notifications

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.
Shiny.Notifications.INotificationManager manager; // inject, resolve, etc
#if IOS || MACCATALYST
manager.Send(new AppleNotification
{
Subtitle = "Nice subtitle",
RelevanceScore = 1.0
});
#elif ANDROID
manager.Send(new AndroidNotification
{
Ticker = "Ticker value",
UseBigTextStyle = true
});
#endif

App Background Centric Logging

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.

App Centric Logging

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
  • AppSettings.ios.json - iOS only
  • AppSettings.maccatalyst.json - Mac Catalyst only
  • AppSettings.android.json - Android only

Other Features

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.

Architecture

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.

Hosting Model

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.

Dependency Injection All The Way

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.

Reactive Extensions

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.

Documentation

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.

Templates

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

dotnet new install Shiny.Templates

Check out all of the options in this monster Template

Samples

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.

Check out our Samples

The Road Ahead (Roadmap)

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!

Be Kind

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