Skip to content

Commit d872906

Browse files
committed
Added ability to make auto registered types in DI singleton instead of only transient
1 parent 5f1d4dd commit d872906

File tree

7 files changed

+162
-28
lines changed

7 files changed

+162
-28
lines changed

sample/MauiSample/MauiProgram.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using CommunityToolkit.Maui;
22
using CoreBTS.Maui.ShieldMVVM.Configuration;
3+
using MauiSample.Features.About;
34
using Microsoft.Extensions.Logging;
45

56
namespace MauiSample
@@ -14,7 +15,11 @@ public static MauiApp CreateMauiApp()
1415
builder
1516
.UseMauiCommunityToolkit()
1617
.UseMauiApp<App>()
17-
.UseShieldMVVM(t => ServiceProvider?.GetService(t), typeof(MauiProgram).Assembly)
18+
.UseShieldMVVM(t => ServiceProvider?.GetService(t),
19+
[
20+
typeof(AboutPageViewModel)
21+
],
22+
typeof(MauiProgram).Assembly)
1823
.ConfigureFonts(fonts =>
1924
{
2025
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");

src/ShieldMVVM/Configuration/MauiAppBuilderExtensionMethods.cs

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,26 @@ public static class MauiAppBuilderExtensionMethods
2525
/// The list of assemblies to scan for IViewModelBase, ContentPageBase, and DialogPageBase types.
2626
/// </param>
2727
/// <returns>The same builder that was sent in for chaining.</returns>
28+
public static MauiAppBuilder UseShieldMVVM(
29+
this MauiAppBuilder builder,
30+
Func<Type, dynamic?> typeResolverCallback,
31+
params Assembly[] assembliesToScan) => UseShieldMVVM(builder, typeResolverCallback, null, assembliesToScan);
32+
33+
/// <summary>
34+
/// Configures the built in INavigationService to navigate between pages via a ViewModel and
35+
/// registers all ViewModels, ContentPageBase, and DialogPageBase types.
36+
/// </summary>
37+
/// <param name="builder">A builder for .NET MAUI cross-platform applications and services.</param>
38+
/// <param name="typeResolverCallback">The callback to an IoC container to return the resolved type.</param>
39+
/// <param name="typesToMakeSingleton">An optional list of types to be added as Singleton instead of Transient.</param>
40+
/// <param name="assembliesToScan">
41+
/// The list of assemblies to scan for IViewModelBase, ContentPageBase, and DialogPageBase types.
42+
/// </param>
43+
/// <returns>The same builder that was sent in for chaining.</returns>
2844
public static MauiAppBuilder UseShieldMVVM(
2945
this MauiAppBuilder builder,
3046
Func<Type, dynamic?> typeResolverCallback,
47+
HashSet<Type>? typesToMakeSingleton,
3148
params Assembly[] assembliesToScan)
3249
{
3350
builder.Services.AddSingleton<INavigationService>(new NavigationService(typeResolverCallback));
@@ -41,9 +58,9 @@ public static MauiAppBuilder UseShieldMVVM(
4158
assembliesToScan = [.. assemblies];
4259
}
4360

44-
ConfigureViewModels(builder, assembliesToScan);
45-
ConfigurePages(builder, assembliesToScan);
46-
ConfigureDialogs(builder, assembliesToScan);
61+
ConfigureViewModels(builder, typesToMakeSingleton, assembliesToScan);
62+
ConfigurePages(builder, typesToMakeSingleton, assembliesToScan);
63+
ConfigureDialogs(builder, typesToMakeSingleton, assembliesToScan);
4764

4865
return builder;
4966
}
@@ -59,11 +76,26 @@ public static MauiAppBuilder UseShieldMVVM(
5976
/// <returns>The same builder that was sent in for chaining.</returns>
6077
public static MauiAppBuilder UseShieldMVVMNoNavigation(
6178
this MauiAppBuilder builder,
79+
params Assembly[] assembliesToScan) => UseShieldMVVMNoNavigation(builder, null, assembliesToScan);
80+
81+
/// <summary>
82+
/// Does NOT configure the built in INavigationService to navigate between pages. Only registers
83+
/// ViewModels, ContentPageBase, and DialogPageBase types.
84+
/// </summary>
85+
/// <param name="builder">A builder for .NET MAUI cross-platform applications and services.</param>
86+
/// <param name="typesToMakeSingleton">An optional list of types to be added as Singleton instead of Transient.</param>
87+
/// <param name="assembliesToScan">
88+
/// The list of assemblies to scan for IViewModelBase, ContentPageBase, and DialogPageBase types.
89+
/// </param>
90+
/// <returns>The same builder that was sent in for chaining.</returns>
91+
public static MauiAppBuilder UseShieldMVVMNoNavigation(
92+
this MauiAppBuilder builder,
93+
HashSet<Type>? typesToMakeSingleton,
6294
params Assembly[] assembliesToScan)
6395
{
64-
ConfigureViewModels(builder, assembliesToScan);
65-
ConfigurePages(builder, assembliesToScan);
66-
ConfigureDialogs(builder, assembliesToScan);
96+
ConfigureViewModels(builder, typesToMakeSingleton, assembliesToScan);
97+
ConfigurePages(builder, typesToMakeSingleton, assembliesToScan);
98+
ConfigureDialogs(builder, typesToMakeSingleton, assembliesToScan);
6799

68100
return builder;
69101
}
@@ -73,16 +105,17 @@ public static MauiAppBuilder UseShieldMVVMNoNavigation(
73105
/// the Services IoC container.
74106
/// </summary>
75107
/// <param name="builder">A builder for .NET MAUI cross-platform applications and services.</param>
108+
/// <param name="typesToMakeSingleton">An optional list of types to be added as Singleton instead of Transient.</param>
76109
/// <param name="assembliesToScan">The list of assemblies to scan for IViewModelBase types.</param>
77110
/// <returns>The same builder that was sent in for chaining.</returns>
78-
private static MauiAppBuilder ConfigureViewModels(MauiAppBuilder builder, params Assembly[] assembliesToScan)
111+
private static MauiAppBuilder ConfigureViewModels(MauiAppBuilder builder, HashSet<Type>? typesToMakeSingleton, params Assembly[] assembliesToScan)
79112
{
80113
if (assembliesToScan == null)
81114
return builder;
82115

83116
foreach (var assembly in assembliesToScan)
84117
{
85-
RegisterViewModelsInAssembly(builder, assembly);
118+
RegisterViewModelsInAssembly(builder, typesToMakeSingleton, assembly);
86119
}
87120

88121
return builder;
@@ -93,16 +126,17 @@ private static MauiAppBuilder ConfigureViewModels(MauiAppBuilder builder, params
93126
/// and registers them with the Services IoC container.
94127
/// </summary>
95128
/// <param name="builder">A builder for .NET MAUI cross-platform applications and services.</param>
129+
/// <param name="typesToMakeSingleton">An optional list of types to be added as Singleton instead of Transient.</param>
96130
/// <param name="assembliesToScan">The list of assemblies to scan for ContentPageBase&lt;&gt; types.</param>
97131
/// <returns>The same builder that was sent in for chaining.</returns>
98-
private static MauiAppBuilder ConfigurePages(MauiAppBuilder builder, params Assembly[] assembliesToScan)
132+
private static MauiAppBuilder ConfigurePages(MauiAppBuilder builder, HashSet<Type>? typesToMakeSingleton, params Assembly[] assembliesToScan)
99133
{
100134
if (assembliesToScan == null)
101135
return builder;
102136

103137
foreach (var assembly in assembliesToScan)
104138
{
105-
RegisterPagesInAssembly(builder, assembly);
139+
RegisterPagesInAssembly(builder, typesToMakeSingleton, assembly);
106140
}
107141

108142
return builder;
@@ -113,33 +147,37 @@ private static MauiAppBuilder ConfigurePages(MauiAppBuilder builder, params Asse
113147
/// and registers them with the Services IoC container.
114148
/// </summary>
115149
/// <param name="builder">A builder for .NET MAUI cross-platform applications and services.</param>
150+
/// <param name="typesToMakeSingleton">An optional list of types to be added as Singleton instead of Transient.</param>
116151
/// <param name="assembliesToScan">The list of assemblies to scan for ContentPageBase&lt;&gt; types.</param>
117152
/// <returns>The same builder that was sent in for chaining.</returns>
118-
private static MauiAppBuilder ConfigureDialogs(MauiAppBuilder builder, params Assembly[] assembliesToScan)
153+
private static MauiAppBuilder ConfigureDialogs(MauiAppBuilder builder, HashSet<Type>? typesToMakeSingleton, params Assembly[] assembliesToScan)
119154
{
120155
if (assembliesToScan == null)
121156
return builder;
122157

123158
foreach (var assembly in assembliesToScan)
124159
{
125-
RegisterDialogsInAssembly(builder, assembly);
160+
RegisterDialogsInAssembly(builder, typesToMakeSingleton, assembly);
126161
}
127162

128163
return builder;
129164
}
130165

131-
private static void RegisterViewModelsInAssembly(MauiAppBuilder builder, Assembly assembly)
166+
private static void RegisterViewModelsInAssembly(MauiAppBuilder builder, HashSet<Type>? typesToMakeSingleton, Assembly assembly)
132167
{
133168
foreach (var type in assembly.GetTypes())
134169
{
135170
if (type.IsAbstract || type.IsInterface || !type.IsAssignableTo(_vmType))
136171
continue;
137172

138-
builder.Services.AddTransient(type);
173+
if (typesToMakeSingleton?.Contains(type) == true)
174+
builder.Services.AddSingleton(type);
175+
else
176+
builder.Services.AddTransient(type);
139177
}
140178
}
141179

142-
private static void RegisterPagesInAssembly(MauiAppBuilder builder, Assembly assembly)
180+
private static void RegisterPagesInAssembly(MauiAppBuilder builder, HashSet<Type>? typesToMakeSingleton, Assembly assembly)
143181
{
144182
var types = assembly.GetTypes();
145183
foreach (var pageType in types)
@@ -150,13 +188,19 @@ private static void RegisterPagesInAssembly(MauiAppBuilder builder, Assembly ass
150188
var vmType = GetPageGenericType(pageType);
151189

152190
NavigationService.ViewModelPageLookup.TryAdd(vmType, pageType);
153-
builder.Services.AddTransient(_pageType.MakeGenericType(vmType), pageType);
154191

155-
RegisterParentTypes(builder, NavigationService.ViewModelPageLookup, _pageType, types, pageType, vmType);
192+
Type genericType = _pageType.MakeGenericType(vmType);
193+
194+
if (typesToMakeSingleton?.Contains(genericType) == true)
195+
builder.Services.AddSingleton(genericType, pageType);
196+
else
197+
builder.Services.AddTransient(genericType, pageType);
198+
199+
RegisterParentTypes(builder, NavigationService.ViewModelPageLookup, _pageType, types, pageType, vmType, typesToMakeSingleton);
156200
}
157201
}
158202

159-
private static void RegisterDialogsInAssembly(MauiAppBuilder builder, Assembly assembly)
203+
private static void RegisterDialogsInAssembly(MauiAppBuilder builder, HashSet<Type>? typesToMakeSingleton, Assembly assembly)
160204
{
161205
var types = assembly.GetTypes();
162206
foreach (var pageType in types)
@@ -167,9 +211,15 @@ private static void RegisterDialogsInAssembly(MauiAppBuilder builder, Assembly a
167211
var vmType = GetPageGenericType(pageType);
168212

169213
NavigationService.ViewModelDialogPageLookup.TryAdd(vmType, pageType);
170-
builder.Services.AddTransient(_dialogPageType.MakeGenericType(vmType), pageType);
171214

172-
RegisterParentTypes(builder, NavigationService.ViewModelDialogPageLookup, _dialogPageType, types, pageType, vmType);
215+
Type genericType = _dialogPageType.MakeGenericType(vmType);
216+
217+
if (typesToMakeSingleton?.Contains(genericType) == true)
218+
builder.Services.AddSingleton(genericType, pageType);
219+
else
220+
builder.Services.AddTransient(_dialogPageType.MakeGenericType(vmType), pageType);
221+
222+
RegisterParentTypes(builder, NavigationService.ViewModelDialogPageLookup, _dialogPageType, types, pageType, vmType, typesToMakeSingleton);
173223
}
174224
}
175225

@@ -179,17 +229,24 @@ private static void RegisterParentTypes(
179229
Type genericType,
180230
Type[] types,
181231
Type pageType,
182-
Type baseViewModelType)
232+
Type baseViewModelType,
233+
HashSet<Type>? typesToMakeSingleton)
183234
{
184235
foreach (var vmType in types)
185236
{
186237
if (vmType.IsAbstract || vmType.IsInterface || vmType.BaseType != baseViewModelType)
187238
continue;
188239

189240
lookup.TryAdd(vmType, pageType);
190-
builder.Services.AddTransient(genericType.MakeGenericType(vmType), pageType);
191241

192-
RegisterParentTypes(builder, lookup, genericType, types, pageType, vmType);
242+
Type subGenericType = genericType.MakeGenericType(vmType);
243+
244+
if (typesToMakeSingleton?.Contains(subGenericType) == true)
245+
builder.Services.AddSingleton(subGenericType, pageType);
246+
else
247+
builder.Services.AddTransient(subGenericType, pageType);
248+
249+
RegisterParentTypes(builder, lookup, genericType, types, pageType, vmType, typesToMakeSingleton);
193250
}
194251
}
195252

src/ShieldMVVM/Navigation/NavigationService.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public async Task ShowDialogPopupAsync<TViewModel, TParameter>(
123123
(TViewModel)Convert.ChangeType(_typeResolverCallback(typeof(TViewModel)), typeof(TViewModel));
124124

125125
viewModel.Prepare(parameter);
126-
126+
127127
await InitializeViewModel(viewModel, token);
128128

129129
var popup = CreateDialogPage(typeof(TViewModel), viewModel);
@@ -159,6 +159,10 @@ public async Task ShowDialogPopupAsync<TViewModel, TParameter>(
159159

160160
viewModel.Prepare(parameter);
161161

162+
// Reset the result if the ViewModel was already initialized (VM is Singleton)
163+
if (viewModel.HasBeenInitialized)
164+
viewModel.Result = default;
165+
162166
await InitializeViewModel(viewModel, token);
163167

164168
var popup = CreateDialogPage(typeof(TViewModel), viewModel);
@@ -292,6 +296,10 @@ public async Task<TResult> NavigateToAsync<TViewModel, TParameter, TResult>(
292296

293297
viewModel.Prepare(parameter);
294298

299+
// Reset the result if the ViewModel was initialized (VM is Singleton)
300+
if (viewModel.HasBeenInitialized)
301+
viewModel.TaskCompletionSource = new TaskCompletionSource<TResult>();
302+
295303
if (viewModel.IsInitializeCalledBeforePageIsCreated)
296304
await InitializeViewModel(viewModel, token);
297305

@@ -507,6 +515,10 @@ public async Task<TResult> NavigateModalToAsync<TViewModel, TParameter, TResult>
507515

508516
viewModel.Prepare(parameter);
509517

518+
// Reset the result if the ViewModel was initialized (VM is Singleton)
519+
if (viewModel.HasBeenInitialized)
520+
viewModel.TaskCompletionSource = new TaskCompletionSource<TResult>();
521+
510522
if (viewModel.IsInitializeCalledBeforePageIsCreated)
511523
await InitializeViewModel(viewModel, token);
512524

@@ -624,10 +636,14 @@ public virtual void ClearModalNavigation()
624636
/// <returns>An awaitable task.</returns>
625637
private static async Task InitializeViewModel(IViewModelBase viewModel, CancellationToken token = default)
626638
{
639+
if (viewModel.HasBeenInitialized)
640+
return;
641+
627642
try
628643
{
629644
viewModel.IsBusy = true;
630645
await viewModel.InitializeAsync(token);
646+
viewModel.HasBeenInitialized = true;
631647
}
632648
finally
633649
{

src/ShieldMVVM/ViewModel/DialogViewModelBase.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ protected virtual void CloseDialog() =>
3939
/// </summary>
4040
public abstract class DialogViewModelBase : BaseDialogViewModelBase, IDialogViewModel
4141
{
42+
/// <summary>
43+
/// Gets or sets whether Initialize has been called.
44+
/// </summary>
45+
/// <remarks>
46+
/// Only needed if the ViewModel was marked as Singleton.
47+
/// </remarks>
48+
bool IViewModelBase.HasBeenInitialized { get; set; }
49+
4250
/// <summary>
4351
/// Constructor that takes the NavigationService in order to navigate between View Models.
4452
/// </summary>
@@ -54,6 +62,14 @@ protected DialogViewModelBase(INavigationService navigationService) : base(navig
5462
/// <typeparam name="TParameter">The type of parameter the ViewModel uses to set itself up.</typeparam>
5563
public abstract class DialogViewModelBase<TParameter> : BaseDialogViewModelBase, IDialogViewModel<TParameter>
5664
{
65+
/// <summary>
66+
/// Gets or sets whether Initialize has been called.
67+
/// </summary>
68+
/// <remarks>
69+
/// Only needed if the ViewModel was marked as Singleton.
70+
/// </remarks>
71+
bool IViewModelBase.HasBeenInitialized { get; set; }
72+
5773
/// <summary>
5874
/// Constructor that takes the NavigationService in order to navigate between View Models.
5975
/// </summary>
@@ -76,6 +92,14 @@ protected DialogViewModelBase(INavigationService navigationService) : base(navig
7692
/// <typeparam name="TResult">The type of result the ViewModel returns.</typeparam>
7793
public abstract class DialogViewModelBase<TParameter, TResult> : BaseDialogViewModelBase, IDialogViewModel<TParameter, TResult>
7894
{
95+
/// <summary>
96+
/// Gets or sets whether Initialize has been called.
97+
/// </summary>
98+
/// <remarks>
99+
/// Only needed if the ViewModel was marked as Singleton.
100+
/// </remarks>
101+
bool IViewModelBase.HasBeenInitialized { get; set; }
102+
79103
/// <summary>
80104
/// Constructor that takes the NavigationService in order to navigate between View Models.
81105
/// </summary>

src/ShieldMVVM/ViewModel/IPageViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public interface IPageViewModel<in TParameter, TResult> : IPageViewModelBase
5959
/// <summary>
6060
/// Gets a TaskCompletionSource that allows a Navigation call to return a result.
6161
/// </summary>
62-
TaskCompletionSource<TResult> TaskCompletionSource { get; }
62+
internal TaskCompletionSource<TResult> TaskCompletionSource { get; set; }
6363

6464
/// <summary>
6565
/// A method that only fires once and sets up any initial data the ViewModel requires to function.

src/ShieldMVVM/ViewModel/IViewModelBase.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,12 @@ public interface IViewModelBase : INotifyPropertyChanging, INotifyPropertyChange
3838
/// </param>
3939
/// <returns>An awaitable task.</returns>
4040
Task OnViewDestroying(CancellationToken token = default);
41+
42+
/// <summary>
43+
/// Gets or sets whether Initialize has been called.
44+
/// </summary>
45+
/// <remarks>
46+
/// Only needed if the ViewModel was marked as Singleton.
47+
/// </remarks>
48+
internal bool HasBeenInitialized { get; set; }
4149
}

0 commit comments

Comments
 (0)