Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PopupService #1165

Merged
merged 28 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a380b84
Added initial PopupService implementation
bijington Feb 20, 2023
3d13946
Tweaks to allowing parameters to be passed to the view model behind a…
bijington Apr 28, 2023
6b5ff39
Tidy up xml docs
bijington Apr 30, 2023
8cd9c7f
Some unit tests
bijington May 6, 2023
48c1291
Only create a view model instance if the BindingContext hasn't been set
bijington May 10, 2023
2653cc6
Remove the reliance on IQueryAttributable in favour of our own interface
bijington May 10, 2023
71eeb15
A better way to find the current Page
bijington May 10, 2023
f519b67
Readonly dictionary and some safety checking around expected BindingC…
bijington May 10, 2023
bddfcd6
Merge branch 'main' into feature/sl/981-add-popupservice
bijington Jul 17, 2023
b616186
A different attempt at passing parameters without an explicit interface
bijington Aug 10, 2023
4e4c4dd
Merge branch 'main' into feature/sl/981-add-popupservice
bijington Aug 11, 2023
9f30cce
Update src/CommunityToolkit.Maui/PopupService.cs
bijington Aug 13, 2023
fc31da8
Now is a time for test
bijington Aug 13, 2023
bfc0517
Merge branch 'feature/sl/981-add-popupservice' of github.com:Communit…
bijington Aug 13, 2023
5977554
Remove unnecessary changes
bijington Aug 13, 2023
2a45736
Merge branch 'main' into feature/sl/981-add-popupservice
bijington Aug 13, 2023
fc9a1e1
Sample to perform a long running process
bijington Aug 15, 2023
95027ec
Merge branch 'feature/sl/981-add-popupservice' of github.com:Communit…
bijington Aug 15, 2023
d1fd1d9
Provide ability to close popup from within popup view model
bijington Aug 16, 2023
bf39d26
Merge branch 'main' into feature/sl/981-add-popupservice
TheCodeTraveler Aug 27, 2023
0f5fed0
Merge branch 'main' into feature/sl/981-add-popupservice
bijington Sep 4, 2023
3f0599f
Prevent unnecessary instance being created
bijington Sep 4, 2023
78a74e9
Merge branch 'feature/sl/981-add-popupservice' of github.com:Communit…
bijington Sep 4, 2023
6739d13
Merge branch 'main' into feature/sl/981-add-popupservice
bijington Sep 17, 2023
e50f1e6
Merge branch 'main' into feature/sl/981-add-popupservice
bijington Oct 26, 2023
5de3828
Merge branch 'main' into feature/sl/981-add-popupservice
TheCodeTraveler Oct 31, 2023
77ffdc9
Refactor `CurrentPage`, Add Default Constructor, Refactor `ValidateBi…
TheCodeTraveler Nov 1, 2023
5276255
Update Unit Tests
TheCodeTraveler Nov 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions samples/CommunityToolkit.Maui.Sample/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ static void RegisterViewsAndViewModels(in IServiceCollection services)
services.AddTransientWithShellRoute<ShowPopupInOnAppearingPage, ShowPopupInOnAppearingPageViewModel>();

// Add Popups
services.AddTransient<CsharpBindingPopup, CsharpBindingPopupViewModel>();
services.AddTransient<XamlBindingPopup, XamlBindingPopupViewModel>();
services.AddTransientPopup<CsharpBindingPopup, CsharpBindingPopupViewModel>();
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,7 @@

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

<Button Text="C# Binding Popup" Clicked="HandleCsharpBindingPopupButtonClicked" />
<Button Text="C# Binding Popup" Command="{Binding CsharpBindingPopupCommand}" />
</VerticalStackLayout>
</ScrollView>
</ContentPage.Content>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
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 this.popupService.ShowPopupAsync<CsharpBindingPopupViewModel>();
}
}
13 changes: 13 additions & 0 deletions src/CommunityToolkit.Maui.Core/IArgumentsReceiver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace CommunityToolkit.Maui.Core;

/// <summary>
/// Represents an implementation that can receive arguments during specific lifecycle events.
/// </summary>
public interface IArgumentsReceiver
{
/// <summary>
/// Sets the arguments ready for use.
/// </summary>
/// <param name="arguments">A set of arguments.</param>
void SetArguments(IReadOnlyDictionary<string, object> arguments);
}
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;
bijington marked this conversation as resolved.
Show resolved Hide resolved
bijington marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Resolves and displays a <see cref="CommunityToolkit.Maui.Core.IPopup"/> and <typeparamref name="TViewModel"/> pair that was registered with <c>AddTransientPopup</c>.
/// The view model will be passed the supplied <paramref name="arguments"/> based on its <see cref="IArgumentsReceiver"/> implementation.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model registered with the <see cref="CommunityToolkit.Maui.Core.IPopup"/>.</typeparam>
/// <param name="arguments">Any parameters that can be passed across to the resolved view model.</param>
void ShowPopup<TViewModel>(IReadOnlyDictionary<string, object> arguments) where TViewModel : IArgumentsReceiver, 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 view model will be passed the supplied <paramref name="arguments"/> based on its <see cref="IArgumentsReceiver"/> implementation.
/// </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="arguments">Any parameters that can be passed across to the resolved view model.</param>
Task<object?> ShowPopupAsync<TViewModel>(IReadOnlyDictionary<string, object> arguments) where TViewModel : IArgumentsReceiver, 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;
}
133 changes: 133 additions & 0 deletions src/CommunityToolkit.Maui.UnitTests/PopupServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.ComponentModel;
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Maui.UnitTests.Mocks;
using CommunityToolkit.Maui.Views;
using Xunit;

namespace CommunityToolkit.Maui.UnitTests;

public class PopupServiceTests : BaseHandlerTest
bijington marked this conversation as resolved.
Show resolved Hide resolved
{
public PopupServiceTests() : base()
{
var appBuilder = MauiApp.CreateBuilder()
.UseMauiCommunityToolkit()
.UseMauiApp<MockApplication>();

var mauiApp = appBuilder.Build();
var application = mauiApp.Services.GetRequiredService<IApplication>();
application.Handler = new ApplicationHandlerStub();
application.Handler.SetMauiContext(new HandlersContextStub(mauiApp.Services));
}

[Fact]
public static void ShowPopupWithNullViewModelShouldThrowArgumentNullException()
{
var popupService = new PopupService(new MockServiceProvider());

#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
Assert.Throws<ArgumentNullException>(() => popupService.ShowPopup<INotifyPropertyChanged>(null));
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

// [Fact]
// public static void ShowPopupWithNullQueryShouldThrowArgumentNullException()
// {
// var popupService = new PopupService(new MockServiceProvider());

//#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
// Assert.Throws<ArgumentNullException>(() => popupService.ShowPopup<IArgumentsReceiver>(null));
//#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
// }

[Fact]
public static void ShowPopupShouldThrowInvalidOperationExceptionWhenNoViewModelIsRegistered()
{
var popupService = new PopupService(new MockServiceProvider());

Assert.Throws<InvalidOperationException>(popupService.ShowPopup<MockPageViewModel>);
}

[Fact]
public static void D()
{
var serviceCollection = new ServiceCollection();
PopupService.AddTransientPopup<MockPopup, MockPageViewModel>(serviceCollection);

var popup = new MockPopup();

var popupService = new PopupService(
MockServiceProvider.ThatProvides(
(implementation: popup, forType: typeof(MockPopup)),
(implementation: new MockPageViewModel(), forType: typeof(MockPageViewModel))));

Application.Current = new MockApplication();
var app = Application.Current ?? throw new NullReferenceException();

var page = new ContentPage
{
Content = new Label
{
Text = "Hello there"
},
IsPlatformEnabled = true
};

//// Make sure that our page will have a Handler
//CreateViewHandler<MockPageHandler>(page);

app.MainPage = page;

// Make sure that our popup will have a Handler
//CreateElementHandler<MockPopupHandler>(popup);

bool popupWasOpened = false;

popup.Opened += (s, e) =>
{
popupWasOpened = true;
};

popupService.ShowPopup<MockPageViewModel>();

Assert.True(popupWasOpened);
}
}

public class MockServiceProvider : IServiceProvider
{
readonly IDictionary<Type, object> registrations;

public MockServiceProvider()
{
registrations = new Dictionary<Type, object>();
}

public MockServiceProvider(params (object implementation, Type forType)[] registrations) : this()
{
foreach (var (implementation, forType) in registrations)
{
this.registrations.Add(forType, implementation);
}
}

public static MockServiceProvider ThatProvides(params (object implementation, Type forType)[] registrations)
{
return new MockServiceProvider(registrations);
}

public object? GetService(Type serviceType)
{
if (this.registrations.TryGetValue(serviceType, out var registeredImplementation))
{
return registeredImplementation;
}

return null;
}
}

public class MockPopup : Popup
{

}
2 changes: 2 additions & 0 deletions src/CommunityToolkit.Maui/AppBuilderExtensions.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public static MauiAppBuilder UseMauiCommunityToolkit(this MauiAppBuilder builder
// Pass `null` because `options?.Invoke()` will set options on both `CommunityToolkit.Maui` and `CommunityToolkit.Maui.Core`
builder.UseMauiCommunityToolkitCore(null);

builder.Services.AddSingleton<IPopupService, PopupService>();

// Invokes options for both `CommunityToolkit.Maui` and `CommunityToolkit.Maui.Core`
options?.Invoke(new Options());

Expand Down
3 changes: 3 additions & 0 deletions src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
<Configurations>Debug;Release</Configurations>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0-ios|AnyCPU'">
<CreatePackage>false</CreatePackage>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CommunityToolkit.Maui.Core\CommunityToolkit.Maui.Core.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using CommunityToolkit.Maui.Views;

// Using root CommunityToolkit.Maui namespace so these extension methods
// light up in MauiProgram when CommunityToolkit.Maui namespace is imported
Expand Down Expand Up @@ -32,6 +33,25 @@ public static IServiceCollection AddTransient<TView, TViewModel>(this IServiceCo
.AddTransient<TView>();
}

/// <summary>
/// Adds a <see cref="Popup"/> of the type specified in <typeparamref name="TPopupView"/> and a ViewModel
/// of the type specified in <typeparamref name="TPopupViewModel"/> to the specified
/// <see cref="IServiceCollection"/> with <see cref="ServiceLifetime.Transient"/> lifetime.
/// </summary>
/// <typeparam name="TPopupView">The type of the Popup to add. Constrained to <see cref="Popup"/></typeparam>
/// <typeparam name="TPopupViewModel">The type of the ViewModel to add. Constrained to
/// <see cref="INotifyPropertyChanged"/></typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IServiceCollection AddTransientPopup<TPopupView, TPopupViewModel>(this IServiceCollection services)
where TPopupView : Popup
where TPopupViewModel : INotifyPropertyChanged
{
PopupService.AddTransientPopup<TPopupView, TPopupViewModel>(services);

return services;
}

/// <summary>
/// Adds a <see cref="NavigableElement"/> of the type specified in <typeparamref name="TView"/> and a ViewModel
/// of the type specified in <typeparamref name="TViewModel"/> to the specified
Expand Down
Loading