Skip to content

PopupService (ported from AwaitablePopups Repo)

Tyson Elliot Hooker edited this page Oct 9, 2023 · 1 revision

PopupService Guide

This page will need a new cleanup, as it was written a while ago, it also needs a style fix

The PopupService is the 'Containment Wrapper' for PopupPages being used within AwaitablePopups, While initially complicated, this little helper will hopefully bring you up to speed on the Whats and Whys of this service, and its optimal use case.

The PopupService, like all Services, is designed with testability in mind. This is why it is created as the 'implementation' of the interface IPopupService. This means it can be Mocked and provided to ViewModels without having to rely on hacks on allowing Xamarin Forms to be called within a testing environment.

The Tendrils (Relevant files) of PopupService are the following

  1. PopupService.cs
  2. Interfaces.cs
  3. Any ViewModel that calls a PopupPage
  4. PopupViewModel.cs

Lets first start with the Interfaces.cs File.

The two interfaces relevant to the PopupService area as follows

  1. IGenericViewModel
  2. IPopupService.

IGenericViewModel<TViewModel> is an interface designed for Xaml.cs files (Codebehind) to implement, as they must derive from a single class (No Multiple Inheritance, C# restriction), and a xaml.cs file will specify which ViewModel it uses in code. The main thought behind this, is that it allows us to keep our code DRY in later functions by giving us a single target to focus on for PopupPage Navigation/Functionality

Quick Read before we go into it all. -> https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters

IGenericViewModel<T> is kept generic to allow the our code to be DRY, however, there is still some functionality we will require of any type that wants to implement this interface. In other words we have constrained this generic functionality so that the compiler can assume that whatever type we pass in, it should have some functionality on top of just System.Object, "which is the ultimate base class for any .NET type. ".

The constraint that we have applied is where T : <base class name>, which has the description "The type argument must be or derive from the specified base class.". The base class we have within our angle brackets is BaseViewPopupModel. With this constraint in place, the compiler can now assume that our Type Derives from BaseViewPopupModel. We know that it can BE BaseViewPopupModel, as that is an abstract class, however it cannot be just BaseViewPopupModel

While this type constraint is not strictly necessary, it is important as it means that the type passed into this Generic that does not implement this required functionality will throw a Compile Time Error/Intellisense error instead of a hard to follow Run Time error.

An example of all this in action is the following.

We have made a PopupPage DualResponsePopupPage. here is its declaration

public partial class DualResponsePopupPage : PopupPage, IGenericViewModel<DualResponseViewModel>

In this, we know that DualResponsePopupPage derives from PopupPage, while it also implements the IGenericViewModel interfaces, supplying the Type DualResponseViewModel.

the DualResponseViewModel looks like this

public sealed class DualResponseViewModel : PopupViewModel<bool>

and PopupViewModel looks like this

public class PopupViewModel<T> : BaseViewPopupModel

Since, the type we passed, DualResponseViewModel derives itself eventually from BaseViewPopupModel (through PopupViewModel), it passes the constraint where we must implement BaseViewPopupModel. This means that it is a valid Type that will pass IGenericViewModels Generic Constraints.

The interface itself has simple functionality, just a simple Get/Set, which we shall see when we investigate IPopupService.

IPopupService is the interface which backs the PopupService class. It is implemented as an interface, over simply allowing PopupService to exist as a wrapper to direct function, to allow for mocking it and its functionality in tests.

IPopupService has the following Functions, which i'll explain alongside the function.


1 -> TPopupPage CreatePopupPage<TPopupPage>() where TPopupPage : PopupPage, new();

CreatePopupPage is a function which allows for any Type which both derives from PopupPage and implements new() to be used. The PopupPage constraint, while not necessary, ensures that the type passed at minimum derives from PopupPage. This is another guard to make sure we use the correct types, and we find out if we dont at Compile instead of runTime.

The new constraint is required as even if PopupPage has a new() without parameters, we could also be passing a class the derives from PopupPage that does not. This Gray area is why we must have this new constraint applied to confirm to the compiler that we will provide a type that has this functionality.

All the function does is create a PopupPage of Type that has been supplied by the calling function, which could be CreatePopupPage<SingleResponsePopupPage> or CreatePopupPage<DualResponsePopupPage>. in our codebase, we only use it in the context of PushAsync, which supplies us the type through its Generic , which is supplied through its calling function. This will be explained more in PushAsync,


2 -> TPopupPage AttachViewModel<TPopupPage, TViewModel>(TPopupPage popupPage, TViewModel viewModel) where TPopupPage : PopupPage, IGenericViewModel<TViewModel> where TViewModel : BaseViewPopupModel;

AttachViewModel is a function that allows us to attach any Generic TViewModel to any generic TPopupPage, provided that these generics comply with the following constraints. TViewModel must derive from BaseViewPopupModel, see IGenericViewModel for more information. TPopupPage must derive from PopupPage (See CreatePopupPage), and it must also implement IGenericViewModel where the Generic Type is Equal to TViewModel.

The main takeaway here is that, the TPopupPage we provide must already implement IGenericViewModel, where the type matches the ViewModel we have passed in. Since each PopupPage has a specific ViewModel its expecting, which could be SingleResponseViewModel, or DualResponseViewModel, we are just ensuring that the Page's ViewModel, and the ViewModel we have passed in to attach to that page, match types.

We call this function in PushAsync, which provides the 'Type Glue' which allows all the functionality to work, as follows TPopupPage popupModal = AttachViewModel(CreatePopupPage<TPopupPage>(), modalViewModel); The context will be made clear when PushAsync is explained.


3 -> Task<TReturnable> PushAsync<TViewModel, TPopupPage, TReturnable>(TViewModel modalViewModel) where TPopupPage : PopupPage, IGenericViewModel<TViewModel>, new() where TViewModel : PopupViewModel<TReturnable>

PushAsync is the function used to push a popup page onto the stack, and also await its outcome. It is the main interaction with PopupService.

To explain this function easiest, we are going to use an example.

var successfulLogin = new SingleResponseViewModel(PopupService);
...  ...
return await PopupService.PushAsync<SingleResponseViewModel, SingleResponsePopupPage, bool>(successfulLogin);

Lets break down the constraints.

TReturnable has no constraints, it could be any type. TViewModel must derive from PopupViewModel<TReturnable>. TPopupPage must derive from PopupPage, and also implement IGenericViewModel where the Generic Type is Equal to TViewModel. (see AttachViewModel)

In this case, TViewModel is our unfamiliar constraint. Lets go through this.

As Shown in IGenericViewModel, we know that anything that derives from BaseViewPopupModel complies with any constraint requiring BaseViewPopupModel. We also know that PopupViewModel derives from BaseViewPopupModel, but it also has two important distinctions that set it apart from BaseViewPopupModel

public TaskCompletionSource<T> Returnable { get; set; } AND

public void SafeCloseModal(T result)
{
    Returnable.SetResult(result);
    PopupService.PopAsync();
}

Returnable of type TaskCompletionSource<T>. Meaning that it is a manually controlled Task (we determine when it is complete) of type T.

SafeCloseModal provides us with a generic method of Controlling that Task, as using SetResult Flips TaskCompletionSource into 'Finished' mode, and then we use PopupService.PopAsync, as any ViewModel that derives from BaseViewPopupModel has access to PopupService.

Now that background is sorted, back to our function call.

The interface version of this Task<TReturnable> PushAsync<TViewModel, TPopupPage, TReturnable>(TViewModel modalViewModel) where TPopupPage : PopupPage, IGenericViewModel<TViewModel>, new() where TViewModel : PopupViewModel<TReturnable>

And our call

PopupService.PushAsync<SingleResponseViewModel, SingleResponsePopupPage, bool>(successfulLogin)

Now, to make things clearer in explanation, we are first going to go through a desk check, to see if our called version of the function would pass the constraints we so painstakingly added.

That call would look like this. Task<bool> PushAsync<SingleResponseViewModel, SingleResponsePopupPage, bool>(SingleResponseViewModel modalViewModel) where SingleResponsePopupPage : PopupPage, IGenericViewModel<SingleResponseViewModel>, new() where SingleResponseViewModel : PopupViewModel<bool>

So, the first constraint is if SingleResponsePopupPage derives from PopupPage, which we can check by going to SingleResponsePopupPage.xaml.cs and seeing its declaration public partial class SingleResponsePopupPage : PopupPage... Next constraint, on the same input, is that if SingleResponsePopupPage implements IGenericViewModel of type SingleResponseViewModel, again, in its declaration we see public partial class SingleResponsePopupPage : PopupPage, IGenericViewModel<SingleResponseViewModel>

So our SingleResponsePopupPage passes all constraints.

The next constrain applied to SingleResponseViewModel, which must derive from PopupViewModel<bool>, if we move to SingleResponseViewModel's declaration, we see public sealed class SingleResponseViewModel : PopupViewModel<bool> That satisfies that constraint, so again, we know all of our constraints are passing with flying colours, so lets get into the function itself.

the function (as of the time of writing) looks like the following

    TPopupPage popupModal = AttachViewModel(CreatePopupPage<TPopupPage>(), modalViewModel);
    await _popupNavigation.PushAsync(popupModal);
    return await modalViewModel.Returnable.Task;

Yes, just 3 lines to push any PopupPage we want, and only one function. Finally the benefits of all these generics are bearing fruit.

so, lets convert this entire function into our example call from above. Reminder PopupService.PushAsync<SingleResponseViewModel, SingleResponsePopupPage, bool>(successfulLogin) (Im also adding our previously calculated function declaration in full)

public async Task<bool> PushAsync<SingleResponseViewModel, SingleResponsePopupPage, bool>(SingleResponseViewModel modalViewModel) where SingleResponsePopupPage : PopupPage, IGenericViewModel<SingleResponseViewModel>, new() where SingleResponseViewModel : PopupViewModel<bool>
{
    SingleResponsePopupPage popupModal = AttachViewModel(CreatePopupPage<SingleResponsePopupPage>(), modalViewModel);
    await _popupNavigation.PushAsync(popupModal);
    return await modalViewModel.Returnable.Task;
}

We create a popupModal (of type SingleResponsePopupPage, specified by the function call), which is the result of attaching the variable modalViewModel (of type SingleResponseViewModel) to a new PopupPage (also specified by the function call).

we then use the internal PushAsync to put this popupModal on the stack. This internal function expects something of type PopupPage, which we derive from. we then await the modalViewModal's Returnable Task.

As we know what the ViewModal is (SingleResponseViewModal), and we know that it implements a returnable of type TaskCompletionSource, that matches with the function calls Task. The important thing however, is that the Compiler knows this at compile time, as SingleResponseViewModel specifies Bool when it derives from PopupViewModel, and we specify that we are expecting a bool in our Function call. The compiler knows exactly what needs to happens because of the following

  1. We specified the exact types that need to be 'fed' into the function when we call it.
  2. The Types we specified all require each other.
  3. The Types we specified all pass the constraints that the compiler relies on for 'assumed functionality'

All of this, is not only for the simplicity of the original call function where we, create our ViewModel, assign its values (thats the ...), pass it in to PushAsync, specify what ViewModel we are passing, what PopupPage we expect to see, and what type the result should be, but also it is mockable and testable, as it provides an interface layer inbetween initialising xamarin forms pages and our viewmodels.


4 -> void PopAsync

Atleast we can finish on a simple function after that.

This function will just wrap the internal PopAsync functionality, however, it will do so using SafeFireAndForget, a function supplied by the AsyncAwaitBestPractises package. All this does is dafely allow us to wrap a task in an void function without worrying about what it returns, giving the app a slightly 'zippier' feel.

Hopefully this wiki has been helpful, i know it will be in the future to myself.

Clone this wiki locally