IVSoftware.Portable.Disposable 1.0.7

There is a newer version of this package available.
See the version list below for details.
dotnet add package IVSoftware.Portable.Disposable --version 1.0.7
NuGet\Install-Package IVSoftware.Portable.Disposable -Version 1.0.7
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="IVSoftware.Portable.Disposable" Version="1.0.7" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add IVSoftware.Portable.Disposable --version 1.0.7
#r "nuget: IVSoftware.Portable.Disposable, 1.0.7"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// 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!

Image Placeholder

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 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. 
.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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.1.99 1 3/6/2024
1.0.7 1 12/1/2023