Align nullable parameter and dialog contracts

This commit is contained in:
Dan Siegel
2026-10-04 14:16:41 -06:00
parent be4f53589b
commit 7cfe491591
19 changed files with 106 additions and 32 deletions
+1
View File
@@ -23,6 +23,7 @@
System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute;
System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes;
System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute;
System.Diagnostics.CodeAnalysis.MaybeNullAttribute;
System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute;
System.Runtime.CompilerServices.IsExternalInit;
</PolySharpIncludeGeneratedTypes>
@@ -2,6 +2,7 @@
using Prism.Extensions;
using Prism.Navigation.Xaml;
#nullable enable
namespace Prism.Behaviors;
internal class ElementParentedCallbackBehavior : Behavior<VisualElement>
@@ -30,7 +31,7 @@ internal class ElementParentedCallbackBehavior : Behavior<VisualElement>
base.OnDetachingFrom(view);
}
private void OnParentChanged(object sender, EventArgs e) => TryInvokeCallback();
private void OnParentChanged(object? sender, EventArgs e) => TryInvokeCallback();
private void TryInvokeCallback()
{
@@ -59,7 +60,7 @@ internal class ElementParentedCallbackBehavior : Behavior<VisualElement>
while (scopePage?.Parent is FlyoutPage flyout && flyout.Flyout == scopePage)
scopePage = flyout;
for (Element element = view; element is not null; element = element.Parent)
for (Element? element = view; element is not null; element = element.Parent)
{
_observedElements.Add(element);
element.ParentChanged += OnParentChanged;
@@ -72,10 +73,10 @@ internal class ElementParentedCallbackBehavior : Behavior<VisualElement>
}
}
private Page GetPage(VisualElement view) =>
private Page? GetPage(VisualElement view) =>
_includeSelf && view is Page page ? page : view.GetParentPage();
private void PagePropertyChanged(object sender, PropertyChangedEventArgs e)
private void PagePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == Navigation.Xaml.Navigation.PrismContainerProvider)
TryInvokeCallback();
@@ -8,6 +8,7 @@ using Prism.Dialogs.Xaml;
using Application = Microsoft.Maui.Controls.Application;
using Page = Microsoft.Maui.Controls.Page;
#nullable enable
namespace Prism.Dialogs;
/// <summary>
@@ -57,12 +58,14 @@ public class DialogContainerPage : ContentPage, IDialogContainer
/// <summary>
/// Gets the dialog view displayed in the container page.
/// </summary>
public View DialogView { get; private set; }
/// <remarks>Returns null until <see cref="ConfigureLayout"/> configures the dialog.</remarks>
public View? DialogView { get; private set; }
/// <summary>
/// Gets the command used to dismiss the dialog.
/// </summary>
public ICommand Dismiss { get; private set; }
/// <remarks>Returns null until <see cref="ConfigureLayout"/> configures the dialog.</remarks>
public ICommand? Dismiss { get; private set; }
/// <summary>
/// Configures the layout of the dialog container page.
@@ -244,8 +247,11 @@ public class DialogContainerPage : ContentPage, IDialogContainer
/// </summary>
/// <param name="element">The element.</param>
/// <returns>The style for the specified element.</returns>
private static Style GetStyle(Element element)
private static Style GetStyle(Element? element)
{
if (element is null)
return DefaultStyle();
if (element is Page page && page.Resources.ContainsKey(DialogLayout.PopupOverlayStyle) && page.Resources[DialogLayout.PopupOverlayStyle] is Style pageStyle)
return pageStyle;
else if (element is Application app)
@@ -195,7 +195,7 @@ public abstract class DialogServiceBase : IDialogService
};
}
var view = dialogModal.DialogView;
var view = dialogModal.DialogView ?? throw new DialogException(DialogException.HostPageIsNotDialogHost);
var dialogAware = GetDialogController(view);
if (!dialogAware.CanCloseDialog())
@@ -22,12 +22,12 @@ public interface IDialogContainer
/// <summary>
/// Gets the view associated with the currently displayed dialog.
/// </summary>
View DialogView { get; }
View? DialogView { get; }
/// <summary>
/// Gets a command that can be used to dismiss the currently displayed dialog.
/// </summary>
ICommand Dismiss { get; }
ICommand? Dismiss { get; }
/// <summary>
/// Configures the layout and behavior of the dialog.
@@ -1,4 +1,6 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
namespace Prism.IoC;
/// <summary>
@@ -53,6 +55,7 @@ public class ContainerProvider<T>
/// Resolves the specified type from the Application's Container
/// </summary>
/// <param name="containerProvider"></param>
[return: MaybeNull]
public static implicit operator T(ContainerProvider<T> containerProvider)
{
var container = ContainerLocator.Container;
+3 -2
View File
@@ -6,14 +6,14 @@ namespace Prism.Common
/// <summary>
/// Defines a contract for specifying values associated with a unique key.
/// </summary>
public interface IParameters : IEnumerable<KeyValuePair<string, object>>
public interface IParameters : IEnumerable<KeyValuePair<string, object?>>
{
/// <summary>
/// Adds the specified key and value to the parameter collection.
/// </summary>
/// <param name="key">The key of the parameter to add.</param>
/// <param name="value">The value of the parameter to add.</param>
void Add(string key, object value);
void Add(string key, object? value);
/// <summary>
/// Determines whether the <see cref="IParameters"/> contains the specified <paramref name="key"/>.
@@ -38,6 +38,7 @@ namespace Prism.Common
/// <typeparam name="T">The type of the parameter to get.</typeparam>
/// <param name="key">The key of the parameter to find.</param>
/// <returns>A matching value of <typeparamref name="T"/> if it exists.</returns>
[return: MaybeNull]
T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(string key);
/// <summary>
+2 -1
View File
@@ -104,7 +104,7 @@ namespace Prism.Common
/// </summary>
/// <param name="key">The key to reference this value in the parameters collection.</param>
/// <param name="value">The value of the parameter to store.</param>
public void Add(string key, object value) =>
public void Add(string key, object? value) =>
_entries.Add(new KeyValuePair<string, object?>(key, value));
/// <summary>
@@ -128,6 +128,7 @@ namespace Prism.Common
/// <typeparam name="T">The type of object to be returned.</typeparam>
/// <param name="key">The key for the value to be returned.</param>
/// <returns>Returns a matching parameter of <typeparamref name="T"/> if one exists in the Collection.</returns>
[return: MaybeNull]
public T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(string key) =>
_entries.GetValue<T>(key);
@@ -21,8 +21,9 @@ namespace Prism.Common
/// <param name="key">The key of the parameter to find</param>
/// <returns>A matching value of <typeparamref name="T"/> if it exists</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable<KeyValuePair<string, object>> parameters, string key) =>
(T)GetValue(parameters, key, typeof(T));
[return: MaybeNull]
public static T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable<KeyValuePair<string, object?>> parameters, string key) =>
(T?)GetValue(parameters, key, typeof(T));
/// <summary>
/// Searches <paramref name="parameters"/> for value referenced by <paramref name="key"/>
@@ -33,7 +34,7 @@ namespace Prism.Common
/// <returns>A matching value of <paramref name="type"/> if it exists</returns>
/// <exception cref="InvalidCastException">Unable to convert the value of Type</exception>
[EditorBrowsable(EditorBrowsableState.Never)]
public static object GetValue(this IEnumerable<KeyValuePair<string, object>> parameters, string key, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
public static object? GetValue(this IEnumerable<KeyValuePair<string, object?>> parameters, string key, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
{
foreach (var kvp in parameters)
{
@@ -42,7 +43,7 @@ namespace Prism.Common
if (TryGetValueInternal(kvp, type, out var value))
return value;
throw new InvalidCastException($"Unable to convert the value of Type '{kvp.Value.GetType().FullName}' to '{type.FullName}' for the key '{key}' ");
throw new InvalidCastException($"Unable to convert the value of Type '{kvp.Value?.GetType().FullName ?? "null"}' to '{type.FullName}' for the key '{key}' ");
}
}
@@ -58,7 +59,7 @@ namespace Prism.Common
/// <param name="value">The value of parameter to return</param>
/// <returns>Success if value is found; otherwise returns <c>false</c></returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool TryGetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable<KeyValuePair<string, object>> parameters, string key, out T value)
public static bool TryGetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable<KeyValuePair<string, object?>> parameters, string key, [MaybeNullWhen(false)] out T value)
{
var type = typeof(T);
@@ -87,7 +88,7 @@ namespace Prism.Common
/// <param name="key">The key of the parameter to find</param>
/// <returns>An IEnumerable{T} of all the values referenced by key</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static IEnumerable<T> GetValues<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable<KeyValuePair<string, object>> parameters, string key)
public static IEnumerable<T> GetValues<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable<KeyValuePair<string, object?>> parameters, string key)
{
List<T> values = [];
var type = typeof(T);
@@ -105,7 +106,7 @@ namespace Prism.Common
return values.ToArray();
}
private static bool TryGetValueInternal(KeyValuePair<string, object> kvp, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type, out object value)
private static bool TryGetValueInternal(KeyValuePair<string, object?> kvp, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type, out object? value)
{
value = GetDefault(type);
var valueAsString = kvp.Value is string str ? str : kvp.Value?.ToString();
@@ -154,7 +155,7 @@ namespace Prism.Common
/// <param name="key">The key to search the <paramref name="parameters"/> for existence</param>
/// <returns><c>true</c> if key exists; <c>false</c> otherwise</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool ContainsKey(this IEnumerable<KeyValuePair<string, object>> parameters, string key) =>
public static bool ContainsKey(this IEnumerable<KeyValuePair<string, object?>> parameters, string key) =>
parameters.Any(x => string.Compare(x.Key, key, StringComparison.Ordinal) == 0);
private static object? GetDefault([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type) => type.IsValueType ? Activator.CreateInstance(type) : null;
+2 -2
View File
@@ -74,7 +74,7 @@ namespace Prism.Common
if (parameters is not null)
{
foreach (KeyValuePair<string, object> navigationParameter in parameters)
foreach (var navigationParameter in parameters)
{
navParameters.Add(navigationParameter.Key, navigationParameter.Value);
}
@@ -116,7 +116,7 @@ namespace Prism.Common
if (parameters != null)
{
foreach (KeyValuePair<string, object> navigationParameter in parameters)
foreach (var navigationParameter in parameters)
{
dialogParameters.Add(navigationParameter.Key, navigationParameter.Value);
}
+1 -1
View File
@@ -135,7 +135,7 @@ namespace Prism.Mvvm
if (hasCurrentValidationResults || hasNewValidationResults)
{
if (hasNewValidationResults)
if (newValidationResults is not null && hasNewValidationResults)
{
this.validationResults[localPropertyName] = new List<T>(newValidationResults);
this.raiseErrorsChanged(localPropertyName);
+3
View File
@@ -54,6 +54,9 @@ namespace Prism.Mvvm
throw new ArgumentException(Resources.PropertySupport_ExpressionNotProperty_Exception, nameof(expression));
var getMethod = property.GetMethod;
if (getMethod == null)
throw new ArgumentException(Resources.PropertySupport_ExpressionNotProperty_Exception, nameof(expression));
if (getMethod.IsStatic)
throw new ArgumentException(Resources.PropertySupport_StaticExpression_Exception, nameof(expression));
@@ -129,7 +129,7 @@ public abstract class ViewRegistryBase<TBaseView> : IViewRegistry
return candidates
.Select(x => Type.GetType(x, false))
.Where(x => x is not null);
.OfType<Type>();
}
return [];
@@ -27,5 +27,6 @@ public interface INavigationParametersInternal
/// <typeparam name="T">The type of object to be returned</typeparam>
/// <param name="key">The key for the value to be returned</param>
/// <returns>Returns a matching parameter of <typeparamref name="T"/> if one exists in the Collection</returns>
[return: MaybeNull]
T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(string key);
}
@@ -41,6 +41,7 @@ public class NavigationParameters : ParametersBase, INavigationParameters, INavi
return _internalParameters.ContainsKey(key);
}
[return: MaybeNull]
T INavigationParametersInternal.GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(string key)
{
return _internalParameters.GetValue<T>(key);
+6 -2
View File
@@ -28,13 +28,17 @@ namespace Prism.Dialogs
var dialogWindow = CreateDialogWindow(windowName);
if (dialogWindow is ContentDialog contentDialog)
{
contentDialog.XamlRoot = _containerProvider.Resolve<Window>().Content.XamlRoot;
var windowContent = _containerProvider.Resolve<Window>().Content;
if (windowContent is null)
throw new InvalidOperationException("A dialog requires a window with content.");
contentDialog.XamlRoot = windowContent.XamlRoot;
}
ConfigureDialogWindowEvents(dialogWindow, callback);
ConfigureDialogWindowContent(name, dialogWindow, parameters);
var placement = parameters.ContainsKey(KnownDialogParameters.DialogPlacement) ?
(parameters[KnownDialogParameters.DialogPlacement] is ContentDialogPlacement placementValue ? placementValue : Enum.Parse<ContentDialogPlacement>(parameters[KnownDialogParameters.DialogPlacement].ToString() ?? string.Empty)) : ContentDialogPlacement.Popup;
(parameters[KnownDialogParameters.DialogPlacement] is ContentDialogPlacement placementValue ? placementValue : Enum.Parse<ContentDialogPlacement>(parameters[KnownDialogParameters.DialogPlacement]?.ToString() ?? string.Empty)) : ContentDialogPlacement.Popup;
await dialogWindow.ShowAsync(placement);
}
@@ -4,6 +4,23 @@ namespace Prism.Maui.Tests.Fixtures.Common;
#nullable enable
public class UriParsingHelperFixture
{
[Fact]
public void SegmentCopiesPreserveExplicitNullNavigationAndDialogParameters()
{
INavigationParameters navigation = new NavigationParameters();
navigation.Add("value", null);
IDialogParameters dialog = new Prism.Dialogs.DialogParameters();
dialog.Add("value", null);
var navigationCopy = UriParsingHelper.GetSegmentParameters("Page", navigation);
var dialogCopy = UriParsingHelper.GetSegmentParameters("Dialog", dialog);
Assert.True(navigationCopy.ContainsKey("value"));
Assert.Null(Assert.Single(navigationCopy).Value);
Assert.True(dialogCopy.ContainsKey("value"));
Assert.Null(Assert.Single(dialogCopy).Value);
}
const string _relativeUri = "MainPage?id=3&name=dan";
const string _absoluteUriWithOutProtocol = "/MainPage?id=3&name=dan";
const string _absoluteUri = "htp://www.dansiegel.net/MainPage?id=3&name=dan";
@@ -9,6 +9,25 @@ namespace Prism.Maui.Tests.Fixtures.Dialogs;
public class DialogContainerLayoutFixture
{
[Fact]
public void UnconfiguredContainerHasNoDialogOrDismissCommand()
{
IDialogContainer container = new DialogContainerPage();
Assert.Null(container.DialogView);
Assert.Null(container.Dismiss);
}
[Fact]
public void UnparentedPageUsesDefaultOverlayStyle()
{
var overlay = new LayoutContainer().CreateLayout(new ContentView(), false, new Command(() => { }));
var mask = Assert.IsType<BoxView>(overlay.Children[0]);
Assert.NotNull(mask.Style);
Assert.Contains(mask.Style.Setters, setter => setter.Property == BoxView.OpacityProperty && Equals(setter.Value, 0.75));
}
[Fact]
public void MaskCoversSafeAreaWithoutChangingPopupPositioning()
{
@@ -7,6 +7,21 @@ namespace Prism.Tests.Common
{
public class ParametersFixture
{
[Fact]
public void InterfaceEnumerationPreservesExplicitNullValues()
{
IParameters parameters = new MockParameters();
parameters.Add("value", null);
var entry = Assert.Single(parameters);
Assert.Equal("value", entry.Key);
Assert.Null(entry.Value);
Assert.True(parameters.ContainsKey("value"));
Assert.Null(parameters.GetValue<string>("value"));
Assert.False(parameters.TryGetValue<string>("value", out var value));
Assert.Null(value);
}
[Fact]
public void TryGetValueOfT()
{
@@ -60,10 +75,10 @@ namespace Prism.Tests.Common
{
IParameters parameters = new MockParameters();
if (addNullParameter)
parameters.Add("value", null!);
parameters.Add("value", null);
Assert.Equal(42, parameters.GetValue<MockStructWithParameterlessConstructor>("value").Value);
Assert.Equal(42, ((MockStructWithParameterlessConstructor)parameters.GetValue("value", typeof(MockStructWithParameterlessConstructor))).Value);
Assert.Equal(42, Assert.IsType<MockStructWithParameterlessConstructor>(parameters.GetValue("value", typeof(MockStructWithParameterlessConstructor))).Value);
}
[Theory]
@@ -73,7 +88,7 @@ namespace Prism.Tests.Common
{
IParameters parameters = new MockParameters();
if (addNullParameter)
parameters.Add("value", null!);
parameters.Add("value", null);
var success = parameters.TryGetValue<MockStructWithParameterlessConstructor>("value", out var value);
@@ -85,7 +100,7 @@ namespace Prism.Tests.Common
public void GetValuesRunsStructParameterlessConstructorForNullParameter()
{
IParameters parameters = new MockParameters();
parameters.Add("value", null!);
parameters.Add("value", null);
var value = Assert.Single(parameters.GetValues<MockStructWithParameterlessConstructor>("value"));
@@ -100,7 +115,7 @@ namespace Prism.Tests.Common
{
IParameters parameters = new MockParameters();
if (addNullParameter)
parameters.Add("value", null!);
parameters.Add("value", null);
Assert.Null(parameters.GetValue<MockStructWithParameterlessConstructor?>("value"));
Assert.Null(parameters.GetValue("value", typeof(MockStructWithParameterlessConstructor?)));