Skip to content

Commit

Permalink
PopupService (#1165)
Browse files Browse the repository at this point in the history
* Added initial PopupService implementation

* Tweaks to allowing parameters to be passed to the view model behind a popup

* Tidy up xml docs

* Some unit tests

* Only create a view model instance if the BindingContext hasn't been set

* Remove the reliance on IQueryAttributable in favour of our own interface

* A better way to find the current Page

* Readonly dictionary and some safety checking around expected BindingContext types.

* A different attempt at passing parameters without an explicit interface

* Update src/CommunityToolkit.Maui/PopupService.cs

Co-authored-by: Pedro Jesus <[email protected]>

* Now is a time for test

* Remove unnecessary changes

* Sample to perform a long running process

* Provide ability to close popup from within popup view model

* Prevent unnecessary instance being created

* Refactor `CurrentPage`, Add Default Constructor, Refactor `ValidateBindingContext`

* Update Unit Tests

---------

Co-authored-by: Pedro Jesus <[email protected]>
Co-authored-by: Brandon Minnick <[email protected]>
  • Loading branch information
3 people authored Nov 1, 2023
1 parent 9043e35 commit 2ec4be1
Show file tree
Hide file tree
Showing 15 changed files with 682 additions and 12 deletions.
5 changes: 3 additions & 2 deletions samples/CommunityToolkit.Maui.Sample/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,9 @@ static void RegisterViewsAndViewModels(in IServiceCollection services)
services.AddTransientWithShellRoute<StylePopupPage, StylePopupViewModel>();

// Add Popups
services.AddTransient<CsharpBindingPopup, CsharpBindingPopupViewModel>();
services.AddTransient<XamlBindingPopup, XamlBindingPopupViewModel>();
services.AddTransientPopup<CsharpBindingPopup, CsharpBindingPopupViewModel>();
services.AddTransientPopup<UpdatingPopup, UpdatingPopupViewModel>();
services.AddTransientPopup<XamlBindingPopup, XamlBindingPopupViewModel>();
}

static void RegisterEssentials(in IServiceCollection services)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<pages:BasePage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
<pages:BasePage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:CommunityToolkit.Maui.Sample.Pages"
xmlns:viewModels="clr-namespace:CommunityToolkit.Maui.Sample.ViewModels.Views"
Expand Down Expand Up @@ -28,7 +28,9 @@

<Button Text="XAML Binding Popup" Clicked="HandleXamlBindingPopupPopupButtonClicked" />

<Button Text="C# Binding Popup" Clicked="HandleCsharpBindingPopupButtonClicked" />
<Button Text="C# Binding Popup" Command="{Binding CsharpBindingPopupCommand}" />

<Button Text="Updating Popup" Command="{Binding UpdatingPopupCommand}" />
</VerticalStackLayout>
</ScrollView>
</ContentPage.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@ public sealed partial class CsharpBindingPopupViewModel : BaseViewModel
{
public string Title { get; } = "C# Binding Popup";

public string Message { get; } = "This is a platform specific popup with a .NET MAUI View being rendered. The behaviors of the popup will confirm to 100% this platform look and feel, but still allows you to use your .NET MAUI Controls.";
public string Message { get; private set; } = "";

internal void Load(string message)
{
Message = message;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
namespace CommunityToolkit.Maui.Sample.ViewModels.Views;
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Mvvm.Input;

public class MultiplePopupViewModel : BaseViewModel
namespace CommunityToolkit.Maui.Sample.ViewModels.Views;

public partial class MultiplePopupViewModel : BaseViewModel
{
readonly IPopupService popupService;

public MultiplePopupViewModel(IPopupService popupService)
{
this.popupService = popupService;
}

[RelayCommand]
Task OnCsharpBindingPopup()
{
return popupService.ShowPopupAsync<CsharpBindingPopupViewModel>(
onPresenting: viewModel => viewModel.Load("This is a platform specific popup with a .NET MAUI View being rendered. The behaviors of the popup will confirm to 100% this platform look and feel, but still allows you to use your .NET MAUI Controls."));
}

[RelayCommand]
Task OnUpdatingPopup()
{
return popupService.ShowPopupAsync<UpdatingPopupViewModel>(
onPresenting: viewModel => viewModel.PerformUpdates(10));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace CommunityToolkit.Maui.Sample.ViewModels.Views;

public partial class UpdatingPopupViewModel : BaseViewModel
{
const double finalUpdateProgressValue = 1;

readonly WeakEventManager finishedEventManager = new();

[ObservableProperty]
string message = "";

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(FinishCommand))]
double updateProgress;

public event EventHandler<EventArgs> Finished
{
add => finishedEventManager.AddEventHandler(value);
remove => finishedEventManager.RemoveEventHandler(value);
}

internal async void PerformUpdates(int numberOfUpdates)
{
double updateTotalForPercentage = numberOfUpdates + 1;

for (var update = 1; update <= numberOfUpdates; update++)
{
Message = $"Updating {update} of {numberOfUpdates}";

UpdateProgress = update / updateTotalForPercentage;

await Task.Delay(TimeSpan.FromSeconds(1));
}

UpdateProgress = finalUpdateProgressValue;
Message = "Updates complete";
}

[RelayCommand(CanExecute = nameof(CanFinish))]
void OnFinish()
{
finishedEventManager.HandleEvent(this, EventArgs.Empty, nameof(Finished));
}

bool CanFinish() => UpdateProgress is finalUpdateProgressValue;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8" ?>
<toolkit:Popup
x:Class="CommunityToolkit.Maui.Sample.Views.Popups.UpdatingPopup"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
CanBeDismissedByTappingOutsideOfPopup="False">

<VerticalStackLayout Style="{StaticResource PopupLayout}" Spacing="12" >
<VerticalStackLayout.Resources>
<ResourceDictionary>
<Style x:Key="Title" TargetType="Label">
<Setter Property="FontSize" Value="26" />
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="TextColor" Value="#000" />
<Setter Property="VerticalTextAlignment" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style x:Key="Divider" TargetType="BoxView">
<Setter Property="HeightRequest" Value="1" />
<Setter Property="Margin" Value="50, 25" />
<Setter Property="Color" Value="#c3c3c3" />
</Style>
<Style x:Key="Content" TargetType="Label">
<Setter Property="HorizontalTextAlignment" Value="Center" />
<Setter Property="VerticalTextAlignment" Value="Center" />
</Style>
<Style x:Key="PopupLayout" TargetType="StackLayout">
<Setter Property="Padding" Value="{OnPlatform Android=20, WinUI=20, iOS=12, MacCatalyst=5, Tizen=20}" />
</Style>
<Style x:Key="ConfirmButton" TargetType="Button">
<Setter Property="VerticalOptions" Value="EndAndExpand" />
</Style>

<x:Double x:Key="ComparingValue">1.0</x:Double>
</ResourceDictionary>
</VerticalStackLayout.Resources>

<Label Style="{StaticResource Title}" Text="Updating" />

<BoxView Style="{StaticResource Divider}" />

<ActivityIndicator
IsRunning="{Binding UpdateProgress, Converter={toolkit:CompareConverter ComparingValue={StaticResource ComparingValue}, ComparisonOperator=Smaller, FalseObject=False, TrueObject=True}}"/>

<Label Style="{StaticResource Content}" Text="{Binding Message}" />

<ProgressBar Progress="{Binding UpdateProgress}" />

<Button
Command="{Binding FinishCommand}"
Style="{StaticResource ConfirmButton}"
Text="Finish" />
</VerticalStackLayout>
</toolkit:Popup>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using CommunityToolkit.Maui.Sample.ViewModels.Views;
using CommunityToolkit.Maui.Views;

namespace CommunityToolkit.Maui.Sample.Views.Popups;

public partial class UpdatingPopup : Popup
{
public UpdatingPopup(UpdatingPopupViewModel updatingPopupViewModel)
{
InitializeComponent();
BindingContext = updatingPopupViewModel;

updatingPopupViewModel.Finished += OnUpdatingPopupViewModelFinished;
}

void OnUpdatingPopupViewModelFinished(object? sender, EventArgs e)
{
this.Close();
}
}
54 changes: 54 additions & 0 deletions src/CommunityToolkit.Maui.Core/IPopupService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.ComponentModel;

namespace CommunityToolkit.Maui.Core;

/// <summary>
/// Provides a mechanism for displaying <see cref="CommunityToolkit.Maui.Core.IPopup"/>s based on the underlying view model.
/// </summary>
public interface IPopupService
{
/// <summary>
/// Resolves and displays a <see cref="CommunityToolkit.Maui.Core.IPopup"/> and <typeparamref name="TViewModel"/> pair that was registered with <c>AddTransientPopup</c>.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model registered with the <see cref="CommunityToolkit.Maui.Core.IPopup"/>.</typeparam>
void ShowPopup<TViewModel>() where TViewModel : INotifyPropertyChanged;

/// <summary>
/// Resolves and displays a <see cref="CommunityToolkit.Maui.Core.IPopup"/> and <typeparamref name="TViewModel"/> pair that was registered with <c>AddTransientPopup</c>.
/// The supplied <paramref name="onPresenting"/> provides a mechanism to invoke any methods on your view model in order to load or pass data to it.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model registered with the <see cref="CommunityToolkit.Maui.Core.IPopup"/>.</typeparam>
/// <param name="onPresenting">An <see cref="Action{TViewModel}"/> that will be performed before the popup is presented.</param>
void ShowPopup<TViewModel>(Action<TViewModel> onPresenting) where TViewModel : INotifyPropertyChanged;

/// <summary>
/// Resolves and displays a <see cref="CommunityToolkit.Maui.Core.IPopup"/> and <typeparamref name="TViewModel"/> pair that was registered with <c>AddTransientPopup</c>.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model registered with the <see cref="CommunityToolkit.Maui.Core.IPopup"/>.</typeparam>
/// <param name="viewModel">The view model to use as the <c>BindingContext</c> for the <see cref="CommunityToolkit.Maui.Core.IPopup"/>.</param>
void ShowPopup<TViewModel>(TViewModel viewModel) where TViewModel : INotifyPropertyChanged;

/// <summary>
/// Resolves and displays a <see cref="CommunityToolkit.Maui.Core.IPopup"/> and <typeparamref name="TViewModel"/> pair that was registered with <c>AddTransientPopup</c>.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model registered with the <see cref="CommunityToolkit.Maui.Core.IPopup"/>.</typeparam>
/// <returns>A <see cref="Task"/> that can be awaited to return the result of the <see cref="CommunityToolkit.Maui.Core.IPopup"/> once it has been dismissed.</returns>
Task<object?> ShowPopupAsync<TViewModel>() where TViewModel : INotifyPropertyChanged;

/// <summary>
/// Resolves and displays a <see cref="CommunityToolkit.Maui.Core.IPopup"/> and <typeparamref name="TViewModel"/> pair that was registered with <c>AddTransientPopup</c>.
/// The supplied <paramref name="onPresenting"/> provides a mechanism to invoke any methods on your view model in order to load or pass data to it.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model registered with the <see cref="CommunityToolkit.Maui.Core.IPopup"/>.</typeparam>
/// <returns>A <see cref="Task"/> that can be awaited to return the result of the <see cref="CommunityToolkit.Maui.Core.IPopup"/> once it has been dismissed.</returns>
/// <param name="onPresenting">An <see cref="Action{TViewModel}"/> that will be performed before the popup is presented.</param>
Task<object?> ShowPopupAsync<TViewModel>(Action<TViewModel> onPresenting) where TViewModel : INotifyPropertyChanged;

/// <summary>
/// Resolves and displays a <see cref="CommunityToolkit.Maui.Core.IPopup"/> and <typeparamref name="TViewModel"/> pair that was registered with <c>AddTransientPopup</c>.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model registered with the <see cref="CommunityToolkit.Maui.Core.IPopup"/>.</typeparam>
/// <param name="viewModel">The view model to use as the <c>BindingContext</c> for the <see cref="CommunityToolkit.Maui.Core.IPopup"/>.</param>
/// <returns>A <see cref="Task"/> that can be awaited to return the result of the <see cref="CommunityToolkit.Maui.Core.IPopup"/> once it has been dismissed.</returns>
Task<object?> ShowPopupAsync<TViewModel>(TViewModel viewModel) where TViewModel : INotifyPropertyChanged;
}
14 changes: 10 additions & 4 deletions src/CommunityToolkit.Maui.UnitTests/BaseHandlerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ public abstract class BaseHandlerTest : BaseTest
{
protected BaseHandlerTest()
{
CreateAndSetMockApplication();
CreateAndSetMockApplication(out var serviceProvider);
ServiceProvider = serviceProvider;
}

protected IServiceProvider ServiceProvider { get; }

protected static TElementHandler CreateElementHandler<TElementHandler>(Microsoft.Maui.IElement view, bool hasMauiContext = true)
where TElementHandler : IElementHandler, new()
Expand All @@ -17,7 +20,7 @@ protected static TElementHandler CreateElementHandler<TElementHandler>(Microsoft

if (hasMauiContext)
{
mockElementHandler.SetMauiContext(Application.Current?.Handler.MauiContext ?? throw new NullReferenceException());
mockElementHandler.SetMauiContext(Application.Current?.Handler?.MauiContext ?? throw new NullReferenceException());
}

return mockElementHandler;
Expand All @@ -31,20 +34,23 @@ protected static TViewHandler CreateViewHandler<TViewHandler>(IView view, bool h

if (hasMauiContext)
{
mockViewHandler.SetMauiContext(Application.Current?.Handler.MauiContext ?? throw new NullReferenceException());
mockViewHandler.SetMauiContext(Application.Current?.Handler?.MauiContext ?? throw new NullReferenceException());
}

return mockViewHandler;
}

static void CreateAndSetMockApplication()
static void CreateAndSetMockApplication(out IServiceProvider serviceProvider)
{
var appBuilder = MauiApp.CreateBuilder()
.UseMauiCommunityToolkit()
.UseMauiApp<MockApplication>();

var mauiApp = appBuilder.Build();

var application = mauiApp.Services.GetRequiredService<IApplication>();
serviceProvider = mauiApp.Services;

application.Handler = new ApplicationHandlerStub();
application.Handler.SetMauiContext(new HandlersContextStub(mauiApp.Services));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

public class MockPageViewModel : BindableObject
{
public bool HasLoaded { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ public class MockPopupHandler : ElementHandler<IPopup, object>
{
public static CommandMapper<IPopup, MockPopupHandler> PopUpCommandMapper = new(ElementCommandMapper)
{
[nameof(IPopup.OnOpened)] = MapOnOpened
[nameof(IPopup.OnOpened)] = MapOnOpened,
[nameof(IPopup.OnClosed)] = MapOnClosed,
};

public MockPopupHandler() : base(new PropertyMapper<IView>(), PopUpCommandMapper)
Expand All @@ -30,5 +31,11 @@ protected override object CreatePlatformElement()
static void MapOnOpened(MockPopupHandler arg1, IPopup arg2, object? arg3)
{
arg1.OnOpenedCount++;
arg2.OnOpened();
}

static void MapOnClosed(MockPopupHandler handler, IPopup view, object? result)
{
view.HandlerCompleteTCS.TrySetResult();
}
}
Loading

0 comments on commit 2ec4be1

Please sign in to comment.