IVSoftware.Portable.Disposable
1.0.7
dotnet add package IVSoftware.Portable.Disposable --version 1.0.7
NuGet\Install-Package IVSoftware.Portable.Disposable -Version 1.0.7
<PackageReference Include="IVSoftware.Portable.Disposable" Version="1.0.7" />
paket add IVSoftware.Portable.Disposable --version 1.0.7
#r "nuget: IVSoftware.Portable.Disposable, 1.0.7"
// Install IVSoftware.Portable.Disposable as a Cake Addin #addin nuget:?package=IVSoftware.Portable.Disposable&version=1.0.7 // Install IVSoftware.Portable.Disposable as a Cake Tool #tool nuget:?package=IVSoftware.Portable.Disposable&version=1.0.7
DisposableHost
What is it?
A simple and powerful reference counting mechanism.
Overview
Reference counting has been with us since the beginning, with the venerable std:auto_ptr being among the earlier manifestations. In a managed runtime, handles to an object are counted, and kept in memory until the last one falls out of scope, before being tossed into garbage collection in order to reclaim the memory they had consumed. But there are also functional reasons to use reference counting that have less to do with memory management.
Example
In the simplest terms, the need for reference counting can be shown with a "Wait Cursor" example. Let's say we have buttonQuery
going out to the cloud to retrieve a single record which takes about 2 seconds (we're still using a 2400 bps modem to access the internet). We also have buttonLoad
which acquires an entire recordset and takes a whopping 10 seconds. In both cases, we spin a wait cursor to let the user know we're working on that.
private async void OnAnyButtonClicked(object sender, EventArgs e)
{
if (ReferenceEquals(sender, buttonLoad))
{
BindingContext.AppInstance.IsBusy = true;
await Task.Delay(TimeSpan.FromSeconds(10));
BindingContext.AppInstance.IsBusy = false;
await DisplayAlert("Alert", "Loaded!", "OK");
}
else if (ReferenceEquals(sender, buttonQuery))
{
BindingContext.AppInstance.IsBusy = true;
await Task.Delay(TimeSpan.FromSeconds(2));
BindingContext.AppInstance.IsBusy = false;
}
}
}
In the xaml, we've got a binding from IsBusy
to the activity indicator.
<ActivityIndicator
IsRunning="{Binding AppInstance.IsBusy}"
IsVisible="True"
HorizontalOptions="CenterAndExpand"
VerticalOptions="CenterAndExpand" />
I'm going to click the [Exec Load] button and wait 10 seconds. It's working great!
But what happens if the [Exec Query] button is clicked while the [Exec Load] is still in progress? This operation takes 2 seconds, and turns off the wait cursor when it finishes. The [Exec Load] might still have several seconds to go, and it serves this remaining time without a wait cursor, at which point it displays an Alert (which might not make a lot of sense because it didn't look like we were waiting for anything).
Usage
The simple idea is to maintain a Reference Count of how many requests there have been for a wait cursor, and not allow IsBusy
to go false until that count returns to zero.
To do this with DisposableHost
we would start by making an instance. In the case of a wait cursor, making it a static
instance allows it to be shared by all the pages in the app.
internal static DisposableHost AutoWaitCursor { get; } = new DisposableHost(nameof(AutoWaitCursor));
As the name implies, this instance hosts disposable tokens which can be consumed as using
statements.
if (ReferenceEquals(sender, buttonLoad))
{
using (AutoWaitCursor.GetToken())
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
await DisplayAlert("Alert", "Loaded!", "OK");
}
else if (ReferenceEquals(sender, buttonQuery))
{
using (AutoWaitCursor.GetToken())
{
await Task.Delay(TimeSpan.FromSeconds(2));
}
}
What that leaves is to respond to the events fired by the AutoWaitCursor
we declared. Here we show that IsBusy
is set true
in response to the BeginUsing
event and IsBusy
is set to false
in response to FinalDispose
. We also sign up to do some reporting on our UI as the count fluctuates.
public MainPage()
{
base.BindingContext = new MainPageModel((App)App.Current);
InitializeComponent();
BindingContext.AppInstance.IsOverlayVisible = true;
AutoWaitCursor.BeginUsing += (sender, e) =>
{
BindingContext.AppInstance.IsBusy = true;
};
AutoWaitCursor.CountChanged += (sender, e) =>
{
BindingContext.ReferenceCountText = $"{e.Name} {e.Count}";
};
AutoWaitCursor.FinalDispose += (sender, e) =>
{
BindingContext.AppInstance.IsBusy = false;
};
}
The result is that the regardless of how many times a button is clicked, the wait cursor won't go away until all of the actions have completed.
Solving circularity
When properties are persisted, it's common to respond to PropertyChanged
notifications by saving the new value to some kind of property bag. There is always the potential for circularity when the app starts up again and the properties are being loaded. For example, in a Winforms app, the InitializeComponent
will be called in the MainForm
constructor, which will set default values instead of persisted ones. And if bound properties are wired to handlers that persist the changes, it's likely that the persisted values could be overwritten with the default values before they have the opportunity to load.
This isn't just an issue when the app starts. Another example would be making changes to the ItemsSource
of a combo box. Regardless of what is being loaded, it's handy to set a static DHostLoading
property and check for IsZero
before saving any changes.
Advanced Usage: Optimizing the UI behavior
To refine the user experience further, we might want rules around concurrent operations. DisposableHost
can enforce such rules, ensuring, for example, that only one intensive "Exec Load" operation proceeds at a time, while allowing for multiple "Query Record" operations.
Our "first attempt" involves checking whether AutoWaitCursor
has a count of zero before letting the operation proceed.
if (ReferenceEquals(sender, buttonLoad))
{
if (AutoWaitCursor.IsZero())
{
using (AutoWaitCursor.GetToken())
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
await DisplayAlert("Alert", "Loaded!", "OK");
}
}
This does the job of ensuring that multiple clicks on the [Exec Load] button will no longer start a new load operation. However, if the [Query Record] button is clicked first, and the ref count is non-zero as a result of that, starting the [Exec Load] (which should be legal) will fail. The condition of if (AutoWaitCursor.IsZero())
is false because of the query click. This is easily resolvable by attributing each token in the stack to a specific sender.
DisposableHost is sender-aware.
Including the sender
argument when the token is captured is one way to address this. The expression below inspects the Sender
property of each of the tokens, and rejects the request if any match is found.
if (AutoWaitCursor.Tokens.Any(_=>ReferenceEquals(_.Sender, buttonLoad)))
{
BindingContext.ReferenceCountText = "Load already in progress!";
}
else
{
using (AutoWaitCursor.GetToken(sender))
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
await DisplayAlert("Alert", "Loaded!", "OK");
}
DisposableHost is a Dictionary
DisposableHost
is a dictionary where values can be added and removed, but which clears all the information when FinalDispose
occurs.
public class DisposableHost : Dictionary<string, object>{ ... }
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
1.0.7 | 2 | 12/1/2023 |