diff --git a/Directory.Build.props b/Directory.Build.props index 27fbb13e..833ab0c7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -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; diff --git a/src/Maui/Prism.Maui/Behaviors/ElementParentedCallbackBehavior.cs b/src/Maui/Prism.Maui/Behaviors/ElementParentedCallbackBehavior.cs index b3467374..121971cb 100644 --- a/src/Maui/Prism.Maui/Behaviors/ElementParentedCallbackBehavior.cs +++ b/src/Maui/Prism.Maui/Behaviors/ElementParentedCallbackBehavior.cs @@ -2,6 +2,7 @@ using Prism.Extensions; using Prism.Navigation.Xaml; +#nullable enable namespace Prism.Behaviors; internal class ElementParentedCallbackBehavior : Behavior @@ -30,7 +31,7 @@ internal class ElementParentedCallbackBehavior : Behavior 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 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 } } - 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(); diff --git a/src/Maui/Prism.Maui/Dialogs/DialogContainerPage.cs b/src/Maui/Prism.Maui/Dialogs/DialogContainerPage.cs index 7ca3a124..700ab6b2 100644 --- a/src/Maui/Prism.Maui/Dialogs/DialogContainerPage.cs +++ b/src/Maui/Prism.Maui/Dialogs/DialogContainerPage.cs @@ -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; /// @@ -57,12 +58,14 @@ public class DialogContainerPage : ContentPage, IDialogContainer /// /// Gets the dialog view displayed in the container page. /// - public View DialogView { get; private set; } + /// Returns null until configures the dialog. + public View? DialogView { get; private set; } /// /// Gets the command used to dismiss the dialog. /// - public ICommand Dismiss { get; private set; } + /// Returns null until configures the dialog. + public ICommand? Dismiss { get; private set; } /// /// Configures the layout of the dialog container page. @@ -244,8 +247,11 @@ public class DialogContainerPage : ContentPage, IDialogContainer /// /// The element. /// The style for the specified element. - 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) diff --git a/src/Maui/Prism.Maui/Dialogs/DialogServiceBase.cs b/src/Maui/Prism.Maui/Dialogs/DialogServiceBase.cs index f936bcb7..6864af04 100644 --- a/src/Maui/Prism.Maui/Dialogs/DialogServiceBase.cs +++ b/src/Maui/Prism.Maui/Dialogs/DialogServiceBase.cs @@ -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()) diff --git a/src/Maui/Prism.Maui/Dialogs/IDialogContainer.cs b/src/Maui/Prism.Maui/Dialogs/IDialogContainer.cs index a3cc6ef0..831869ae 100644 --- a/src/Maui/Prism.Maui/Dialogs/IDialogContainer.cs +++ b/src/Maui/Prism.Maui/Dialogs/IDialogContainer.cs @@ -22,12 +22,12 @@ public interface IDialogContainer /// /// Gets the view associated with the currently displayed dialog. /// - View DialogView { get; } + View? DialogView { get; } /// /// Gets a command that can be used to dismiss the currently displayed dialog. /// - ICommand Dismiss { get; } + ICommand? Dismiss { get; } /// /// Configures the layout and behavior of the dialog. diff --git a/src/Maui/Prism.Maui/Ioc/ContainerProvider.cs b/src/Maui/Prism.Maui/Ioc/ContainerProvider.cs index c2c25858..1ee2c9c6 100644 --- a/src/Maui/Prism.Maui/Ioc/ContainerProvider.cs +++ b/src/Maui/Prism.Maui/Ioc/ContainerProvider.cs @@ -1,4 +1,6 @@ #nullable enable +using System.Diagnostics.CodeAnalysis; + namespace Prism.IoC; /// @@ -53,6 +55,7 @@ public class ContainerProvider /// Resolves the specified type from the Application's Container /// /// + [return: MaybeNull] public static implicit operator T(ContainerProvider containerProvider) { var container = ContainerLocator.Container; diff --git a/src/Prism.Core/Common/IParameters.cs b/src/Prism.Core/Common/IParameters.cs index d60d039f..dfc88531 100644 --- a/src/Prism.Core/Common/IParameters.cs +++ b/src/Prism.Core/Common/IParameters.cs @@ -6,14 +6,14 @@ namespace Prism.Common /// /// Defines a contract for specifying values associated with a unique key. /// - public interface IParameters : IEnumerable> + public interface IParameters : IEnumerable> { /// /// Adds the specified key and value to the parameter collection. /// /// The key of the parameter to add. /// The value of the parameter to add. - void Add(string key, object value); + void Add(string key, object? value); /// /// Determines whether the contains the specified . @@ -38,6 +38,7 @@ namespace Prism.Common /// The type of the parameter to get. /// The key of the parameter to find. /// A matching value of if it exists. + [return: MaybeNull] T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(string key); /// diff --git a/src/Prism.Core/Common/ParametersBase.cs b/src/Prism.Core/Common/ParametersBase.cs index a5d32a19..71c3cca4 100644 --- a/src/Prism.Core/Common/ParametersBase.cs +++ b/src/Prism.Core/Common/ParametersBase.cs @@ -104,7 +104,7 @@ namespace Prism.Common /// /// The key to reference this value in the parameters collection. /// The value of the parameter to store. - public void Add(string key, object value) => + public void Add(string key, object? value) => _entries.Add(new KeyValuePair(key, value)); /// @@ -128,6 +128,7 @@ namespace Prism.Common /// The type of object to be returned. /// The key for the value to be returned. /// Returns a matching parameter of if one exists in the Collection. + [return: MaybeNull] public T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(string key) => _entries.GetValue(key); diff --git a/src/Prism.Core/Common/ParametersExtensions.cs b/src/Prism.Core/Common/ParametersExtensions.cs index bc431179..a1cf1c78 100644 --- a/src/Prism.Core/Common/ParametersExtensions.cs +++ b/src/Prism.Core/Common/ParametersExtensions.cs @@ -21,8 +21,9 @@ namespace Prism.Common /// The key of the parameter to find /// A matching value of if it exists [EditorBrowsable(EditorBrowsableState.Never)] - public static T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable> parameters, string key) => - (T)GetValue(parameters, key, typeof(T)); + [return: MaybeNull] + public static T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable> parameters, string key) => + (T?)GetValue(parameters, key, typeof(T)); /// /// Searches for value referenced by @@ -33,7 +34,7 @@ namespace Prism.Common /// A matching value of if it exists /// Unable to convert the value of Type [EditorBrowsable(EditorBrowsableState.Never)] - public static object GetValue(this IEnumerable> parameters, string key, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type) + public static object? GetValue(this IEnumerable> 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 /// The value of parameter to return /// Success if value is found; otherwise returns false [EditorBrowsable(EditorBrowsableState.Never)] - public static bool TryGetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable> parameters, string key, out T value) + public static bool TryGetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable> parameters, string key, [MaybeNullWhen(false)] out T value) { var type = typeof(T); @@ -87,7 +88,7 @@ namespace Prism.Common /// The key of the parameter to find /// An IEnumerable{T} of all the values referenced by key [EditorBrowsable(EditorBrowsableState.Never)] - public static IEnumerable GetValues<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable> parameters, string key) + public static IEnumerable GetValues<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(this IEnumerable> parameters, string key) { List values = []; var type = typeof(T); @@ -105,7 +106,7 @@ namespace Prism.Common return values.ToArray(); } - private static bool TryGetValueInternal(KeyValuePair kvp, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type, out object value) + private static bool TryGetValueInternal(KeyValuePair 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 /// The key to search the for existence /// true if key exists; false otherwise [EditorBrowsable(EditorBrowsableState.Never)] - public static bool ContainsKey(this IEnumerable> parameters, string key) => + public static bool ContainsKey(this IEnumerable> 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; diff --git a/src/Prism.Core/Common/UriParsingHelper.cs b/src/Prism.Core/Common/UriParsingHelper.cs index e13dbcf7..be1b4fea 100644 --- a/src/Prism.Core/Common/UriParsingHelper.cs +++ b/src/Prism.Core/Common/UriParsingHelper.cs @@ -74,7 +74,7 @@ namespace Prism.Common if (parameters is not null) { - foreach (KeyValuePair 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 navigationParameter in parameters) + foreach (var navigationParameter in parameters) { dialogParameters.Add(navigationParameter.Key, navigationParameter.Value); } diff --git a/src/Prism.Core/Mvvm/ErrorsContainer.cs b/src/Prism.Core/Mvvm/ErrorsContainer.cs index ad5bbeb8..4202049e 100644 --- a/src/Prism.Core/Mvvm/ErrorsContainer.cs +++ b/src/Prism.Core/Mvvm/ErrorsContainer.cs @@ -135,7 +135,7 @@ namespace Prism.Mvvm if (hasCurrentValidationResults || hasNewValidationResults) { - if (hasNewValidationResults) + if (newValidationResults is not null && hasNewValidationResults) { this.validationResults[localPropertyName] = new List(newValidationResults); this.raiseErrorsChanged(localPropertyName); diff --git a/src/Prism.Core/Mvvm/PropertySupport.cs b/src/Prism.Core/Mvvm/PropertySupport.cs index 26197d80..c094deee 100644 --- a/src/Prism.Core/Mvvm/PropertySupport.cs +++ b/src/Prism.Core/Mvvm/PropertySupport.cs @@ -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)); diff --git a/src/Prism.Core/Mvvm/ViewRegistryBase{TBaseView}.cs b/src/Prism.Core/Mvvm/ViewRegistryBase{TBaseView}.cs index 74484508..1824a2b2 100644 --- a/src/Prism.Core/Mvvm/ViewRegistryBase{TBaseView}.cs +++ b/src/Prism.Core/Mvvm/ViewRegistryBase{TBaseView}.cs @@ -129,7 +129,7 @@ public abstract class ViewRegistryBase : IViewRegistry return candidates .Select(x => Type.GetType(x, false)) - .Where(x => x is not null); + .OfType(); } return []; diff --git a/src/Prism.Core/Navigation/INavigationParametersInternal.cs b/src/Prism.Core/Navigation/INavigationParametersInternal.cs index c03d0f5f..76eab757 100644 --- a/src/Prism.Core/Navigation/INavigationParametersInternal.cs +++ b/src/Prism.Core/Navigation/INavigationParametersInternal.cs @@ -27,5 +27,6 @@ public interface INavigationParametersInternal /// The type of object to be returned /// The key for the value to be returned /// Returns a matching parameter of if one exists in the Collection + [return: MaybeNull] T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(string key); } diff --git a/src/Prism.Core/Navigation/NavigationParameters.cs b/src/Prism.Core/Navigation/NavigationParameters.cs index 02fec421..ba397103 100644 --- a/src/Prism.Core/Navigation/NavigationParameters.cs +++ b/src/Prism.Core/Navigation/NavigationParameters.cs @@ -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(key); diff --git a/src/Uno/Prism.Uno/Dialogs/DialogService.cs b/src/Uno/Prism.Uno/Dialogs/DialogService.cs index 16995183..8c5b6200 100644 --- a/src/Uno/Prism.Uno/Dialogs/DialogService.cs +++ b/src/Uno/Prism.Uno/Dialogs/DialogService.cs @@ -28,13 +28,17 @@ namespace Prism.Dialogs var dialogWindow = CreateDialogWindow(windowName); if (dialogWindow is ContentDialog contentDialog) { - contentDialog.XamlRoot = _containerProvider.Resolve().Content.XamlRoot; + var windowContent = _containerProvider.Resolve().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(parameters[KnownDialogParameters.DialogPlacement].ToString() ?? string.Empty)) : ContentDialogPlacement.Popup; + (parameters[KnownDialogParameters.DialogPlacement] is ContentDialogPlacement placementValue ? placementValue : Enum.Parse(parameters[KnownDialogParameters.DialogPlacement]?.ToString() ?? string.Empty)) : ContentDialogPlacement.Popup; await dialogWindow.ShowAsync(placement); } diff --git a/tests/Maui/Prism.Maui.Tests/Fixtures/Common/UriParsingHelperFixture.cs b/tests/Maui/Prism.Maui.Tests/Fixtures/Common/UriParsingHelperFixture.cs index 9e3b00f3..d16583e1 100644 --- a/tests/Maui/Prism.Maui.Tests/Fixtures/Common/UriParsingHelperFixture.cs +++ b/tests/Maui/Prism.Maui.Tests/Fixtures/Common/UriParsingHelperFixture.cs @@ -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"; diff --git a/tests/Maui/Prism.Maui.Tests/Fixtures/Dialogs/DialogContainerLayoutFixture.cs b/tests/Maui/Prism.Maui.Tests/Fixtures/Dialogs/DialogContainerLayoutFixture.cs index 106f6be0..0fc70622 100644 --- a/tests/Maui/Prism.Maui.Tests/Fixtures/Dialogs/DialogContainerLayoutFixture.cs +++ b/tests/Maui/Prism.Maui.Tests/Fixtures/Dialogs/DialogContainerLayoutFixture.cs @@ -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(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() { diff --git a/tests/Prism.Core.Tests/Common/ParametersFixture.cs b/tests/Prism.Core.Tests/Common/ParametersFixture.cs index 24fc03a9..43833b3b 100644 --- a/tests/Prism.Core.Tests/Common/ParametersFixture.cs +++ b/tests/Prism.Core.Tests/Common/ParametersFixture.cs @@ -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("value")); + Assert.False(parameters.TryGetValue("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("value").Value); - Assert.Equal(42, ((MockStructWithParameterlessConstructor)parameters.GetValue("value", typeof(MockStructWithParameterlessConstructor))).Value); + Assert.Equal(42, Assert.IsType(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("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("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("value")); Assert.Null(parameters.GetValue("value", typeof(MockStructWithParameterlessConstructor?)));